diff --git a/PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md b/PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md index 06676de8..5413171f 100644 --- a/PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md +++ b/PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md @@ -9,12 +9,12 @@ EntityJoinService는 **5개의 Prisma 호출**이 있으며, 엔티티 간 조 | 항목 | 내용 | | --------------- | ------------------------------------------------ | | 파일 위치 | `backend-node/src/services/entityJoinService.ts` | -| 파일 크기 | 574 라인 | -| Prisma 호출 | 5개 | -| **현재 진행률** | **0/5 (0%)** 🔄 **진행 예정** | +| 파일 크기 | 575 라인 | +| Prisma 호출 | 0개 (전환 완료) | +| **현재 진행률** | **5/5 (100%)** ✅ **전환 완료** | | 복잡도 | 중간 (조인 쿼리, 관계 설정) | | 우선순위 | 🟡 중간 (Phase 3.13) | -| **상태** | ⏳ **대기 중** | +| **상태** | ✅ **완료** | ### 🎯 전환 목표 @@ -32,23 +32,28 @@ EntityJoinService는 **5개의 Prisma 호출**이 있으며, 엔티티 간 조 ### 주요 기능 (5개 예상) #### 1. **엔티티 조인 목록 조회** + - findMany with filters - 동적 WHERE 조건 - 페이징, 정렬 #### 2. **엔티티 조인 단건 조회** + - findUnique or findFirst - join_id 기준 #### 3. **엔티티 조인 생성** + - create - 조인 유효성 검증 #### 4. **엔티티 조인 수정** + - update - 동적 UPDATE 쿼리 #### 5. **엔티티 조인 삭제** + - delete --- @@ -56,6 +61,7 @@ EntityJoinService는 **5개의 Prisma 호출**이 있으며, 엔티티 간 조 ## 💡 전환 전략 ### 1단계: 기본 CRUD 전환 (5개) + - getEntityJoins() - 목록 조회 - getEntityJoin() - 단건 조회 - createEntityJoin() - 생성 @@ -69,6 +75,7 @@ EntityJoinService는 **5개의 Prisma 호출**이 있으며, 엔티티 간 조 ### 예시 1: 조인 설정 조회 (LEFT JOIN으로 테이블 정보 포함) **변경 전**: + ```typescript const joins = await prisma.entity_joins.findMany({ where: { @@ -84,6 +91,7 @@ const joins = await prisma.entity_joins.findMany({ ``` **변경 후**: + ```typescript const joins = await query( `SELECT @@ -104,6 +112,7 @@ const joins = await query( ### 예시 2: 조인 생성 (유효성 검증 포함) **변경 전**: + ```typescript // 조인 유효성 검증 const sourceTable = await prisma.tables.findUnique({ @@ -131,17 +140,12 @@ const join = await prisma.entity_joins.create({ ``` **변경 후**: + ```typescript // 조인 유효성 검증 (Promise.all로 병렬 실행) const [sourceTable, targetTable] = await Promise.all([ - queryOne( - `SELECT * FROM tables WHERE table_id = $1`, - [sourceTableId] - ), - queryOne( - `SELECT * FROM tables WHERE table_id = $1`, - [targetTableId] - ), + queryOne(`SELECT * FROM tables WHERE table_id = $1`, [sourceTableId]), + queryOne(`SELECT * FROM tables WHERE table_id = $1`, [targetTableId]), ]); if (!sourceTable || !targetTable) { @@ -162,6 +166,7 @@ const join = await queryOne( ### 예시 3: 조인 수정 **변경 전**: + ```typescript const join = await prisma.entity_joins.update({ where: { join_id: joinId }, @@ -174,6 +179,7 @@ const join = await prisma.entity_joins.update({ ``` **변경 후**: + ```typescript const updateFields: string[] = ["updated_at = NOW()"]; const values: any[] = []; @@ -208,6 +214,7 @@ const join = await queryOne( ## 🔧 기술적 고려사항 ### 1. 조인 타입 검증 + ```typescript const VALID_JOIN_TYPES = ["INNER", "LEFT", "RIGHT", "FULL"]; if (!VALID_JOIN_TYPES.includes(joinType)) { @@ -216,6 +223,7 @@ if (!VALID_JOIN_TYPES.includes(joinType)) { ``` ### 2. 조인 조건 검증 + ```typescript // 조인 조건은 SQL 조건식 형태 (예: "source.id = target.parent_id") // SQL 인젝션 방지를 위한 검증 필요 @@ -226,6 +234,7 @@ if (!isValidJoinCondition) { ``` ### 3. 순환 참조 방지 + ```typescript // 조인이 순환 참조를 만들지 않는지 검증 async function checkCircularReference( @@ -238,13 +247,54 @@ async function checkCircularReference( ``` ### 4. LEFT JOIN으로 관련 테이블 정보 조회 + 조인 설정 조회 시 source/target 테이블 정보를 함께 가져오기 위해 LEFT JOIN 사용 --- -## 📝 전환 체크리스트 +## ✅ 전환 완료 내역 + +### 전환된 Prisma 호출 (5개) + +1. **`detectEntityJoins()`** - 엔티티 컬럼 감지 (findMany → query) + - column_labels 조회 + - web_type = 'entity' 필터 + - reference_table/reference_column IS NOT NULL + +2. **`validateJoinConfig()`** - 테이블 존재 확인 ($queryRaw → query) + - information_schema.tables 조회 + - 참조 테이블 검증 + +3. **`validateJoinConfig()`** - 컬럼 존재 확인 ($queryRaw → query) + - information_schema.columns 조회 + - 표시 컬럼 검증 + +4. **`getReferenceTableColumns()`** - 컬럼 정보 조회 ($queryRaw → query) + - information_schema.columns 조회 + - 문자열 타입 컬럼만 필터 + +5. **`getReferenceTableColumns()`** - 라벨 정보 조회 (findMany → query) + - column_labels 조회 + - 컬럼명과 라벨 매핑 + +### 주요 기술적 개선사항 + +- **information_schema 쿼리**: 파라미터 바인딩으로 변경 ($1, $2) +- **타입 안전성**: 명확한 반환 타입 지정 +- **IS NOT NULL 조건**: Prisma의 { not: null } → IS NOT NULL +- **IN 조건**: 여러 데이터 타입 필터링 + +### 코드 정리 + +- [x] PrismaClient import 제거 +- [x] import 문 수정 완료 +- [x] TypeScript 컴파일 성공 +- [x] Linter 오류 없음 + +## 📝 원본 전환 체크리스트 + +### 1단계: Prisma 호출 전환 (✅ 완료) -### 1단계: Prisma 호출 전환 - [ ] `getEntityJoins()` - 목록 조회 (findMany with include) - [ ] `getEntityJoin()` - 단건 조회 (findUnique) - [ ] `createEntityJoin()` - 생성 (create with validation) @@ -252,17 +302,20 @@ async function checkCircularReference( - [ ] `deleteEntityJoin()` - 삭제 (delete) ### 2단계: 코드 정리 + - [ ] import 문 수정 (`prisma` → `query, queryOne`) - [ ] 조인 유효성 검증 로직 유지 - [ ] Prisma import 완전 제거 ### 3단계: 테스트 + - [ ] 단위 테스트 작성 (5개) - [ ] 조인 유효성 검증 테스트 - [ ] 순환 참조 방지 테스트 - [ ] 통합 테스트 작성 (2개) ### 4단계: 문서화 + - [ ] 전환 완료 문서 업데이트 --- @@ -273,11 +326,9 @@ async function checkCircularReference( - LEFT JOIN 쿼리 - 조인 유효성 검증 - 순환 참조 방지 - - **예상 소요 시간**: 1시간 --- **상태**: ⏳ **대기 중** **특이사항**: LEFT JOIN, 조인 유효성 검증, 순환 참조 방지 포함 - diff --git a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md index 75bfd9c4..0abb663d 100644 --- a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md +++ b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md @@ -137,7 +137,7 @@ backend-node/ (루트) - `ddlAuditLogger.ts` (0개) - ✅ **전환 완료** (Phase 3.11) - [계획서](PHASE3.11_DDL_AUDIT_LOGGER_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) +- `entityJoinService.ts` (0개) - ✅ **전환 완료** (Phase 3.13) - [계획서](PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md) - `authService.ts` (5개) - 사용자 인증 - [계획서](PHASE3.14_AUTH_SERVICE_MIGRATION.md) - **배치 관련 서비스 (24개)** - [통합 계획서](PHASE3.15_BATCH_SERVICES_MIGRATION.md) - `batchExternalDbService.ts` (8개) - 배치 외부DB diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index 3b00ee5c..095ac938 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -1,5 +1,4 @@ -import { PrismaClient } from "@prisma/client"; -import prisma from "../config/database"; +import { query, queryOne } from "../database/db"; import { logger } from "../utils/logger"; import { EntityJoinConfig, @@ -26,20 +25,20 @@ export class EntityJoinService { logger.info(`Entity 컬럼 감지 시작: ${tableName}`); // column_labels에서 entity 타입인 컬럼들 조회 - const entityColumns = await prisma.column_labels.findMany({ - where: { - table_name: tableName, - web_type: "entity", - reference_table: { not: null }, - reference_column: { not: null }, - }, - select: { - column_name: true, - reference_table: true, - reference_column: true, - display_column: true, - }, - }); + const entityColumns = await query<{ + column_name: string; + reference_table: string; + reference_column: string; + display_column: string | null; + }>( + `SELECT column_name, reference_table, reference_column, display_column + FROM column_labels + WHERE table_name = $1 + AND web_type = $2 + AND reference_table IS NOT NULL + AND reference_column IS NOT NULL`, + [tableName, "entity"] + ); logger.info(`🔍 Entity 컬럼 조회 결과: ${entityColumns.length}개 발견`); entityColumns.forEach((col, index) => { @@ -401,13 +400,14 @@ export class EntityJoinService { }); // 참조 테이블 존재 확인 - const tableExists = await prisma.$queryRaw` - SELECT 1 FROM information_schema.tables - WHERE table_name = ${config.referenceTable} - LIMIT 1 - `; + const tableExists = await query<{ exists: number }>( + `SELECT 1 as exists FROM information_schema.tables + WHERE table_name = $1 + LIMIT 1`, + [config.referenceTable] + ); - if (!Array.isArray(tableExists) || tableExists.length === 0) { + if (tableExists.length === 0) { logger.warn(`참조 테이블이 존재하지 않음: ${config.referenceTable}`); return false; } @@ -420,14 +420,15 @@ export class EntityJoinService { // 🚨 display_column이 항상 "none"이므로, 표시 컬럼이 없어도 조인 허용 if (displayColumn && displayColumn !== "none") { - const columnExists = await prisma.$queryRaw` - SELECT 1 FROM information_schema.columns - WHERE table_name = ${config.referenceTable} - AND column_name = ${displayColumn} - LIMIT 1 - `; + const columnExists = await query<{ exists: number }>( + `SELECT 1 as exists FROM information_schema.columns + WHERE table_name = $1 + AND column_name = $2 + LIMIT 1`, + [config.referenceTable, displayColumn] + ); - if (!Array.isArray(columnExists) || columnExists.length === 0) { + if (columnExists.length === 0) { logger.warn( `표시 컬럼이 존재하지 않음: ${config.referenceTable}.${displayColumn}` ); @@ -528,27 +529,30 @@ export class EntityJoinService { > { try { // 1. 테이블의 기본 컬럼 정보 조회 - const columns = (await prisma.$queryRaw` - SELECT + const columns = await query<{ + column_name: string; + data_type: string; + }>( + `SELECT column_name, data_type FROM information_schema.columns - WHERE table_name = ${tableName} + WHERE table_name = $1 AND data_type IN ('character varying', 'varchar', 'text', 'char') - ORDER BY ordinal_position - `) as Array<{ - column_name: string; - data_type: string; - }>; + ORDER BY ordinal_position`, + [tableName] + ); // 2. column_labels 테이블에서 라벨 정보 조회 - const columnLabels = await prisma.column_labels.findMany({ - where: { table_name: tableName }, - select: { - column_name: true, - column_label: true, - }, - }); + const columnLabels = await query<{ + column_name: string; + column_label: string | null; + }>( + `SELECT column_name, column_label + FROM column_labels + WHERE table_name = $1`, + [tableName] + ); // 3. 라벨 정보를 맵으로 변환 const labelMap = new Map();