docs: Phase 4 남은 Prisma 호출 전환 계획서 작성
현재 상황 분석 및 문서화: 컨트롤러 레이어: - ✅ adminController.ts (28개) 완료 - ✅ screenFileController.ts (2개) 완료 - 🔄 남은 파일 (12개 호출): * webTypeStandardController.ts (11개) * fileController.ts (1개) Routes & Services: - ddlRoutes.ts (2개) - companyManagementRoutes.ts (2개) - multiConnectionQueryService.ts (4개) Config: - database.ts (4개 - 제거 예정) 새로운 계획서: - PHASE4_REMAINING_PRISMA_CALLS.md (상세 전환 계획) - 파일별 Prisma 호출 상세 분석 - 전환 패턴 및 우선순위 정리 전체 진행률: 445/444 (100.2%) 남은 작업: 12개 (추가 조사 필요한 파일 제외)
This commit is contained in:
parent
381d19caee
commit
7919079362
|
|
@ -294,11 +294,9 @@ COUNT(CASE WHEN status = 'success' THEN 1 END) as successful
|
||||||
1. **`logDDLExecution()`** - DDL 실행 로그 INSERT
|
1. **`logDDLExecution()`** - DDL 실행 로그 INSERT
|
||||||
- Before: `prisma.$executeRaw`
|
- Before: `prisma.$executeRaw`
|
||||||
- After: `query()` with 7 parameters
|
- After: `query()` with 7 parameters
|
||||||
|
|
||||||
2. **`getAuditLogs()`** - 감사 로그 목록 조회
|
2. **`getAuditLogs()`** - 감사 로그 목록 조회
|
||||||
- Before: `prisma.$queryRawUnsafe`
|
- Before: `prisma.$queryRawUnsafe`
|
||||||
- After: `query<any>()` with dynamic WHERE clause
|
- After: `query<any>()` with dynamic WHERE clause
|
||||||
|
|
||||||
3. **`getDDLStatistics()`** - 통계 조회 (4개 쿼리)
|
3. **`getDDLStatistics()`** - 통계 조회 (4개 쿼리)
|
||||||
- Before: 4x `prisma.$queryRawUnsafe`
|
- Before: 4x `prisma.$queryRawUnsafe`
|
||||||
- After: 4x `query<any>()`
|
- After: 4x `query<any>()`
|
||||||
|
|
@ -306,11 +304,9 @@ COUNT(CASE WHEN status = 'success' THEN 1 END) as successful
|
||||||
- ddlTypeStats: DDL 타입별 통계 (GROUP BY)
|
- ddlTypeStats: DDL 타입별 통계 (GROUP BY)
|
||||||
- userStats: 사용자별 통계 (GROUP BY, LIMIT 10)
|
- userStats: 사용자별 통계 (GROUP BY, LIMIT 10)
|
||||||
- recentFailures: 최근 실패 로그 (WHERE success = false)
|
- recentFailures: 최근 실패 로그 (WHERE success = false)
|
||||||
|
|
||||||
4. **`getTableDDLHistory()`** - 테이블별 DDL 히스토리
|
4. **`getTableDDLHistory()`** - 테이블별 DDL 히스토리
|
||||||
- Before: `prisma.$queryRawUnsafe`
|
- Before: `prisma.$queryRawUnsafe`
|
||||||
- After: `query<any>()` with table_name filter
|
- After: `query<any>()` with table_name filter
|
||||||
|
|
||||||
5. **`cleanupOldLogs()`** - 오래된 로그 삭제
|
5. **`cleanupOldLogs()`** - 오래된 로그 삭제
|
||||||
- Before: `prisma.$executeRaw`
|
- Before: `prisma.$executeRaw`
|
||||||
- After: `query()` with date filter
|
- After: `query()` with date filter
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ ExternalCallConfigService는 **8개의 Prisma 호출**이 있으며, 외부 API
|
||||||
| --------------- | -------------------------------------------------------- |
|
| --------------- | -------------------------------------------------------- |
|
||||||
| 파일 위치 | `backend-node/src/services/externalCallConfigService.ts` |
|
| 파일 위치 | `backend-node/src/services/externalCallConfigService.ts` |
|
||||||
| 파일 크기 | 612 라인 |
|
| 파일 크기 | 612 라인 |
|
||||||
| Prisma 호출 | 0개 (전환 완료) |
|
| Prisma 호출 | 0개 (전환 완료) |
|
||||||
| **현재 진행률** | **8/8 (100%)** ✅ **전환 완료** |
|
| **현재 진행률** | **8/8 (100%)** ✅ **전환 완료** |
|
||||||
| 복잡도 | 중간 (JSON 필드, 복잡한 CRUD) |
|
| 복잡도 | 중간 (JSON 필드, 복잡한 CRUD) |
|
||||||
| 우선순위 | 🟡 중간 (Phase 3.12) |
|
| 우선순위 | 🟡 중간 (Phase 3.12) |
|
||||||
|
|
@ -286,7 +286,7 @@ try {
|
||||||
|
|
||||||
1. **`getConfigs()`** - 목록 조회 (findMany → query)
|
1. **`getConfigs()`** - 목록 조회 (findMany → query)
|
||||||
2. **`getConfigById()`** - 단건 조회 (findUnique → queryOne)
|
2. **`getConfigById()`** - 단건 조회 (findUnique → queryOne)
|
||||||
3. **`createConfig()`** - 중복 검사 (findFirst → queryOne)
|
3. **`createConfig()`** - 중복 검사 (findFirst → queryOne)
|
||||||
4. **`createConfig()`** - 생성 (create → queryOne with INSERT)
|
4. **`createConfig()`** - 생성 (create → queryOne with INSERT)
|
||||||
5. **`updateConfig()`** - 중복 검사 (findFirst → queryOne)
|
5. **`updateConfig()`** - 중복 검사 (findFirst → queryOne)
|
||||||
6. **`updateConfig()`** - 수정 (update → queryOne with 동적 UPDATE)
|
6. **`updateConfig()`** - 수정 (update → queryOne with 동적 UPDATE)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ EntityJoinService는 **5개의 Prisma 호출**이 있으며, 엔티티 간 조
|
||||||
| --------------- | ------------------------------------------------ |
|
| --------------- | ------------------------------------------------ |
|
||||||
| 파일 위치 | `backend-node/src/services/entityJoinService.ts` |
|
| 파일 위치 | `backend-node/src/services/entityJoinService.ts` |
|
||||||
| 파일 크기 | 575 라인 |
|
| 파일 크기 | 575 라인 |
|
||||||
| Prisma 호출 | 0개 (전환 완료) |
|
| Prisma 호출 | 0개 (전환 완료) |
|
||||||
| **현재 진행률** | **5/5 (100%)** ✅ **전환 완료** |
|
| **현재 진행률** | **5/5 (100%)** ✅ **전환 완료** |
|
||||||
| 복잡도 | 중간 (조인 쿼리, 관계 설정) |
|
| 복잡도 | 중간 (조인 쿼리, 관계 설정) |
|
||||||
| 우선순위 | 🟡 중간 (Phase 3.13) |
|
| 우선순위 | 🟡 중간 (Phase 3.13) |
|
||||||
|
|
@ -257,19 +257,23 @@ async function checkCircularReference(
|
||||||
### 전환된 Prisma 호출 (5개)
|
### 전환된 Prisma 호출 (5개)
|
||||||
|
|
||||||
1. **`detectEntityJoins()`** - 엔티티 컬럼 감지 (findMany → query)
|
1. **`detectEntityJoins()`** - 엔티티 컬럼 감지 (findMany → query)
|
||||||
|
|
||||||
- column_labels 조회
|
- column_labels 조회
|
||||||
- web_type = 'entity' 필터
|
- web_type = 'entity' 필터
|
||||||
- reference_table/reference_column IS NOT NULL
|
- reference_table/reference_column IS NOT NULL
|
||||||
|
|
||||||
2. **`validateJoinConfig()`** - 테이블 존재 확인 ($queryRaw → query)
|
2. **`validateJoinConfig()`** - 테이블 존재 확인 ($queryRaw → query)
|
||||||
|
|
||||||
- information_schema.tables 조회
|
- information_schema.tables 조회
|
||||||
- 참조 테이블 검증
|
- 참조 테이블 검증
|
||||||
|
|
||||||
3. **`validateJoinConfig()`** - 컬럼 존재 확인 ($queryRaw → query)
|
3. **`validateJoinConfig()`** - 컬럼 존재 확인 ($queryRaw → query)
|
||||||
|
|
||||||
- information_schema.columns 조회
|
- information_schema.columns 조회
|
||||||
- 표시 컬럼 검증
|
- 표시 컬럼 검증
|
||||||
|
|
||||||
4. **`getReferenceTableColumns()`** - 컬럼 정보 조회 ($queryRaw → query)
|
4. **`getReferenceTableColumns()`** - 컬럼 정보 조회 ($queryRaw → query)
|
||||||
|
|
||||||
- information_schema.columns 조회
|
- information_schema.columns 조회
|
||||||
- 문자열 타입 컬럼만 필터
|
- 문자열 타입 컬럼만 필터
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -350,19 +350,23 @@ AuthService는 Phase 1.5에서 이미 Raw Query로 전환이 완료되었습니
|
||||||
### 전환된 Prisma 호출 (5개)
|
### 전환된 Prisma 호출 (5개)
|
||||||
|
|
||||||
1. **`loginPwdCheck()`** - 로그인 비밀번호 검증
|
1. **`loginPwdCheck()`** - 로그인 비밀번호 검증
|
||||||
|
|
||||||
- user_info 테이블에서 비밀번호 조회
|
- user_info 테이블에서 비밀번호 조회
|
||||||
- EncryptUtil을 활용한 비밀번호 검증
|
- EncryptUtil을 활용한 비밀번호 검증
|
||||||
- 마스터 패스워드 지원
|
- 마스터 패스워드 지원
|
||||||
|
|
||||||
2. **`insertLoginAccessLog()`** - 로그인 로그 기록
|
2. **`insertLoginAccessLog()`** - 로그인 로그 기록
|
||||||
|
|
||||||
- login_access_log 테이블에 INSERT
|
- login_access_log 테이블에 INSERT
|
||||||
- 로그인 시간, IP 주소 등 기록
|
- 로그인 시간, IP 주소 등 기록
|
||||||
|
|
||||||
3. **`getUserInfo()`** - 사용자 정보 조회
|
3. **`getUserInfo()`** - 사용자 정보 조회
|
||||||
|
|
||||||
- user_info 테이블 조회
|
- user_info 테이블 조회
|
||||||
- PersonBean 객체로 반환
|
- PersonBean 객체로 반환
|
||||||
|
|
||||||
4. **`updateLastLoginDate()`** - 마지막 로그인 시간 업데이트
|
4. **`updateLastLoginDate()`** - 마지막 로그인 시간 업데이트
|
||||||
|
|
||||||
- user_info 테이블 UPDATE
|
- user_info 테이블 UPDATE
|
||||||
- last_login_date 갱신
|
- last_login_date 갱신
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
| 대상 서비스 | 4개 (BatchExternalDb, ExecutionLog, Management, Scheduler) |
|
| 대상 서비스 | 4개 (BatchExternalDb, ExecutionLog, Management, Scheduler) |
|
||||||
| 파일 위치 | `backend-node/src/services/batch*.ts` |
|
| 파일 위치 | `backend-node/src/services/batch*.ts` |
|
||||||
| 총 파일 크기 | 2,161 라인 |
|
| 총 파일 크기 | 2,161 라인 |
|
||||||
| Prisma 호출 | 0개 (전환 완료) |
|
| Prisma 호출 | 0개 (전환 완료) |
|
||||||
| **현재 진행률** | **24/24 (100%)** ✅ **전환 완료** |
|
| **현재 진행률** | **24/24 (100%)** ✅ **전환 완료** |
|
||||||
| 복잡도 | 높음 (외부 DB 연동, 스케줄링, 트랜잭션) |
|
| 복잡도 | 높음 (외부 DB 연동, 스케줄링, 트랜잭션) |
|
||||||
| 우선순위 | 🔴 높음 (Phase 3.15) |
|
| 우선순위 | 🔴 높음 (Phase 3.15) |
|
||||||
|
|
@ -24,12 +24,14 @@
|
||||||
### 전환된 Prisma 호출 (24개)
|
### 전환된 Prisma 호출 (24개)
|
||||||
|
|
||||||
#### 1. BatchExternalDbService (8개)
|
#### 1. BatchExternalDbService (8개)
|
||||||
|
|
||||||
- `getAvailableConnections()` - findMany → query
|
- `getAvailableConnections()` - findMany → query
|
||||||
- `getTables()` - $queryRaw → query (information_schema)
|
- `getTables()` - $queryRaw → query (information_schema)
|
||||||
- `getTableColumns()` - $queryRaw → query (information_schema)
|
- `getTableColumns()` - $queryRaw → query (information_schema)
|
||||||
- `getExternalTables()` - findUnique → queryOne (x5)
|
- `getExternalTables()` - findUnique → queryOne (x5)
|
||||||
|
|
||||||
#### 2. BatchExecutionLogService (7개)
|
#### 2. BatchExecutionLogService (7개)
|
||||||
|
|
||||||
- `getExecutionLogs()` - findMany + count → query (JOIN + 동적 WHERE)
|
- `getExecutionLogs()` - findMany + count → query (JOIN + 동적 WHERE)
|
||||||
- `createExecutionLog()` - create → queryOne (INSERT RETURNING)
|
- `createExecutionLog()` - create → queryOne (INSERT RETURNING)
|
||||||
- `updateExecutionLog()` - update → queryOne (동적 UPDATE)
|
- `updateExecutionLog()` - update → queryOne (동적 UPDATE)
|
||||||
|
|
@ -38,12 +40,14 @@
|
||||||
- `getExecutionStats()` - findMany → query (동적 WHERE)
|
- `getExecutionStats()` - findMany → query (동적 WHERE)
|
||||||
|
|
||||||
#### 3. BatchManagementService (5개)
|
#### 3. BatchManagementService (5개)
|
||||||
|
|
||||||
- `getAvailableConnections()` - findMany → query
|
- `getAvailableConnections()` - findMany → query
|
||||||
- `getTables()` - $queryRaw → query (information_schema)
|
- `getTables()` - $queryRaw → query (information_schema)
|
||||||
- `getTableColumns()` - $queryRaw → query (information_schema)
|
- `getTableColumns()` - $queryRaw → query (information_schema)
|
||||||
- `getExternalTables()` - findUnique → queryOne (x2)
|
- `getExternalTables()` - findUnique → queryOne (x2)
|
||||||
|
|
||||||
#### 4. BatchSchedulerService (4개)
|
#### 4. BatchSchedulerService (4개)
|
||||||
|
|
||||||
- `loadActiveBatchConfigs()` - findMany → query (JOIN with json_agg)
|
- `loadActiveBatchConfigs()` - findMany → query (JOIN with json_agg)
|
||||||
- `updateBatchSchedule()` - findUnique → query (JOIN with json_agg)
|
- `updateBatchSchedule()` - findUnique → query (JOIN with json_agg)
|
||||||
- `getDataFromSource()` - $queryRawUnsafe → query
|
- `getDataFromSource()` - $queryRawUnsafe → query
|
||||||
|
|
@ -52,19 +56,23 @@
|
||||||
### 주요 기술적 해결 사항
|
### 주요 기술적 해결 사항
|
||||||
|
|
||||||
1. **외부 DB 연결 조회 반복**
|
1. **외부 DB 연결 조회 반복**
|
||||||
|
|
||||||
- 5개의 `findUnique` 호출을 `queryOne`으로 일괄 전환
|
- 5개의 `findUnique` 호출을 `queryOne`으로 일괄 전환
|
||||||
- 암호화/복호화 로직 유지
|
- 암호화/복호화 로직 유지
|
||||||
|
|
||||||
2. **배치 설정 + 매핑 JOIN**
|
2. **배치 설정 + 매핑 JOIN**
|
||||||
|
|
||||||
- Prisma `include` → `json_agg` + `json_build_object`
|
- Prisma `include` → `json_agg` + `json_build_object`
|
||||||
- `FILTER (WHERE bm.id IS NOT NULL)` 로 NULL 방지
|
- `FILTER (WHERE bm.id IS NOT NULL)` 로 NULL 방지
|
||||||
- 계층적 JSON 데이터 생성
|
- 계층적 JSON 데이터 생성
|
||||||
|
|
||||||
3. **동적 WHERE 절 생성**
|
3. **동적 WHERE 절 생성**
|
||||||
|
|
||||||
- 조건부 필터링 (batch_config_id, execution_status, 날짜 범위)
|
- 조건부 필터링 (batch_config_id, execution_status, 날짜 범위)
|
||||||
- 파라미터 인덱스 동적 관리
|
- 파라미터 인덱스 동적 관리
|
||||||
|
|
||||||
4. **동적 UPDATE 쿼리**
|
4. **동적 UPDATE 쿼리**
|
||||||
|
|
||||||
- undefined 필드 제외
|
- undefined 필드 제외
|
||||||
- 8개 필드의 조건부 업데이트
|
- 8개 필드의 조건부 업데이트
|
||||||
|
|
||||||
|
|
@ -73,6 +81,7 @@
|
||||||
- 원본 데이터만 쿼리로 조회
|
- 원본 데이터만 쿼리로 조회
|
||||||
|
|
||||||
### 컴파일 상태
|
### 컴파일 상태
|
||||||
|
|
||||||
✅ TypeScript 컴파일 성공
|
✅ TypeScript 컴파일 성공
|
||||||
✅ Linter 오류 없음
|
✅ Linter 오류 없음
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
| 대상 서비스 | 4개 (EnhancedDynamicForm, DataMapping, Data, Admin) |
|
| 대상 서비스 | 4개 (EnhancedDynamicForm, DataMapping, Data, Admin) |
|
||||||
| 파일 위치 | `backend-node/src/services/{enhanced,data,admin}*.ts` |
|
| 파일 위치 | `backend-node/src/services/{enhanced,data,admin}*.ts` |
|
||||||
| 총 파일 크기 | 2,062 라인 |
|
| 총 파일 크기 | 2,062 라인 |
|
||||||
| Prisma 호출 | 0개 (전환 완료) |
|
| Prisma 호출 | 0개 (전환 완료) |
|
||||||
| **현재 진행률** | **18/18 (100%)** ✅ **전환 완료** |
|
| **현재 진행률** | **18/18 (100%)** ✅ **전환 완료** |
|
||||||
| 복잡도 | 중간 (동적 쿼리, JSON 필드, 관리자 기능) |
|
| 복잡도 | 중간 (동적 쿼리, JSON 필드, 관리자 기능) |
|
||||||
| 우선순위 | 🟡 중간 (Phase 3.16) |
|
| 우선순위 | 🟡 중간 (Phase 3.16) |
|
||||||
|
|
@ -24,14 +24,16 @@
|
||||||
### 전환된 Prisma 호출 (18개)
|
### 전환된 Prisma 호출 (18개)
|
||||||
|
|
||||||
#### 1. EnhancedDynamicFormService (6개)
|
#### 1. EnhancedDynamicFormService (6개)
|
||||||
|
|
||||||
- `validateTableExists()` - $queryRawUnsafe → query
|
- `validateTableExists()` - $queryRawUnsafe → query
|
||||||
- `getTableColumns()` - $queryRawUnsafe → query
|
- `getTableColumns()` - $queryRawUnsafe → query
|
||||||
- `getColumnWebTypes()` - $queryRawUnsafe → query
|
- `getColumnWebTypes()` - $queryRawUnsafe → query
|
||||||
- `getPrimaryKeys()` - $queryRawUnsafe → query
|
- `getPrimaryKeys()` - $queryRawUnsafe → query
|
||||||
- `performInsert()` - $queryRawUnsafe → query
|
- `performInsert()` - $queryRawUnsafe → query
|
||||||
- `performUpdate()` - $queryRawUnsafe → query
|
- `performUpdate()` - $queryRawUnsafe → query
|
||||||
|
|
||||||
#### 2. DataMappingService (5개)
|
#### 2. DataMappingService (5개)
|
||||||
|
|
||||||
- `getSourceData()` - $queryRawUnsafe → query
|
- `getSourceData()` - $queryRawUnsafe → query
|
||||||
- `executeInsert()` - $executeRawUnsafe → query
|
- `executeInsert()` - $executeRawUnsafe → query
|
||||||
- `executeUpsert()` - $executeRawUnsafe → query
|
- `executeUpsert()` - $executeRawUnsafe → query
|
||||||
|
|
@ -39,12 +41,14 @@
|
||||||
- `disconnect()` - 제거 (Raw Query는 disconnect 불필요)
|
- `disconnect()` - 제거 (Raw Query는 disconnect 불필요)
|
||||||
|
|
||||||
#### 3. DataService (4개)
|
#### 3. DataService (4개)
|
||||||
|
|
||||||
- `getTableData()` - $queryRawUnsafe → query
|
- `getTableData()` - $queryRawUnsafe → query
|
||||||
- `checkTableExists()` - $queryRawUnsafe → query
|
- `checkTableExists()` - $queryRawUnsafe → query
|
||||||
- `getTableColumnsSimple()` - $queryRawUnsafe → query
|
- `getTableColumnsSimple()` - $queryRawUnsafe → query
|
||||||
- `getColumnLabel()` - $queryRawUnsafe → query
|
- `getColumnLabel()` - $queryRawUnsafe → query
|
||||||
|
|
||||||
#### 4. AdminService (3개)
|
#### 4. AdminService (3개)
|
||||||
|
|
||||||
- `getAdminMenuList()` - $queryRaw → query (WITH RECURSIVE)
|
- `getAdminMenuList()` - $queryRaw → query (WITH RECURSIVE)
|
||||||
- `getUserMenuList()` - $queryRaw → query (WITH RECURSIVE)
|
- `getUserMenuList()` - $queryRaw → query (WITH RECURSIVE)
|
||||||
- `getMenuInfo()` - findUnique → query (JOIN)
|
- `getMenuInfo()` - findUnique → query (JOIN)
|
||||||
|
|
@ -52,14 +56,17 @@
|
||||||
### 주요 기술적 해결 사항
|
### 주요 기술적 해결 사항
|
||||||
|
|
||||||
1. **변수명 충돌 해결**
|
1. **변수명 충돌 해결**
|
||||||
|
|
||||||
- `dataService.ts`에서 `query` 변수 → `sql` 변수로 변경
|
- `dataService.ts`에서 `query` 변수 → `sql` 변수로 변경
|
||||||
- `query()` 함수와 로컬 변수 충돌 방지
|
- `query()` 함수와 로컬 변수 충돌 방지
|
||||||
|
|
||||||
2. **WITH RECURSIVE 쿼리 전환**
|
2. **WITH RECURSIVE 쿼리 전환**
|
||||||
|
|
||||||
- Prisma의 `$queryRaw` 템플릿 리터럴 → 일반 문자열
|
- Prisma의 `$queryRaw` 템플릿 리터럴 → 일반 문자열
|
||||||
- `${userLang}` → `$1` 파라미터 바인딩
|
- `${userLang}` → `$1` 파라미터 바인딩
|
||||||
|
|
||||||
3. **JOIN 쿼리 전환**
|
3. **JOIN 쿼리 전환**
|
||||||
|
|
||||||
- Prisma의 `include` 옵션 → `LEFT JOIN` 쿼리
|
- Prisma의 `include` 옵션 → `LEFT JOIN` 쿼리
|
||||||
- 관계 데이터를 단일 쿼리로 조회
|
- 관계 데이터를 단일 쿼리로 조회
|
||||||
|
|
||||||
|
|
@ -69,6 +76,7 @@
|
||||||
- 동적 ORDER BY 처리
|
- 동적 ORDER BY 처리
|
||||||
|
|
||||||
### 컴파일 상태
|
### 컴파일 상태
|
||||||
|
|
||||||
✅ TypeScript 컴파일 성공
|
✅ TypeScript 컴파일 성공
|
||||||
✅ Linter 오류 없음
|
✅ Linter 오류 없음
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,15 @@ ReferenceCacheService는 **0개의 Prisma 호출**이 있으며, 참조 데이
|
||||||
|
|
||||||
### 📊 기본 정보
|
### 📊 기본 정보
|
||||||
|
|
||||||
| 항목 | 내용 |
|
| 항목 | 내용 |
|
||||||
| --------------- | ------------------------------------------------- |
|
| --------------- | ---------------------------------------------------- |
|
||||||
| 파일 위치 | `backend-node/src/services/referenceCacheService.ts` |
|
| 파일 위치 | `backend-node/src/services/referenceCacheService.ts` |
|
||||||
| 파일 크기 | 499 라인 |
|
| 파일 크기 | 499 라인 |
|
||||||
| Prisma 호출 | 0개 (이미 전환 완료) |
|
| Prisma 호출 | 0개 (이미 전환 완료) |
|
||||||
| **현재 진행률** | **3/3 (100%)** ✅ **전환 완료** |
|
| **현재 진행률** | **3/3 (100%)** ✅ **전환 완료** |
|
||||||
| 복잡도 | 낮음 (캐싱 로직) |
|
| 복잡도 | 낮음 (캐싱 로직) |
|
||||||
| 우선순위 | 🟢 낮음 (Phase 3.17) |
|
| 우선순위 | 🟢 낮음 (Phase 3.17) |
|
||||||
| **상태** | ✅ **완료** (이미 전환 완료됨) |
|
| **상태** | ✅ **완료** (이미 전환 완료됨) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -25,10 +25,12 @@ ReferenceCacheService는 이미 Raw Query로 전환이 완료되었습니다.
|
||||||
### 주요 기능
|
### 주요 기능
|
||||||
|
|
||||||
1. **참조 데이터 캐싱**
|
1. **참조 데이터 캐싱**
|
||||||
|
|
||||||
- 자주 사용되는 참조 테이블 데이터를 메모리에 캐싱
|
- 자주 사용되는 참조 테이블 데이터를 메모리에 캐싱
|
||||||
- 성능 향상을 위한 캐시 전략
|
- 성능 향상을 위한 캐시 전략
|
||||||
|
|
||||||
2. **캐시 관리**
|
2. **캐시 관리**
|
||||||
|
|
||||||
- 캐시 갱신 로직
|
- 캐시 갱신 로직
|
||||||
- TTL(Time To Live) 관리
|
- TTL(Time To Live) 관리
|
||||||
- 캐시 무효화
|
- 캐시 무효화
|
||||||
|
|
@ -58,4 +60,3 @@ ReferenceCacheService는 이미 Raw Query로 전환이 완료되었습니다.
|
||||||
|
|
||||||
**상태**: ✅ **완료**
|
**상태**: ✅ **완료**
|
||||||
**특이사항**: 캐싱 로직으로 성능에 중요한 서비스
|
**특이사항**: 캐싱 로직으로 성능에 중요한 서비스
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ DDLExecutionService는 **0개의 Prisma 호출**이 있으며, DDL 실행 및
|
||||||
|
|
||||||
### 📊 기본 정보
|
### 📊 기본 정보
|
||||||
|
|
||||||
| 항목 | 내용 |
|
| 항목 | 내용 |
|
||||||
| --------------- | ------------------------------------------------- |
|
| --------------- | -------------------------------------------------- |
|
||||||
| 파일 위치 | `backend-node/src/services/ddlExecutionService.ts` |
|
| 파일 위치 | `backend-node/src/services/ddlExecutionService.ts` |
|
||||||
| 파일 크기 | 786 라인 |
|
| 파일 크기 | 786 라인 |
|
||||||
| Prisma 호출 | 0개 (이미 전환 완료) |
|
| Prisma 호출 | 0개 (이미 전환 완료) |
|
||||||
| **현재 진행률** | **6/6 (100%)** ✅ **전환 완료** |
|
| **현재 진행률** | **6/6 (100%)** ✅ **전환 완료** |
|
||||||
| 복잡도 | 높음 (DDL 실행, 안전성 검증) |
|
| 복잡도 | 높음 (DDL 실행, 안전성 검증) |
|
||||||
| 우선순위 | 🔴 높음 (Phase 3.18) |
|
| 우선순위 | 🔴 높음 (Phase 3.18) |
|
||||||
| **상태** | ✅ **완료** (이미 전환 완료됨) |
|
| **상태** | ✅ **완료** (이미 전환 완료됨) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -25,26 +25,31 @@ DDLExecutionService는 이미 Raw Query로 전환이 완료되었습니다.
|
||||||
### 주요 기능
|
### 주요 기능
|
||||||
|
|
||||||
1. **테이블 생성 (CREATE TABLE)**
|
1. **테이블 생성 (CREATE TABLE)**
|
||||||
|
|
||||||
- 동적 테이블 생성
|
- 동적 테이블 생성
|
||||||
- 컬럼 정의 및 제약조건
|
- 컬럼 정의 및 제약조건
|
||||||
- 인덱스 생성
|
- 인덱스 생성
|
||||||
|
|
||||||
2. **컬럼 추가 (ADD COLUMN)**
|
2. **컬럼 추가 (ADD COLUMN)**
|
||||||
|
|
||||||
- 기존 테이블에 컬럼 추가
|
- 기존 테이블에 컬럼 추가
|
||||||
- 데이터 타입 검증
|
- 데이터 타입 검증
|
||||||
- 기본값 설정
|
- 기본값 설정
|
||||||
|
|
||||||
3. **테이블/컬럼 삭제 (DROP)**
|
3. **테이블/컬럼 삭제 (DROP)**
|
||||||
|
|
||||||
- 안전한 삭제 검증
|
- 안전한 삭제 검증
|
||||||
- 의존성 체크
|
- 의존성 체크
|
||||||
- 롤백 가능성
|
- 롤백 가능성
|
||||||
|
|
||||||
4. **DDL 안전성 검증**
|
4. **DDL 안전성 검증**
|
||||||
|
|
||||||
- DDL 실행 전 검증
|
- DDL 실행 전 검증
|
||||||
- 순환 참조 방지
|
- 순환 참조 방지
|
||||||
- 데이터 손실 방지
|
- 데이터 손실 방지
|
||||||
|
|
||||||
5. **DDL 실행 이력**
|
5. **DDL 실행 이력**
|
||||||
|
|
||||||
- 모든 DDL 실행 기록
|
- 모든 DDL 실행 기록
|
||||||
- 성공/실패 로그
|
- 성공/실패 로그
|
||||||
- 롤백 정보
|
- 롤백 정보
|
||||||
|
|
@ -85,4 +90,3 @@ DDLExecutionService는 이미 Raw Query로 전환이 완료되었습니다.
|
||||||
**상태**: ✅ **완료**
|
**상태**: ✅ **완료**
|
||||||
**특이사항**: DDL 실행의 핵심 서비스로 안전성이 매우 중요
|
**특이사항**: DDL 실행의 핵심 서비스로 안전성이 매우 중요
|
||||||
**⚠️ 주의**: 프로덕션 환경에서 DDL 실행 시 각별한 주의 필요
|
**⚠️ 주의**: 프로덕션 환경에서 DDL 실행 시 각별한 주의 필요
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,15 @@
|
||||||
|
|
||||||
### 📊 기본 정보
|
### 📊 기본 정보
|
||||||
|
|
||||||
| 항목 | 내용 |
|
| 항목 | 내용 |
|
||||||
| --------------- | --------------------------------------------- |
|
| --------------- | ------------------------------------------------- |
|
||||||
| 파일 위치 | `backend-node/src/controllers/adminController.ts` |
|
| 파일 위치 | `backend-node/src/controllers/adminController.ts` |
|
||||||
| 파일 크기 | 2,571 라인 |
|
| 파일 크기 | 2,569 라인 |
|
||||||
| Prisma 호출 | 28개 |
|
| Prisma 호출 | 28개 → 0개 |
|
||||||
| **현재 진행률** | **0/28 (0%)** 🔄 **진행 예정** |
|
| **현재 진행률** | **28/28 (100%)** ✅ **완료** |
|
||||||
| 복잡도 | 중간 (다양한 CRUD 패턴) |
|
| 복잡도 | 중간 (다양한 CRUD 패턴) |
|
||||||
| 우선순위 | 🔴 높음 (Phase 4.1) |
|
| 우선순위 | 🔴 높음 (Phase 4.1) |
|
||||||
| **상태** | ⏳ **대기 중** |
|
| **상태** | ✅ **완료** (2025-10-01) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -26,145 +26,181 @@
|
||||||
### 사용자 관리 (13개)
|
### 사용자 관리 (13개)
|
||||||
|
|
||||||
#### 1. getUserList (라인 312-317)
|
#### 1. getUserList (라인 312-317)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const totalCount = await prisma.user_info.count({ where });
|
const totalCount = await prisma.user_info.count({ where });
|
||||||
const users = await prisma.user_info.findMany({ where, skip, take, orderBy });
|
const users = await prisma.user_info.findMany({ where, skip, take, orderBy });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: count → `queryOne`, findMany → `query`
|
- **전환**: count → `queryOne`, findMany → `query`
|
||||||
- **복잡도**: 중간 (동적 WHERE, 페이징)
|
- **복잡도**: 중간 (동적 WHERE, 페이징)
|
||||||
|
|
||||||
#### 2. getUserInfo (라인 419)
|
#### 2. getUserInfo (라인 419)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const userInfo = await prisma.user_info.findFirst({ where });
|
const userInfo = await prisma.user_info.findFirst({ where });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: findFirst → `queryOne`
|
- **전환**: findFirst → `queryOne`
|
||||||
- **복잡도**: 낮음
|
- **복잡도**: 낮음
|
||||||
|
|
||||||
#### 3. updateUserStatus (라인 498)
|
#### 3. updateUserStatus (라인 498)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
await prisma.user_info.update({ where, data });
|
await prisma.user_info.update({ where, data });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: update → `query`
|
- **전환**: update → `query`
|
||||||
- **복잡도**: 낮음
|
- **복잡도**: 낮음
|
||||||
|
|
||||||
#### 4. deleteUserByAdmin (라인 2387)
|
#### 4. deleteUserByAdmin (라인 2387)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
await prisma.user_info.update({ where, data: { is_active: 'N' } });
|
await prisma.user_info.update({ where, data: { is_active: "N" } });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: update (soft delete) → `query`
|
- **전환**: update (soft delete) → `query`
|
||||||
- **복잡도**: 낮음
|
- **복잡도**: 낮음
|
||||||
|
|
||||||
#### 5. getMyProfile (라인 1468, 1488, 2479)
|
#### 5. getMyProfile (라인 1468, 1488, 2479)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const user = await prisma.user_info.findUnique({ where });
|
const user = await prisma.user_info.findUnique({ where });
|
||||||
const dept = await prisma.dept_info.findUnique({ where });
|
const dept = await prisma.dept_info.findUnique({ where });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: findUnique → `queryOne`
|
- **전환**: findUnique → `queryOne`
|
||||||
- **복잡도**: 낮음
|
- **복잡도**: 낮음
|
||||||
|
|
||||||
#### 6. updateMyProfile (라인 1864, 2527)
|
#### 6. updateMyProfile (라인 1864, 2527)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const updateResult = await prisma.user_info.update({ where, data });
|
const updateResult = await prisma.user_info.update({ where, data });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: update → `queryOne` with RETURNING
|
- **전환**: update → `queryOne` with RETURNING
|
||||||
- **복잡도**: 중간 (동적 UPDATE)
|
- **복잡도**: 중간 (동적 UPDATE)
|
||||||
|
|
||||||
#### 7. createOrUpdateUser (라인 1929, 1975)
|
#### 7. createOrUpdateUser (라인 1929, 1975)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const savedUser = await prisma.user_info.upsert({ where, update, create });
|
const savedUser = await prisma.user_info.upsert({ where, update, create });
|
||||||
const userCount = await prisma.user_info.count({ where });
|
const userCount = await prisma.user_info.count({ where });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: upsert → `INSERT ... ON CONFLICT`, count → `queryOne`
|
- **전환**: upsert → `INSERT ... ON CONFLICT`, count → `queryOne`
|
||||||
- **복잡도**: 높음
|
- **복잡도**: 높음
|
||||||
|
|
||||||
#### 8. 기타 findUnique (라인 1596, 1832, 2393)
|
#### 8. 기타 findUnique (라인 1596, 1832, 2393)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const existingUser = await prisma.user_info.findUnique({ where });
|
const existingUser = await prisma.user_info.findUnique({ where });
|
||||||
const currentUser = await prisma.user_info.findUnique({ where });
|
const currentUser = await prisma.user_info.findUnique({ where });
|
||||||
const updatedUser = await prisma.user_info.findUnique({ where });
|
const updatedUser = await prisma.user_info.findUnique({ where });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: findUnique → `queryOne`
|
- **전환**: findUnique → `queryOne`
|
||||||
- **복잡도**: 낮음
|
- **복잡도**: 낮음
|
||||||
|
|
||||||
### 회사 관리 (7개)
|
### 회사 관리 (7개)
|
||||||
|
|
||||||
#### 9. getCompanyList (라인 550, 1276)
|
#### 9. getCompanyList (라인 550, 1276)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const companies = await prisma.company_mng.findMany({ orderBy });
|
const companies = await prisma.company_mng.findMany({ orderBy });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: findMany → `query`
|
- **전환**: findMany → `query`
|
||||||
- **복잡도**: 낮음
|
- **복잡도**: 낮음
|
||||||
|
|
||||||
#### 10. createCompany (라인 2035)
|
#### 10. createCompany (라인 2035)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const existingCompany = await prisma.company_mng.findFirst({ where });
|
const existingCompany = await prisma.company_mng.findFirst({ where });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: findFirst (중복 체크) → `queryOne`
|
- **전환**: findFirst (중복 체크) → `queryOne`
|
||||||
- **복잡도**: 낮음
|
- **복잡도**: 낮음
|
||||||
|
|
||||||
#### 11. updateCompany (라인 2172, 2192)
|
#### 11. updateCompany (라인 2172, 2192)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const duplicateCompany = await prisma.company_mng.findFirst({ where });
|
const duplicateCompany = await prisma.company_mng.findFirst({ where });
|
||||||
const updatedCompany = await prisma.company_mng.update({ where, data });
|
const updatedCompany = await prisma.company_mng.update({ where, data });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: findFirst → `queryOne`, update → `queryOne`
|
- **전환**: findFirst → `queryOne`, update → `queryOne`
|
||||||
- **복잡도**: 중간
|
- **복잡도**: 중간
|
||||||
|
|
||||||
#### 12. deleteCompany (라인 2261, 2281)
|
#### 12. deleteCompany (라인 2261, 2281)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const existingCompany = await prisma.company_mng.findUnique({ where });
|
const existingCompany = await prisma.company_mng.findUnique({ where });
|
||||||
await prisma.company_mng.delete({ where });
|
await prisma.company_mng.delete({ where });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: findUnique → `queryOne`, delete → `query`
|
- **전환**: findUnique → `queryOne`, delete → `query`
|
||||||
- **복잡도**: 낮음
|
- **복잡도**: 낮음
|
||||||
|
|
||||||
### 부서 관리 (2개)
|
### 부서 관리 (2개)
|
||||||
|
|
||||||
#### 13. getDepartmentList (라인 1348)
|
#### 13. getDepartmentList (라인 1348)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const departments = await prisma.dept_info.findMany({ where, orderBy });
|
const departments = await prisma.dept_info.findMany({ where, orderBy });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: findMany → `query`
|
- **전환**: findMany → `query`
|
||||||
- **복잡도**: 낮음
|
- **복잡도**: 낮음
|
||||||
|
|
||||||
#### 14. getDeptInfo (라인 1488)
|
#### 14. getDeptInfo (라인 1488)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const dept = await prisma.dept_info.findUnique({ where });
|
const dept = await prisma.dept_info.findUnique({ where });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: findUnique → `queryOne`
|
- **전환**: findUnique → `queryOne`
|
||||||
- **복잡도**: 낮음
|
- **복잡도**: 낮음
|
||||||
|
|
||||||
### 메뉴 관리 (3개)
|
### 메뉴 관리 (3개)
|
||||||
|
|
||||||
#### 15. createMenu (라인 1021)
|
#### 15. createMenu (라인 1021)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const savedMenu = await prisma.menu_info.create({ data });
|
const savedMenu = await prisma.menu_info.create({ data });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: create → `queryOne` with INSERT RETURNING
|
- **전환**: create → `queryOne` with INSERT RETURNING
|
||||||
- **복잡도**: 중간
|
- **복잡도**: 중간
|
||||||
|
|
||||||
#### 16. updateMenu (라인 1087)
|
#### 16. updateMenu (라인 1087)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const updatedMenu = await prisma.menu_info.update({ where, data });
|
const updatedMenu = await prisma.menu_info.update({ where, data });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: update → `queryOne` with UPDATE RETURNING
|
- **전환**: update → `queryOne` with UPDATE RETURNING
|
||||||
- **복잡도**: 중간
|
- **복잡도**: 중간
|
||||||
|
|
||||||
#### 17. deleteMenu (라인 1149, 1211)
|
#### 17. deleteMenu (라인 1149, 1211)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const deletedMenu = await prisma.menu_info.delete({ where });
|
const deletedMenu = await prisma.menu_info.delete({ where });
|
||||||
// 재귀 삭제
|
// 재귀 삭제
|
||||||
const deletedMenu = await prisma.menu_info.delete({ where });
|
const deletedMenu = await prisma.menu_info.delete({ where });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: delete → `query`
|
- **전환**: delete → `query`
|
||||||
- **복잡도**: 중간 (재귀 삭제 로직)
|
- **복잡도**: 중간 (재귀 삭제 로직)
|
||||||
|
|
||||||
### 다국어 (1개)
|
### 다국어 (1개)
|
||||||
|
|
||||||
#### 18. getMultiLangKeys (라인 665)
|
#### 18. getMultiLangKeys (라인 665)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const result = await prisma.multi_lang_key_master.findMany({ where, orderBy });
|
const result = await prisma.multi_lang_key_master.findMany({ where, orderBy });
|
||||||
```
|
```
|
||||||
|
|
||||||
- **전환**: findMany → `query`
|
- **전환**: findMany → `query`
|
||||||
- **복잡도**: 낮음
|
- **복잡도**: 낮음
|
||||||
|
|
||||||
|
|
@ -173,6 +209,7 @@ const result = await prisma.multi_lang_key_master.findMany({ where, orderBy });
|
||||||
## 📝 전환 전략
|
## 📝 전환 전략
|
||||||
|
|
||||||
### 1단계: Import 변경
|
### 1단계: Import 변경
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 제거
|
// 제거
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
@ -183,10 +220,12 @@ import { query, queryOne } from "../database/db";
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2단계: 단순 조회 전환
|
### 2단계: 단순 조회 전환
|
||||||
|
|
||||||
- findMany → `query<T>`
|
- findMany → `query<T>`
|
||||||
- findUnique/findFirst → `queryOne<T>`
|
- findUnique/findFirst → `queryOne<T>`
|
||||||
|
|
||||||
### 3단계: 동적 WHERE 처리
|
### 3단계: 동적 WHERE 처리
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const whereConditions: string[] = [];
|
const whereConditions: string[] = [];
|
||||||
const params: any[] = [];
|
const params: any[] = [];
|
||||||
|
|
@ -197,17 +236,18 @@ if (companyCode) {
|
||||||
params.push(companyCode);
|
params.push(companyCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
const whereClause = whereConditions.length > 0
|
const whereClause =
|
||||||
? `WHERE ${whereConditions.join(' AND ')}`
|
whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
|
||||||
: '';
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4단계: 복잡한 로직 전환
|
### 4단계: 복잡한 로직 전환
|
||||||
|
|
||||||
- count → `SELECT COUNT(*) as count`
|
- count → `SELECT COUNT(*) as count`
|
||||||
- upsert → `INSERT ... ON CONFLICT DO UPDATE`
|
- upsert → `INSERT ... ON CONFLICT DO UPDATE`
|
||||||
- 동적 UPDATE → 조건부 SET 절 생성
|
- 동적 UPDATE → 조건부 SET 절 생성
|
||||||
|
|
||||||
### 5단계: 테스트 및 검증
|
### 5단계: 테스트 및 검증
|
||||||
|
|
||||||
- 각 함수별 동작 확인
|
- 각 함수별 동작 확인
|
||||||
- 에러 처리 확인
|
- 에러 처리 확인
|
||||||
- 타입 안전성 확인
|
- 타입 안전성 확인
|
||||||
|
|
@ -217,6 +257,7 @@ const whereClause = whereConditions.length > 0
|
||||||
## 🎯 주요 변경 예시
|
## 🎯 주요 변경 예시
|
||||||
|
|
||||||
### getUserList (count + findMany)
|
### getUserList (count + findMany)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Before
|
// Before
|
||||||
const totalCount = await prisma.user_info.count({ where });
|
const totalCount = await prisma.user_info.count({ where });
|
||||||
|
|
@ -224,7 +265,7 @@ const users = await prisma.user_info.findMany({
|
||||||
where,
|
where,
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
orderBy
|
orderBy,
|
||||||
});
|
});
|
||||||
|
|
||||||
// After
|
// After
|
||||||
|
|
@ -242,16 +283,15 @@ if (where.user_name) {
|
||||||
params.push(`%${where.user_name}%`);
|
params.push(`%${where.user_name}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const whereClause = whereConditions.length > 0
|
const whereClause =
|
||||||
? `WHERE ${whereConditions.join(' AND ')}`
|
whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
|
||||||
: '';
|
|
||||||
|
|
||||||
// Count
|
// Count
|
||||||
const countResult = await queryOne<{ count: number }>(
|
const countResult = await queryOne<{ count: number }>(
|
||||||
`SELECT COUNT(*) as count FROM user_info ${whereClause}`,
|
`SELECT COUNT(*) as count FROM user_info ${whereClause}`,
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
const totalCount = parseInt(countResult?.count?.toString() || '0', 10);
|
const totalCount = parseInt(countResult?.count?.toString() || "0", 10);
|
||||||
|
|
||||||
// 데이터 조회
|
// 데이터 조회
|
||||||
const usersQuery = `
|
const usersQuery = `
|
||||||
|
|
@ -266,6 +306,7 @@ const users = await query<UserInfo>(usersQuery, params);
|
||||||
```
|
```
|
||||||
|
|
||||||
### createOrUpdateUser (upsert)
|
### createOrUpdateUser (upsert)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Before
|
// Before
|
||||||
const savedUser = await prisma.user_info.upsert({
|
const savedUser = await prisma.user_info.upsert({
|
||||||
|
|
@ -278,8 +319,8 @@ const savedUser = await prisma.user_info.upsert({
|
||||||
const savedUser = await queryOne<UserInfo>(
|
const savedUser = await queryOne<UserInfo>(
|
||||||
`INSERT INTO user_info (user_id, user_name, email, ...)
|
`INSERT INTO user_info (user_id, user_name, email, ...)
|
||||||
VALUES ($1, $2, $3, ...)
|
VALUES ($1, $2, $3, ...)
|
||||||
ON CONFLICT (user_id)
|
ON CONFLICT (user_id)
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
user_name = EXCLUDED.user_name,
|
user_name = EXCLUDED.user_name,
|
||||||
email = EXCLUDED.email,
|
email = EXCLUDED.email,
|
||||||
...
|
...
|
||||||
|
|
@ -289,11 +330,12 @@ const savedUser = await queryOne<UserInfo>(
|
||||||
```
|
```
|
||||||
|
|
||||||
### updateMyProfile (동적 UPDATE)
|
### updateMyProfile (동적 UPDATE)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Before
|
// Before
|
||||||
const updateResult = await prisma.user_info.update({
|
const updateResult = await prisma.user_info.update({
|
||||||
where: { user_id: userId },
|
where: { user_id: userId },
|
||||||
data: updateData
|
data: updateData,
|
||||||
});
|
});
|
||||||
|
|
||||||
// After
|
// After
|
||||||
|
|
@ -315,7 +357,7 @@ params.push(userId);
|
||||||
|
|
||||||
const updateResult = await queryOne<UserInfo>(
|
const updateResult = await queryOne<UserInfo>(
|
||||||
`UPDATE user_info
|
`UPDATE user_info
|
||||||
SET ${updates.join(', ')}, updated_date = NOW()
|
SET ${updates.join(", ")}, updated_date = NOW()
|
||||||
WHERE user_id = $${paramIndex}
|
WHERE user_id = $${paramIndex}
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
params
|
params
|
||||||
|
|
@ -327,50 +369,61 @@ const updateResult = await queryOne<UserInfo>(
|
||||||
## ✅ 체크리스트
|
## ✅ 체크리스트
|
||||||
|
|
||||||
### 기본 설정
|
### 기본 설정
|
||||||
- [ ] Prisma import 제거
|
|
||||||
- [ ] query, queryOne import 추가
|
- ✅ Prisma import 제거 (완전 제거 확인)
|
||||||
- [ ] 타입 import 확인
|
- ✅ query, queryOne import 추가 (이미 존재)
|
||||||
|
- ✅ 타입 import 확인
|
||||||
|
|
||||||
### 사용자 관리
|
### 사용자 관리
|
||||||
- [ ] getUserList (count + findMany)
|
|
||||||
- [ ] getUserInfo (findFirst)
|
- ✅ getUserList (count + findMany → Raw Query)
|
||||||
- [ ] updateUserStatus (update)
|
- ✅ getUserLocale (findFirst → queryOne)
|
||||||
- [ ] deleteUserByAdmin (soft delete)
|
- ✅ setUserLocale (update → query)
|
||||||
- [ ] getMyProfile (findUnique x3)
|
- ✅ getUserInfo (findUnique → queryOne)
|
||||||
- [ ] updateMyProfile (update x2)
|
- ✅ checkDuplicateUserId (findUnique → queryOne)
|
||||||
- [ ] createOrUpdateUser (upsert + count)
|
- ✅ changeUserStatus (findUnique + update → queryOne + query)
|
||||||
- [ ] 기타 findUnique (x3)
|
- ✅ saveUser (upsert → INSERT ON CONFLICT)
|
||||||
|
- ✅ updateProfile (동적 update → 동적 query)
|
||||||
|
- ✅ resetUserPassword (update → query)
|
||||||
|
|
||||||
### 회사 관리
|
### 회사 관리
|
||||||
- [ ] getCompanyList (findMany x2)
|
|
||||||
- [ ] createCompany (findFirst 중복체크)
|
- ✅ getCompanyList (findMany → query)
|
||||||
- [ ] updateCompany (findFirst + update)
|
- ✅ getCompanyListFromDB (findMany → query)
|
||||||
- [ ] deleteCompany (findUnique + delete)
|
- ✅ createCompany (findFirst → queryOne)
|
||||||
|
- ✅ updateCompany (findFirst + update → queryOne + query)
|
||||||
|
- ✅ deleteCompany (delete → query with RETURNING)
|
||||||
|
|
||||||
### 부서 관리
|
### 부서 관리
|
||||||
- [ ] getDepartmentList (findMany)
|
|
||||||
- [ ] getDeptInfo (findUnique)
|
- ✅ getDepartmentList (findMany → query with 동적 WHERE)
|
||||||
|
|
||||||
### 메뉴 관리
|
### 메뉴 관리
|
||||||
- [ ] createMenu (create)
|
|
||||||
- [ ] updateMenu (update)
|
- ✅ saveMenu (create → query with INSERT RETURNING)
|
||||||
- [ ] deleteMenu (delete x2, 재귀)
|
- ✅ updateMenu (update → query with UPDATE RETURNING)
|
||||||
|
- ✅ deleteMenu (delete → query with DELETE RETURNING)
|
||||||
|
- ✅ deleteMenusBatch (다중 delete → 반복 query)
|
||||||
|
|
||||||
### 다국어
|
### 다국어
|
||||||
- [ ] getMultiLangKeys (findMany)
|
|
||||||
|
- ✅ getLangKeyList (findMany → query)
|
||||||
|
|
||||||
### 검증
|
### 검증
|
||||||
- [ ] TypeScript 컴파일 확인
|
|
||||||
- [ ] Linter 오류 확인
|
- ✅ TypeScript 컴파일 확인 (에러 없음)
|
||||||
- [ ] 기능 테스트
|
- ✅ Linter 오류 확인
|
||||||
- [ ] 에러 처리 확인
|
- ⏳ 기능 테스트 (실행 필요)
|
||||||
|
- ✅ 에러 처리 확인 (기존 구조 유지)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📌 참고사항
|
## 📌 참고사항
|
||||||
|
|
||||||
### 동적 쿼리 생성 패턴
|
### 동적 쿼리 생성 패턴
|
||||||
|
|
||||||
모든 동적 WHERE/UPDATE는 다음 패턴을 따릅니다:
|
모든 동적 WHERE/UPDATE는 다음 패턴을 따릅니다:
|
||||||
|
|
||||||
1. 조건/필드 배열 생성
|
1. 조건/필드 배열 생성
|
||||||
2. 파라미터 배열 생성
|
2. 파라미터 배열 생성
|
||||||
3. 파라미터 인덱스 관리
|
3. 파라미터 인덱스 관리
|
||||||
|
|
@ -378,8 +431,92 @@ const updateResult = await queryOne<UserInfo>(
|
||||||
5. query/queryOne 실행
|
5. query/queryOne 실행
|
||||||
|
|
||||||
### 에러 처리
|
### 에러 처리
|
||||||
|
|
||||||
기존 try-catch 구조를 유지하며, 데이터베이스 에러를 적절히 변환합니다.
|
기존 try-catch 구조를 유지하며, 데이터베이스 에러를 적절히 변환합니다.
|
||||||
|
|
||||||
### 트랜잭션
|
### 트랜잭션
|
||||||
|
|
||||||
복잡한 로직은 Service Layer로 이동을 고려합니다.
|
복잡한 로직은 Service Layer로 이동을 고려합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 완료 요약 (2025-10-01)
|
||||||
|
|
||||||
|
### ✅ 전환 완료 현황
|
||||||
|
|
||||||
|
| 카테고리 | 함수 수 | 상태 |
|
||||||
|
|---------|--------|------|
|
||||||
|
| 사용자 관리 | 9개 | ✅ 완료 |
|
||||||
|
| 회사 관리 | 5개 | ✅ 완료 |
|
||||||
|
| 부서 관리 | 1개 | ✅ 완료 |
|
||||||
|
| 메뉴 관리 | 4개 | ✅ 완료 |
|
||||||
|
| 다국어 | 1개 | ✅ 완료 |
|
||||||
|
| **총계** | **20개** | **✅ 100% 완료** |
|
||||||
|
|
||||||
|
### 📊 주요 성과
|
||||||
|
|
||||||
|
1. **완전한 Prisma 제거**: adminController.ts에서 모든 Prisma 코드 제거 완료
|
||||||
|
2. **동적 쿼리 지원**: 런타임 테이블 생성/수정 가능
|
||||||
|
3. **일관된 에러 처리**: 모든 함수에서 통일된 에러 처리 유지
|
||||||
|
4. **타입 안전성**: TypeScript 컴파일 에러 없음
|
||||||
|
5. **코드 품질 향상**: 949줄 변경 (+474/-475)
|
||||||
|
|
||||||
|
### 🔑 주요 변환 패턴
|
||||||
|
|
||||||
|
#### 1. 동적 WHERE 조건
|
||||||
|
```typescript
|
||||||
|
let whereConditions: string[] = [];
|
||||||
|
let queryParams: any[] = [];
|
||||||
|
let paramIndex = 1;
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
whereConditions.push(`field = $${paramIndex}`);
|
||||||
|
queryParams.push(filter);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause = whereConditions.length > 0
|
||||||
|
? `WHERE ${whereConditions.join(" AND ")}`
|
||||||
|
: "";
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. UPSERT (INSERT ON CONFLICT)
|
||||||
|
```typescript
|
||||||
|
const [result] = await query<any>(
|
||||||
|
`INSERT INTO table (col1, col2) VALUES ($1, $2)
|
||||||
|
ON CONFLICT (col1) DO UPDATE SET col2 = $2
|
||||||
|
RETURNING *`,
|
||||||
|
[val1, val2]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 동적 UPDATE
|
||||||
|
```typescript
|
||||||
|
const updateFields: string[] = [];
|
||||||
|
const updateValues: any[] = [];
|
||||||
|
let paramIndex = 1;
|
||||||
|
|
||||||
|
if (data.field !== undefined) {
|
||||||
|
updateFields.push(`field = $${paramIndex}`);
|
||||||
|
updateValues.push(data.field);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await query(
|
||||||
|
`UPDATE table SET ${updateFields.join(", ")} WHERE id = $${paramIndex}`,
|
||||||
|
[...updateValues, id]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🚀 다음 단계
|
||||||
|
|
||||||
|
1. **테스트 실행**: 개발 서버에서 모든 API 엔드포인트 테스트
|
||||||
|
2. **문서 업데이트**: Phase 4 전체 계획서 진행 상황 반영
|
||||||
|
3. **다음 Phase**: screenFileController.ts 마이그레이션 진행
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**마지막 업데이트**: 2025-10-01
|
||||||
|
**작업자**: Claude Agent
|
||||||
|
**완료 시간**: 약 15분
|
||||||
|
**변경 라인 수**: 949줄 (추가 474줄, 삭제 475줄)
|
||||||
|
|
|
||||||
|
|
@ -9,57 +9,66 @@
|
||||||
|
|
||||||
### 📊 기본 정보
|
### 📊 기본 정보
|
||||||
|
|
||||||
| 항목 | 내용 |
|
| 항목 | 내용 |
|
||||||
| --------------- | --------------------------------------- |
|
| --------------- | ---------------------------------- |
|
||||||
| 대상 파일 | 7개 컨트롤러 |
|
| 대상 파일 | 7개 컨트롤러 |
|
||||||
| 파일 위치 | `backend-node/src/controllers/` |
|
| 파일 위치 | `backend-node/src/controllers/` |
|
||||||
| Prisma 호출 | 70개 |
|
| Prisma 호출 | 70개 (28개 완료) |
|
||||||
| **현재 진행률** | **0/70 (0%)** 🔄 **진행 예정** |
|
| **현재 진행률** | **28/70 (40%)** 🔄 **진행 중** |
|
||||||
| 복잡도 | 중간 (대부분 단순 CRUD) |
|
| 복잡도 | 중간 (대부분 단순 CRUD) |
|
||||||
| 우선순위 | 🟡 중간 (Phase 4) |
|
| 우선순위 | 🟡 중간 (Phase 4) |
|
||||||
| **상태** | ⏳ **대기 중** |
|
| **상태** | 🔄 **진행 중** (adminController 완료) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 전환 대상 컨트롤러
|
## 🎯 전환 대상 컨트롤러
|
||||||
|
|
||||||
### 1. adminController.ts (28개)
|
### 1. adminController.ts ✅ 완료 (28개)
|
||||||
- **라인 수**: 2,571 라인
|
|
||||||
- **Prisma 호출**: 28개
|
- **라인 수**: 2,569 라인
|
||||||
|
- **Prisma 호출**: 28개 → 0개
|
||||||
- **주요 기능**:
|
- **주요 기능**:
|
||||||
- 사용자 관리 (조회, 생성, 수정, 삭제)
|
- 사용자 관리 (조회, 생성, 수정, 삭제) ✅
|
||||||
- 회사 관리 (조회, 생성, 수정, 삭제)
|
- 회사 관리 (조회, 생성, 수정, 삭제) ✅
|
||||||
- 부서 관리 (조회)
|
- 부서 관리 (조회) ✅
|
||||||
- 메뉴 관리 (생성, 수정, 삭제)
|
- 메뉴 관리 (생성, 수정, 삭제) ✅
|
||||||
- 다국어 키 조회
|
- 다국어 키 조회 ✅
|
||||||
- **우선순위**: 🔴 높음
|
- **우선순위**: 🔴 높음
|
||||||
|
- **상태**: ✅ **완료** (2025-10-01)
|
||||||
|
- **문서**: [PHASE4.1_ADMIN_CONTROLLER_MIGRATION.md](PHASE4.1_ADMIN_CONTROLLER_MIGRATION.md)
|
||||||
|
|
||||||
### 2. webTypeStandardController.ts (11개)
|
### 2. webTypeStandardController.ts (11개)
|
||||||
|
|
||||||
- **Prisma 호출**: 11개
|
- **Prisma 호출**: 11개
|
||||||
- **주요 기능**: 웹타입 표준 관리
|
- **주요 기능**: 웹타입 표준 관리
|
||||||
- **우선순위**: 🟡 중간
|
- **우선순위**: 🟡 중간
|
||||||
|
|
||||||
### 3. fileController.ts (11개)
|
### 3. fileController.ts (11개)
|
||||||
|
|
||||||
- **Prisma 호출**: 11개
|
- **Prisma 호출**: 11개
|
||||||
- **주요 기능**: 파일 업로드/다운로드 관리
|
- **주요 기능**: 파일 업로드/다운로드 관리
|
||||||
- **우선순위**: 🟡 중간
|
- **우선순위**: 🟡 중간
|
||||||
|
|
||||||
### 4. buttonActionStandardController.ts (11개)
|
### 4. buttonActionStandardController.ts (11개)
|
||||||
|
|
||||||
- **Prisma 호출**: 11개
|
- **Prisma 호출**: 11개
|
||||||
- **주요 기능**: 버튼 액션 표준 관리
|
- **주요 기능**: 버튼 액션 표준 관리
|
||||||
- **우선순위**: 🟡 중간
|
- **우선순위**: 🟡 중간
|
||||||
|
|
||||||
### 5. entityReferenceController.ts (4개)
|
### 5. entityReferenceController.ts (4개)
|
||||||
|
|
||||||
- **Prisma 호출**: 4개
|
- **Prisma 호출**: 4개
|
||||||
- **주요 기능**: 엔티티 참조 관리
|
- **주요 기능**: 엔티티 참조 관리
|
||||||
- **우선순위**: 🟢 낮음
|
- **우선순위**: 🟢 낮음
|
||||||
|
|
||||||
### 6. dataflowExecutionController.ts (3개)
|
### 6. dataflowExecutionController.ts (3개)
|
||||||
|
|
||||||
- **Prisma 호출**: 3개
|
- **Prisma 호출**: 3개
|
||||||
- **주요 기능**: 데이터플로우 실행
|
- **주요 기능**: 데이터플로우 실행
|
||||||
- **우선순위**: 🟢 낮음
|
- **우선순위**: 🟢 낮음
|
||||||
|
|
||||||
### 7. screenFileController.ts (2개)
|
### 7. screenFileController.ts (2개)
|
||||||
|
|
||||||
- **Prisma 호출**: 2개
|
- **Prisma 호출**: 2개
|
||||||
- **주요 기능**: 화면 파일 관리
|
- **주요 기능**: 화면 파일 관리
|
||||||
- **우선순위**: 🟢 낮음
|
- **우선순위**: 🟢 낮음
|
||||||
|
|
@ -71,10 +80,12 @@
|
||||||
### 기본 원칙
|
### 기본 원칙
|
||||||
|
|
||||||
1. **Service Layer 우선**
|
1. **Service Layer 우선**
|
||||||
|
|
||||||
- 가능하면 Service로 로직 이동
|
- 가능하면 Service로 로직 이동
|
||||||
- Controller는 최소한의 로직만 유지
|
- Controller는 최소한의 로직만 유지
|
||||||
|
|
||||||
2. **단순 전환**
|
2. **단순 전환**
|
||||||
|
|
||||||
- 대부분 단순 CRUD → `query`, `queryOne` 사용
|
- 대부분 단순 CRUD → `query`, `queryOne` 사용
|
||||||
- 복잡한 로직은 Service로 이동
|
- 복잡한 로직은 Service로 이동
|
||||||
|
|
||||||
|
|
@ -85,10 +96,11 @@
|
||||||
### 전환 패턴
|
### 전환 패턴
|
||||||
|
|
||||||
#### 1. findMany → query
|
#### 1. findMany → query
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Before
|
// Before
|
||||||
const users = await prisma.user_info.findMany({
|
const users = await prisma.user_info.findMany({
|
||||||
where: { company_code: companyCode }
|
where: { company_code: companyCode },
|
||||||
});
|
});
|
||||||
|
|
||||||
// After
|
// After
|
||||||
|
|
@ -99,10 +111,11 @@ const users = await query<UserInfo>(
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. findUnique → queryOne
|
#### 2. findUnique → queryOne
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Before
|
// Before
|
||||||
const user = await prisma.user_info.findUnique({
|
const user = await prisma.user_info.findUnique({
|
||||||
where: { user_id: userId }
|
where: { user_id: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// After
|
// After
|
||||||
|
|
@ -113,6 +126,7 @@ const user = await queryOne<UserInfo>(
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3. create → queryOne with INSERT
|
#### 3. create → queryOne with INSERT
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Before
|
// Before
|
||||||
const newUser = await prisma.user_info.create({
|
const newUser = await prisma.user_info.create({
|
||||||
|
|
@ -121,13 +135,14 @@ const newUser = await prisma.user_info.create({
|
||||||
|
|
||||||
// After
|
// After
|
||||||
const newUser = await queryOne<UserInfo>(
|
const newUser = await queryOne<UserInfo>(
|
||||||
`INSERT INTO user_info (user_id, user_name, ...)
|
`INSERT INTO user_info (user_id, user_name, ...)
|
||||||
VALUES ($1, $2, ...) RETURNING *`,
|
VALUES ($1, $2, ...) RETURNING *`,
|
||||||
[userData.user_id, userData.user_name, ...]
|
[userData.user_id, userData.user_name, ...]
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 4. update → queryOne with UPDATE
|
#### 4. update → queryOne with UPDATE
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Before
|
// Before
|
||||||
const updated = await prisma.user_info.update({
|
const updated = await prisma.user_info.update({
|
||||||
|
|
@ -137,31 +152,30 @@ const updated = await prisma.user_info.update({
|
||||||
|
|
||||||
// After
|
// After
|
||||||
const updated = await queryOne<UserInfo>(
|
const updated = await queryOne<UserInfo>(
|
||||||
`UPDATE user_info SET user_name = $1, ...
|
`UPDATE user_info SET user_name = $1, ...
|
||||||
WHERE user_id = $2 RETURNING *`,
|
WHERE user_id = $2 RETURNING *`,
|
||||||
[updateData.user_name, ..., userId]
|
[updateData.user_name, ..., userId]
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 5. delete → query with DELETE
|
#### 5. delete → query with DELETE
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Before
|
// Before
|
||||||
await prisma.user_info.delete({
|
await prisma.user_info.delete({
|
||||||
where: { user_id: userId }
|
where: { user_id: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// After
|
// After
|
||||||
await query(
|
await query(`DELETE FROM user_info WHERE user_id = $1`, [userId]);
|
||||||
`DELETE FROM user_info WHERE user_id = $1`,
|
|
||||||
[userId]
|
|
||||||
);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 6. count → queryOne
|
#### 6. count → queryOne
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Before
|
// Before
|
||||||
const count = await prisma.user_info.count({
|
const count = await prisma.user_info.count({
|
||||||
where: { company_code: companyCode }
|
where: { company_code: companyCode },
|
||||||
});
|
});
|
||||||
|
|
||||||
// After
|
// After
|
||||||
|
|
@ -169,7 +183,7 @@ const result = await queryOne<{ count: number }>(
|
||||||
`SELECT COUNT(*) as count FROM user_info WHERE company_code = $1`,
|
`SELECT COUNT(*) as count FROM user_info WHERE company_code = $1`,
|
||||||
[companyCode]
|
[companyCode]
|
||||||
);
|
);
|
||||||
const count = parseInt(result?.count?.toString() || '0', 10);
|
const count = parseInt(result?.count?.toString() || "0", 10);
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -177,6 +191,7 @@ const count = parseInt(result?.count?.toString() || '0', 10);
|
||||||
## ✅ 체크리스트
|
## ✅ 체크리스트
|
||||||
|
|
||||||
### Phase 4.1: adminController.ts
|
### Phase 4.1: adminController.ts
|
||||||
|
|
||||||
- [ ] Prisma import 제거
|
- [ ] Prisma import 제거
|
||||||
- [ ] query, queryOne import 추가
|
- [ ] query, queryOne import 추가
|
||||||
- [ ] 사용자 관리 함수 전환 (8개)
|
- [ ] 사용자 관리 함수 전환 (8개)
|
||||||
|
|
@ -206,6 +221,7 @@ const count = parseInt(result?.count?.toString() || '0', 10);
|
||||||
- [ ] 린터 확인
|
- [ ] 린터 확인
|
||||||
|
|
||||||
### Phase 4.2: webTypeStandardController.ts
|
### Phase 4.2: webTypeStandardController.ts
|
||||||
|
|
||||||
- [ ] Prisma import 제거
|
- [ ] Prisma import 제거
|
||||||
- [ ] query, queryOne import 추가
|
- [ ] query, queryOne import 추가
|
||||||
- [ ] 모든 함수 전환 (11개)
|
- [ ] 모든 함수 전환 (11개)
|
||||||
|
|
@ -213,6 +229,7 @@ const count = parseInt(result?.count?.toString() || '0', 10);
|
||||||
- [ ] 린터 확인
|
- [ ] 린터 확인
|
||||||
|
|
||||||
### Phase 4.3: fileController.ts
|
### Phase 4.3: fileController.ts
|
||||||
|
|
||||||
- [ ] Prisma import 제거
|
- [ ] Prisma import 제거
|
||||||
- [ ] query, queryOne import 추가
|
- [ ] query, queryOne import 추가
|
||||||
- [ ] 모든 함수 전환 (11개)
|
- [ ] 모든 함수 전환 (11개)
|
||||||
|
|
@ -220,6 +237,7 @@ const count = parseInt(result?.count?.toString() || '0', 10);
|
||||||
- [ ] 린터 확인
|
- [ ] 린터 확인
|
||||||
|
|
||||||
### Phase 4.4: buttonActionStandardController.ts
|
### Phase 4.4: buttonActionStandardController.ts
|
||||||
|
|
||||||
- [ ] Prisma import 제거
|
- [ ] Prisma import 제거
|
||||||
- [ ] query, queryOne import 추가
|
- [ ] query, queryOne import 추가
|
||||||
- [ ] 모든 함수 전환 (11개)
|
- [ ] 모든 함수 전환 (11개)
|
||||||
|
|
@ -227,6 +245,7 @@ const count = parseInt(result?.count?.toString() || '0', 10);
|
||||||
- [ ] 린터 확인
|
- [ ] 린터 확인
|
||||||
|
|
||||||
### Phase 4.5: entityReferenceController.ts
|
### Phase 4.5: entityReferenceController.ts
|
||||||
|
|
||||||
- [ ] Prisma import 제거
|
- [ ] Prisma import 제거
|
||||||
- [ ] query, queryOne import 추가
|
- [ ] query, queryOne import 추가
|
||||||
- [ ] 모든 함수 전환 (4개)
|
- [ ] 모든 함수 전환 (4개)
|
||||||
|
|
@ -234,6 +253,7 @@ const count = parseInt(result?.count?.toString() || '0', 10);
|
||||||
- [ ] 린터 확인
|
- [ ] 린터 확인
|
||||||
|
|
||||||
### Phase 4.6: dataflowExecutionController.ts
|
### Phase 4.6: dataflowExecutionController.ts
|
||||||
|
|
||||||
- [ ] Prisma import 제거
|
- [ ] Prisma import 제거
|
||||||
- [ ] query, queryOne import 추가
|
- [ ] query, queryOne import 추가
|
||||||
- [ ] 모든 함수 전환 (3개)
|
- [ ] 모든 함수 전환 (3개)
|
||||||
|
|
@ -241,6 +261,7 @@ const count = parseInt(result?.count?.toString() || '0', 10);
|
||||||
- [ ] 린터 확인
|
- [ ] 린터 확인
|
||||||
|
|
||||||
### Phase 4.7: screenFileController.ts
|
### Phase 4.7: screenFileController.ts
|
||||||
|
|
||||||
- [ ] Prisma import 제거
|
- [ ] Prisma import 제거
|
||||||
- [ ] query, queryOne import 추가
|
- [ ] query, queryOne import 추가
|
||||||
- [ ] 모든 함수 전환 (2개)
|
- [ ] 모든 함수 전환 (2개)
|
||||||
|
|
@ -252,15 +273,18 @@ const count = parseInt(result?.count?.toString() || '0', 10);
|
||||||
## 🎯 예상 결과
|
## 🎯 예상 결과
|
||||||
|
|
||||||
### 코드 품질
|
### 코드 품질
|
||||||
|
|
||||||
- ✅ Prisma 의존성 완전 제거
|
- ✅ Prisma 의존성 완전 제거
|
||||||
- ✅ 직접적인 SQL 제어
|
- ✅ 직접적인 SQL 제어
|
||||||
- ✅ 타입 안전성 유지
|
- ✅ 타입 안전성 유지
|
||||||
|
|
||||||
### 성능
|
### 성능
|
||||||
|
|
||||||
- ✅ 불필요한 ORM 오버헤드 제거
|
- ✅ 불필요한 ORM 오버헤드 제거
|
||||||
- ✅ 쿼리 최적화 가능
|
- ✅ 쿼리 최적화 가능
|
||||||
|
|
||||||
### 유지보수성
|
### 유지보수성
|
||||||
|
|
||||||
- ✅ 명확한 SQL 쿼리
|
- ✅ 명확한 SQL 쿼리
|
||||||
- ✅ 디버깅 용이
|
- ✅ 디버깅 용이
|
||||||
- ✅ 데이터베이스 마이그레이션 용이
|
- ✅ 데이터베이스 마이그레이션 용이
|
||||||
|
|
@ -270,6 +294,7 @@ const count = parseInt(result?.count?.toString() || '0', 10);
|
||||||
## 📌 참고사항
|
## 📌 참고사항
|
||||||
|
|
||||||
### Import 변경
|
### Import 변경
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Before
|
// Before
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
@ -280,11 +305,12 @@ import { query, queryOne } from "../database/db";
|
||||||
```
|
```
|
||||||
|
|
||||||
### 타입 정의
|
### 타입 정의
|
||||||
|
|
||||||
- 각 테이블의 타입은 `types/` 디렉토리에서 import
|
- 각 테이블의 타입은 `types/` 디렉토리에서 import
|
||||||
- 필요시 새로운 타입 정의 추가
|
- 필요시 새로운 타입 정의 추가
|
||||||
|
|
||||||
### 에러 처리
|
### 에러 처리
|
||||||
|
|
||||||
- 기존 try-catch 구조 유지
|
- 기존 try-catch 구조 유지
|
||||||
- 적절한 HTTP 상태 코드 반환
|
- 적절한 HTTP 상태 코드 반환
|
||||||
- 사용자 친화적 에러 메시지
|
- 사용자 친화적 에러 메시지
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,492 @@
|
||||||
|
# Phase 4: 남은 Prisma 호출 전환 계획
|
||||||
|
|
||||||
|
## 📊 현재 상황
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
| --------------- | --------------------------------------- |
|
||||||
|
| 총 Prisma 호출 | 29개 |
|
||||||
|
| 대상 파일 | 7개 |
|
||||||
|
| **현재 진행률** | **17/29 (58.6%)** 🔄 **진행 중** |
|
||||||
|
| 복잡도 | 중간 |
|
||||||
|
| 우선순위 | 🔴 높음 (Phase 4) |
|
||||||
|
| **상태** | ⏳ **진행 중** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 파일별 현황
|
||||||
|
|
||||||
|
### ✅ 완료된 파일 (2개)
|
||||||
|
|
||||||
|
1. **adminController.ts** - ✅ **28개 완료**
|
||||||
|
- 사용자 관리: getUserList, getUserInfo, updateUserStatus, deleteUser
|
||||||
|
- 프로필 관리: getMyProfile, updateMyProfile, resetPassword
|
||||||
|
- 사용자 생성/수정: createOrUpdateUser (UPSERT)
|
||||||
|
- 회사 관리: getCompanyList, createCompany, updateCompany, deleteCompany
|
||||||
|
- 부서 관리: getDepartmentList, getDeptInfo
|
||||||
|
- 메뉴 관리: createMenu, updateMenu, deleteMenu
|
||||||
|
- 다국어: getMultiLangKeys, updateLocale
|
||||||
|
|
||||||
|
2. **screenFileController.ts** - ✅ **2개 완료**
|
||||||
|
- getScreenComponentFiles: findMany → query (LIKE)
|
||||||
|
- getComponentFiles: findMany → query (LIKE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⏳ 남은 파일 (5개, 총 12개 호출)
|
||||||
|
|
||||||
|
### 1. webTypeStandardController.ts (11개) 🔴 최우선
|
||||||
|
|
||||||
|
**위치**: `backend-node/src/controllers/webTypeStandardController.ts`
|
||||||
|
|
||||||
|
#### Prisma 호출 목록:
|
||||||
|
|
||||||
|
1. **라인 33**: `getWebTypeStandards()` - findMany
|
||||||
|
```typescript
|
||||||
|
const webTypes = await prisma.web_type_standards.findMany({
|
||||||
|
where, orderBy, select
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **라인 58**: `getWebTypeStandard()` - findUnique
|
||||||
|
```typescript
|
||||||
|
const webTypeData = await prisma.web_type_standards.findUnique({
|
||||||
|
where: { id }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **라인 112**: `createWebTypeStandard()` - findUnique (중복 체크)
|
||||||
|
```typescript
|
||||||
|
const existingWebType = await prisma.web_type_standards.findUnique({
|
||||||
|
where: { web_type: webType }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **라인 123**: `createWebTypeStandard()` - create
|
||||||
|
```typescript
|
||||||
|
const newWebType = await prisma.web_type_standards.create({
|
||||||
|
data: { ... }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **라인 178**: `updateWebTypeStandard()` - findUnique (존재 확인)
|
||||||
|
```typescript
|
||||||
|
const existingWebType = await prisma.web_type_standards.findUnique({
|
||||||
|
where: { id }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **라인 189**: `updateWebTypeStandard()` - update
|
||||||
|
```typescript
|
||||||
|
const updatedWebType = await prisma.web_type_standards.update({
|
||||||
|
where: { id }, data: { ... }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **라인 230**: `deleteWebTypeStandard()` - findUnique (존재 확인)
|
||||||
|
```typescript
|
||||||
|
const existingWebType = await prisma.web_type_standards.findUnique({
|
||||||
|
where: { id }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
8. **라인 241**: `deleteWebTypeStandard()` - delete
|
||||||
|
```typescript
|
||||||
|
await prisma.web_type_standards.delete({
|
||||||
|
where: { id }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
9. **라인 275**: `updateSortOrder()` - $transaction
|
||||||
|
```typescript
|
||||||
|
await prisma.$transaction(
|
||||||
|
updates.map((item) =>
|
||||||
|
prisma.web_type_standards.update({ ... })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
10. **라인 277**: `updateSortOrder()` - update (트랜잭션 내부)
|
||||||
|
|
||||||
|
11. **라인 305**: `getCategories()` - groupBy
|
||||||
|
```typescript
|
||||||
|
const categories = await prisma.web_type_standards.groupBy({
|
||||||
|
by: ['category'], where, _count: true
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**전환 전략**:
|
||||||
|
- findMany → `query<WebTypeStandard>` with dynamic WHERE
|
||||||
|
- findUnique → `queryOne<WebTypeStandard>`
|
||||||
|
- create → `queryOne` with INSERT RETURNING
|
||||||
|
- update → `queryOne` with UPDATE RETURNING
|
||||||
|
- delete → `query` with DELETE
|
||||||
|
- $transaction → `transaction` with client.query
|
||||||
|
- groupBy → `query` with GROUP BY, COUNT
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. fileController.ts (1개) 🟡
|
||||||
|
|
||||||
|
**위치**: `backend-node/src/controllers/fileController.ts`
|
||||||
|
|
||||||
|
#### Prisma 호출:
|
||||||
|
|
||||||
|
1. **라인 726**: `downloadFile()` - findUnique
|
||||||
|
```typescript
|
||||||
|
const fileRecord = await prisma.attach_file_info.findUnique({
|
||||||
|
where: { objid: BigInt(objid) }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**전환 전략**:
|
||||||
|
- findUnique → `queryOne<AttachFileInfo>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. multiConnectionQueryService.ts (4개) 🟢
|
||||||
|
|
||||||
|
**위치**: `backend-node/src/services/multiConnectionQueryService.ts`
|
||||||
|
|
||||||
|
#### Prisma 호출 목록:
|
||||||
|
|
||||||
|
1. **라인 1005**: `executeSelect()` - $queryRawUnsafe
|
||||||
|
```typescript
|
||||||
|
return await prisma.$queryRawUnsafe(query, ...queryParams);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **라인 1022**: `executeInsert()` - $queryRawUnsafe
|
||||||
|
```typescript
|
||||||
|
const insertResult = await prisma.$queryRawUnsafe(...);
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **라인 1055**: `executeUpdate()` - $queryRawUnsafe
|
||||||
|
```typescript
|
||||||
|
return await prisma.$queryRawUnsafe(updateQuery, ...updateParams);
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **라인 1071**: `executeDelete()` - $queryRawUnsafe
|
||||||
|
```typescript
|
||||||
|
return await prisma.$queryRawUnsafe(...);
|
||||||
|
```
|
||||||
|
|
||||||
|
**전환 전략**:
|
||||||
|
- $queryRawUnsafe → `query<any>` (이미 Raw SQL 사용 중)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. config/database.ts (4개) 🟢
|
||||||
|
|
||||||
|
**위치**: `backend-node/src/config/database.ts`
|
||||||
|
|
||||||
|
#### Prisma 호출:
|
||||||
|
|
||||||
|
1. **라인 1**: PrismaClient import
|
||||||
|
2. **라인 17**: prisma 인스턴스 생성
|
||||||
|
3. **라인 22**: `await prisma.$connect()`
|
||||||
|
4. **라인 31, 35, 40**: `await prisma.$disconnect()`
|
||||||
|
|
||||||
|
**전환 전략**:
|
||||||
|
- 이 파일은 데이터베이스 설정 파일이므로 완전히 제거
|
||||||
|
- 기존 `db.ts`의 connection pool로 대체
|
||||||
|
- 모든 import 경로를 `database` → `database/db`로 변경
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. routes/ddlRoutes.ts (2개) 🟢
|
||||||
|
|
||||||
|
**위치**: `backend-node/src/routes/ddlRoutes.ts`
|
||||||
|
|
||||||
|
#### Prisma 호출:
|
||||||
|
|
||||||
|
1. **라인 183-184**: 동적 PrismaClient import
|
||||||
|
```typescript
|
||||||
|
const { PrismaClient } = await import("@prisma/client");
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **라인 186-187**: 연결 테스트
|
||||||
|
```typescript
|
||||||
|
await prisma.$queryRaw`SELECT 1`;
|
||||||
|
await prisma.$disconnect();
|
||||||
|
```
|
||||||
|
|
||||||
|
**전환 전략**:
|
||||||
|
- 동적 import 제거
|
||||||
|
- `query('SELECT 1')` 사용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. routes/companyManagementRoutes.ts (2개) 🟢
|
||||||
|
|
||||||
|
**위치**: `backend-node/src/routes/companyManagementRoutes.ts`
|
||||||
|
|
||||||
|
#### Prisma 호출:
|
||||||
|
|
||||||
|
1. **라인 32**: findUnique (중복 체크)
|
||||||
|
```typescript
|
||||||
|
const existingCompany = await prisma.company_mng.findUnique({
|
||||||
|
where: { company_code }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **라인 61**: update (회사명 업데이트)
|
||||||
|
```typescript
|
||||||
|
await prisma.company_mng.update({
|
||||||
|
where: { company_code }, data: { company_name }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**전환 전략**:
|
||||||
|
- findUnique → `queryOne`
|
||||||
|
- update → `query`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. tests/authService.test.ts (2개) ⚠️
|
||||||
|
|
||||||
|
**위치**: `backend-node/src/tests/authService.test.ts`
|
||||||
|
|
||||||
|
테스트 파일은 별도 처리 필요 (Phase 5에서 처리)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 전환 우선순위
|
||||||
|
|
||||||
|
### Phase 4.1: 컨트롤러 (완료)
|
||||||
|
- [x] screenFileController.ts (2개)
|
||||||
|
- [x] adminController.ts (28개)
|
||||||
|
|
||||||
|
### Phase 4.2: 남은 컨트롤러 (진행 예정)
|
||||||
|
- [ ] webTypeStandardController.ts (11개) - 🔴 최우선
|
||||||
|
- [ ] fileController.ts (1개)
|
||||||
|
|
||||||
|
### Phase 4.3: Routes (진행 예정)
|
||||||
|
- [ ] ddlRoutes.ts (2개)
|
||||||
|
- [ ] companyManagementRoutes.ts (2개)
|
||||||
|
|
||||||
|
### Phase 4.4: Services (진행 예정)
|
||||||
|
- [ ] multiConnectionQueryService.ts (4개)
|
||||||
|
|
||||||
|
### Phase 4.5: Config (진행 예정)
|
||||||
|
- [ ] database.ts (4개) - 전체 파일 제거
|
||||||
|
|
||||||
|
### Phase 4.6: Tests (Phase 5)
|
||||||
|
- [ ] authService.test.ts (2개) - 별도 처리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 체크리스트
|
||||||
|
|
||||||
|
### webTypeStandardController.ts
|
||||||
|
- [ ] Prisma import 제거
|
||||||
|
- [ ] query, queryOne import 추가
|
||||||
|
- [ ] getWebTypeStandards (findMany → query)
|
||||||
|
- [ ] getWebTypeStandard (findUnique → queryOne)
|
||||||
|
- [ ] createWebTypeStandard (findUnique + create → queryOne)
|
||||||
|
- [ ] updateWebTypeStandard (findUnique + update → queryOne)
|
||||||
|
- [ ] deleteWebTypeStandard (findUnique + delete → query)
|
||||||
|
- [ ] updateSortOrder ($transaction → transaction)
|
||||||
|
- [ ] getCategories (groupBy → query with GROUP BY)
|
||||||
|
- [ ] TypeScript 컴파일 확인
|
||||||
|
- [ ] Linter 오류 확인
|
||||||
|
- [ ] 동작 테스트
|
||||||
|
|
||||||
|
### fileController.ts
|
||||||
|
- [ ] Prisma import 제거
|
||||||
|
- [ ] queryOne import 추가
|
||||||
|
- [ ] downloadFile (findUnique → queryOne)
|
||||||
|
- [ ] TypeScript 컴파일 확인
|
||||||
|
|
||||||
|
### routes/ddlRoutes.ts
|
||||||
|
- [ ] 동적 PrismaClient import 제거
|
||||||
|
- [ ] query import 추가
|
||||||
|
- [ ] 연결 테스트 로직 변경
|
||||||
|
- [ ] TypeScript 컴파일 확인
|
||||||
|
|
||||||
|
### routes/companyManagementRoutes.ts
|
||||||
|
- [ ] Prisma import 제거
|
||||||
|
- [ ] query, queryOne import 추가
|
||||||
|
- [ ] findUnique → queryOne
|
||||||
|
- [ ] update → query
|
||||||
|
- [ ] TypeScript 컴파일 확인
|
||||||
|
|
||||||
|
### services/multiConnectionQueryService.ts
|
||||||
|
- [ ] Prisma import 제거
|
||||||
|
- [ ] query import 추가
|
||||||
|
- [ ] $queryRawUnsafe → query (4곳)
|
||||||
|
- [ ] TypeScript 컴파일 확인
|
||||||
|
|
||||||
|
### config/database.ts
|
||||||
|
- [ ] 파일 전체 분석
|
||||||
|
- [ ] 의존성 확인
|
||||||
|
- [ ] 대체 방안 구현
|
||||||
|
- [ ] 모든 import 경로 변경
|
||||||
|
- [ ] 파일 삭제 또는 완전 재작성
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 전환 패턴 요약
|
||||||
|
|
||||||
|
### 1. findMany → query
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
const items = await prisma.table.findMany({ where, orderBy });
|
||||||
|
|
||||||
|
// After
|
||||||
|
const items = await query<T>(`SELECT * FROM table WHERE ... ORDER BY ...`, params);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. findUnique → queryOne
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
const item = await prisma.table.findUnique({ where: { id } });
|
||||||
|
|
||||||
|
// After
|
||||||
|
const item = await queryOne<T>(`SELECT * FROM table WHERE id = $1`, [id]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. create → queryOne with RETURNING
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
const newItem = await prisma.table.create({ data });
|
||||||
|
|
||||||
|
// After
|
||||||
|
const [newItem] = await query<T>(
|
||||||
|
`INSERT INTO table (col1, col2) VALUES ($1, $2) RETURNING *`,
|
||||||
|
[val1, val2]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. update → query with RETURNING
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
const updated = await prisma.table.update({ where, data });
|
||||||
|
|
||||||
|
// After
|
||||||
|
const [updated] = await query<T>(
|
||||||
|
`UPDATE table SET col1 = $1 WHERE id = $2 RETURNING *`,
|
||||||
|
[val1, id]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. delete → query
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
await prisma.table.delete({ where: { id } });
|
||||||
|
|
||||||
|
// After
|
||||||
|
await query(`DELETE FROM table WHERE id = $1`, [id]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. $transaction → transaction
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.table.update({ ... }),
|
||||||
|
prisma.table.update({ ... })
|
||||||
|
]);
|
||||||
|
|
||||||
|
// After
|
||||||
|
await transaction(async (client) => {
|
||||||
|
await client.query(`UPDATE table SET ...`, params1);
|
||||||
|
await client.query(`UPDATE table SET ...`, params2);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. groupBy → query with GROUP BY
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
const result = await prisma.table.groupBy({
|
||||||
|
by: ['category'],
|
||||||
|
_count: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// After
|
||||||
|
const result = await query<T>(
|
||||||
|
`SELECT category, COUNT(*) as count FROM table GROUP BY category`,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 진행 상황
|
||||||
|
|
||||||
|
### 전체 진행률: 17/29 (58.6%)
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1-3: Service Layer ████████████████████████████ 100% (415/415)
|
||||||
|
Phase 4.1: Controllers ████████████████████████████ 100% (30/30)
|
||||||
|
Phase 4.2: 남은 파일 ███████░░░░░░░░░░░░░░░░░░░░ 58% (17/29)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 상세 진행 상황
|
||||||
|
|
||||||
|
| 카테고리 | 완료 | 남음 | 진행률 |
|
||||||
|
| -------------- | ---- | ---- | ------ |
|
||||||
|
| Services | 415 | 0 | 100% |
|
||||||
|
| Controllers | 30 | 11 | 73% |
|
||||||
|
| Routes | 0 | 4 | 0% |
|
||||||
|
| Config | 0 | 4 | 0% |
|
||||||
|
| **총계** | 445 | 19 | 95.9% |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎬 다음 단계
|
||||||
|
|
||||||
|
1. **webTypeStandardController.ts 전환** (11개)
|
||||||
|
- 가장 많은 Prisma 호출을 가진 남은 컨트롤러
|
||||||
|
- 웹 타입 표준 관리 핵심 기능
|
||||||
|
|
||||||
|
2. **fileController.ts 전환** (1개)
|
||||||
|
- 단순 findUnique만 있어 빠르게 처리 가능
|
||||||
|
|
||||||
|
3. **Routes 전환** (4개)
|
||||||
|
- ddlRoutes.ts
|
||||||
|
- companyManagementRoutes.ts
|
||||||
|
|
||||||
|
4. **Service 전환** (4개)
|
||||||
|
- multiConnectionQueryService.ts
|
||||||
|
|
||||||
|
5. **Config 제거** (4개)
|
||||||
|
- database.ts 완전 제거 또는 재작성
|
||||||
|
- 모든 의존성 제거
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 주의사항
|
||||||
|
|
||||||
|
1. **database.ts 처리**
|
||||||
|
- 현재 많은 파일이 `import prisma from '../config/database'` 사용
|
||||||
|
- 모든 import를 `import { query, queryOne } from '../database/db'`로 변경 필요
|
||||||
|
- 단계적으로 진행하여 빌드 오류 방지
|
||||||
|
|
||||||
|
2. **BigInt 처리**
|
||||||
|
- fileController의 `objid: BigInt(objid)` → `objid::bigint` 또는 `CAST(objid AS BIGINT)`
|
||||||
|
|
||||||
|
3. **트랜잭션 처리**
|
||||||
|
- webTypeStandardController의 `updateSortOrder`는 복잡한 트랜잭션
|
||||||
|
- `transaction` 함수 사용 필요
|
||||||
|
|
||||||
|
4. **타입 안전성**
|
||||||
|
- 모든 Raw Query에 명시적 타입 지정 필요
|
||||||
|
- `query<WebTypeStandard>`, `queryOne<AttachFileInfo>` 등
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 완료 후 작업
|
||||||
|
|
||||||
|
- [ ] 전체 컴파일 확인
|
||||||
|
- [ ] Linter 오류 해결
|
||||||
|
- [ ] 통합 테스트 실행
|
||||||
|
- [ ] Prisma 관련 의존성 완전 제거 (package.json)
|
||||||
|
- [ ] `prisma/` 디렉토리 정리
|
||||||
|
- [ ] 문서 업데이트
|
||||||
|
- [ ] 커밋 및 Push
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**작성일**: 2025-10-01
|
||||||
|
**최종 업데이트**: 2025-10-01
|
||||||
|
**상태**: 🔄 진행 중 (58.6% 완료)
|
||||||
|
|
||||||
|
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
## 📊 Prisma 사용 현황 분석
|
## 📊 Prisma 사용 현황 분석
|
||||||
|
|
||||||
**총 42개 파일에서 444개의 Prisma 호출 발견** ⚡ (Scripts 제외)
|
**총 42개 파일에서 444개의 Prisma 호출 발견** ⚡ (Scripts 제외)
|
||||||
|
**현재 진행률: 445/444 (100.2%)** 🎉 **거의 완료!** 남은 12개는 추가 조사 필요
|
||||||
|
|
||||||
### 1. **Prisma 사용 파일 분류**
|
### 1. **Prisma 사용 파일 분류**
|
||||||
|
|
||||||
|
|
@ -1253,13 +1254,18 @@ describe("Performance Benchmarks", () => {
|
||||||
- [ ] EnhancedDynamicFormService (6개), EntityJoinService (5개)
|
- [ ] EnhancedDynamicFormService (6개), EntityJoinService (5개)
|
||||||
- [ ] DataMappingService (5개), DataService (4개)
|
- [ ] DataMappingService (5개), DataService (4개)
|
||||||
- [ ] AdminService (3개), ReferenceCacheService (3개)
|
- [ ] AdminService (3개), ReferenceCacheService (3개)
|
||||||
- [ ] 컨트롤러 레이어 전환 (72개) ⭐ 대규모 신규 발견
|
- [x] **컨트롤러 레이어 전환** ⭐ **진행 중 (17/29, 58.6%)** - [상세 계획서](PHASE4_REMAINING_PRISMA_CALLS.md)
|
||||||
- [ ] AdminController (28개), WebTypeStandardController (11개)
|
- [x] ~~AdminController (28개)~~ ✅ 완료
|
||||||
- [ ] FileController (11개), ButtonActionStandardController (11개)
|
- [x] ~~ScreenFileController (2개)~~ ✅ 완료
|
||||||
- [ ] EntityReferenceController (4개), DataflowExecutionController (3개)
|
- [ ] WebTypeStandardController (11개) 🔄 다음 대상
|
||||||
- [ ] ScreenFileController (2개), DDLRoutes (2개)
|
- [ ] FileController (1개)
|
||||||
- [ ] 설정 및 기반 구조 (6개)
|
- [ ] DDLRoutes (2개)
|
||||||
- [ ] Database.ts (4개), CompanyManagementRoutes (2개)
|
- [ ] CompanyManagementRoutes (2개)
|
||||||
|
- [ ] MultiConnectionQueryService (4개)
|
||||||
|
- [ ] Database.ts (4개 - 제거 예정)
|
||||||
|
- [ ] ~~ButtonActionStandardController (11개)~~ ⚠️ 추가 조사 필요
|
||||||
|
- [ ] ~~EntityReferenceController (4개)~~ ⚠️ 추가 조사 필요
|
||||||
|
- [ ] ~~DataflowExecutionController (3개)~~ ⚠️ 추가 조사 필요
|
||||||
- [ ] 전체 기능 테스트
|
- [ ] 전체 기능 테스트
|
||||||
|
|
||||||
### **Phase 5: Scripts 삭제 (0.5주) - 60개 호출 제거 🗑️**
|
### **Phase 5: Scripts 삭제 (0.5주) - 60개 호출 제거 🗑️**
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,8 +1,6 @@
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import { AuthenticatedRequest } from "../types/auth";
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
|
import { query, queryOne, pool } from "../database/db";
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export class ButtonActionStandardController {
|
export class ButtonActionStandardController {
|
||||||
// 버튼 액션 목록 조회
|
// 버튼 액션 목록 조회
|
||||||
|
|
@ -10,33 +8,36 @@ export class ButtonActionStandardController {
|
||||||
try {
|
try {
|
||||||
const { active, category, search } = req.query;
|
const { active, category, search } = req.query;
|
||||||
|
|
||||||
const where: any = {};
|
const whereConditions: string[] = [];
|
||||||
|
const queryParams: any[] = [];
|
||||||
|
let paramIndex = 1;
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
where.is_active = active as string;
|
whereConditions.push(`is_active = $${paramIndex}`);
|
||||||
|
queryParams.push(active as string);
|
||||||
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category) {
|
if (category) {
|
||||||
where.category = category as string;
|
whereConditions.push(`category = $${paramIndex}`);
|
||||||
|
queryParams.push(category as string);
|
||||||
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
where.OR = [
|
whereConditions.push(`(action_name ILIKE $${paramIndex} OR action_name_eng ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`);
|
||||||
{ action_name: { contains: search as string, mode: "insensitive" } },
|
queryParams.push(`%${search}%`);
|
||||||
{
|
paramIndex++;
|
||||||
action_name_eng: {
|
|
||||||
contains: search as string,
|
|
||||||
mode: "insensitive",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ description: { contains: search as string, mode: "insensitive" } },
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttonActions = await prisma.button_action_standards.findMany({
|
const whereClause = whereConditions.length > 0
|
||||||
where,
|
? `WHERE ${whereConditions.join(" AND ")}`
|
||||||
orderBy: [{ sort_order: "asc" }, { action_type: "asc" }],
|
: "";
|
||||||
});
|
|
||||||
|
const buttonActions = await query<any>(
|
||||||
|
`SELECT * FROM button_action_standards ${whereClause} ORDER BY sort_order ASC, action_type ASC`,
|
||||||
|
queryParams
|
||||||
|
);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -58,9 +59,10 @@ export class ButtonActionStandardController {
|
||||||
try {
|
try {
|
||||||
const { actionType } = req.params;
|
const { actionType } = req.params;
|
||||||
|
|
||||||
const buttonAction = await prisma.button_action_standards.findUnique({
|
const buttonAction = await queryOne<any>(
|
||||||
where: { action_type: actionType },
|
"SELECT * FROM button_action_standards WHERE action_type = $1 LIMIT 1",
|
||||||
});
|
[actionType]
|
||||||
|
);
|
||||||
|
|
||||||
if (!buttonAction) {
|
if (!buttonAction) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
|
|
@ -115,9 +117,10 @@ export class ButtonActionStandardController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 중복 체크
|
// 중복 체크
|
||||||
const existingAction = await prisma.button_action_standards.findUnique({
|
const existingAction = await queryOne<any>(
|
||||||
where: { action_type },
|
"SELECT * FROM button_action_standards WHERE action_type = $1 LIMIT 1",
|
||||||
});
|
[action_type]
|
||||||
|
);
|
||||||
|
|
||||||
if (existingAction) {
|
if (existingAction) {
|
||||||
return res.status(409).json({
|
return res.status(409).json({
|
||||||
|
|
@ -126,28 +129,25 @@ export class ButtonActionStandardController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newButtonAction = await prisma.button_action_standards.create({
|
const [newButtonAction] = await query<any>(
|
||||||
data: {
|
`INSERT INTO button_action_standards (
|
||||||
action_type,
|
action_type, action_name, action_name_eng, description, category,
|
||||||
action_name,
|
default_text, default_text_eng, default_icon, default_color, default_variant,
|
||||||
action_name_eng,
|
confirmation_required, confirmation_message, validation_rules, action_config,
|
||||||
description,
|
sort_order, is_active, created_by, updated_by, created_date, updated_date
|
||||||
category,
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, NOW(), NOW())
|
||||||
default_text,
|
RETURNING *`,
|
||||||
default_text_eng,
|
[
|
||||||
default_icon,
|
action_type, action_name, action_name_eng, description, category,
|
||||||
default_color,
|
default_text, default_text_eng, default_icon, default_color, default_variant,
|
||||||
default_variant,
|
confirmation_required, confirmation_message,
|
||||||
confirmation_required,
|
validation_rules ? JSON.stringify(validation_rules) : null,
|
||||||
confirmation_message,
|
action_config ? JSON.stringify(action_config) : null,
|
||||||
validation_rules,
|
sort_order, is_active,
|
||||||
action_config,
|
req.user?.userId || "system",
|
||||||
sort_order,
|
req.user?.userId || "system"
|
||||||
is_active,
|
]
|
||||||
created_by: req.user?.userId || "system",
|
);
|
||||||
updated_by: req.user?.userId || "system",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(201).json({
|
return res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -187,9 +187,10 @@ export class ButtonActionStandardController {
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// 존재 여부 확인
|
// 존재 여부 확인
|
||||||
const existingAction = await prisma.button_action_standards.findUnique({
|
const existingAction = await queryOne<any>(
|
||||||
where: { action_type: actionType },
|
"SELECT * FROM button_action_standards WHERE action_type = $1 LIMIT 1",
|
||||||
});
|
[actionType]
|
||||||
|
);
|
||||||
|
|
||||||
if (!existingAction) {
|
if (!existingAction) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
|
|
@ -198,28 +199,101 @@ export class ButtonActionStandardController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedButtonAction = await prisma.button_action_standards.update({
|
const updateFields: string[] = [];
|
||||||
where: { action_type: actionType },
|
const updateParams: any[] = [];
|
||||||
data: {
|
let paramIndex = 1;
|
||||||
action_name,
|
|
||||||
action_name_eng,
|
if (action_name !== undefined) {
|
||||||
description,
|
updateFields.push(`action_name = $${paramIndex}`);
|
||||||
category,
|
updateParams.push(action_name);
|
||||||
default_text,
|
paramIndex++;
|
||||||
default_text_eng,
|
}
|
||||||
default_icon,
|
if (action_name_eng !== undefined) {
|
||||||
default_color,
|
updateFields.push(`action_name_eng = $${paramIndex}`);
|
||||||
default_variant,
|
updateParams.push(action_name_eng);
|
||||||
confirmation_required,
|
paramIndex++;
|
||||||
confirmation_message,
|
}
|
||||||
validation_rules,
|
if (description !== undefined) {
|
||||||
action_config,
|
updateFields.push(`description = $${paramIndex}`);
|
||||||
sort_order,
|
updateParams.push(description);
|
||||||
is_active,
|
paramIndex++;
|
||||||
updated_by: req.user?.userId || "system",
|
}
|
||||||
updated_date: new Date(),
|
if (category !== undefined) {
|
||||||
},
|
updateFields.push(`category = $${paramIndex}`);
|
||||||
});
|
updateParams.push(category);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
if (default_text !== undefined) {
|
||||||
|
updateFields.push(`default_text = $${paramIndex}`);
|
||||||
|
updateParams.push(default_text);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
if (default_text_eng !== undefined) {
|
||||||
|
updateFields.push(`default_text_eng = $${paramIndex}`);
|
||||||
|
updateParams.push(default_text_eng);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
if (default_icon !== undefined) {
|
||||||
|
updateFields.push(`default_icon = $${paramIndex}`);
|
||||||
|
updateParams.push(default_icon);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
if (default_color !== undefined) {
|
||||||
|
updateFields.push(`default_color = $${paramIndex}`);
|
||||||
|
updateParams.push(default_color);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
if (default_variant !== undefined) {
|
||||||
|
updateFields.push(`default_variant = $${paramIndex}`);
|
||||||
|
updateParams.push(default_variant);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
if (confirmation_required !== undefined) {
|
||||||
|
updateFields.push(`confirmation_required = $${paramIndex}`);
|
||||||
|
updateParams.push(confirmation_required);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
if (confirmation_message !== undefined) {
|
||||||
|
updateFields.push(`confirmation_message = $${paramIndex}`);
|
||||||
|
updateParams.push(confirmation_message);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
if (validation_rules !== undefined) {
|
||||||
|
updateFields.push(`validation_rules = $${paramIndex}`);
|
||||||
|
updateParams.push(validation_rules ? JSON.stringify(validation_rules) : null);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
if (action_config !== undefined) {
|
||||||
|
updateFields.push(`action_config = $${paramIndex}`);
|
||||||
|
updateParams.push(action_config ? JSON.stringify(action_config) : null);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
if (sort_order !== undefined) {
|
||||||
|
updateFields.push(`sort_order = $${paramIndex}`);
|
||||||
|
updateParams.push(sort_order);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
if (is_active !== undefined) {
|
||||||
|
updateFields.push(`is_active = $${paramIndex}`);
|
||||||
|
updateParams.push(is_active);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFields.push(`updated_by = $${paramIndex}`);
|
||||||
|
updateParams.push(req.user?.userId || "system");
|
||||||
|
paramIndex++;
|
||||||
|
|
||||||
|
updateFields.push(`updated_date = $${paramIndex}`);
|
||||||
|
updateParams.push(new Date());
|
||||||
|
paramIndex++;
|
||||||
|
|
||||||
|
updateParams.push(actionType);
|
||||||
|
|
||||||
|
const [updatedButtonAction] = await query<any>(
|
||||||
|
`UPDATE button_action_standards SET ${updateFields.join(", ")}
|
||||||
|
WHERE action_type = $${paramIndex} RETURNING *`,
|
||||||
|
updateParams
|
||||||
|
);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -242,9 +316,10 @@ export class ButtonActionStandardController {
|
||||||
const { actionType } = req.params;
|
const { actionType } = req.params;
|
||||||
|
|
||||||
// 존재 여부 확인
|
// 존재 여부 확인
|
||||||
const existingAction = await prisma.button_action_standards.findUnique({
|
const existingAction = await queryOne<any>(
|
||||||
where: { action_type: actionType },
|
"SELECT * FROM button_action_standards WHERE action_type = $1 LIMIT 1",
|
||||||
});
|
[actionType]
|
||||||
|
);
|
||||||
|
|
||||||
if (!existingAction) {
|
if (!existingAction) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
|
|
@ -253,9 +328,10 @@ export class ButtonActionStandardController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.button_action_standards.delete({
|
await query<any>(
|
||||||
where: { action_type: actionType },
|
"DELETE FROM button_action_standards WHERE action_type = $1",
|
||||||
});
|
[actionType]
|
||||||
|
);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -287,18 +363,26 @@ export class ButtonActionStandardController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 트랜잭션으로 일괄 업데이트
|
// 트랜잭션으로 일괄 업데이트
|
||||||
await prisma.$transaction(
|
const client = await pool.connect();
|
||||||
buttonActions.map((item) =>
|
try {
|
||||||
prisma.button_action_standards.update({
|
await client.query("BEGIN");
|
||||||
where: { action_type: item.action_type },
|
|
||||||
data: {
|
for (const item of buttonActions) {
|
||||||
sort_order: item.sort_order,
|
await client.query(
|
||||||
updated_by: req.user?.userId || "system",
|
`UPDATE button_action_standards
|
||||||
updated_date: new Date(),
|
SET sort_order = $1, updated_by = $2, updated_date = $3
|
||||||
},
|
WHERE action_type = $4`,
|
||||||
})
|
[item.sort_order, req.user?.userId || "system", new Date(), item.action_type]
|
||||||
)
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -317,19 +401,17 @@ export class ButtonActionStandardController {
|
||||||
// 버튼 액션 카테고리 목록 조회
|
// 버튼 액션 카테고리 목록 조회
|
||||||
static async getButtonActionCategories(req: Request, res: Response) {
|
static async getButtonActionCategories(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const categories = await prisma.button_action_standards.groupBy({
|
const categories = await query<{ category: string; count: string }>(
|
||||||
by: ["category"],
|
`SELECT category, COUNT(*) as count
|
||||||
where: {
|
FROM button_action_standards
|
||||||
is_active: "Y",
|
WHERE is_active = $1
|
||||||
},
|
GROUP BY category`,
|
||||||
_count: {
|
["Y"]
|
||||||
category: true,
|
);
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const categoryList = categories.map((item) => ({
|
const categoryList = categories.map((item) => ({
|
||||||
category: item.category,
|
category: item.category,
|
||||||
count: item._count.category,
|
count: parseInt(item.count),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* 🔥 데이터플로우 실행 컨트롤러
|
* 🔥 데이터플로우 실행 컨트롤러
|
||||||
*
|
*
|
||||||
* 버튼 제어에서 관계 실행 시 사용되는 컨트롤러
|
* 버튼 제어에서 관계 실행 시 사용되는 컨트롤러
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { AuthenticatedRequest } from "../types/auth";
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
import prisma from "../config/database";
|
import { query } from "../database/db";
|
||||||
import logger from "../utils/logger";
|
import logger from "../utils/logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -146,18 +146,18 @@ async function executeInsert(tableName: string, data: Record<string, any>): Prom
|
||||||
const values = Object.values(data);
|
const values = Object.values(data);
|
||||||
const placeholders = values.map((_, index) => `$${index + 1}`).join(', ');
|
const placeholders = values.map((_, index) => `$${index + 1}`).join(', ');
|
||||||
|
|
||||||
const query = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders}) RETURNING *`;
|
const insertQuery = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders}) RETURNING *`;
|
||||||
|
|
||||||
logger.info(`INSERT 쿼리 실행:`, { query, values });
|
logger.info(`INSERT 쿼리 실행:`, { query: insertQuery, values });
|
||||||
|
|
||||||
|
const result = await query<any>(insertQuery, values);
|
||||||
|
|
||||||
const result = await prisma.$queryRawUnsafe(query, ...values);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
action: 'insert',
|
action: 'insert',
|
||||||
tableName,
|
tableName,
|
||||||
data: result,
|
data: result,
|
||||||
affectedRows: Array.isArray(result) ? result.length : 1,
|
affectedRows: result.length,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`INSERT 실행 오류:`, error);
|
logger.error(`INSERT 실행 오류:`, error);
|
||||||
|
|
@ -172,7 +172,7 @@ async function executeUpdate(tableName: string, data: Record<string, any>): Prom
|
||||||
try {
|
try {
|
||||||
// ID 또는 기본키를 기준으로 업데이트
|
// ID 또는 기본키를 기준으로 업데이트
|
||||||
const { id, ...updateData } = data;
|
const { id, ...updateData } = data;
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new Error('UPDATE를 위한 ID가 필요합니다');
|
throw new Error('UPDATE를 위한 ID가 필요합니다');
|
||||||
}
|
}
|
||||||
|
|
@ -180,20 +180,20 @@ async function executeUpdate(tableName: string, data: Record<string, any>): Prom
|
||||||
const setClause = Object.keys(updateData)
|
const setClause = Object.keys(updateData)
|
||||||
.map((key, index) => `${key} = $${index + 1}`)
|
.map((key, index) => `${key} = $${index + 1}`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
const values = Object.values(updateData);
|
|
||||||
const query = `UPDATE ${tableName} SET ${setClause} WHERE id = $${values.length + 1} RETURNING *`;
|
|
||||||
|
|
||||||
logger.info(`UPDATE 쿼리 실행:`, { query, values: [...values, id] });
|
|
||||||
|
|
||||||
const result = await prisma.$queryRawUnsafe(query, ...values, id);
|
const values = Object.values(updateData);
|
||||||
|
const updateQuery = `UPDATE ${tableName} SET ${setClause} WHERE id = $${values.length + 1} RETURNING *`;
|
||||||
|
|
||||||
|
logger.info(`UPDATE 쿼리 실행:`, { query: updateQuery, values: [...values, id] });
|
||||||
|
|
||||||
|
const result = await query<any>(updateQuery, [...values, id]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
action: 'update',
|
action: 'update',
|
||||||
tableName,
|
tableName,
|
||||||
data: result,
|
data: result,
|
||||||
affectedRows: Array.isArray(result) ? result.length : 1,
|
affectedRows: result.length,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`UPDATE 실행 오류:`, error);
|
logger.error(`UPDATE 실행 오류:`, error);
|
||||||
|
|
@ -226,23 +226,23 @@ async function executeUpsert(tableName: string, data: Record<string, any>): Prom
|
||||||
async function executeDelete(tableName: string, data: Record<string, any>): Promise<any> {
|
async function executeDelete(tableName: string, data: Record<string, any>): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const { id } = data;
|
const { id } = data;
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new Error('DELETE를 위한 ID가 필요합니다');
|
throw new Error('DELETE를 위한 ID가 필요합니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = `DELETE FROM ${tableName} WHERE id = $1 RETURNING *`;
|
const deleteQuery = `DELETE FROM ${tableName} WHERE id = $1 RETURNING *`;
|
||||||
|
|
||||||
logger.info(`DELETE 쿼리 실행:`, { query, values: [id] });
|
logger.info(`DELETE 쿼리 실행:`, { query: deleteQuery, values: [id] });
|
||||||
|
|
||||||
|
const result = await query<any>(deleteQuery, [id]);
|
||||||
|
|
||||||
const result = await prisma.$queryRawUnsafe(query, id);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
action: 'delete',
|
action: 'delete',
|
||||||
tableName,
|
tableName,
|
||||||
data: result,
|
data: result,
|
||||||
affectedRows: Array.isArray(result) ? result.length : 1,
|
affectedRows: result.length,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`DELETE 실행 오류:`, error);
|
logger.error(`DELETE 실행 오류:`, error);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { query, queryOne } from "../database/db";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export interface EntityReferenceOption {
|
export interface EntityReferenceOption {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -39,12 +37,12 @@ export class EntityReferenceController {
|
||||||
});
|
});
|
||||||
|
|
||||||
// 컬럼 정보 조회
|
// 컬럼 정보 조회
|
||||||
const columnInfo = await prisma.column_labels.findFirst({
|
const columnInfo = await queryOne<any>(
|
||||||
where: {
|
`SELECT * FROM column_labels
|
||||||
table_name: tableName,
|
WHERE table_name = $1 AND column_name = $2
|
||||||
column_name: columnName,
|
LIMIT 1`,
|
||||||
},
|
[tableName, columnName]
|
||||||
});
|
);
|
||||||
|
|
||||||
if (!columnInfo) {
|
if (!columnInfo) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
|
|
@ -76,7 +74,7 @@ export class EntityReferenceController {
|
||||||
|
|
||||||
// 참조 테이블이 실제로 존재하는지 확인
|
// 참조 테이블이 실제로 존재하는지 확인
|
||||||
try {
|
try {
|
||||||
await prisma.$queryRawUnsafe(`SELECT 1 FROM ${referenceTable} LIMIT 1`);
|
await query<any>(`SELECT 1 FROM ${referenceTable} LIMIT 1`);
|
||||||
logger.info(
|
logger.info(
|
||||||
`Entity 참조 설정: ${tableName}.${columnName} -> ${referenceTable}.${referenceColumn} (display: ${displayColumn})`
|
`Entity 참조 설정: ${tableName}.${columnName} -> ${referenceTable}.${referenceColumn} (display: ${displayColumn})`
|
||||||
);
|
);
|
||||||
|
|
@ -92,26 +90,26 @@ export class EntityReferenceController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 동적 쿼리로 참조 데이터 조회
|
// 동적 쿼리로 참조 데이터 조회
|
||||||
let query = `SELECT ${referenceColumn}, ${displayColumn} as display_name FROM ${referenceTable}`;
|
let sqlQuery = `SELECT ${referenceColumn}, ${displayColumn} as display_name FROM ${referenceTable}`;
|
||||||
const queryParams: any[] = [];
|
const queryParams: any[] = [];
|
||||||
|
|
||||||
// 검색 조건 추가
|
// 검색 조건 추가
|
||||||
if (search) {
|
if (search) {
|
||||||
query += ` WHERE ${displayColumn} ILIKE $1`;
|
sqlQuery += ` WHERE ${displayColumn} ILIKE $1`;
|
||||||
queryParams.push(`%${search}%`);
|
queryParams.push(`%${search}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
query += ` ORDER BY ${displayColumn} LIMIT $${queryParams.length + 1}`;
|
sqlQuery += ` ORDER BY ${displayColumn} LIMIT $${queryParams.length + 1}`;
|
||||||
queryParams.push(Number(limit));
|
queryParams.push(Number(limit));
|
||||||
|
|
||||||
logger.info(`실행할 쿼리: ${query}`, {
|
logger.info(`실행할 쿼리: ${sqlQuery}`, {
|
||||||
queryParams,
|
queryParams,
|
||||||
referenceTable,
|
referenceTable,
|
||||||
referenceColumn,
|
referenceColumn,
|
||||||
displayColumn,
|
displayColumn,
|
||||||
});
|
});
|
||||||
|
|
||||||
const referenceData = await prisma.$queryRawUnsafe(query, ...queryParams);
|
const referenceData = await query<any>(sqlQuery, queryParams);
|
||||||
|
|
||||||
// 옵션 형태로 변환
|
// 옵션 형태로 변환
|
||||||
const options: EntityReferenceOption[] = (referenceData as any[]).map(
|
const options: EntityReferenceOption[] = (referenceData as any[]).map(
|
||||||
|
|
@ -158,29 +156,22 @@ export class EntityReferenceController {
|
||||||
});
|
});
|
||||||
|
|
||||||
// code_info 테이블에서 코드 데이터 조회
|
// code_info 테이블에서 코드 데이터 조회
|
||||||
let whereCondition: any = {
|
const queryParams: any[] = [codeCategory, 'Y'];
|
||||||
code_category: codeCategory,
|
let sqlQuery = `
|
||||||
is_active: "Y",
|
SELECT code_value, code_name
|
||||||
};
|
FROM code_info
|
||||||
|
WHERE code_category = $1 AND is_active = $2
|
||||||
|
`;
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
whereCondition.code_name = {
|
sqlQuery += ` AND code_name ILIKE $3`;
|
||||||
contains: String(search),
|
queryParams.push(`%${search}%`);
|
||||||
mode: "insensitive",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeData = await prisma.code_info.findMany({
|
sqlQuery += ` ORDER BY code_name ASC LIMIT $${queryParams.length + 1}`;
|
||||||
where: whereCondition,
|
queryParams.push(Number(limit));
|
||||||
select: {
|
|
||||||
code_value: true,
|
const codeData = await query<any>(sqlQuery, queryParams);
|
||||||
code_name: true,
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
code_name: "asc",
|
|
||||||
},
|
|
||||||
take: Number(limit),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 옵션 형태로 변환
|
// 옵션 형태로 변환
|
||||||
const options: EntityReferenceOption[] = codeData.map((code) => ({
|
const options: EntityReferenceOption[] = codeData.map((code) => ({
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,8 @@ import { AuthenticatedRequest } from "../types/auth";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import { generateUUID } from "../utils/generateId";
|
import { generateUUID } from "../utils/generateId";
|
||||||
|
import { query, queryOne } from "../database/db";
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// 임시 토큰 저장소 (메모리 기반, 실제 운영에서는 Redis 사용 권장)
|
// 임시 토큰 저장소 (메모리 기반, 실제 운영에서는 Redis 사용 권장)
|
||||||
const tempTokens = new Map<string, { objid: string; expires: number }>();
|
const tempTokens = new Map<string, { objid: string; expires: number }>();
|
||||||
|
|
@ -283,27 +281,22 @@ export const uploadFiles = async (
|
||||||
const fullFilePath = `/uploads${relativePath}`;
|
const fullFilePath = `/uploads${relativePath}`;
|
||||||
|
|
||||||
// attach_file_info 테이블에 저장
|
// attach_file_info 테이블에 저장
|
||||||
const fileRecord = await prisma.attach_file_info.create({
|
const objidValue = parseInt(
|
||||||
data: {
|
generateUUID().replace(/-/g, "").substring(0, 15),
|
||||||
objid: parseInt(
|
16
|
||||||
generateUUID().replace(/-/g, "").substring(0, 15),
|
);
|
||||||
16
|
|
||||||
),
|
const [fileRecord] = await query<any>(
|
||||||
target_objid: finalTargetObjid,
|
`INSERT INTO attach_file_info (
|
||||||
saved_file_name: file.filename,
|
objid, target_objid, saved_file_name, real_file_name, doc_type, doc_type_name,
|
||||||
real_file_name: decodedOriginalName,
|
file_size, file_ext, file_path, company_code, writer, regdate, status, parent_target_objid
|
||||||
doc_type: docType,
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||||
doc_type_name: docTypeName,
|
RETURNING *`,
|
||||||
file_size: file.size,
|
[
|
||||||
file_ext: fileExt,
|
objidValue, finalTargetObjid, file.filename, decodedOriginalName, docType, docTypeName,
|
||||||
file_path: fullFilePath, // 회사별 디렉토리 포함된 경로
|
file.size, fileExt, fullFilePath, companyCode, writer, new Date(), "ACTIVE", parentTargetObjid
|
||||||
company_code: companyCode, // 회사코드 추가
|
]
|
||||||
writer: writer,
|
);
|
||||||
regdate: new Date(),
|
|
||||||
status: "ACTIVE",
|
|
||||||
parent_target_objid: parentTargetObjid,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
savedFiles.push({
|
savedFiles.push({
|
||||||
objid: fileRecord.objid.toString(),
|
objid: fileRecord.objid.toString(),
|
||||||
|
|
@ -350,14 +343,10 @@ export const deleteFile = async (
|
||||||
const { writer = "system" } = req.body;
|
const { writer = "system" } = req.body;
|
||||||
|
|
||||||
// 파일 상태를 DELETED로 변경 (논리적 삭제)
|
// 파일 상태를 DELETED로 변경 (논리적 삭제)
|
||||||
const deletedFile = await prisma.attach_file_info.update({
|
await query<any>(
|
||||||
where: {
|
"UPDATE attach_file_info SET status = $1 WHERE objid = $2",
|
||||||
objid: parseInt(objid),
|
["DELETED", parseInt(objid)]
|
||||||
},
|
);
|
||||||
data: {
|
|
||||||
status: "DELETED",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -387,17 +376,12 @@ export const getLinkedFiles = async (
|
||||||
const baseTargetObjid = `${tableName}:${recordId}`;
|
const baseTargetObjid = `${tableName}:${recordId}`;
|
||||||
|
|
||||||
// 기본 target_objid와 파일 컬럼 패턴 모두 조회 (tableName:recordId% 패턴)
|
// 기본 target_objid와 파일 컬럼 패턴 모두 조회 (tableName:recordId% 패턴)
|
||||||
const files = await prisma.attach_file_info.findMany({
|
const files = await query<any>(
|
||||||
where: {
|
`SELECT * FROM attach_file_info
|
||||||
target_objid: {
|
WHERE target_objid LIKE $1 AND status = $2
|
||||||
startsWith: baseTargetObjid, // tableName:recordId로 시작하는 모든 파일
|
ORDER BY regdate DESC`,
|
||||||
},
|
[`${baseTargetObjid}%`, "ACTIVE"]
|
||||||
status: "ACTIVE",
|
);
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
regdate: "desc",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileList = files.map((file: any) => ({
|
const fileList = files.map((file: any) => ({
|
||||||
objid: file.objid.toString(),
|
objid: file.objid.toString(),
|
||||||
|
|
@ -441,24 +425,28 @@ export const getFileList = async (
|
||||||
try {
|
try {
|
||||||
const { targetObjid, docType, companyCode } = req.query;
|
const { targetObjid, docType, companyCode } = req.query;
|
||||||
|
|
||||||
const where: any = {
|
const whereConditions: string[] = ["status = $1"];
|
||||||
status: "ACTIVE",
|
const queryParams: any[] = ["ACTIVE"];
|
||||||
};
|
let paramIndex = 2;
|
||||||
|
|
||||||
if (targetObjid) {
|
if (targetObjid) {
|
||||||
where.target_objid = targetObjid as string;
|
whereConditions.push(`target_objid = $${paramIndex}`);
|
||||||
|
queryParams.push(targetObjid as string);
|
||||||
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (docType) {
|
if (docType) {
|
||||||
where.doc_type = docType as string;
|
whereConditions.push(`doc_type = $${paramIndex}`);
|
||||||
|
queryParams.push(docType as string);
|
||||||
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = await prisma.attach_file_info.findMany({
|
const files = await query<any>(
|
||||||
where,
|
`SELECT * FROM attach_file_info
|
||||||
orderBy: {
|
WHERE ${whereConditions.join(" AND ")}
|
||||||
regdate: "desc",
|
ORDER BY regdate DESC`,
|
||||||
},
|
queryParams
|
||||||
});
|
);
|
||||||
|
|
||||||
const fileList = files.map((file: any) => ({
|
const fileList = files.map((file: any) => ({
|
||||||
objid: file.objid.toString(),
|
objid: file.objid.toString(),
|
||||||
|
|
@ -523,31 +511,22 @@ export const getComponentFiles = async (
|
||||||
console.log("🔍 [getComponentFiles] 템플릿 파일 조회:", { templateTargetObjid });
|
console.log("🔍 [getComponentFiles] 템플릿 파일 조회:", { templateTargetObjid });
|
||||||
|
|
||||||
// 모든 파일 조회해서 실제 저장된 target_objid 패턴 확인
|
// 모든 파일 조회해서 실제 저장된 target_objid 패턴 확인
|
||||||
const allFiles = await prisma.attach_file_info.findMany({
|
const allFiles = await query<any>(
|
||||||
where: {
|
`SELECT target_objid, real_file_name, regdate
|
||||||
status: "ACTIVE",
|
FROM attach_file_info
|
||||||
},
|
WHERE status = $1
|
||||||
select: {
|
ORDER BY regdate DESC
|
||||||
target_objid: true,
|
LIMIT 10`,
|
||||||
real_file_name: true,
|
["ACTIVE"]
|
||||||
regdate: true,
|
);
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
regdate: "desc",
|
|
||||||
},
|
|
||||||
take: 10,
|
|
||||||
});
|
|
||||||
console.log("🗂️ [getComponentFiles] 최근 저장된 파일들의 target_objid:", allFiles.map(f => ({ target_objid: f.target_objid, name: f.real_file_name })));
|
console.log("🗂️ [getComponentFiles] 최근 저장된 파일들의 target_objid:", allFiles.map(f => ({ target_objid: f.target_objid, name: f.real_file_name })));
|
||||||
|
|
||||||
const templateFiles = await prisma.attach_file_info.findMany({
|
const templateFiles = await query<any>(
|
||||||
where: {
|
`SELECT * FROM attach_file_info
|
||||||
target_objid: templateTargetObjid,
|
WHERE target_objid = $1 AND status = $2
|
||||||
status: "ACTIVE",
|
ORDER BY regdate DESC`,
|
||||||
},
|
[templateTargetObjid, "ACTIVE"]
|
||||||
orderBy: {
|
);
|
||||||
regdate: "desc",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("📁 [getComponentFiles] 템플릿 파일 결과:", templateFiles.length);
|
console.log("📁 [getComponentFiles] 템플릿 파일 결과:", templateFiles.length);
|
||||||
|
|
||||||
|
|
@ -555,15 +534,12 @@ export const getComponentFiles = async (
|
||||||
let dataFiles: any[] = [];
|
let dataFiles: any[] = [];
|
||||||
if (tableName && recordId && columnName) {
|
if (tableName && recordId && columnName) {
|
||||||
const dataTargetObjid = `${tableName}:${recordId}:${columnName}`;
|
const dataTargetObjid = `${tableName}:${recordId}:${columnName}`;
|
||||||
dataFiles = await prisma.attach_file_info.findMany({
|
dataFiles = await query<any>(
|
||||||
where: {
|
`SELECT * FROM attach_file_info
|
||||||
target_objid: dataTargetObjid,
|
WHERE target_objid = $1 AND status = $2
|
||||||
status: "ACTIVE",
|
ORDER BY regdate DESC`,
|
||||||
},
|
[dataTargetObjid, "ACTIVE"]
|
||||||
orderBy: {
|
);
|
||||||
regdate: "desc",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 파일 정보 포맷팅 함수
|
// 파일 정보 포맷팅 함수
|
||||||
|
|
@ -628,11 +604,10 @@ export const previewFile = async (
|
||||||
const { objid } = req.params;
|
const { objid } = req.params;
|
||||||
const { serverFilename } = req.query;
|
const { serverFilename } = req.query;
|
||||||
|
|
||||||
const fileRecord = await prisma.attach_file_info.findUnique({
|
const fileRecord = await queryOne<any>(
|
||||||
where: {
|
"SELECT * FROM attach_file_info WHERE objid = $1 LIMIT 1",
|
||||||
objid: parseInt(objid),
|
[parseInt(objid)]
|
||||||
},
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (!fileRecord || fileRecord.status !== "ACTIVE") {
|
if (!fileRecord || fileRecord.status !== "ACTIVE") {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
|
|
@ -842,9 +817,10 @@ export const generateTempToken = async (req: AuthenticatedRequest, res: Response
|
||||||
}
|
}
|
||||||
|
|
||||||
// 파일 존재 확인
|
// 파일 존재 확인
|
||||||
const fileRecord = await prisma.attach_file_info.findUnique({
|
const fileRecord = await queryOne<any>(
|
||||||
where: { objid: objid },
|
"SELECT * FROM attach_file_info WHERE objid = $1 LIMIT 1",
|
||||||
});
|
[objid]
|
||||||
|
);
|
||||||
|
|
||||||
if (!fileRecord) {
|
if (!fileRecord) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
|
|
@ -924,9 +900,10 @@ export const getFileByToken = async (req: Request, res: Response) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 파일 정보 조회
|
// 파일 정보 조회
|
||||||
const fileRecord = await prisma.attach_file_info.findUnique({
|
const fileRecord = await queryOne<any>(
|
||||||
where: { objid: tokenData.objid },
|
"SELECT * FROM attach_file_info WHERE objid = $1 LIMIT 1",
|
||||||
});
|
[tokenData.objid]
|
||||||
|
);
|
||||||
|
|
||||||
if (!fileRecord) {
|
if (!fileRecord) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from "express";
|
||||||
import { AuthenticatedRequest } from '../middleware/authMiddleware';
|
import { AuthenticatedRequest } from "../middleware/authMiddleware";
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { query } from "../database/db";
|
||||||
import logger from '../utils/logger';
|
import logger from "../utils/logger";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 화면 컴포넌트별 파일 정보 조회 및 복원
|
* 화면 컴포넌트별 파일 정보 조회 및 복원
|
||||||
|
|
@ -14,37 +12,33 @@ export const getScreenComponentFiles = async (
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { screenId } = req.params;
|
const { screenId } = req.params;
|
||||||
|
|
||||||
logger.info(`화면 컴포넌트 파일 조회 시작: screenId=${screenId}`);
|
logger.info(`화면 컴포넌트 파일 조회 시작: screenId=${screenId}`);
|
||||||
|
|
||||||
// screen_files: 접두사로 해당 화면의 모든 파일 조회
|
// screen_files: 접두사로 해당 화면의 모든 파일 조회
|
||||||
const targetObjidPattern = `screen_files:${screenId}:%`;
|
const targetObjidPattern = `screen_files:${screenId}:%`;
|
||||||
|
|
||||||
const files = await prisma.attach_file_info.findMany({
|
const files = await query<any>(
|
||||||
where: {
|
`SELECT * FROM attach_file_info
|
||||||
target_objid: {
|
WHERE target_objid LIKE $1
|
||||||
startsWith: `screen_files:${screenId}:`
|
AND status = 'ACTIVE'
|
||||||
},
|
ORDER BY regdate DESC`,
|
||||||
status: 'ACTIVE'
|
[`screen_files:${screenId}:%`]
|
||||||
},
|
);
|
||||||
orderBy: {
|
|
||||||
regdate: 'desc'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 컴포넌트별로 파일 그룹화
|
// 컴포넌트별로 파일 그룹화
|
||||||
const componentFiles: { [componentId: string]: any[] } = {};
|
const componentFiles: { [componentId: string]: any[] } = {};
|
||||||
|
|
||||||
files.forEach(file => {
|
files.forEach((file) => {
|
||||||
// target_objid 형식: screen_files:screenId:componentId:fieldName
|
// target_objid 형식: screen_files:screenId:componentId:fieldName
|
||||||
const targetParts = file.target_objid?.split(':') || [];
|
const targetParts = file.target_objid?.split(":") || [];
|
||||||
if (targetParts.length >= 3) {
|
if (targetParts.length >= 3) {
|
||||||
const componentId = targetParts[2];
|
const componentId = targetParts[2];
|
||||||
|
|
||||||
if (!componentFiles[componentId]) {
|
if (!componentFiles[componentId]) {
|
||||||
componentFiles[componentId] = [];
|
componentFiles[componentId] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
componentFiles[componentId].push({
|
componentFiles[componentId].push({
|
||||||
objid: file.objid.toString(),
|
objid: file.objid.toString(),
|
||||||
savedFileName: file.saved_file_name,
|
savedFileName: file.saved_file_name,
|
||||||
|
|
@ -58,26 +52,27 @@ export const getScreenComponentFiles = async (
|
||||||
parentTargetObjid: file.parent_target_objid,
|
parentTargetObjid: file.parent_target_objid,
|
||||||
writer: file.writer,
|
writer: file.writer,
|
||||||
regdate: file.regdate?.toISOString(),
|
regdate: file.regdate?.toISOString(),
|
||||||
status: file.status
|
status: file.status,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`화면 컴포넌트 파일 조회 완료: ${Object.keys(componentFiles).length}개 컴포넌트, 총 ${files.length}개 파일`);
|
logger.info(
|
||||||
|
`화면 컴포넌트 파일 조회 완료: ${Object.keys(componentFiles).length}개 컴포넌트, 총 ${files.length}개 파일`
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
componentFiles: componentFiles,
|
componentFiles: componentFiles,
|
||||||
totalFiles: files.length,
|
totalFiles: files.length,
|
||||||
componentCount: Object.keys(componentFiles).length
|
componentCount: Object.keys(componentFiles).length,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('화면 컴포넌트 파일 조회 오류:', error);
|
logger.error("화면 컴포넌트 파일 조회 오류:", error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '화면 컴포넌트 파일 조회 중 오류가 발생했습니다.',
|
message: "화면 컴포넌트 파일 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : '알 수 없는 오류'
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -91,25 +86,23 @@ export const getComponentFiles = async (
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { screenId, componentId } = req.params;
|
const { screenId, componentId } = req.params;
|
||||||
|
|
||||||
logger.info(`컴포넌트 파일 조회: screenId=${screenId}, componentId=${componentId}`);
|
logger.info(
|
||||||
|
`컴포넌트 파일 조회: screenId=${screenId}, componentId=${componentId}`
|
||||||
|
);
|
||||||
|
|
||||||
// target_objid 패턴: screen_files:screenId:componentId:*
|
// target_objid 패턴: screen_files:screenId:componentId:*
|
||||||
const targetObjidPattern = `screen_files:${screenId}:${componentId}:`;
|
const targetObjidPattern = `screen_files:${screenId}:${componentId}:`;
|
||||||
|
|
||||||
const files = await prisma.attach_file_info.findMany({
|
|
||||||
where: {
|
|
||||||
target_objid: {
|
|
||||||
startsWith: targetObjidPattern
|
|
||||||
},
|
|
||||||
status: 'ACTIVE'
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
regdate: 'desc'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileList = files.map(file => ({
|
const files = await query<any>(
|
||||||
|
`SELECT * FROM attach_file_info
|
||||||
|
WHERE target_objid LIKE $1
|
||||||
|
AND status = 'ACTIVE'
|
||||||
|
ORDER BY regdate DESC`,
|
||||||
|
[`${targetObjidPattern}%`]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileList = files.map((file) => ({
|
||||||
objid: file.objid.toString(),
|
objid: file.objid.toString(),
|
||||||
savedFileName: file.saved_file_name,
|
savedFileName: file.saved_file_name,
|
||||||
realFileName: file.real_file_name,
|
realFileName: file.real_file_name,
|
||||||
|
|
@ -122,7 +115,7 @@ export const getComponentFiles = async (
|
||||||
parentTargetObjid: file.parent_target_objid,
|
parentTargetObjid: file.parent_target_objid,
|
||||||
writer: file.writer,
|
writer: file.writer,
|
||||||
regdate: file.regdate?.toISOString(),
|
regdate: file.regdate?.toISOString(),
|
||||||
status: file.status
|
status: file.status,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
logger.info(`컴포넌트 파일 조회 완료: ${fileList.length}개 파일`);
|
logger.info(`컴포넌트 파일 조회 완료: ${fileList.length}개 파일`);
|
||||||
|
|
@ -131,15 +124,14 @@ export const getComponentFiles = async (
|
||||||
success: true,
|
success: true,
|
||||||
files: fileList,
|
files: fileList,
|
||||||
componentId: componentId,
|
componentId: componentId,
|
||||||
screenId: screenId
|
screenId: screenId,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('컴포넌트 파일 조회 오류:', error);
|
logger.error("컴포넌트 파일 조회 오류:", error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '컴포넌트 파일 조회 중 오류가 발생했습니다.',
|
message: "컴포넌트 파일 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : '알 수 없는 오류'
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ export class AdminService {
|
||||||
|
|
||||||
// 기존 Java의 selectAdminMenuList 쿼리를 Raw Query로 포팅
|
// 기존 Java의 selectAdminMenuList 쿼리를 Raw Query로 포팅
|
||||||
// WITH RECURSIVE 쿼리 구현
|
// WITH RECURSIVE 쿼리 구현
|
||||||
const menuList = await query<any>(`
|
const menuList = await query<any>(
|
||||||
|
`
|
||||||
WITH RECURSIVE v_menu(
|
WITH RECURSIVE v_menu(
|
||||||
LEVEL,
|
LEVEL,
|
||||||
MENU_TYPE,
|
MENU_TYPE,
|
||||||
|
|
@ -188,7 +189,9 @@ export class AdminService {
|
||||||
LEFT JOIN COMPANY_MNG CM ON A.COMPANY_CODE = CM.COMPANY_CODE
|
LEFT JOIN COMPANY_MNG CM ON A.COMPANY_CODE = CM.COMPANY_CODE
|
||||||
WHERE 1 = 1
|
WHERE 1 = 1
|
||||||
ORDER BY PATH, SEQ
|
ORDER BY PATH, SEQ
|
||||||
`, [userLang]);
|
`,
|
||||||
|
[userLang]
|
||||||
|
);
|
||||||
|
|
||||||
logger.info(`관리자 메뉴 목록 조회 결과: ${menuList.length}개`);
|
logger.info(`관리자 메뉴 목록 조회 결과: ${menuList.length}개`);
|
||||||
if (menuList.length > 0) {
|
if (menuList.length > 0) {
|
||||||
|
|
@ -212,7 +215,8 @@ export class AdminService {
|
||||||
const { userLang = "ko" } = paramMap;
|
const { userLang = "ko" } = paramMap;
|
||||||
|
|
||||||
// 기존 Java의 selectUserMenuList 쿼리를 Raw Query로 포팅
|
// 기존 Java의 selectUserMenuList 쿼리를 Raw Query로 포팅
|
||||||
const menuList = await query<any>(`
|
const menuList = await query<any>(
|
||||||
|
`
|
||||||
WITH RECURSIVE v_menu(
|
WITH RECURSIVE v_menu(
|
||||||
LEVEL,
|
LEVEL,
|
||||||
MENU_TYPE,
|
MENU_TYPE,
|
||||||
|
|
@ -313,7 +317,9 @@ export class AdminService {
|
||||||
LEFT JOIN MULTI_LANG_TEXT MLT_DESC ON MLKM_DESC.key_id = MLT_DESC.key_id AND MLT_DESC.lang_code = $1
|
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
|
WHERE 1 = 1
|
||||||
ORDER BY PATH, SEQ
|
ORDER BY PATH, SEQ
|
||||||
`, [userLang]);
|
`,
|
||||||
|
[userLang]
|
||||||
|
);
|
||||||
|
|
||||||
logger.info(`사용자 메뉴 목록 조회 결과: ${menuList.length}개`);
|
logger.info(`사용자 메뉴 목록 조회 결과: ${menuList.length}개`);
|
||||||
if (menuList.length > 0) {
|
if (menuList.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
CreateBatchExecutionLogRequest,
|
CreateBatchExecutionLogRequest,
|
||||||
UpdateBatchExecutionLogRequest,
|
UpdateBatchExecutionLogRequest,
|
||||||
BatchExecutionLogFilter,
|
BatchExecutionLogFilter,
|
||||||
BatchExecutionLogWithConfig
|
BatchExecutionLogWithConfig,
|
||||||
} from "../types/batchExecutionLogTypes";
|
} from "../types/batchExecutionLogTypes";
|
||||||
import { ApiResponse } from "../types/batchTypes";
|
import { ApiResponse } from "../types/batchTypes";
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ export class BatchExecutionLogService {
|
||||||
start_date,
|
start_date,
|
||||||
end_date,
|
end_date,
|
||||||
page = 1,
|
page = 1,
|
||||||
limit = 50
|
limit = 50,
|
||||||
} = filter;
|
} = filter;
|
||||||
|
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
@ -35,28 +35,31 @@ export class BatchExecutionLogService {
|
||||||
const whereConditions: string[] = [];
|
const whereConditions: string[] = [];
|
||||||
const params: any[] = [];
|
const params: any[] = [];
|
||||||
let paramIndex = 1;
|
let paramIndex = 1;
|
||||||
|
|
||||||
if (batch_config_id) {
|
if (batch_config_id) {
|
||||||
whereConditions.push(`bel.batch_config_id = $${paramIndex++}`);
|
whereConditions.push(`bel.batch_config_id = $${paramIndex++}`);
|
||||||
params.push(batch_config_id);
|
params.push(batch_config_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (execution_status) {
|
if (execution_status) {
|
||||||
whereConditions.push(`bel.execution_status = $${paramIndex++}`);
|
whereConditions.push(`bel.execution_status = $${paramIndex++}`);
|
||||||
params.push(execution_status);
|
params.push(execution_status);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start_date) {
|
if (start_date) {
|
||||||
whereConditions.push(`bel.start_time >= $${paramIndex++}`);
|
whereConditions.push(`bel.start_time >= $${paramIndex++}`);
|
||||||
params.push(start_date);
|
params.push(start_date);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (end_date) {
|
if (end_date) {
|
||||||
whereConditions.push(`bel.start_time <= $${paramIndex++}`);
|
whereConditions.push(`bel.start_time <= $${paramIndex++}`);
|
||||||
params.push(end_date);
|
params.push(end_date);
|
||||||
}
|
}
|
||||||
|
|
||||||
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';
|
const whereClause =
|
||||||
|
whereConditions.length > 0
|
||||||
|
? `WHERE ${whereConditions.join(" AND ")}`
|
||||||
|
: "";
|
||||||
|
|
||||||
// 로그 조회 (batch_config 정보 포함)
|
// 로그 조회 (batch_config 정보 포함)
|
||||||
const sql = `
|
const sql = `
|
||||||
|
|
@ -75,7 +78,7 @@ export class BatchExecutionLogService {
|
||||||
ORDER BY bel.start_time DESC
|
ORDER BY bel.start_time DESC
|
||||||
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const countSql = `
|
const countSql = `
|
||||||
SELECT COUNT(*) as count
|
SELECT COUNT(*) as count
|
||||||
FROM batch_execution_logs bel
|
FROM batch_execution_logs bel
|
||||||
|
|
@ -86,10 +89,10 @@ export class BatchExecutionLogService {
|
||||||
|
|
||||||
const [logs, countResult] = await Promise.all([
|
const [logs, countResult] = await Promise.all([
|
||||||
query<any>(sql, params),
|
query<any>(sql, params),
|
||||||
query<{ count: number }>(countSql, params.slice(0, -2))
|
query<{ count: number }>(countSql, params.slice(0, -2)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const total = parseInt(countResult[0]?.count?.toString() || '0', 10);
|
const total = parseInt(countResult[0]?.count?.toString() || "0", 10);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -98,15 +101,15 @@ export class BatchExecutionLogService {
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
total,
|
total,
|
||||||
totalPages: Math.ceil(total / limit)
|
totalPages: Math.ceil(total / limit),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("배치 실행 로그 조회 실패:", error);
|
console.error("배치 실행 로그 조회 실패:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 실행 로그 조회 중 오류가 발생했습니다.",
|
message: "배치 실행 로그 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,22 +139,22 @@ export class BatchExecutionLogService {
|
||||||
data.failed_records || 0,
|
data.failed_records || 0,
|
||||||
data.error_message,
|
data.error_message,
|
||||||
data.error_details,
|
data.error_details,
|
||||||
data.server_name || process.env.HOSTNAME || 'unknown',
|
data.server_name || process.env.HOSTNAME || "unknown",
|
||||||
data.process_id || process.pid?.toString()
|
data.process_id || process.pid?.toString(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: log as BatchExecutionLog,
|
data: log as BatchExecutionLog,
|
||||||
message: "배치 실행 로그가 생성되었습니다."
|
message: "배치 실행 로그가 생성되었습니다.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("배치 실행 로그 생성 실패:", error);
|
console.error("배치 실행 로그 생성 실패:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 실행 로그 생성 중 오류가 발생했습니다.",
|
message: "배치 실행 로그 생성 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -206,7 +209,7 @@ export class BatchExecutionLogService {
|
||||||
|
|
||||||
const log = await queryOne<BatchExecutionLog>(
|
const log = await queryOne<BatchExecutionLog>(
|
||||||
`UPDATE batch_execution_logs
|
`UPDATE batch_execution_logs
|
||||||
SET ${updates.join(', ')}
|
SET ${updates.join(", ")}
|
||||||
WHERE id = $${paramIndex}
|
WHERE id = $${paramIndex}
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
params
|
params
|
||||||
|
|
@ -215,14 +218,14 @@ export class BatchExecutionLogService {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: log as BatchExecutionLog,
|
data: log as BatchExecutionLog,
|
||||||
message: "배치 실행 로그가 업데이트되었습니다."
|
message: "배치 실행 로그가 업데이트되었습니다.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("배치 실행 로그 업데이트 실패:", error);
|
console.error("배치 실행 로그 업데이트 실패:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 실행 로그 업데이트 중 오류가 발생했습니다.",
|
message: "배치 실행 로그 업데이트 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -236,14 +239,14 @@ export class BatchExecutionLogService {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "배치 실행 로그가 삭제되었습니다."
|
message: "배치 실행 로그가 삭제되었습니다.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("배치 실행 로그 삭제 실패:", error);
|
console.error("배치 실행 로그 삭제 실패:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 실행 로그 삭제 중 오류가 발생했습니다.",
|
message: "배치 실행 로그 삭제 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -265,14 +268,14 @@ export class BatchExecutionLogService {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: log || null
|
data: log || null,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("최신 배치 실행 로그 조회 실패:", error);
|
console.error("최신 배치 실행 로그 조회 실패:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "최신 배치 실행 로그 조회 중 오류가 발생했습니다.",
|
message: "최신 배치 실행 로그 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -284,35 +287,40 @@ export class BatchExecutionLogService {
|
||||||
batchConfigId?: number,
|
batchConfigId?: number,
|
||||||
startDate?: Date,
|
startDate?: Date,
|
||||||
endDate?: Date
|
endDate?: Date
|
||||||
): Promise<ApiResponse<{
|
): Promise<
|
||||||
total_executions: number;
|
ApiResponse<{
|
||||||
success_count: number;
|
total_executions: number;
|
||||||
failed_count: number;
|
success_count: number;
|
||||||
success_rate: number;
|
failed_count: number;
|
||||||
average_duration_ms: number;
|
success_rate: number;
|
||||||
total_records_processed: number;
|
average_duration_ms: number;
|
||||||
}>> {
|
total_records_processed: number;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
try {
|
try {
|
||||||
const whereConditions: string[] = [];
|
const whereConditions: string[] = [];
|
||||||
const params: any[] = [];
|
const params: any[] = [];
|
||||||
let paramIndex = 1;
|
let paramIndex = 1;
|
||||||
|
|
||||||
if (batchConfigId) {
|
if (batchConfigId) {
|
||||||
whereConditions.push(`batch_config_id = $${paramIndex++}`);
|
whereConditions.push(`batch_config_id = $${paramIndex++}`);
|
||||||
params.push(batchConfigId);
|
params.push(batchConfigId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startDate) {
|
if (startDate) {
|
||||||
whereConditions.push(`start_time >= $${paramIndex++}`);
|
whereConditions.push(`start_time >= $${paramIndex++}`);
|
||||||
params.push(startDate);
|
params.push(startDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endDate) {
|
if (endDate) {
|
||||||
whereConditions.push(`start_time <= $${paramIndex++}`);
|
whereConditions.push(`start_time <= $${paramIndex++}`);
|
||||||
params.push(endDate);
|
params.push(endDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';
|
const whereClause =
|
||||||
|
whereConditions.length > 0
|
||||||
|
? `WHERE ${whereConditions.join(" AND ")}`
|
||||||
|
: "";
|
||||||
|
|
||||||
const logs = await query<{
|
const logs = await query<{
|
||||||
execution_status: string;
|
execution_status: string;
|
||||||
|
|
@ -326,17 +334,26 @@ export class BatchExecutionLogService {
|
||||||
);
|
);
|
||||||
|
|
||||||
const total_executions = logs.length;
|
const total_executions = logs.length;
|
||||||
const success_count = logs.filter((log: any) => log.execution_status === 'SUCCESS').length;
|
const success_count = logs.filter(
|
||||||
const failed_count = logs.filter((log: any) => log.execution_status === 'FAILED').length;
|
(log: any) => log.execution_status === "SUCCESS"
|
||||||
const success_rate = total_executions > 0 ? (success_count / total_executions) * 100 : 0;
|
).length;
|
||||||
|
const failed_count = logs.filter(
|
||||||
|
(log: any) => log.execution_status === "FAILED"
|
||||||
|
).length;
|
||||||
|
const success_rate =
|
||||||
|
total_executions > 0 ? (success_count / total_executions) * 100 : 0;
|
||||||
|
|
||||||
const validDurations = logs
|
const validDurations = logs
|
||||||
.filter((log: any) => log.duration_ms !== null)
|
.filter((log: any) => log.duration_ms !== null)
|
||||||
.map((log: any) => log.duration_ms!);
|
.map((log: any) => log.duration_ms!);
|
||||||
const average_duration_ms = validDurations.length > 0
|
const average_duration_ms =
|
||||||
? validDurations.reduce((sum: number, duration: number) => sum + duration, 0) / validDurations.length
|
validDurations.length > 0
|
||||||
: 0;
|
? validDurations.reduce(
|
||||||
|
(sum: number, duration: number) => sum + duration,
|
||||||
|
0
|
||||||
|
) / validDurations.length
|
||||||
|
: 0;
|
||||||
|
|
||||||
const total_records_processed = logs
|
const total_records_processed = logs
|
||||||
.filter((log: any) => log.total_records !== null)
|
.filter((log: any) => log.total_records !== null)
|
||||||
.reduce((sum: number, log: any) => sum + (log.total_records || 0), 0);
|
.reduce((sum: number, log: any) => sum + (log.total_records || 0), 0);
|
||||||
|
|
@ -349,15 +366,15 @@ export class BatchExecutionLogService {
|
||||||
failed_count,
|
failed_count,
|
||||||
success_rate,
|
success_rate,
|
||||||
average_duration_ms,
|
average_duration_ms,
|
||||||
total_records_processed
|
total_records_processed,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("배치 실행 통계 조회 실패:", error);
|
console.error("배치 실행 통계 조회 실패:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 실행 통계 조회 중 오류가 발생했습니다.",
|
message: "배치 실행 통계 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,7 +7,7 @@ import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory";
|
||||||
|
|
||||||
// 배치관리 전용 타입 정의
|
// 배치관리 전용 타입 정의
|
||||||
export interface BatchConnectionInfo {
|
export interface BatchConnectionInfo {
|
||||||
type: 'internal' | 'external';
|
type: "internal" | "external";
|
||||||
id?: number;
|
id?: number;
|
||||||
name: string;
|
name: string;
|
||||||
db_type?: string;
|
db_type?: string;
|
||||||
|
|
@ -37,15 +37,17 @@ export class BatchManagementService {
|
||||||
/**
|
/**
|
||||||
* 배치관리용 연결 목록 조회
|
* 배치관리용 연결 목록 조회
|
||||||
*/
|
*/
|
||||||
static async getAvailableConnections(): Promise<BatchApiResponse<BatchConnectionInfo[]>> {
|
static async getAvailableConnections(): Promise<
|
||||||
|
BatchApiResponse<BatchConnectionInfo[]>
|
||||||
|
> {
|
||||||
try {
|
try {
|
||||||
const connections: BatchConnectionInfo[] = [];
|
const connections: BatchConnectionInfo[] = [];
|
||||||
|
|
||||||
// 내부 DB 추가
|
// 내부 DB 추가
|
||||||
connections.push({
|
connections.push({
|
||||||
type: 'internal',
|
type: "internal",
|
||||||
name: '내부 데이터베이스 (PostgreSQL)',
|
name: "내부 데이터베이스 (PostgreSQL)",
|
||||||
db_type: 'postgresql'
|
db_type: "postgresql",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 활성화된 외부 DB 연결 조회
|
// 활성화된 외부 DB 연결 조회
|
||||||
|
|
@ -63,26 +65,26 @@ export class BatchManagementService {
|
||||||
);
|
);
|
||||||
|
|
||||||
// 외부 DB 연결 추가
|
// 외부 DB 연결 추가
|
||||||
externalConnections.forEach(conn => {
|
externalConnections.forEach((conn) => {
|
||||||
connections.push({
|
connections.push({
|
||||||
type: 'external',
|
type: "external",
|
||||||
id: conn.id,
|
id: conn.id,
|
||||||
name: `${conn.connection_name} (${conn.db_type?.toUpperCase()})`,
|
name: `${conn.connection_name} (${conn.db_type?.toUpperCase()})`,
|
||||||
db_type: conn.db_type || undefined
|
db_type: conn.db_type || undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: connections,
|
data: connections,
|
||||||
message: `${connections.length}개의 연결을 조회했습니다.`
|
message: `${connections.length}개의 연결을 조회했습니다.`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("배치관리 연결 목록 조회 실패:", error);
|
console.error("배치관리 연결 목록 조회 실패:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "연결 목록 조회 중 오류가 발생했습니다.",
|
message: "연결 목록 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -91,13 +93,13 @@ export class BatchManagementService {
|
||||||
* 배치관리용 테이블 목록 조회
|
* 배치관리용 테이블 목록 조회
|
||||||
*/
|
*/
|
||||||
static async getTablesFromConnection(
|
static async getTablesFromConnection(
|
||||||
connectionType: 'internal' | 'external',
|
connectionType: "internal" | "external",
|
||||||
connectionId?: number
|
connectionId?: number
|
||||||
): Promise<BatchApiResponse<BatchTableInfo[]>> {
|
): Promise<BatchApiResponse<BatchTableInfo[]>> {
|
||||||
try {
|
try {
|
||||||
let tables: BatchTableInfo[] = [];
|
let tables: BatchTableInfo[] = [];
|
||||||
|
|
||||||
if (connectionType === 'internal') {
|
if (connectionType === "internal") {
|
||||||
// 내부 DB 테이블 조회
|
// 내부 DB 테이블 조회
|
||||||
const result = await query<{ table_name: string }>(
|
const result = await query<{ table_name: string }>(
|
||||||
`SELECT table_name
|
`SELECT table_name
|
||||||
|
|
@ -108,11 +110,11 @@ export class BatchManagementService {
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
tables = result.map(row => ({
|
tables = result.map((row) => ({
|
||||||
table_name: row.table_name,
|
table_name: row.table_name,
|
||||||
columns: []
|
columns: [],
|
||||||
}));
|
}));
|
||||||
} else if (connectionType === 'external' && connectionId) {
|
} else if (connectionType === "external" && connectionId) {
|
||||||
// 외부 DB 테이블 조회
|
// 외부 DB 테이블 조회
|
||||||
const tablesResult = await this.getExternalTables(connectionId);
|
const tablesResult = await this.getExternalTables(connectionId);
|
||||||
if (tablesResult.success && tablesResult.data) {
|
if (tablesResult.success && tablesResult.data) {
|
||||||
|
|
@ -123,14 +125,14 @@ export class BatchManagementService {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: tables,
|
data: tables,
|
||||||
message: `${tables.length}개의 테이블을 조회했습니다.`
|
message: `${tables.length}개의 테이블을 조회했습니다.`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("배치관리 테이블 목록 조회 실패:", error);
|
console.error("배치관리 테이블 목록 조회 실패:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "테이블 목록 조회 중 오류가 발생했습니다.",
|
message: "테이블 목록 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +141,7 @@ export class BatchManagementService {
|
||||||
* 배치관리용 테이블 컬럼 정보 조회
|
* 배치관리용 테이블 컬럼 정보 조회
|
||||||
*/
|
*/
|
||||||
static async getTableColumns(
|
static async getTableColumns(
|
||||||
connectionType: 'internal' | 'external',
|
connectionType: "internal" | "external",
|
||||||
connectionId: number | undefined,
|
connectionId: number | undefined,
|
||||||
tableName: string
|
tableName: string
|
||||||
): Promise<BatchApiResponse<BatchColumnInfo[]>> {
|
): Promise<BatchApiResponse<BatchColumnInfo[]>> {
|
||||||
|
|
@ -147,20 +149,22 @@ export class BatchManagementService {
|
||||||
console.log(`[BatchManagementService] getTableColumns 호출:`, {
|
console.log(`[BatchManagementService] getTableColumns 호출:`, {
|
||||||
connectionType,
|
connectionType,
|
||||||
connectionId,
|
connectionId,
|
||||||
tableName
|
tableName,
|
||||||
});
|
});
|
||||||
|
|
||||||
let columns: BatchColumnInfo[] = [];
|
let columns: BatchColumnInfo[] = [];
|
||||||
|
|
||||||
if (connectionType === 'internal') {
|
if (connectionType === "internal") {
|
||||||
// 내부 DB 컬럼 조회
|
// 내부 DB 컬럼 조회
|
||||||
console.log(`[BatchManagementService] 내부 DB 컬럼 조회 시작: ${tableName}`);
|
console.log(
|
||||||
|
`[BatchManagementService] 내부 DB 컬럼 조회 시작: ${tableName}`
|
||||||
|
);
|
||||||
|
|
||||||
const result = await query<{
|
const result = await query<{
|
||||||
column_name: string;
|
column_name: string;
|
||||||
data_type: string;
|
data_type: string;
|
||||||
is_nullable: string;
|
is_nullable: string;
|
||||||
column_default: string | null
|
column_default: string | null;
|
||||||
}>(
|
}>(
|
||||||
`SELECT
|
`SELECT
|
||||||
column_name,
|
column_name,
|
||||||
|
|
@ -178,19 +182,27 @@ export class BatchManagementService {
|
||||||
|
|
||||||
console.log(`[BatchManagementService] 내부 DB 컬럼 조회 결과:`, result);
|
console.log(`[BatchManagementService] 내부 DB 컬럼 조회 결과:`, result);
|
||||||
|
|
||||||
columns = result.map(row => ({
|
columns = result.map((row) => ({
|
||||||
column_name: row.column_name,
|
column_name: row.column_name,
|
||||||
data_type: row.data_type,
|
data_type: row.data_type,
|
||||||
is_nullable: row.is_nullable,
|
is_nullable: row.is_nullable,
|
||||||
column_default: row.column_default,
|
column_default: row.column_default,
|
||||||
}));
|
}));
|
||||||
} else if (connectionType === 'external' && connectionId) {
|
} else if (connectionType === "external" && connectionId) {
|
||||||
// 외부 DB 컬럼 조회
|
// 외부 DB 컬럼 조회
|
||||||
console.log(`[BatchManagementService] 외부 DB 컬럼 조회 시작: connectionId=${connectionId}, tableName=${tableName}`);
|
console.log(
|
||||||
|
`[BatchManagementService] 외부 DB 컬럼 조회 시작: connectionId=${connectionId}, tableName=${tableName}`
|
||||||
|
);
|
||||||
|
|
||||||
const columnsResult = await this.getExternalTableColumns(connectionId, tableName);
|
const columnsResult = await this.getExternalTableColumns(
|
||||||
|
connectionId,
|
||||||
console.log(`[BatchManagementService] 외부 DB 컬럼 조회 결과:`, columnsResult);
|
tableName
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[BatchManagementService] 외부 DB 컬럼 조회 결과:`,
|
||||||
|
columnsResult
|
||||||
|
);
|
||||||
|
|
||||||
if (columnsResult.success && columnsResult.data) {
|
if (columnsResult.success && columnsResult.data) {
|
||||||
columns = columnsResult.data;
|
columns = columnsResult.data;
|
||||||
|
|
@ -201,14 +213,14 @@ export class BatchManagementService {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: columns,
|
data: columns,
|
||||||
message: `${columns.length}개의 컬럼을 조회했습니다.`
|
message: `${columns.length}개의 컬럼을 조회했습니다.`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[BatchManagementService] 컬럼 정보 조회 오류:", error);
|
console.error("[BatchManagementService] 컬럼 정보 조회 오류:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
|
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -216,7 +228,9 @@ export class BatchManagementService {
|
||||||
/**
|
/**
|
||||||
* 외부 DB 테이블 목록 조회 (내부 구현)
|
* 외부 DB 테이블 목록 조회 (내부 구현)
|
||||||
*/
|
*/
|
||||||
private static async getExternalTables(connectionId: number): Promise<BatchApiResponse<BatchTableInfo[]>> {
|
private static async getExternalTables(
|
||||||
|
connectionId: number
|
||||||
|
): Promise<BatchApiResponse<BatchTableInfo[]>> {
|
||||||
try {
|
try {
|
||||||
// 연결 정보 조회
|
// 연결 정보 조회
|
||||||
const connection = await queryOne<any>(
|
const connection = await queryOne<any>(
|
||||||
|
|
@ -227,7 +241,7 @@ export class BatchManagementService {
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "연결 정보를 찾을 수 없습니다."
|
message: "연결 정보를 찾을 수 없습니다.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,7 +250,7 @@ export class BatchManagementService {
|
||||||
if (!decryptedPassword) {
|
if (!decryptedPassword) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "비밀번호 복호화에 실패했습니다."
|
message: "비밀번호 복호화에 실패했습니다.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,26 +261,39 @@ export class BatchManagementService {
|
||||||
database: connection.database_name,
|
database: connection.database_name,
|
||||||
user: connection.username,
|
user: connection.username,
|
||||||
password: decryptedPassword,
|
password: decryptedPassword,
|
||||||
connectionTimeoutMillis: connection.connection_timeout != null ? connection.connection_timeout * 1000 : undefined,
|
connectionTimeoutMillis:
|
||||||
queryTimeoutMillis: connection.query_timeout != null ? connection.query_timeout * 1000 : undefined,
|
connection.connection_timeout != null
|
||||||
ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
|
? connection.connection_timeout * 1000
|
||||||
|
: undefined,
|
||||||
|
queryTimeoutMillis:
|
||||||
|
connection.query_timeout != null
|
||||||
|
? connection.query_timeout * 1000
|
||||||
|
: undefined,
|
||||||
|
ssl:
|
||||||
|
connection.ssl_enabled === "Y"
|
||||||
|
? { rejectUnauthorized: false }
|
||||||
|
: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// DatabaseConnectorFactory를 통한 테이블 목록 조회
|
// DatabaseConnectorFactory를 통한 테이블 목록 조회
|
||||||
const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, connectionId);
|
const connector = await DatabaseConnectorFactory.createConnector(
|
||||||
|
connection.db_type,
|
||||||
|
config,
|
||||||
|
connectionId
|
||||||
|
);
|
||||||
const tables = await connector.getTables();
|
const tables = await connector.getTables();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "테이블 목록을 조회했습니다.",
|
message: "테이블 목록을 조회했습니다.",
|
||||||
data: tables
|
data: tables,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("외부 DB 테이블 목록 조회 오류:", error);
|
console.error("외부 DB 테이블 목록 조회 오류:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "테이블 목록 조회 중 오류가 발생했습니다.",
|
message: "테이블 목록 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -274,10 +301,15 @@ export class BatchManagementService {
|
||||||
/**
|
/**
|
||||||
* 외부 DB 테이블 컬럼 정보 조회 (내부 구현)
|
* 외부 DB 테이블 컬럼 정보 조회 (내부 구현)
|
||||||
*/
|
*/
|
||||||
private static async getExternalTableColumns(connectionId: number, tableName: string): Promise<BatchApiResponse<BatchColumnInfo[]>> {
|
private static async getExternalTableColumns(
|
||||||
|
connectionId: number,
|
||||||
|
tableName: string
|
||||||
|
): Promise<BatchApiResponse<BatchColumnInfo[]>> {
|
||||||
try {
|
try {
|
||||||
console.log(`[BatchManagementService] getExternalTableColumns 호출: connectionId=${connectionId}, tableName=${tableName}`);
|
console.log(
|
||||||
|
`[BatchManagementService] getExternalTableColumns 호출: connectionId=${connectionId}, tableName=${tableName}`
|
||||||
|
);
|
||||||
|
|
||||||
// 연결 정보 조회
|
// 연결 정보 조회
|
||||||
const connection = await queryOne<any>(
|
const connection = await queryOne<any>(
|
||||||
`SELECT * FROM external_db_connections WHERE id = $1`,
|
`SELECT * FROM external_db_connections WHERE id = $1`,
|
||||||
|
|
@ -285,10 +317,12 @@ export class BatchManagementService {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
console.log(`[BatchManagementService] 연결 정보를 찾을 수 없음: connectionId=${connectionId}`);
|
console.log(
|
||||||
|
`[BatchManagementService] 연결 정보를 찾을 수 없음: connectionId=${connectionId}`
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "연결 정보를 찾을 수 없습니다."
|
message: "연결 정보를 찾을 수 없습니다.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,12 +332,12 @@ export class BatchManagementService {
|
||||||
db_type: connection.db_type,
|
db_type: connection.db_type,
|
||||||
host: connection.host,
|
host: connection.host,
|
||||||
port: connection.port,
|
port: connection.port,
|
||||||
database_name: connection.database_name
|
database_name: connection.database_name,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 비밀번호 복호화
|
// 비밀번호 복호화
|
||||||
const decryptedPassword = PasswordEncryption.decrypt(connection.password);
|
const decryptedPassword = PasswordEncryption.decrypt(connection.password);
|
||||||
|
|
||||||
// 연결 설정 준비
|
// 연결 설정 준비
|
||||||
const config = {
|
const config = {
|
||||||
host: connection.host,
|
host: connection.host,
|
||||||
|
|
@ -311,38 +345,61 @@ export class BatchManagementService {
|
||||||
database: connection.database_name,
|
database: connection.database_name,
|
||||||
user: connection.username,
|
user: connection.username,
|
||||||
password: decryptedPassword,
|
password: decryptedPassword,
|
||||||
connectionTimeoutMillis: connection.connection_timeout != null ? connection.connection_timeout * 1000 : undefined,
|
connectionTimeoutMillis:
|
||||||
queryTimeoutMillis: connection.query_timeout != null ? connection.query_timeout * 1000 : undefined,
|
connection.connection_timeout != null
|
||||||
ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
|
? connection.connection_timeout * 1000
|
||||||
|
: undefined,
|
||||||
|
queryTimeoutMillis:
|
||||||
|
connection.query_timeout != null
|
||||||
|
? connection.query_timeout * 1000
|
||||||
|
: undefined,
|
||||||
|
ssl:
|
||||||
|
connection.ssl_enabled === "Y"
|
||||||
|
? { rejectUnauthorized: false }
|
||||||
|
: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`[BatchManagementService] 커넥터 생성 시작: db_type=${connection.db_type}`);
|
console.log(
|
||||||
|
`[BatchManagementService] 커넥터 생성 시작: db_type=${connection.db_type}`
|
||||||
|
);
|
||||||
|
|
||||||
// 데이터베이스 타입에 따른 커넥터 생성
|
// 데이터베이스 타입에 따른 커넥터 생성
|
||||||
const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, connectionId);
|
const connector = await DatabaseConnectorFactory.createConnector(
|
||||||
|
connection.db_type,
|
||||||
console.log(`[BatchManagementService] 커넥터 생성 완료, 컬럼 조회 시작: tableName=${tableName}`);
|
config,
|
||||||
|
connectionId
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[BatchManagementService] 커넥터 생성 완료, 컬럼 조회 시작: tableName=${tableName}`
|
||||||
|
);
|
||||||
|
|
||||||
// 컬럼 정보 조회
|
// 컬럼 정보 조회
|
||||||
console.log(`[BatchManagementService] connector.getColumns 호출 전`);
|
console.log(`[BatchManagementService] connector.getColumns 호출 전`);
|
||||||
const columns = await connector.getColumns(tableName);
|
const columns = await connector.getColumns(tableName);
|
||||||
|
|
||||||
console.log(`[BatchManagementService] 원본 컬럼 조회 결과:`, columns);
|
console.log(`[BatchManagementService] 원본 컬럼 조회 결과:`, columns);
|
||||||
console.log(`[BatchManagementService] 원본 컬럼 개수:`, columns ? columns.length : 'null/undefined');
|
console.log(
|
||||||
|
`[BatchManagementService] 원본 컬럼 개수:`,
|
||||||
|
columns ? columns.length : "null/undefined"
|
||||||
|
);
|
||||||
|
|
||||||
// 각 데이터베이스 커넥터의 반환 구조가 다르므로 통일된 구조로 변환
|
// 각 데이터베이스 커넥터의 반환 구조가 다르므로 통일된 구조로 변환
|
||||||
const standardizedColumns: BatchColumnInfo[] = columns.map((col: any) => {
|
const standardizedColumns: BatchColumnInfo[] = columns.map((col: any) => {
|
||||||
console.log(`[BatchManagementService] 컬럼 변환 중:`, col);
|
console.log(`[BatchManagementService] 컬럼 변환 중:`, col);
|
||||||
|
|
||||||
// MySQL/MariaDB 구조: {name, dataType, isNullable, defaultValue} (MySQLConnector만)
|
// MySQL/MariaDB 구조: {name, dataType, isNullable, defaultValue} (MySQLConnector만)
|
||||||
if (col.name && col.dataType !== undefined) {
|
if (col.name && col.dataType !== undefined) {
|
||||||
const result = {
|
const result = {
|
||||||
column_name: col.name,
|
column_name: col.name,
|
||||||
data_type: col.dataType,
|
data_type: col.dataType,
|
||||||
is_nullable: col.isNullable ? 'YES' : 'NO',
|
is_nullable: col.isNullable ? "YES" : "NO",
|
||||||
column_default: col.defaultValue || null,
|
column_default: col.defaultValue || null,
|
||||||
};
|
};
|
||||||
console.log(`[BatchManagementService] MySQL/MariaDB 구조로 변환:`, result);
|
console.log(
|
||||||
|
`[BatchManagementService] MySQL/MariaDB 구조로 변환:`,
|
||||||
|
result
|
||||||
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
// PostgreSQL/Oracle/MSSQL/MariaDB 구조: {column_name, data_type, is_nullable, column_default}
|
// PostgreSQL/Oracle/MSSQL/MariaDB 구조: {column_name, data_type, is_nullable, column_default}
|
||||||
|
|
@ -350,30 +407,41 @@ export class BatchManagementService {
|
||||||
const result = {
|
const result = {
|
||||||
column_name: col.column_name || col.COLUMN_NAME,
|
column_name: col.column_name || col.COLUMN_NAME,
|
||||||
data_type: col.data_type || col.DATA_TYPE,
|
data_type: col.data_type || col.DATA_TYPE,
|
||||||
is_nullable: col.is_nullable || col.IS_NULLABLE || (col.nullable === 'Y' ? 'YES' : 'NO'),
|
is_nullable:
|
||||||
|
col.is_nullable ||
|
||||||
|
col.IS_NULLABLE ||
|
||||||
|
(col.nullable === "Y" ? "YES" : "NO"),
|
||||||
column_default: col.column_default || col.COLUMN_DEFAULT || null,
|
column_default: col.column_default || col.COLUMN_DEFAULT || null,
|
||||||
};
|
};
|
||||||
console.log(`[BatchManagementService] 표준 구조로 변환:`, result);
|
console.log(`[BatchManagementService] 표준 구조로 변환:`, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[BatchManagementService] 표준화된 컬럼 목록:`, standardizedColumns);
|
console.log(
|
||||||
|
`[BatchManagementService] 표준화된 컬럼 목록:`,
|
||||||
|
standardizedColumns
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: standardizedColumns,
|
data: standardizedColumns,
|
||||||
message: "컬럼 정보를 조회했습니다."
|
message: "컬럼 정보를 조회했습니다.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[BatchManagementService] 외부 DB 컬럼 정보 조회 오류:", error);
|
console.error(
|
||||||
console.error("[BatchManagementService] 오류 스택:", error instanceof Error ? error.stack : 'No stack trace');
|
"[BatchManagementService] 외부 DB 컬럼 정보 조회 오류:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"[BatchManagementService] 오류 스택:",
|
||||||
|
error instanceof Error ? error.stack : "No stack trace"
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
|
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,14 +50,14 @@ export class DbTypeCategoryService {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: categories,
|
data: categories,
|
||||||
message: "DB 타입 카테고리 목록을 조회했습니다."
|
message: "DB 타입 카테고리 목록을 조회했습니다.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("DB 타입 카테고리 조회 오류:", error);
|
console.error("DB 타입 카테고리 조회 오류:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "DB 타입 카테고리 조회 중 오류가 발생했습니다.",
|
message: "DB 타입 카테고리 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +65,9 @@ export class DbTypeCategoryService {
|
||||||
/**
|
/**
|
||||||
* 특정 DB 타입 카테고리 조회
|
* 특정 DB 타입 카테고리 조회
|
||||||
*/
|
*/
|
||||||
static async getCategoryByTypeCode(typeCode: string): Promise<ApiResponse<DbTypeCategory>> {
|
static async getCategoryByTypeCode(
|
||||||
|
typeCode: string
|
||||||
|
): Promise<ApiResponse<DbTypeCategory>> {
|
||||||
try {
|
try {
|
||||||
const category = await queryOne<DbTypeCategory>(
|
const category = await queryOne<DbTypeCategory>(
|
||||||
`SELECT * FROM db_type_categories WHERE type_code = $1`,
|
`SELECT * FROM db_type_categories WHERE type_code = $1`,
|
||||||
|
|
@ -75,21 +77,21 @@ export class DbTypeCategoryService {
|
||||||
if (!category) {
|
if (!category) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "해당 DB 타입 카테고리를 찾을 수 없습니다."
|
message: "해당 DB 타입 카테고리를 찾을 수 없습니다.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: category,
|
data: category,
|
||||||
message: "DB 타입 카테고리를 조회했습니다."
|
message: "DB 타입 카테고리를 조회했습니다.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("DB 타입 카테고리 조회 오류:", error);
|
console.error("DB 타입 카테고리 조회 오류:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "DB 타입 카테고리 조회 중 오류가 발생했습니다.",
|
message: "DB 타입 카테고리 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -97,7 +99,9 @@ export class DbTypeCategoryService {
|
||||||
/**
|
/**
|
||||||
* DB 타입 카테고리 생성
|
* DB 타입 카테고리 생성
|
||||||
*/
|
*/
|
||||||
static async createCategory(data: CreateDbTypeCategoryRequest): Promise<ApiResponse<DbTypeCategory>> {
|
static async createCategory(
|
||||||
|
data: CreateDbTypeCategoryRequest
|
||||||
|
): Promise<ApiResponse<DbTypeCategory>> {
|
||||||
try {
|
try {
|
||||||
// 중복 체크
|
// 중복 체크
|
||||||
const existing = await queryOne<DbTypeCategory>(
|
const existing = await queryOne<DbTypeCategory>(
|
||||||
|
|
@ -108,7 +112,7 @@ export class DbTypeCategoryService {
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "이미 존재하는 DB 타입 코드입니다."
|
message: "이미 존재하는 DB 타입 코드입니다.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,14 +134,14 @@ export class DbTypeCategoryService {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: category,
|
data: category,
|
||||||
message: "DB 타입 카테고리가 생성되었습니다."
|
message: "DB 타입 카테고리가 생성되었습니다.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("DB 타입 카테고리 생성 오류:", error);
|
console.error("DB 타입 카테고리 생성 오류:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "DB 타입 카테고리 생성 중 오류가 발생했습니다.",
|
message: "DB 타입 카테고리 생성 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +149,10 @@ export class DbTypeCategoryService {
|
||||||
/**
|
/**
|
||||||
* DB 타입 카테고리 수정
|
* DB 타입 카테고리 수정
|
||||||
*/
|
*/
|
||||||
static async updateCategory(typeCode: string, data: UpdateDbTypeCategoryRequest): Promise<ApiResponse<DbTypeCategory>> {
|
static async updateCategory(
|
||||||
|
typeCode: string,
|
||||||
|
data: UpdateDbTypeCategoryRequest
|
||||||
|
): Promise<ApiResponse<DbTypeCategory>> {
|
||||||
try {
|
try {
|
||||||
// 동적 UPDATE 쿼리 생성
|
// 동적 UPDATE 쿼리 생성
|
||||||
const updateFields: string[] = ["updated_at = NOW()"];
|
const updateFields: string[] = ["updated_at = NOW()"];
|
||||||
|
|
@ -184,14 +191,14 @@ export class DbTypeCategoryService {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: category,
|
data: category,
|
||||||
message: "DB 타입 카테고리가 수정되었습니다."
|
message: "DB 타입 카테고리가 수정되었습니다.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("DB 타입 카테고리 수정 오류:", error);
|
console.error("DB 타입 카테고리 수정 오류:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "DB 타입 카테고리 수정 중 오류가 발생했습니다.",
|
message: "DB 타입 카테고리 수정 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -212,7 +219,7 @@ export class DbTypeCategoryService {
|
||||||
if (connectionsCount > 0) {
|
if (connectionsCount > 0) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: `해당 DB 타입을 사용하는 연결이 ${connectionsCount}개 있어 삭제할 수 없습니다.`
|
message: `해당 DB 타입을 사용하는 연결이 ${connectionsCount}개 있어 삭제할 수 없습니다.`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,14 +232,14 @@ export class DbTypeCategoryService {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "DB 타입 카테고리가 삭제되었습니다."
|
message: "DB 타입 카테고리가 삭제되었습니다.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("DB 타입 카테고리 삭제 오류:", error);
|
console.error("DB 타입 카테고리 삭제 오류:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "DB 타입 카테고리 삭제 중 오류가 발생했습니다.",
|
message: "DB 타입 카테고리 삭제 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -256,22 +263,22 @@ export class DbTypeCategoryService {
|
||||||
);
|
);
|
||||||
|
|
||||||
// connection_count를 숫자로 변환
|
// connection_count를 숫자로 변환
|
||||||
const formattedResult = result.map(row => ({
|
const formattedResult = result.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
connection_count: parseInt(row.connection_count)
|
connection_count: parseInt(row.connection_count),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: formattedResult,
|
data: formattedResult,
|
||||||
message: "DB 타입별 연결 통계를 조회했습니다."
|
message: "DB 타입별 연결 통계를 조회했습니다.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("DB 타입별 통계 조회 오류:", error);
|
console.error("DB 타입별 통계 조회 오류:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "DB 타입별 통계 조회 중 오류가 발생했습니다.",
|
message: "DB 타입별 통계 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -283,40 +290,40 @@ export class DbTypeCategoryService {
|
||||||
try {
|
try {
|
||||||
const defaultCategories = [
|
const defaultCategories = [
|
||||||
{
|
{
|
||||||
type_code: 'postgresql',
|
type_code: "postgresql",
|
||||||
display_name: 'PostgreSQL',
|
display_name: "PostgreSQL",
|
||||||
icon: 'postgresql',
|
icon: "postgresql",
|
||||||
color: '#336791',
|
color: "#336791",
|
||||||
sort_order: 1
|
sort_order: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type_code: 'oracle',
|
type_code: "oracle",
|
||||||
display_name: 'Oracle',
|
display_name: "Oracle",
|
||||||
icon: 'oracle',
|
icon: "oracle",
|
||||||
color: '#F80000',
|
color: "#F80000",
|
||||||
sort_order: 2
|
sort_order: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type_code: 'mysql',
|
type_code: "mysql",
|
||||||
display_name: 'MySQL',
|
display_name: "MySQL",
|
||||||
icon: 'mysql',
|
icon: "mysql",
|
||||||
color: '#4479A1',
|
color: "#4479A1",
|
||||||
sort_order: 3
|
sort_order: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type_code: 'mariadb',
|
type_code: "mariadb",
|
||||||
display_name: 'MariaDB',
|
display_name: "MariaDB",
|
||||||
icon: 'mariadb',
|
icon: "mariadb",
|
||||||
color: '#003545',
|
color: "#003545",
|
||||||
sort_order: 4
|
sort_order: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type_code: 'mssql',
|
type_code: "mssql",
|
||||||
display_name: 'SQL Server',
|
display_name: "SQL Server",
|
||||||
icon: 'mssql',
|
icon: "mssql",
|
||||||
color: '#CC2927',
|
color: "#CC2927",
|
||||||
sort_order: 5
|
sort_order: 5,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const category of defaultCategories) {
|
for (const category of defaultCategories) {
|
||||||
|
|
@ -338,14 +345,14 @@ export class DbTypeCategoryService {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "기본 DB 타입 카테고리가 초기화되었습니다."
|
message: "기본 DB 타입 카테고리가 초기화되었습니다.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("기본 카테고리 초기화 오류:", error);
|
console.error("기본 카테고리 초기화 오류:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "기본 카테고리 초기화 중 오류가 발생했습니다.",
|
message: "기본 카테고리 초기화 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue