diff --git a/PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md b/PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md index 28bee063..d93f22c5 100644 --- a/PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md +++ b/PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md @@ -6,15 +6,15 @@ ExternalCallConfigService는 **8개의 Prisma 호출**이 있으며, 외부 API ### 📊 기본 정보 -| 항목 | 내용 | -| --------------- | --------------------------------------------------------- | +| 항목 | 내용 | +| --------------- | -------------------------------------------------------- | | 파일 위치 | `backend-node/src/services/externalCallConfigService.ts` | -| 파일 크기 | 581 라인 | -| Prisma 호출 | 8개 | -| **현재 진행률** | **0/8 (0%)** 🔄 **진행 예정** | -| 복잡도 | 중간 (JSON 필드, 복잡한 CRUD) | -| 우선순위 | 🟡 중간 (Phase 3.12) | -| **상태** | ⏳ **대기 중** | +| 파일 크기 | 612 라인 | +| Prisma 호출 | 0개 (전환 완료) | +| **현재 진행률** | **8/8 (100%)** ✅ **전환 완료** | +| 복잡도 | 중간 (JSON 필드, 복잡한 CRUD) | +| 우선순위 | 🟡 중간 (Phase 3.12) | +| **상태** | ✅ **완료** | ### 🎯 전환 목표 @@ -33,35 +33,43 @@ ExternalCallConfigService는 **8개의 Prisma 호출**이 있으며, 외부 API ### 주요 기능 (8개 예상) #### 1. **외부 호출 설정 목록 조회** + - findMany with filters - 페이징, 정렬 - 동적 WHERE 조건 (is_active, company_code, search) #### 2. **외부 호출 설정 단건 조회** + - findUnique or findFirst - config_id 기준 #### 3. **외부 호출 설정 생성** + - create - JSON 필드 처리 (headers, params, auth_config) - 민감 정보 암호화 #### 4. **외부 호출 설정 수정** + - update - 동적 UPDATE 쿼리 - JSON 필드 업데이트 #### 5. **외부 호출 설정 삭제** + - delete or soft delete #### 6. **외부 호출 설정 복제** + - findUnique + create #### 7. **외부 호출 설정 테스트** + - findUnique - 실제 HTTP 호출 #### 8. **외부 호출 이력 조회** + - findMany with 관계 조인 - 통계 쿼리 @@ -70,6 +78,7 @@ ExternalCallConfigService는 **8개의 Prisma 호출**이 있으며, 외부 API ## 💡 전환 전략 ### 1단계: 기본 CRUD 전환 (5개) + - getExternalCallConfigs() - 목록 조회 - getExternalCallConfig() - 단건 조회 - createExternalCallConfig() - 생성 @@ -77,6 +86,7 @@ ExternalCallConfigService는 **8개의 Prisma 호출**이 있으며, 외부 API - deleteExternalCallConfig() - 삭제 ### 2단계: 추가 기능 전환 (3개) + - duplicateExternalCallConfig() - 복제 - testExternalCallConfig() - 테스트 - getExternalCallHistory() - 이력 조회 @@ -88,6 +98,7 @@ ExternalCallConfigService는 **8개의 Prisma 호출**이 있으며, 외부 API ### 예시 1: 목록 조회 (동적 WHERE + JSON) **변경 전**: + ```typescript const configs = await prisma.external_call_configs.findMany({ where: { @@ -105,6 +116,7 @@ const configs = await prisma.external_call_configs.findMany({ ``` **변경 후**: + ```typescript const conditions: string[] = ["company_code = $1"]; const params: any[] = [companyCode]; @@ -135,6 +147,7 @@ const configs = await query( ### 예시 2: JSON 필드 생성 **변경 전**: + ```typescript const config = await prisma.external_call_configs.create({ data: { @@ -150,6 +163,7 @@ const config = await prisma.external_call_configs.create({ ``` **변경 후**: + ```typescript const config = await queryOne( `INSERT INTO external_call_configs @@ -172,6 +186,7 @@ const config = await queryOne( ### 예시 3: 동적 UPDATE (JSON 포함) **변경 전**: + ```typescript const updateData: any = {}; if (data.headers) updateData.headers = data.headers; @@ -184,6 +199,7 @@ const config = await prisma.external_call_configs.update({ ``` **변경 후**: + ```typescript const updateFields: string[] = ["updated_at = NOW()"]; const values: any[] = []; @@ -213,23 +229,26 @@ const config = await queryOne( ## 🔧 기술적 고려사항 ### 1. JSON 필드 처리 + 3개의 JSON 필드가 있을 것으로 예상: + - `headers` - HTTP 헤더 - `params` - 쿼리 파라미터 - `auth_config` - 인증 설정 (암호화됨) ```typescript // INSERT/UPDATE 시 -JSON.stringify(jsonData) +JSON.stringify(jsonData); // SELECT 후 -const parsedData = typeof row.headers === 'string' - ? JSON.parse(row.headers) - : row.headers; +const parsedData = + typeof row.headers === "string" ? JSON.parse(row.headers) : row.headers; ``` ### 2. 민감 정보 암호화 + auth_config는 암호화되어 저장되므로, 기존 암호화/복호화 로직 유지: + ```typescript import { encrypt, decrypt } from "../utils/encryption"; @@ -241,6 +260,7 @@ const decryptedAuthConfig = JSON.parse(decrypt(row.auth_config)); ``` ### 3. HTTP 메소드 검증 + ```typescript const VALID_HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"]; if (!VALID_HTTP_METHODS.includes(httpMethod)) { @@ -249,6 +269,7 @@ if (!VALID_HTTP_METHODS.includes(httpMethod)) { ``` ### 4. URL 검증 + ```typescript try { new URL(endpointUrl); @@ -259,9 +280,38 @@ try { --- -## 📝 전환 체크리스트 +## ✅ 전환 완료 내역 + +### 전환된 Prisma 호출 (8개) + +1. **`getConfigs()`** - 목록 조회 (findMany → query) +2. **`getConfigById()`** - 단건 조회 (findUnique → queryOne) +3. **`createConfig()`** - 중복 검사 (findFirst → queryOne) +4. **`createConfig()`** - 생성 (create → queryOne with INSERT) +5. **`updateConfig()`** - 중복 검사 (findFirst → queryOne) +6. **`updateConfig()`** - 수정 (update → queryOne with 동적 UPDATE) +7. **`deleteConfig()`** - 삭제 (update → query) +8. **`getExternalCallConfigsForButtonControl()`** - 조회 (findMany → query) + +### 주요 기술적 개선사항 + +- 동적 WHERE 조건 생성 (company_code, call_type, api_type, is_active, search) +- ILIKE를 활용한 대소문자 구분 없는 검색 +- 동적 UPDATE 쿼리 (9개 필드) +- JSON 필드 처리 (`config_data` → `JSON.stringify()`) +- 중복 검사 로직 유지 + +### 코드 정리 + +- [x] import 문 수정 완료 +- [x] Prisma import 완전 제거 +- [x] TypeScript 컴파일 성공 +- [x] Linter 오류 없음 + +## 📝 원본 전환 체크리스트 + +### 1단계: Prisma 호출 전환 (✅ 완료) -### 1단계: Prisma 호출 전환 - [ ] `getExternalCallConfigs()` - 목록 조회 (findMany + count) - [ ] `getExternalCallConfig()` - 단건 조회 (findUnique) - [ ] `createExternalCallConfig()` - 생성 (create) @@ -272,18 +322,21 @@ try { - [ ] `getExternalCallHistory()` - 이력 조회 (findMany) ### 2단계: 코드 정리 + - [ ] import 문 수정 (`prisma` → `query, queryOne`) - [ ] JSON 필드 처리 확인 - [ ] 암호화/복호화 로직 유지 - [ ] Prisma import 완전 제거 ### 3단계: 테스트 + - [ ] 단위 테스트 작성 (8개) - [ ] 통합 테스트 작성 (3개) - [ ] 암호화 테스트 - [ ] HTTP 호출 테스트 ### 4단계: 문서화 + - [ ] 전환 완료 문서 업데이트 - [ ] API 문서 업데이트 @@ -295,11 +348,9 @@ try { - JSON 필드 처리 - 암호화/복호화 로직 - HTTP 호출 테스트 - - **예상 소요 시간**: 1~1.5시간 --- **상태**: ⏳ **대기 중** **특이사항**: JSON 필드, 민감 정보 암호화, HTTP 호출 포함 - diff --git a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md index 8f9bcf12..75bfd9c4 100644 --- a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md +++ b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md @@ -136,7 +136,7 @@ backend-node/ (루트) #### 🟡 **중간 (단순 CRUD) - 3순위** - `ddlAuditLogger.ts` (0개) - ✅ **전환 완료** (Phase 3.11) - [계획서](PHASE3.11_DDL_AUDIT_LOGGER_MIGRATION.md) -- `externalCallConfigService.ts` (8개) - 외부 호출 설정 - [계획서](PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md) +- `externalCallConfigService.ts` (0개) - ✅ **전환 완료** (Phase 3.12) - [계획서](PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md) - `entityJoinService.ts` (5개) - 엔티티 조인 - [계획서](PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md) - `authService.ts` (5개) - 사용자 인증 - [계획서](PHASE3.14_AUTH_SERVICE_MIGRATION.md) - **배치 관련 서비스 (24개)** - [통합 계획서](PHASE3.15_BATCH_SERVICES_MIGRATION.md) diff --git a/backend-node/src/services/externalCallConfigService.ts b/backend-node/src/services/externalCallConfigService.ts index 3fb60407..a5f3ee33 100644 --- a/backend-node/src/services/externalCallConfigService.ts +++ b/backend-node/src/services/externalCallConfigService.ts @@ -1,4 +1,4 @@ -import prisma from "../config/database"; +import { query, queryOne } from "../database/db"; import { logger } from "../utils/logger"; // 외부 호출 설정 타입 정의 @@ -34,43 +34,55 @@ export class ExternalCallConfigService { logger.info("=== 외부 호출 설정 목록 조회 시작 ==="); logger.info(`필터 조건:`, filter); - const where: any = {}; + const conditions: string[] = []; + const params: any[] = []; + let paramIndex = 1; // 회사 코드 필터 if (filter.company_code) { - where.company_code = filter.company_code; + conditions.push(`company_code = $${paramIndex++}`); + params.push(filter.company_code); } // 호출 타입 필터 if (filter.call_type) { - where.call_type = filter.call_type; + conditions.push(`call_type = $${paramIndex++}`); + params.push(filter.call_type); } // API 타입 필터 if (filter.api_type) { - where.api_type = filter.api_type; + conditions.push(`api_type = $${paramIndex++}`); + params.push(filter.api_type); } // 활성화 상태 필터 if (filter.is_active) { - where.is_active = filter.is_active; + conditions.push(`is_active = $${paramIndex++}`); + params.push(filter.is_active); } // 검색어 필터 (설정 이름 또는 설명) if (filter.search) { - where.OR = [ - { config_name: { contains: filter.search, mode: "insensitive" } }, - { description: { contains: filter.search, mode: "insensitive" } }, - ]; + conditions.push( + `(config_name ILIKE $${paramIndex} OR description ILIKE $${paramIndex})` + ); + params.push(`%${filter.search}%`); + paramIndex++; } - const configs = await prisma.external_call_configs.findMany({ - where, - orderBy: [{ is_active: "desc" }, { created_date: "desc" }], - }); + const whereClause = + conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + + const configs = await query( + `SELECT * FROM external_call_configs + ${whereClause} + ORDER BY is_active DESC, created_date DESC`, + params + ); logger.info(`외부 호출 설정 조회 결과: ${configs.length}개`); - return configs as ExternalCallConfig[]; + return configs; } catch (error) { logger.error("외부 호출 설정 목록 조회 실패:", error); throw error; @@ -84,9 +96,10 @@ export class ExternalCallConfigService { try { logger.info(`=== 외부 호출 설정 조회: ID ${id} ===`); - const config = await prisma.external_call_configs.findUnique({ - where: { id }, - }); + const config = await queryOne( + `SELECT * FROM external_call_configs WHERE id = $1`, + [id] + ); if (config) { logger.info(`외부 호출 설정 조회 성공: ${config.config_name}`); @@ -94,7 +107,7 @@ export class ExternalCallConfigService { logger.warn(`외부 호출 설정을 찾을 수 없음: ID ${id}`); } - return config as ExternalCallConfig | null; + return config || null; } catch (error) { logger.error(`외부 호출 설정 조회 실패 (ID: ${id}):`, error); throw error; @@ -115,13 +128,11 @@ export class ExternalCallConfigService { }); // 중복 이름 검사 - const existingConfig = await prisma.external_call_configs.findFirst({ - where: { - config_name: data.config_name, - company_code: data.company_code || "*", - is_active: "Y", - }, - }); + const existingConfig = await queryOne( + `SELECT * FROM external_call_configs + WHERE config_name = $1 AND company_code = $2 AND is_active = $3`, + [data.config_name, data.company_code || "*", "Y"] + ); if (existingConfig) { throw new Error( @@ -129,24 +140,29 @@ export class ExternalCallConfigService { ); } - const newConfig = await prisma.external_call_configs.create({ - data: { - config_name: data.config_name, - call_type: data.call_type, - api_type: data.api_type, - config_data: data.config_data, - description: data.description, - company_code: data.company_code || "*", - is_active: data.is_active || "Y", - created_by: data.created_by, - updated_by: data.updated_by, - }, - }); + const newConfig = await queryOne( + `INSERT INTO external_call_configs + (config_name, call_type, api_type, config_data, description, + company_code, is_active, created_by, updated_by, created_date, updated_date) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW()) + RETURNING *`, + [ + data.config_name, + data.call_type, + data.api_type, + JSON.stringify(data.config_data), + data.description, + data.company_code || "*", + data.is_active || "Y", + data.created_by, + data.updated_by, + ] + ); logger.info( - `외부 호출 설정 생성 완료: ${newConfig.config_name} (ID: ${newConfig.id})` + `외부 호출 설정 생성 완료: ${newConfig!.config_name} (ID: ${newConfig!.id})` ); - return newConfig as ExternalCallConfig; + return newConfig!; } catch (error) { logger.error("외부 호출 설정 생성 실패:", error); throw error; @@ -171,14 +187,16 @@ export class ExternalCallConfigService { // 이름 중복 검사 (다른 설정과 중복되는지) if (data.config_name && data.config_name !== existingConfig.config_name) { - const duplicateConfig = await prisma.external_call_configs.findFirst({ - where: { - config_name: data.config_name, - company_code: data.company_code || existingConfig.company_code, - is_active: "Y", - id: { not: id }, - }, - }); + const duplicateConfig = await queryOne( + `SELECT * FROM external_call_configs + WHERE config_name = $1 AND company_code = $2 AND is_active = $3 AND id != $4`, + [ + data.config_name, + data.company_code || existingConfig.company_code, + "Y", + id, + ] + ); if (duplicateConfig) { throw new Error( @@ -187,27 +205,58 @@ export class ExternalCallConfigService { } } - const updatedConfig = await prisma.external_call_configs.update({ - where: { id }, - data: { - ...(data.config_name && { config_name: data.config_name }), - ...(data.call_type && { call_type: data.call_type }), - ...(data.api_type !== undefined && { api_type: data.api_type }), - ...(data.config_data && { config_data: data.config_data }), - ...(data.description !== undefined && { - description: data.description, - }), - ...(data.company_code && { company_code: data.company_code }), - ...(data.is_active && { is_active: data.is_active }), - ...(data.updated_by && { updated_by: data.updated_by }), - updated_date: new Date(), - }, - }); + // 동적 UPDATE 쿼리 생성 + const updateFields: string[] = ["updated_date = NOW()"]; + const params: any[] = []; + let paramIndex = 1; + + if (data.config_name) { + updateFields.push(`config_name = $${paramIndex++}`); + params.push(data.config_name); + } + if (data.call_type) { + updateFields.push(`call_type = $${paramIndex++}`); + params.push(data.call_type); + } + if (data.api_type !== undefined) { + updateFields.push(`api_type = $${paramIndex++}`); + params.push(data.api_type); + } + if (data.config_data) { + updateFields.push(`config_data = $${paramIndex++}`); + params.push(JSON.stringify(data.config_data)); + } + if (data.description !== undefined) { + updateFields.push(`description = $${paramIndex++}`); + params.push(data.description); + } + if (data.company_code) { + updateFields.push(`company_code = $${paramIndex++}`); + params.push(data.company_code); + } + if (data.is_active) { + updateFields.push(`is_active = $${paramIndex++}`); + params.push(data.is_active); + } + if (data.updated_by) { + updateFields.push(`updated_by = $${paramIndex++}`); + params.push(data.updated_by); + } + + params.push(id); + + const updatedConfig = await queryOne( + `UPDATE external_call_configs + SET ${updateFields.join(", ")} + WHERE id = $${paramIndex} + RETURNING *`, + params + ); logger.info( - `외부 호출 설정 수정 완료: ${updatedConfig.config_name} (ID: ${id})` + `외부 호출 설정 수정 완료: ${updatedConfig!.config_name} (ID: ${id})` ); - return updatedConfig as ExternalCallConfig; + return updatedConfig!; } catch (error) { logger.error(`외부 호출 설정 수정 실패 (ID: ${id}):`, error); throw error; @@ -228,14 +277,12 @@ export class ExternalCallConfigService { } // 논리 삭제 (is_active = 'N') - await prisma.external_call_configs.update({ - where: { id }, - data: { - is_active: "N", - updated_by: deletedBy, - updated_date: new Date(), - }, - }); + await query( + `UPDATE external_call_configs + SET is_active = $1, updated_by = $2, updated_date = NOW() + WHERE id = $3`, + ["N", deletedBy, id] + ); logger.info( `외부 호출 설정 삭제 완료: ${existingConfig.config_name} (ID: ${id})` @@ -401,21 +448,18 @@ export class ExternalCallConfigService { }> > { try { - const configs = await prisma.external_call_configs.findMany({ - where: { - company_code: companyCode, - is_active: "Y", - }, - select: { - id: true, - config_name: true, - description: true, - config_data: true, - }, - orderBy: { - config_name: "asc", - }, - }); + const configs = await query<{ + id: number; + config_name: string; + description: string | null; + config_data: any; + }>( + `SELECT id, config_name, description, config_data + FROM external_call_configs + WHERE company_code = $1 AND is_active = $2 + ORDER BY config_name ASC`, + [companyCode, "Y"] + ); return configs.map((config) => { const configData = config.config_data as any;