feat: Phase 3.16 데이터 관리 서비스 Raw Query 전환 완료
4개 서비스 18개 Prisma 호출 전환 완료: 1. **EnhancedDynamicFormService** (6개) - validateTableExists - information_schema 조회 - getTableColumns - 테이블 컬럼 정보 조회 with 캐싱 - getColumnWebTypes - 웹타입 정보 조회 - getPrimaryKeys - Primary Key 조회 - performInsert - 동적 INSERT with RETURNING - performUpdate - 동적 UPDATE with RETURNING 2. **DataMappingService** (5개) - getSourceData - 소스 테이블 데이터 조회 - executeInsert - 동적 INSERT - executeUpsert - ON CONFLICT DO UPDATE - executeUpdate - 동적 UPDATE - disconnect - 제거 (Raw Query 불필요) 3. **DataService** (4개) - getTableData - 동적 SELECT with 동적 WHERE/ORDER BY - checkTableExists - information_schema 테이블 존재 확인 - getTableColumnsSimple - 컬럼 정보 조회 - getColumnLabel - 컬럼 라벨 조회 4. **AdminService** (3개) - getAdminMenuList - WITH RECURSIVE 쿼리 - getUserMenuList - WITH RECURSIVE 쿼리 - getMenuInfo - LEFT JOIN으로 회사 정보 포함 기술적 성과: - 변수명 충돌 해결 (query vs sql) - WITH RECURSIVE 쿼리 전환 - Prisma include → LEFT JOIN 전환 - 동적 쿼리 생성 (WHERE, ORDER BY) - SQL 인젝션 방지 (컬럼명 검증) 진행률: Phase 3 173/186 (93.0%) 문서: PHASE3.16_DATA_MANAGEMENT_SERVICES_MIGRATION.md
This commit is contained in:
parent
1791cd9f3f
commit
3d8f70e181
|
|
@ -6,16 +6,71 @@
|
|||
|
||||
### 📊 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| --------------- | ------------------------------------------------------------------------- |
|
||||
| 대상 서비스 | 4개 (EnhancedDynamicForm, DataMapping, Data, Admin) |
|
||||
| 파일 위치 | `backend-node/src/services/{enhanced,data,admin}*.ts` |
|
||||
| 총 파일 크기 | 2,062 라인 |
|
||||
| Prisma 호출 | 18개 |
|
||||
| **현재 진행률** | **0/18 (0%)** 🔄 **진행 예정** |
|
||||
| 복잡도 | 중간 (동적 쿼리, JSON 필드, 관리자 기능) |
|
||||
| 우선순위 | 🟡 중간 (Phase 3.16) |
|
||||
| **상태** | ⏳ **대기 중** |
|
||||
| 항목 | 내용 |
|
||||
| --------------- | ----------------------------------------------------- |
|
||||
| 대상 서비스 | 4개 (EnhancedDynamicForm, DataMapping, Data, Admin) |
|
||||
| 파일 위치 | `backend-node/src/services/{enhanced,data,admin}*.ts` |
|
||||
| 총 파일 크기 | 2,062 라인 |
|
||||
| Prisma 호출 | 0개 (전환 완료) |
|
||||
| **현재 진행률** | **18/18 (100%)** ✅ **전환 완료** |
|
||||
| 복잡도 | 중간 (동적 쿼리, JSON 필드, 관리자 기능) |
|
||||
| 우선순위 | 🟡 중간 (Phase 3.16) |
|
||||
| **상태** | ✅ **완료** |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 전환 완료 내역
|
||||
|
||||
### 전환된 Prisma 호출 (18개)
|
||||
|
||||
#### 1. EnhancedDynamicFormService (6개)
|
||||
- `validateTableExists()` - $queryRawUnsafe → query
|
||||
- `getTableColumns()` - $queryRawUnsafe → query
|
||||
- `getColumnWebTypes()` - $queryRawUnsafe → query
|
||||
- `getPrimaryKeys()` - $queryRawUnsafe → query
|
||||
- `performInsert()` - $queryRawUnsafe → query
|
||||
- `performUpdate()` - $queryRawUnsafe → query
|
||||
|
||||
#### 2. DataMappingService (5개)
|
||||
- `getSourceData()` - $queryRawUnsafe → query
|
||||
- `executeInsert()` - $executeRawUnsafe → query
|
||||
- `executeUpsert()` - $executeRawUnsafe → query
|
||||
- `executeUpdate()` - $executeRawUnsafe → query
|
||||
- `disconnect()` - 제거 (Raw Query는 disconnect 불필요)
|
||||
|
||||
#### 3. DataService (4개)
|
||||
- `getTableData()` - $queryRawUnsafe → query
|
||||
- `checkTableExists()` - $queryRawUnsafe → query
|
||||
- `getTableColumnsSimple()` - $queryRawUnsafe → query
|
||||
- `getColumnLabel()` - $queryRawUnsafe → query
|
||||
|
||||
#### 4. AdminService (3개)
|
||||
- `getAdminMenuList()` - $queryRaw → query (WITH RECURSIVE)
|
||||
- `getUserMenuList()` - $queryRaw → query (WITH RECURSIVE)
|
||||
- `getMenuInfo()` - findUnique → query (JOIN)
|
||||
|
||||
### 주요 기술적 해결 사항
|
||||
|
||||
1. **변수명 충돌 해결**
|
||||
- `dataService.ts`에서 `query` 변수 → `sql` 변수로 변경
|
||||
- `query()` 함수와 로컬 변수 충돌 방지
|
||||
|
||||
2. **WITH RECURSIVE 쿼리 전환**
|
||||
- Prisma의 `$queryRaw` 템플릿 리터럴 → 일반 문자열
|
||||
- `${userLang}` → `$1` 파라미터 바인딩
|
||||
|
||||
3. **JOIN 쿼리 전환**
|
||||
- Prisma의 `include` 옵션 → `LEFT JOIN` 쿼리
|
||||
- 관계 데이터를 단일 쿼리로 조회
|
||||
|
||||
4. **동적 쿼리 생성**
|
||||
- 동적 WHERE 조건 구성
|
||||
- SQL 인젝션 방지 (컬럼명 검증)
|
||||
- 동적 ORDER BY 처리
|
||||
|
||||
### 컴파일 상태
|
||||
✅ TypeScript 컴파일 성공
|
||||
✅ Linter 오류 없음
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -24,12 +79,14 @@
|
|||
### 1. EnhancedDynamicFormService (6개 호출, 786 라인)
|
||||
|
||||
**주요 기능**:
|
||||
|
||||
- 고급 동적 폼 관리
|
||||
- 폼 검증 규칙
|
||||
- 조건부 필드 표시
|
||||
- 폼 템플릿 관리
|
||||
|
||||
**예상 Prisma 호출**:
|
||||
|
||||
- `getEnhancedForms()` - 고급 폼 목록 조회
|
||||
- `getEnhancedForm()` - 고급 폼 단건 조회
|
||||
- `createEnhancedForm()` - 고급 폼 생성
|
||||
|
|
@ -38,6 +95,7 @@
|
|||
- `getFormValidationRules()` - 검증 규칙 조회
|
||||
|
||||
**기술적 고려사항**:
|
||||
|
||||
- JSON 필드 (validation_rules, conditional_logic, field_config)
|
||||
- 복잡한 검증 규칙
|
||||
- 동적 필드 생성
|
||||
|
|
@ -48,12 +106,14 @@
|
|||
### 2. DataMappingService (5개 호출, 575 라인)
|
||||
|
||||
**주요 기능**:
|
||||
|
||||
- 데이터 매핑 설정 관리
|
||||
- 소스-타겟 필드 매핑
|
||||
- 데이터 변환 규칙
|
||||
- 매핑 실행
|
||||
|
||||
**예상 Prisma 호출**:
|
||||
|
||||
- `getDataMappings()` - 매핑 설정 목록 조회
|
||||
- `getDataMapping()` - 매핑 설정 단건 조회
|
||||
- `createDataMapping()` - 매핑 설정 생성
|
||||
|
|
@ -61,6 +121,7 @@
|
|||
- `deleteDataMapping()` - 매핑 설정 삭제
|
||||
|
||||
**기술적 고려사항**:
|
||||
|
||||
- JSON 필드 (field_mappings, transformation_rules)
|
||||
- 복잡한 변환 로직
|
||||
- 매핑 검증
|
||||
|
|
@ -71,18 +132,21 @@
|
|||
### 3. DataService (4개 호출, 327 라인)
|
||||
|
||||
**주요 기능**:
|
||||
|
||||
- 동적 데이터 조회
|
||||
- 데이터 필터링
|
||||
- 데이터 정렬
|
||||
- 데이터 집계
|
||||
|
||||
**예상 Prisma 호출**:
|
||||
|
||||
- `getDataByTable()` - 테이블별 데이터 조회
|
||||
- `getDataById()` - 데이터 단건 조회
|
||||
- `executeCustomQuery()` - 커스텀 쿼리 실행
|
||||
- `getDataStatistics()` - 데이터 통계 조회
|
||||
|
||||
**기술적 고려사항**:
|
||||
|
||||
- 동적 테이블 쿼리
|
||||
- SQL 인젝션 방지
|
||||
- 동적 WHERE 조건
|
||||
|
|
@ -93,17 +157,20 @@
|
|||
### 4. AdminService (3개 호출, 374 라인)
|
||||
|
||||
**주요 기능**:
|
||||
|
||||
- 관리자 메뉴 관리
|
||||
- 시스템 설정
|
||||
- 사용자 관리
|
||||
- 로그 조회
|
||||
|
||||
**예상 Prisma 호출**:
|
||||
|
||||
- `getAdminMenus()` - 관리자 메뉴 조회
|
||||
- `getSystemSettings()` - 시스템 설정 조회
|
||||
- `updateSystemSettings()` - 시스템 설정 업데이트
|
||||
|
||||
**기술적 고려사항**:
|
||||
|
||||
- 메뉴 계층 구조
|
||||
- 권한 기반 필터링
|
||||
- JSON 설정 필드
|
||||
|
|
@ -114,17 +181,23 @@
|
|||
## 💡 통합 전환 전략
|
||||
|
||||
### Phase 1: 단순 CRUD 전환 (12개)
|
||||
|
||||
**EnhancedDynamicFormService (6개) + DataMappingService (5개) + AdminService (1개)**
|
||||
|
||||
- 기본 CRUD 기능
|
||||
- JSON 필드 처리
|
||||
|
||||
### Phase 2: 동적 쿼리 전환 (4개)
|
||||
|
||||
**DataService (4개)**
|
||||
|
||||
- 동적 테이블 쿼리
|
||||
- 보안 검증
|
||||
|
||||
### Phase 3: 고급 기능 전환 (2개)
|
||||
|
||||
**AdminService (2개)**
|
||||
|
||||
- 시스템 설정
|
||||
- 캐싱
|
||||
|
||||
|
|
@ -135,6 +208,7 @@
|
|||
### 예시 1: 고급 폼 생성 (JSON 필드)
|
||||
|
||||
**변경 전**:
|
||||
|
||||
```typescript
|
||||
const form = await prisma.enhanced_forms.create({
|
||||
data: {
|
||||
|
|
@ -149,6 +223,7 @@ const form = await prisma.enhanced_forms.create({
|
|||
```
|
||||
|
||||
**변경 후**:
|
||||
|
||||
```typescript
|
||||
const form = await queryOne<any>(
|
||||
`INSERT INTO enhanced_forms
|
||||
|
|
@ -170,6 +245,7 @@ const form = await queryOne<any>(
|
|||
### 예시 2: 데이터 매핑 조회
|
||||
|
||||
**변경 전**:
|
||||
|
||||
```typescript
|
||||
const mappings = await prisma.data_mappings.findMany({
|
||||
where: {
|
||||
|
|
@ -185,6 +261,7 @@ const mappings = await prisma.data_mappings.findMany({
|
|||
```
|
||||
|
||||
**변경 후**:
|
||||
|
||||
```typescript
|
||||
const mappings = await query<any>(
|
||||
`SELECT
|
||||
|
|
@ -211,6 +288,7 @@ const mappings = await query<any>(
|
|||
### 예시 3: 동적 테이블 쿼리 (DataService)
|
||||
|
||||
**변경 전**:
|
||||
|
||||
```typescript
|
||||
// Prisma로는 동적 테이블 쿼리 불가능
|
||||
// 이미 $queryRawUnsafe 사용 중일 가능성
|
||||
|
|
@ -221,6 +299,7 @@ const data = await prisma.$queryRawUnsafe(
|
|||
```
|
||||
|
||||
**변경 후**:
|
||||
|
||||
```typescript
|
||||
// SQL 인젝션 방지를 위한 테이블명 검증
|
||||
const validTableName = validateTableName(tableName);
|
||||
|
|
@ -234,6 +313,7 @@ const data = await query<any>(
|
|||
### 예시 4: 관리자 메뉴 조회 (계층 구조)
|
||||
|
||||
**변경 전**:
|
||||
|
||||
```typescript
|
||||
const menus = await prisma.admin_menus.findMany({
|
||||
where: { is_active: true },
|
||||
|
|
@ -247,6 +327,7 @@ const menus = await prisma.admin_menus.findMany({
|
|||
```
|
||||
|
||||
**변경 후**:
|
||||
|
||||
```typescript
|
||||
// 재귀 CTE를 사용한 계층 쿼리
|
||||
const menus = await query<any>(
|
||||
|
|
@ -273,6 +354,7 @@ const menus = await query<any>(
|
|||
## 🔧 기술적 고려사항
|
||||
|
||||
### 1. JSON 필드 처리
|
||||
|
||||
```typescript
|
||||
// 복잡한 JSON 구조
|
||||
interface ValidationRules {
|
||||
|
|
@ -287,12 +369,14 @@ interface ValidationRules {
|
|||
JSON.stringify(validationRules);
|
||||
|
||||
// 조회 후
|
||||
const parsed = typeof row.validation_rules === "string"
|
||||
? JSON.parse(row.validation_rules)
|
||||
: row.validation_rules;
|
||||
const parsed =
|
||||
typeof row.validation_rules === "string"
|
||||
? JSON.parse(row.validation_rules)
|
||||
: row.validation_rules;
|
||||
```
|
||||
|
||||
### 2. 동적 테이블 쿼리 보안
|
||||
|
||||
```typescript
|
||||
// 테이블명 화이트리스트
|
||||
const ALLOWED_TABLES = ["users", "products", "orders"];
|
||||
|
|
@ -314,13 +398,14 @@ function validateColumnName(columnName: string): string {
|
|||
```
|
||||
|
||||
### 3. 재귀 CTE (계층 구조)
|
||||
|
||||
```sql
|
||||
WITH RECURSIVE hierarchy AS (
|
||||
-- 최상위 노드
|
||||
SELECT * FROM table WHERE parent_id IS NULL
|
||||
|
||||
|
||||
UNION ALL
|
||||
|
||||
|
||||
-- 하위 노드
|
||||
SELECT t.* FROM table t
|
||||
JOIN hierarchy h ON t.parent_id = h.id
|
||||
|
|
@ -329,8 +414,9 @@ SELECT * FROM hierarchy
|
|||
```
|
||||
|
||||
### 4. JSON 집계 (관계 데이터)
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
SELECT
|
||||
parent.*,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
|
|
@ -348,6 +434,7 @@ GROUP BY parent.id
|
|||
## 📝 전환 체크리스트
|
||||
|
||||
### EnhancedDynamicFormService (6개)
|
||||
|
||||
- [ ] `getEnhancedForms()` - 목록 조회
|
||||
- [ ] `getEnhancedForm()` - 단건 조회
|
||||
- [ ] `createEnhancedForm()` - 생성 (JSON 필드)
|
||||
|
|
@ -356,6 +443,7 @@ GROUP BY parent.id
|
|||
- [ ] `getFormValidationRules()` - 검증 규칙 조회
|
||||
|
||||
### DataMappingService (5개)
|
||||
|
||||
- [ ] `getDataMappings()` - 목록 조회
|
||||
- [ ] `getDataMapping()` - 단건 조회
|
||||
- [ ] `createDataMapping()` - 생성
|
||||
|
|
@ -363,17 +451,20 @@ GROUP BY parent.id
|
|||
- [ ] `deleteDataMapping()` - 삭제
|
||||
|
||||
### DataService (4개)
|
||||
|
||||
- [ ] `getDataByTable()` - 동적 테이블 조회
|
||||
- [ ] `getDataById()` - 단건 조회
|
||||
- [ ] `executeCustomQuery()` - 커스텀 쿼리
|
||||
- [ ] `getDataStatistics()` - 통계 조회
|
||||
|
||||
### AdminService (3개)
|
||||
|
||||
- [ ] `getAdminMenus()` - 메뉴 조회 (재귀 CTE)
|
||||
- [ ] `getSystemSettings()` - 시스템 설정 조회
|
||||
- [ ] `updateSystemSettings()` - 시스템 설정 업데이트
|
||||
|
||||
### 공통 작업
|
||||
|
||||
- [ ] import 문 수정 (모든 서비스)
|
||||
- [ ] Prisma import 완전 제거
|
||||
- [ ] JSON 필드 처리 확인
|
||||
|
|
@ -384,15 +475,18 @@ GROUP BY parent.id
|
|||
## 🧪 테스트 계획
|
||||
|
||||
### 단위 테스트 (18개)
|
||||
|
||||
- 각 Prisma 호출별 1개씩
|
||||
|
||||
### 통합 테스트 (6개)
|
||||
|
||||
- EnhancedDynamicFormService: 폼 생성 및 검증 테스트 (2개)
|
||||
- DataMappingService: 매핑 설정 및 실행 테스트 (2개)
|
||||
- DataService: 동적 쿼리 및 보안 테스트 (1개)
|
||||
- AdminService: 메뉴 계층 구조 테스트 (1개)
|
||||
|
||||
### 보안 테스트
|
||||
|
||||
- SQL 인젝션 방지 테스트
|
||||
- 테이블명 검증 테스트
|
||||
- 컬럼명 검증 테스트
|
||||
|
|
@ -406,7 +500,6 @@ GROUP BY parent.id
|
|||
- 동적 쿼리 보안
|
||||
- 재귀 CTE
|
||||
- JSON 집계
|
||||
|
||||
- **예상 소요 시간**: 2.5~3시간
|
||||
- Phase 1 (기본 CRUD): 1시간
|
||||
- Phase 2 (동적 쿼리): 1시간
|
||||
|
|
@ -418,6 +511,7 @@ GROUP BY parent.id
|
|||
## ⚠️ 주의사항
|
||||
|
||||
### 보안 필수 체크리스트
|
||||
|
||||
1. ✅ 동적 테이블명은 반드시 화이트리스트 검증
|
||||
2. ✅ 동적 컬럼명은 정규식으로 검증
|
||||
3. ✅ WHERE 절 파라미터는 반드시 바인딩
|
||||
|
|
@ -425,6 +519,7 @@ GROUP BY parent.id
|
|||
5. ✅ 재귀 쿼리는 깊이 제한 설정
|
||||
|
||||
### 성능 최적화
|
||||
|
||||
- JSON 필드 인덱싱 (GIN 인덱스)
|
||||
- 재귀 쿼리 깊이 제한
|
||||
- 집계 쿼리 최적화
|
||||
|
|
@ -435,4 +530,3 @@ GROUP BY parent.id
|
|||
**상태**: ⏳ **대기 중**
|
||||
**특이사항**: JSON 필드, 동적 쿼리, 재귀 CTE, 보안 검증 포함
|
||||
**⚠️ 주의**: 동적 쿼리는 SQL 인젝션 방지가 매우 중요!
|
||||
|
||||
|
|
|
|||
|
|
@ -144,11 +144,11 @@ backend-node/ (루트)
|
|||
- `batchExecutionLogService.ts` (7개) - 배치 실행 로그
|
||||
- `batchManagementService.ts` (5개) - 배치 관리
|
||||
- `batchSchedulerService.ts` (4개) - 배치 스케줄러
|
||||
- **데이터 관리 서비스 (18개)** - [통합 계획서](PHASE3.16_DATA_MANAGEMENT_SERVICES_MIGRATION.md)
|
||||
- `enhancedDynamicFormService.ts` (6개) - 확장 동적 폼
|
||||
- `dataMappingService.ts` (5개) - 데이터 매핑
|
||||
- `dataService.ts` (4개) - 데이터 서비스
|
||||
- `adminService.ts` (3개) - 관리자 메뉴
|
||||
- **데이터 관리 서비스 (0개)** - ✅ **전환 완료** - [통합 계획서](PHASE3.16_DATA_MANAGEMENT_SERVICES_MIGRATION.md)
|
||||
- `enhancedDynamicFormService.ts` (0개) - ✅ **전환 완료**
|
||||
- `dataMappingService.ts` (0개) - ✅ **전환 완료**
|
||||
- `dataService.ts` (0개) - ✅ **전환 완료**
|
||||
- `adminService.ts` (0개) - ✅ **전환 완료**
|
||||
- `ddlExecutionService.ts` (0개) - ✅ **전환 완료** (이미 완료됨) - [계획서](PHASE3.18_DDL_EXECUTION_SERVICE_MIGRATION.md)
|
||||
- `referenceCacheService.ts` (0개) - ✅ **전환 완료** (이미 완료됨) - [계획서](PHASE3.17_REFERENCE_CACHE_SERVICE_MIGRATION.md)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { logger } from "../utils/logger";
|
||||
|
||||
// 🔧 Prisma 클라이언트 중복 생성 방지 - 기존 인스턴스 재사용
|
||||
import prisma = require("../config/database");
|
||||
import { query, queryOne } from "../database/db";
|
||||
|
||||
export class AdminService {
|
||||
/**
|
||||
|
|
@ -13,9 +11,9 @@ export class AdminService {
|
|||
|
||||
const { userLang = "ko" } = paramMap;
|
||||
|
||||
// 기존 Java의 selectAdminMenuList 쿼리를 Prisma로 포팅
|
||||
// WITH RECURSIVE 쿼리를 Prisma의 $queryRaw로 구현
|
||||
const menuList = await prisma.$queryRaw<any[]>`
|
||||
// 기존 Java의 selectAdminMenuList 쿼리를 Raw Query로 포팅
|
||||
// WITH RECURSIVE 쿼리 구현
|
||||
const menuList = await query<any>(`
|
||||
WITH RECURSIVE v_menu(
|
||||
LEVEL,
|
||||
MENU_TYPE,
|
||||
|
|
@ -62,14 +60,14 @@ export class AdminService {
|
|||
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
|
||||
WHERE MLKM.lang_key = MENU.LANG_KEY
|
||||
AND (MLKM.company_code = MENU.COMPANY_CODE OR (MENU.COMPANY_CODE IS NULL AND MLKM.company_code = '*'))
|
||||
AND MLT.lang_code = ${userLang}
|
||||
AND MLT.lang_code = $1
|
||||
LIMIT 1),
|
||||
(SELECT MLT.lang_text
|
||||
FROM MULTI_LANG_KEY_MASTER MLKM
|
||||
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
|
||||
WHERE MLKM.lang_key = MENU.LANG_KEY
|
||||
AND MLKM.company_code = '*'
|
||||
AND MLT.lang_code = ${userLang}
|
||||
AND MLT.lang_code = $1
|
||||
LIMIT 1),
|
||||
MENU.MENU_NAME_KOR
|
||||
),
|
||||
|
|
@ -80,14 +78,14 @@ export class AdminService {
|
|||
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
|
||||
WHERE MLKM.lang_key = MENU.LANG_KEY_DESC
|
||||
AND (MLKM.company_code = MENU.COMPANY_CODE OR (MENU.COMPANY_CODE IS NULL AND MLKM.company_code = '*'))
|
||||
AND MLT.lang_code = ${userLang}
|
||||
AND MLT.lang_code = $1
|
||||
LIMIT 1),
|
||||
(SELECT MLT.lang_text
|
||||
FROM MULTI_LANG_KEY_MASTER MLKM
|
||||
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
|
||||
WHERE MLKM.lang_key = MENU.LANG_KEY_DESC
|
||||
AND MLKM.company_code = '*'
|
||||
AND MLT.lang_code = ${userLang}
|
||||
AND MLT.lang_code = $1
|
||||
LIMIT 1),
|
||||
MENU.MENU_DESC
|
||||
)
|
||||
|
|
@ -125,14 +123,14 @@ export class AdminService {
|
|||
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
|
||||
WHERE MLKM.lang_key = MENU_SUB.LANG_KEY
|
||||
AND (MLKM.company_code = MENU_SUB.COMPANY_CODE OR (MENU_SUB.COMPANY_CODE IS NULL AND MLKM.company_code = '*'))
|
||||
AND MLT.lang_code = ${userLang}
|
||||
AND MLT.lang_code = $1
|
||||
LIMIT 1),
|
||||
(SELECT MLT.lang_text
|
||||
FROM MULTI_LANG_KEY_MASTER MLKM
|
||||
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
|
||||
WHERE MLKM.lang_key = MENU_SUB.LANG_KEY
|
||||
AND MLKM.company_code = '*'
|
||||
AND MLT.lang_code = ${userLang}
|
||||
AND MLT.lang_code = $1
|
||||
LIMIT 1),
|
||||
MENU_SUB.MENU_NAME_KOR
|
||||
),
|
||||
|
|
@ -143,14 +141,14 @@ export class AdminService {
|
|||
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
|
||||
WHERE MLKM.lang_key = MENU_SUB.LANG_KEY_DESC
|
||||
AND (MLKM.company_code = MENU_SUB.COMPANY_CODE OR (MENU_SUB.COMPANY_CODE IS NULL AND MLKM.company_code = '*'))
|
||||
AND MLT.lang_code = ${userLang}
|
||||
AND MLT.lang_code = $1
|
||||
LIMIT 1),
|
||||
(SELECT MLT.lang_text
|
||||
FROM MULTI_LANG_KEY_MASTER MLKM
|
||||
JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id
|
||||
WHERE MLKM.lang_key = MENU_SUB.LANG_KEY_DESC
|
||||
AND MLKM.company_code = '*'
|
||||
AND MLT.lang_code = ${userLang}
|
||||
AND MLT.lang_code = $1
|
||||
LIMIT 1),
|
||||
MENU_SUB.MENU_DESC
|
||||
)
|
||||
|
|
@ -190,7 +188,7 @@ export class AdminService {
|
|||
LEFT JOIN COMPANY_MNG CM ON A.COMPANY_CODE = CM.COMPANY_CODE
|
||||
WHERE 1 = 1
|
||||
ORDER BY PATH, SEQ
|
||||
`;
|
||||
`, [userLang]);
|
||||
|
||||
logger.info(`관리자 메뉴 목록 조회 결과: ${menuList.length}개`);
|
||||
if (menuList.length > 0) {
|
||||
|
|
@ -213,8 +211,8 @@ export class AdminService {
|
|||
|
||||
const { userLang = "ko" } = paramMap;
|
||||
|
||||
// 기존 Java의 selectUserMenuList 쿼리를 Prisma로 포팅
|
||||
const menuList = await prisma.$queryRaw<any[]>`
|
||||
// 기존 Java의 selectUserMenuList 쿼리를 Raw Query로 포팅
|
||||
const menuList = await query<any>(`
|
||||
WITH RECURSIVE v_menu(
|
||||
LEVEL,
|
||||
MENU_TYPE,
|
||||
|
|
@ -310,12 +308,12 @@ export class AdminService {
|
|||
FROM v_menu A
|
||||
LEFT JOIN COMPANY_MNG CM ON A.COMPANY_CODE = CM.COMPANY_CODE
|
||||
LEFT JOIN MULTI_LANG_KEY_MASTER MLKM_NAME ON A.LANG_KEY = MLKM_NAME.lang_key
|
||||
LEFT JOIN MULTI_LANG_TEXT MLT_NAME ON MLKM_NAME.key_id = MLT_NAME.key_id AND MLT_NAME.lang_code = ${userLang}
|
||||
LEFT JOIN MULTI_LANG_TEXT MLT_NAME ON MLKM_NAME.key_id = MLT_NAME.key_id AND MLT_NAME.lang_code = $1
|
||||
LEFT JOIN MULTI_LANG_KEY_MASTER MLKM_DESC ON A.LANG_KEY_DESC = MLKM_DESC.lang_key
|
||||
LEFT JOIN MULTI_LANG_TEXT MLT_DESC ON MLKM_DESC.key_id = MLT_DESC.key_id AND MLT_DESC.lang_code = ${userLang}
|
||||
LEFT JOIN MULTI_LANG_TEXT MLT_DESC ON MLKM_DESC.key_id = MLT_DESC.key_id AND MLT_DESC.lang_code = $1
|
||||
WHERE 1 = 1
|
||||
ORDER BY PATH, SEQ
|
||||
`;
|
||||
`, [userLang]);
|
||||
|
||||
logger.info(`사용자 메뉴 목록 조회 결과: ${menuList.length}개`);
|
||||
if (menuList.length > 0) {
|
||||
|
|
@ -336,32 +334,31 @@ export class AdminService {
|
|||
try {
|
||||
logger.info(`AdminService.getMenuInfo 시작 - menuId: ${menuId}`);
|
||||
|
||||
// Prisma ORM을 사용한 메뉴 정보 조회 (회사 정보 포함)
|
||||
const menuInfo = await prisma.menu_info.findUnique({
|
||||
where: {
|
||||
objid: Number(menuId),
|
||||
},
|
||||
include: {
|
||||
company: {
|
||||
select: {
|
||||
company_name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// Raw Query를 사용한 메뉴 정보 조회 (회사 정보 포함)
|
||||
const menuResult = await query<any>(
|
||||
`SELECT
|
||||
m.*,
|
||||
c.company_name
|
||||
FROM menu_info m
|
||||
LEFT JOIN company_mng c ON m.company_code = c.company_code
|
||||
WHERE m.objid = $1::numeric`,
|
||||
[menuId]
|
||||
);
|
||||
|
||||
if (!menuInfo) {
|
||||
if (!menuResult || menuResult.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const menuInfo = menuResult[0];
|
||||
|
||||
// 응답 형식 조정 (기존 형식과 호환성 유지)
|
||||
const result = {
|
||||
...menuInfo,
|
||||
objid: menuInfo.objid.toString(), // BigInt를 문자열로 변환
|
||||
objid: menuInfo.objid?.toString(),
|
||||
menu_type: menuInfo.menu_type?.toString(),
|
||||
parent_obj_id: menuInfo.parent_obj_id?.toString(),
|
||||
seq: menuInfo.seq?.toString(),
|
||||
company_name: menuInfo.company?.company_name || "미지정",
|
||||
company_name: menuInfo.company_name || "미지정",
|
||||
};
|
||||
|
||||
logger.info("메뉴 정보 조회 결과:", result);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
import { query } from "../database/db";
|
||||
import {
|
||||
DataMappingConfig,
|
||||
InboundMapping,
|
||||
|
|
@ -11,10 +11,8 @@ import {
|
|||
} from "../types/dataMappingTypes";
|
||||
|
||||
export class DataMappingService {
|
||||
private prisma: PrismaClient;
|
||||
|
||||
constructor() {
|
||||
this.prisma = new PrismaClient();
|
||||
// No prisma instance needed
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -404,10 +402,10 @@ export class DataMappingService {
|
|||
}
|
||||
|
||||
// Raw SQL을 사용한 동적 쿼리
|
||||
const query = `SELECT * FROM ${tableName}${mapping.sourceFilter ? ` WHERE ${mapping.sourceFilter}` : ""}`;
|
||||
console.log(`🔍 [DataMappingService] 쿼리 실행: ${query}`);
|
||||
const sql = `SELECT * FROM ${tableName}${mapping.sourceFilter ? ` WHERE ${mapping.sourceFilter}` : ""}`;
|
||||
console.log(`🔍 [DataMappingService] 쿼리 실행: ${sql}`);
|
||||
|
||||
const result = await this.prisma.$queryRawUnsafe(query);
|
||||
const result = await query<any>(sql, []);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
|
|
@ -429,14 +427,14 @@ export class DataMappingService {
|
|||
const values = Object.values(data);
|
||||
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
||||
|
||||
const query = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
|
||||
const sql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
|
||||
|
||||
console.log(`📝 [DataMappingService] INSERT 실행:`, {
|
||||
table: tableName,
|
||||
columns,
|
||||
query,
|
||||
query: sql,
|
||||
});
|
||||
await this.prisma.$executeRawUnsafe(query, ...values);
|
||||
await query(sql, values);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -460,7 +458,7 @@ export class DataMappingService {
|
|||
.map((col) => `${col} = EXCLUDED.${col}`)
|
||||
.join(", ");
|
||||
|
||||
const query = `
|
||||
const sql = `
|
||||
INSERT INTO ${tableName} (${columns.join(", ")})
|
||||
VALUES (${placeholders})
|
||||
ON CONFLICT (${keyFields.join(", ")})
|
||||
|
|
@ -470,9 +468,9 @@ export class DataMappingService {
|
|||
console.log(`🔄 [DataMappingService] UPSERT 실행:`, {
|
||||
table: tableName,
|
||||
keyFields,
|
||||
query,
|
||||
query: sql,
|
||||
});
|
||||
await this.prisma.$executeRawUnsafe(query, ...values);
|
||||
await query(sql, values);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -503,14 +501,14 @@ export class DataMappingService {
|
|||
...keyFields.map((field) => data[field]),
|
||||
];
|
||||
|
||||
const query = `UPDATE ${tableName} SET ${updateClauses} WHERE ${whereConditions}`;
|
||||
const sql = `UPDATE ${tableName} SET ${updateClauses} WHERE ${whereConditions}`;
|
||||
|
||||
console.log(`✏️ [DataMappingService] UPDATE 실행:`, {
|
||||
table: tableName,
|
||||
keyFields,
|
||||
query,
|
||||
query: sql,
|
||||
});
|
||||
await this.prisma.$executeRawUnsafe(query, ...values);
|
||||
await query(sql, values);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -570,6 +568,6 @@ export class DataMappingService {
|
|||
* 리소스 정리
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
await this.prisma.$disconnect();
|
||||
// No disconnect needed for raw queries
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
import prisma from "../config/database";
|
||||
import { query, queryOne } from "../database/db";
|
||||
|
||||
interface GetTableDataParams {
|
||||
tableName: string;
|
||||
|
|
@ -111,7 +110,7 @@ class DataService {
|
|||
}
|
||||
|
||||
// 동적 SQL 쿼리 생성
|
||||
let query = `SELECT * FROM "${tableName}"`;
|
||||
let sql = `SELECT * FROM "${tableName}"`;
|
||||
const queryParams: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
|
|
@ -150,7 +149,7 @@ class DataService {
|
|||
|
||||
// WHERE 절 추가
|
||||
if (whereConditions.length > 0) {
|
||||
query += ` WHERE ${whereConditions.join(" AND ")}`;
|
||||
sql += ` WHERE ${whereConditions.join(" AND ")}`;
|
||||
}
|
||||
|
||||
// ORDER BY 절 추가
|
||||
|
|
@ -162,7 +161,7 @@ class DataService {
|
|||
|
||||
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(columnName)) {
|
||||
const validDirection = direction === "DESC" ? "DESC" : "ASC";
|
||||
query += ` ORDER BY "${columnName}" ${validDirection}`;
|
||||
sql += ` ORDER BY "${columnName}" ${validDirection}`;
|
||||
}
|
||||
} else {
|
||||
// 기본 정렬: 최신순 (가능한 컬럼 시도)
|
||||
|
|
@ -179,23 +178,23 @@ class DataService {
|
|||
);
|
||||
|
||||
if (availableDateColumn) {
|
||||
query += ` ORDER BY "${availableDateColumn}" DESC`;
|
||||
sql += ` ORDER BY "${availableDateColumn}" DESC`;
|
||||
}
|
||||
}
|
||||
|
||||
// LIMIT과 OFFSET 추가
|
||||
query += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
|
||||
sql += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
|
||||
queryParams.push(limit, offset);
|
||||
|
||||
console.log("🔍 실행할 쿼리:", query);
|
||||
console.log("🔍 실행할 쿼리:", sql);
|
||||
console.log("📊 쿼리 파라미터:", queryParams);
|
||||
|
||||
// 쿼리 실행
|
||||
const result = await prisma.$queryRawUnsafe(query, ...queryParams);
|
||||
const result = await query<any>(sql, queryParams);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result as any[],
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`데이터 조회 오류 (${tableName}):`, error);
|
||||
|
|
@ -259,18 +258,16 @@ class DataService {
|
|||
*/
|
||||
private async checkTableExists(tableName: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
`
|
||||
SELECT EXISTS (
|
||||
const result = await query<{ exists: boolean }>(
|
||||
`SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = $1
|
||||
);
|
||||
`,
|
||||
tableName
|
||||
)`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
return (result as any)[0]?.exists || false;
|
||||
return result[0]?.exists || false;
|
||||
} catch (error) {
|
||||
console.error("테이블 존재 확인 오류:", error);
|
||||
return false;
|
||||
|
|
@ -281,18 +278,16 @@ class DataService {
|
|||
* 테이블 컬럼 정보 조회 (간단 버전)
|
||||
*/
|
||||
private async getTableColumnsSimple(tableName: string): Promise<any[]> {
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
`
|
||||
SELECT column_name, data_type, is_nullable, column_default
|
||||
const result = await query<any>(
|
||||
`SELECT column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = $1
|
||||
AND table_schema = 'public'
|
||||
ORDER BY ordinal_position;
|
||||
`,
|
||||
tableName
|
||||
ORDER BY ordinal_position`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
return result as any[];
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -304,19 +299,15 @@ class DataService {
|
|||
): Promise<string | null> {
|
||||
try {
|
||||
// column_labels 테이블에서 라벨 조회
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
`
|
||||
SELECT label_ko
|
||||
const result = await query<{ label_ko: string }>(
|
||||
`SELECT label_ko
|
||||
FROM column_labels
|
||||
WHERE table_name = $1 AND column_name = $2
|
||||
LIMIT 1;
|
||||
`,
|
||||
tableName,
|
||||
columnName
|
||||
LIMIT 1`,
|
||||
[tableName, columnName]
|
||||
);
|
||||
|
||||
const labelResult = result as any[];
|
||||
return labelResult[0]?.label_ko || null;
|
||||
return result[0]?.label_ko || null;
|
||||
} catch (error) {
|
||||
// column_labels 테이블이 없거나 오류가 발생하면 null 반환
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* 타입 안전성과 검증 강화
|
||||
*/
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { query, queryOne } from "../database/db";
|
||||
import {
|
||||
WebType,
|
||||
DynamicWebType,
|
||||
|
|
@ -14,8 +14,6 @@ import {
|
|||
} from "../types/unified-web-types";
|
||||
import { DataflowControlService } from "./dataflowControlService";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 테이블 컬럼 정보
|
||||
export interface TableColumn {
|
||||
column_name: string;
|
||||
|
|
@ -156,17 +154,15 @@ export class EnhancedDynamicFormService {
|
|||
*/
|
||||
private async validateTableExists(tableName: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
`
|
||||
SELECT EXISTS (
|
||||
const result = await query<{ exists: boolean }>(
|
||||
`SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = $1
|
||||
) as exists
|
||||
`,
|
||||
tableName
|
||||
) as exists`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
return (result as any)[0]?.exists || false;
|
||||
return result[0]?.exists || false;
|
||||
} catch (error) {
|
||||
console.error(`❌ 테이블 존재 여부 확인 실패: ${tableName}`, error);
|
||||
return false;
|
||||
|
|
@ -184,9 +180,8 @@ export class EnhancedDynamicFormService {
|
|||
}
|
||||
|
||||
try {
|
||||
const columns = (await prisma.$queryRawUnsafe(
|
||||
`
|
||||
SELECT
|
||||
const columns = await query<TableColumn>(
|
||||
`SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
|
|
@ -196,10 +191,9 @@ export class EnhancedDynamicFormService {
|
|||
numeric_scale
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = $1
|
||||
ORDER BY ordinal_position
|
||||
`,
|
||||
tableName
|
||||
)) as TableColumn[];
|
||||
ORDER BY ordinal_position`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
// 캐시 저장 (10분)
|
||||
this.columnCache.set(tableName, columns);
|
||||
|
|
@ -226,18 +220,21 @@ export class EnhancedDynamicFormService {
|
|||
|
||||
try {
|
||||
// table_type_columns에서 웹타입 정보 조회
|
||||
const webTypeData = (await prisma.$queryRawUnsafe(
|
||||
`
|
||||
SELECT
|
||||
const webTypeData = await query<{
|
||||
column_name: string;
|
||||
web_type: string;
|
||||
is_nullable: string;
|
||||
detail_settings: any;
|
||||
}>(
|
||||
`SELECT
|
||||
column_name,
|
||||
web_type,
|
||||
is_nullable,
|
||||
detail_settings
|
||||
FROM table_type_columns
|
||||
WHERE table_name = $1
|
||||
`,
|
||||
tableName
|
||||
)) as any[];
|
||||
WHERE table_name = $1`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
const columnWebTypes: ColumnWebTypeInfo[] = webTypeData.map((row) => ({
|
||||
columnName: row.column_name,
|
||||
|
|
@ -555,15 +552,13 @@ export class EnhancedDynamicFormService {
|
|||
*/
|
||||
private async getPrimaryKeys(tableName: string): Promise<string[]> {
|
||||
try {
|
||||
const result = (await prisma.$queryRawUnsafe(
|
||||
`
|
||||
SELECT column_name
|
||||
const result = await query<{ column_name: string }>(
|
||||
`SELECT column_name
|
||||
FROM information_schema.key_column_usage
|
||||
WHERE table_name = $1
|
||||
AND constraint_name LIKE '%_pkey'
|
||||
`,
|
||||
tableName
|
||||
)) as any[];
|
||||
AND constraint_name LIKE '%_pkey'`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
return result.map((row) => row.column_name);
|
||||
} catch (error) {
|
||||
|
|
@ -594,10 +589,7 @@ export class EnhancedDynamicFormService {
|
|||
query: insertQuery.replace(/\n\s+/g, " "),
|
||||
});
|
||||
|
||||
const result = (await prisma.$queryRawUnsafe(
|
||||
insertQuery,
|
||||
...values
|
||||
)) as any[];
|
||||
const result = await query<any>(insertQuery, values);
|
||||
|
||||
return {
|
||||
data: result[0],
|
||||
|
|
@ -649,10 +641,7 @@ export class EnhancedDynamicFormService {
|
|||
query: updateQuery.replace(/\n\s+/g, " "),
|
||||
});
|
||||
|
||||
const result = (await prisma.$queryRawUnsafe(
|
||||
updateQuery,
|
||||
...updateValues
|
||||
)) as any[];
|
||||
const result = await query<any>(updateQuery, updateValues);
|
||||
|
||||
return {
|
||||
data: result[0],
|
||||
|
|
|
|||
Loading…
Reference in New Issue