feat: Phase 3.13 EntityJoinService Raw Query 전환 완료

엔티티 조인 관계 관리 서비스의 모든 Prisma 호출을 Raw Query로 전환

전환 완료: 5개 Prisma 호출

1. detectEntityJoins - 엔티티 컬럼 감지
   - column_labels.findMany to query
   - web_type = entity 필터

2-3. validateJoinConfig - 테이블/컬럼 존재 확인
   - queryRaw to query
   - information_schema 조회

4-5. getReferenceTableColumns - 컬럼 정보/라벨 조회
   - queryRaw, findMany to query
   - 문자열 타입 컬럼 필터링

기술적 개선사항:
- information_schema 쿼리 파라미터 바인딩
- IS NOT NULL 조건 변환
- 타입 안전성 강화

문서: PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md
진행률: Phase 3 141/162 (87.0%)
This commit is contained in:
kjs
2025-10-01 12:10:34 +09:00
parent b4b4c774fb
commit 28eff9ecc1
3 changed files with 116 additions and 61 deletions

View File

@@ -9,12 +9,12 @@ EntityJoinService는 **5개의 Prisma 호출**이 있으며, 엔티티 간 조
| 항목 | 내용 | | 항목 | 내용 |
| --------------- | ------------------------------------------------ | | --------------- | ------------------------------------------------ |
| 파일 위치 | `backend-node/src/services/entityJoinService.ts` | | 파일 위치 | `backend-node/src/services/entityJoinService.ts` |
| 파일 크기 | 574 라인 | | 파일 크기 | 575 라인 |
| Prisma 호출 | 5 | | Prisma 호출 | 0(전환 완료) |
| **현재 진행률** | **0/5 (0%)** 🔄 **진행 예정** | | **현재 진행률** | **5/5 (100%)** **전환 완료** |
| 복잡도 | 중간 (조인 쿼리, 관계 설정) | | 복잡도 | 중간 (조인 쿼리, 관계 설정) |
| 우선순위 | 🟡 중간 (Phase 3.13) | | 우선순위 | 🟡 중간 (Phase 3.13) |
| **상태** | **대기 중** | | **상태** | **완료** |
### 🎯 전환 목표 ### 🎯 전환 목표
@@ -32,23 +32,28 @@ EntityJoinService는 **5개의 Prisma 호출**이 있으며, 엔티티 간 조
### 주요 기능 (5개 예상) ### 주요 기능 (5개 예상)
#### 1. **엔티티 조인 목록 조회** #### 1. **엔티티 조인 목록 조회**
- findMany with filters - findMany with filters
- 동적 WHERE 조건 - 동적 WHERE 조건
- 페이징, 정렬 - 페이징, 정렬
#### 2. **엔티티 조인 단건 조회** #### 2. **엔티티 조인 단건 조회**
- findUnique or findFirst - findUnique or findFirst
- join_id 기준 - join_id 기준
#### 3. **엔티티 조인 생성** #### 3. **엔티티 조인 생성**
- create - create
- 조인 유효성 검증 - 조인 유효성 검증
#### 4. **엔티티 조인 수정** #### 4. **엔티티 조인 수정**
- update - update
- 동적 UPDATE 쿼리 - 동적 UPDATE 쿼리
#### 5. **엔티티 조인 삭제** #### 5. **엔티티 조인 삭제**
- delete - delete
--- ---
@@ -56,6 +61,7 @@ EntityJoinService는 **5개의 Prisma 호출**이 있으며, 엔티티 간 조
## 💡 전환 전략 ## 💡 전환 전략
### 1단계: 기본 CRUD 전환 (5개) ### 1단계: 기본 CRUD 전환 (5개)
- getEntityJoins() - 목록 조회 - getEntityJoins() - 목록 조회
- getEntityJoin() - 단건 조회 - getEntityJoin() - 단건 조회
- createEntityJoin() - 생성 - createEntityJoin() - 생성
@@ -69,6 +75,7 @@ EntityJoinService는 **5개의 Prisma 호출**이 있으며, 엔티티 간 조
### 예시 1: 조인 설정 조회 (LEFT JOIN으로 테이블 정보 포함) ### 예시 1: 조인 설정 조회 (LEFT JOIN으로 테이블 정보 포함)
**변경 전**: **변경 전**:
```typescript ```typescript
const joins = await prisma.entity_joins.findMany({ const joins = await prisma.entity_joins.findMany({
where: { where: {
@@ -84,6 +91,7 @@ const joins = await prisma.entity_joins.findMany({
``` ```
**변경 후**: **변경 후**:
```typescript ```typescript
const joins = await query<any>( const joins = await query<any>(
`SELECT `SELECT
@@ -104,6 +112,7 @@ const joins = await query<any>(
### 예시 2: 조인 생성 (유효성 검증 포함) ### 예시 2: 조인 생성 (유효성 검증 포함)
**변경 전**: **변경 전**:
```typescript ```typescript
// 조인 유효성 검증 // 조인 유효성 검증
const sourceTable = await prisma.tables.findUnique({ const sourceTable = await prisma.tables.findUnique({
@@ -131,17 +140,12 @@ const join = await prisma.entity_joins.create({
``` ```
**변경 후**: **변경 후**:
```typescript ```typescript
// 조인 유효성 검증 (Promise.all로 병렬 실행) // 조인 유효성 검증 (Promise.all로 병렬 실행)
const [sourceTable, targetTable] = await Promise.all([ const [sourceTable, targetTable] = await Promise.all([
queryOne<any>( queryOne<any>(`SELECT * FROM tables WHERE table_id = $1`, [sourceTableId]),
`SELECT * FROM tables WHERE table_id = $1`, queryOne<any>(`SELECT * FROM tables WHERE table_id = $1`, [targetTableId]),
[sourceTableId]
),
queryOne<any>(
`SELECT * FROM tables WHERE table_id = $1`,
[targetTableId]
),
]); ]);
if (!sourceTable || !targetTable) { if (!sourceTable || !targetTable) {
@@ -162,6 +166,7 @@ const join = await queryOne<any>(
### 예시 3: 조인 수정 ### 예시 3: 조인 수정
**변경 전**: **변경 전**:
```typescript ```typescript
const join = await prisma.entity_joins.update({ const join = await prisma.entity_joins.update({
where: { join_id: joinId }, where: { join_id: joinId },
@@ -174,6 +179,7 @@ const join = await prisma.entity_joins.update({
``` ```
**변경 후**: **변경 후**:
```typescript ```typescript
const updateFields: string[] = ["updated_at = NOW()"]; const updateFields: string[] = ["updated_at = NOW()"];
const values: any[] = []; const values: any[] = [];
@@ -208,6 +214,7 @@ const join = await queryOne<any>(
## 🔧 기술적 고려사항 ## 🔧 기술적 고려사항
### 1. 조인 타입 검증 ### 1. 조인 타입 검증
```typescript ```typescript
const VALID_JOIN_TYPES = ["INNER", "LEFT", "RIGHT", "FULL"]; const VALID_JOIN_TYPES = ["INNER", "LEFT", "RIGHT", "FULL"];
if (!VALID_JOIN_TYPES.includes(joinType)) { if (!VALID_JOIN_TYPES.includes(joinType)) {
@@ -216,6 +223,7 @@ if (!VALID_JOIN_TYPES.includes(joinType)) {
``` ```
### 2. 조인 조건 검증 ### 2. 조인 조건 검증
```typescript ```typescript
// 조인 조건은 SQL 조건식 형태 (예: "source.id = target.parent_id") // 조인 조건은 SQL 조건식 형태 (예: "source.id = target.parent_id")
// SQL 인젝션 방지를 위한 검증 필요 // SQL 인젝션 방지를 위한 검증 필요
@@ -226,6 +234,7 @@ if (!isValidJoinCondition) {
``` ```
### 3. 순환 참조 방지 ### 3. 순환 참조 방지
```typescript ```typescript
// 조인이 순환 참조를 만들지 않는지 검증 // 조인이 순환 참조를 만들지 않는지 검증
async function checkCircularReference( async function checkCircularReference(
@@ -238,13 +247,54 @@ async function checkCircularReference(
``` ```
### 4. LEFT JOIN으로 관련 테이블 정보 조회 ### 4. LEFT JOIN으로 관련 테이블 정보 조회
조인 설정 조회 시 source/target 테이블 정보를 함께 가져오기 위해 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) - [ ] `getEntityJoins()` - 목록 조회 (findMany with include)
- [ ] `getEntityJoin()` - 단건 조회 (findUnique) - [ ] `getEntityJoin()` - 단건 조회 (findUnique)
- [ ] `createEntityJoin()` - 생성 (create with validation) - [ ] `createEntityJoin()` - 생성 (create with validation)
@@ -252,17 +302,20 @@ async function checkCircularReference(
- [ ] `deleteEntityJoin()` - 삭제 (delete) - [ ] `deleteEntityJoin()` - 삭제 (delete)
### 2단계: 코드 정리 ### 2단계: 코드 정리
- [ ] import 문 수정 (`prisma``query, queryOne`) - [ ] import 문 수정 (`prisma``query, queryOne`)
- [ ] 조인 유효성 검증 로직 유지 - [ ] 조인 유효성 검증 로직 유지
- [ ] Prisma import 완전 제거 - [ ] Prisma import 완전 제거
### 3단계: 테스트 ### 3단계: 테스트
- [ ] 단위 테스트 작성 (5개) - [ ] 단위 테스트 작성 (5개)
- [ ] 조인 유효성 검증 테스트 - [ ] 조인 유효성 검증 테스트
- [ ] 순환 참조 방지 테스트 - [ ] 순환 참조 방지 테스트
- [ ] 통합 테스트 작성 (2개) - [ ] 통합 테스트 작성 (2개)
### 4단계: 문서화 ### 4단계: 문서화
- [ ] 전환 완료 문서 업데이트 - [ ] 전환 완료 문서 업데이트
--- ---
@@ -273,11 +326,9 @@ async function checkCircularReference(
- LEFT JOIN 쿼리 - LEFT JOIN 쿼리
- 조인 유효성 검증 - 조인 유효성 검증
- 순환 참조 방지 - 순환 참조 방지
- **예상 소요 시간**: 1시간 - **예상 소요 시간**: 1시간
--- ---
**상태**: ⏳ **대기 중** **상태**: ⏳ **대기 중**
**특이사항**: LEFT JOIN, 조인 유효성 검증, 순환 참조 방지 포함 **특이사항**: LEFT JOIN, 조인 유효성 검증, 순환 참조 방지 포함

View File

@@ -137,7 +137,7 @@ backend-node/ (루트)
- `ddlAuditLogger.ts` (0개) - ✅ **전환 완료** (Phase 3.11) - [계획서](PHASE3.11_DDL_AUDIT_LOGGER_MIGRATION.md) - `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) - `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) - `authService.ts` (5개) - 사용자 인증 - [계획서](PHASE3.14_AUTH_SERVICE_MIGRATION.md)
- **배치 관련 서비스 (24개)** - [통합 계획서](PHASE3.15_BATCH_SERVICES_MIGRATION.md) - **배치 관련 서비스 (24개)** - [통합 계획서](PHASE3.15_BATCH_SERVICES_MIGRATION.md)
- `batchExternalDbService.ts` (8개) - 배치 외부DB - `batchExternalDbService.ts` (8개) - 배치 외부DB

View File

@@ -1,5 +1,4 @@
import { PrismaClient } from "@prisma/client"; import { query, queryOne } from "../database/db";
import prisma from "../config/database";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { import {
EntityJoinConfig, EntityJoinConfig,
@@ -26,20 +25,20 @@ export class EntityJoinService {
logger.info(`Entity 컬럼 감지 시작: ${tableName}`); logger.info(`Entity 컬럼 감지 시작: ${tableName}`);
// column_labels에서 entity 타입인 컬럼들 조회 // column_labels에서 entity 타입인 컬럼들 조회
const entityColumns = await prisma.column_labels.findMany({ const entityColumns = await query<{
where: { column_name: string;
table_name: tableName, reference_table: string;
web_type: "entity", reference_column: string;
reference_table: { not: null }, display_column: string | null;
reference_column: { not: null }, }>(
}, `SELECT column_name, reference_table, reference_column, display_column
select: { FROM column_labels
column_name: true, WHERE table_name = $1
reference_table: true, AND web_type = $2
reference_column: true, AND reference_table IS NOT NULL
display_column: true, AND reference_column IS NOT NULL`,
}, [tableName, "entity"]
}); );
logger.info(`🔍 Entity 컬럼 조회 결과: ${entityColumns.length}개 발견`); logger.info(`🔍 Entity 컬럼 조회 결과: ${entityColumns.length}개 발견`);
entityColumns.forEach((col, index) => { entityColumns.forEach((col, index) => {
@@ -401,13 +400,14 @@ export class EntityJoinService {
}); });
// 참조 테이블 존재 확인 // 참조 테이블 존재 확인
const tableExists = await prisma.$queryRaw` const tableExists = await query<{ exists: number }>(
SELECT 1 FROM information_schema.tables `SELECT 1 as exists FROM information_schema.tables
WHERE table_name = ${config.referenceTable} WHERE table_name = $1
LIMIT 1 LIMIT 1`,
`; [config.referenceTable]
);
if (!Array.isArray(tableExists) || tableExists.length === 0) { if (tableExists.length === 0) {
logger.warn(`참조 테이블이 존재하지 않음: ${config.referenceTable}`); logger.warn(`참조 테이블이 존재하지 않음: ${config.referenceTable}`);
return false; return false;
} }
@@ -420,14 +420,15 @@ export class EntityJoinService {
// 🚨 display_column이 항상 "none"이므로, 표시 컬럼이 없어도 조인 허용 // 🚨 display_column이 항상 "none"이므로, 표시 컬럼이 없어도 조인 허용
if (displayColumn && displayColumn !== "none") { if (displayColumn && displayColumn !== "none") {
const columnExists = await prisma.$queryRaw` const columnExists = await query<{ exists: number }>(
SELECT 1 FROM information_schema.columns `SELECT 1 as exists FROM information_schema.columns
WHERE table_name = ${config.referenceTable} WHERE table_name = $1
AND column_name = ${displayColumn} AND column_name = $2
LIMIT 1 LIMIT 1`,
`; [config.referenceTable, displayColumn]
);
if (!Array.isArray(columnExists) || columnExists.length === 0) { if (columnExists.length === 0) {
logger.warn( logger.warn(
`표시 컬럼이 존재하지 않음: ${config.referenceTable}.${displayColumn}` `표시 컬럼이 존재하지 않음: ${config.referenceTable}.${displayColumn}`
); );
@@ -528,27 +529,30 @@ export class EntityJoinService {
> { > {
try { try {
// 1. 테이블의 기본 컬럼 정보 조회 // 1. 테이블의 기본 컬럼 정보 조회
const columns = (await prisma.$queryRaw` const columns = await query<{
SELECT column_name: string;
data_type: string;
}>(
`SELECT
column_name, column_name,
data_type data_type
FROM information_schema.columns FROM information_schema.columns
WHERE table_name = ${tableName} WHERE table_name = $1
AND data_type IN ('character varying', 'varchar', 'text', 'char') AND data_type IN ('character varying', 'varchar', 'text', 'char')
ORDER BY ordinal_position ORDER BY ordinal_position`,
`) as Array<{ [tableName]
column_name: string; );
data_type: string;
}>;
// 2. column_labels 테이블에서 라벨 정보 조회 // 2. column_labels 테이블에서 라벨 정보 조회
const columnLabels = await prisma.column_labels.findMany({ const columnLabels = await query<{
where: { table_name: tableName }, column_name: string;
select: { column_label: string | null;
column_name: true, }>(
column_label: true, `SELECT column_name, column_label
}, FROM column_labels
}); WHERE table_name = $1`,
[tableName]
);
// 3. 라벨 정보를 맵으로 변환 // 3. 라벨 정보를 맵으로 변환
const labelMap = new Map<string, string>(); const labelMap = new Map<string, string>();