339 lines
8.2 KiB
Markdown
339 lines
8.2 KiB
Markdown
# 📋 Phase 3.13: EntityJoinService Raw Query 전환 계획
|
|
|
|
## 📋 개요
|
|
|
|
EntityJoinService는 **5개의 Prisma 호출**이 있으며, 엔티티 간 조인 관계 관리를 담당하는 서비스입니다.
|
|
|
|
### 📊 기본 정보
|
|
|
|
| 항목 | 내용 |
|
|
| --------------- | ------------------------------------------------ |
|
|
| 파일 위치 | `backend-node/src/services/entityJoinService.ts` |
|
|
| 파일 크기 | 575 라인 |
|
|
| Prisma 호출 | 0개 (전환 완료) |
|
|
| **현재 진행률** | **5/5 (100%)** ✅ **전환 완료** |
|
|
| 복잡도 | 중간 (조인 쿼리, 관계 설정) |
|
|
| 우선순위 | 🟡 중간 (Phase 3.13) |
|
|
| **상태** | ✅ **완료** |
|
|
|
|
### 🎯 전환 목표
|
|
|
|
- ⏳ **5개 모든 Prisma 호출을 `db.ts`의 `query()`, `queryOne()` 함수로 교체**
|
|
- ⏳ 엔티티 조인 설정 CRUD 기능 정상 동작
|
|
- ⏳ 복잡한 조인 쿼리 전환 (LEFT JOIN, INNER JOIN)
|
|
- ⏳ 조인 유효성 검증
|
|
- ⏳ TypeScript 컴파일 성공
|
|
- ⏳ **Prisma import 완전 제거**
|
|
|
|
---
|
|
|
|
## 🔍 예상 Prisma 사용 패턴
|
|
|
|
### 주요 기능 (5개 예상)
|
|
|
|
#### 1. **엔티티 조인 목록 조회**
|
|
|
|
- findMany with filters
|
|
- 동적 WHERE 조건
|
|
- 페이징, 정렬
|
|
|
|
#### 2. **엔티티 조인 단건 조회**
|
|
|
|
- findUnique or findFirst
|
|
- join_id 기준
|
|
|
|
#### 3. **엔티티 조인 생성**
|
|
|
|
- create
|
|
- 조인 유효성 검증
|
|
|
|
#### 4. **엔티티 조인 수정**
|
|
|
|
- update
|
|
- 동적 UPDATE 쿼리
|
|
|
|
#### 5. **엔티티 조인 삭제**
|
|
|
|
- delete
|
|
|
|
---
|
|
|
|
## 💡 전환 전략
|
|
|
|
### 1단계: 기본 CRUD 전환 (5개)
|
|
|
|
- getEntityJoins() - 목록 조회
|
|
- getEntityJoin() - 단건 조회
|
|
- createEntityJoin() - 생성
|
|
- updateEntityJoin() - 수정
|
|
- deleteEntityJoin() - 삭제
|
|
|
|
---
|
|
|
|
## 💻 전환 예시
|
|
|
|
### 예시 1: 조인 설정 조회 (LEFT JOIN으로 테이블 정보 포함)
|
|
|
|
**변경 전**:
|
|
|
|
```typescript
|
|
const joins = await prisma.entity_joins.findMany({
|
|
where: {
|
|
company_code: companyCode,
|
|
is_active: true,
|
|
},
|
|
include: {
|
|
source_table: true,
|
|
target_table: true,
|
|
},
|
|
orderBy: { created_at: "desc" },
|
|
});
|
|
```
|
|
|
|
**변경 후**:
|
|
|
|
```typescript
|
|
const joins = await query<any>(
|
|
`SELECT
|
|
ej.*,
|
|
st.table_name as source_table_name,
|
|
st.table_label as source_table_label,
|
|
tt.table_name as target_table_name,
|
|
tt.table_label as target_table_label
|
|
FROM entity_joins ej
|
|
LEFT JOIN tables st ON ej.source_table_id = st.table_id
|
|
LEFT JOIN tables tt ON ej.target_table_id = tt.table_id
|
|
WHERE ej.company_code = $1 AND ej.is_active = $2
|
|
ORDER BY ej.created_at DESC`,
|
|
[companyCode, true]
|
|
);
|
|
```
|
|
|
|
### 예시 2: 조인 생성 (유효성 검증 포함)
|
|
|
|
**변경 전**:
|
|
|
|
```typescript
|
|
// 조인 유효성 검증
|
|
const sourceTable = await prisma.tables.findUnique({
|
|
where: { table_id: sourceTableId },
|
|
});
|
|
|
|
const targetTable = await prisma.tables.findUnique({
|
|
where: { table_id: targetTableId },
|
|
});
|
|
|
|
if (!sourceTable || !targetTable) {
|
|
throw new Error("Invalid table references");
|
|
}
|
|
|
|
// 조인 생성
|
|
const join = await prisma.entity_joins.create({
|
|
data: {
|
|
source_table_id: sourceTableId,
|
|
target_table_id: targetTableId,
|
|
join_type: joinType,
|
|
join_condition: joinCondition,
|
|
company_code: companyCode,
|
|
},
|
|
});
|
|
```
|
|
|
|
**변경 후**:
|
|
|
|
```typescript
|
|
// 조인 유효성 검증 (Promise.all로 병렬 실행)
|
|
const [sourceTable, targetTable] = await Promise.all([
|
|
queryOne<any>(`SELECT * FROM tables WHERE table_id = $1`, [sourceTableId]),
|
|
queryOne<any>(`SELECT * FROM tables WHERE table_id = $1`, [targetTableId]),
|
|
]);
|
|
|
|
if (!sourceTable || !targetTable) {
|
|
throw new Error("Invalid table references");
|
|
}
|
|
|
|
// 조인 생성
|
|
const join = await queryOne<any>(
|
|
`INSERT INTO entity_joins
|
|
(source_table_id, target_table_id, join_type, join_condition,
|
|
company_code, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
|
|
RETURNING *`,
|
|
[sourceTableId, targetTableId, joinType, joinCondition, companyCode]
|
|
);
|
|
```
|
|
|
|
### 예시 3: 조인 수정
|
|
|
|
**변경 전**:
|
|
|
|
```typescript
|
|
const join = await prisma.entity_joins.update({
|
|
where: { join_id: joinId },
|
|
data: {
|
|
join_type: joinType,
|
|
join_condition: joinCondition,
|
|
is_active: isActive,
|
|
},
|
|
});
|
|
```
|
|
|
|
**변경 후**:
|
|
|
|
```typescript
|
|
const updateFields: string[] = ["updated_at = NOW()"];
|
|
const values: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
if (joinType !== undefined) {
|
|
updateFields.push(`join_type = $${paramIndex++}`);
|
|
values.push(joinType);
|
|
}
|
|
|
|
if (joinCondition !== undefined) {
|
|
updateFields.push(`join_condition = $${paramIndex++}`);
|
|
values.push(joinCondition);
|
|
}
|
|
|
|
if (isActive !== undefined) {
|
|
updateFields.push(`is_active = $${paramIndex++}`);
|
|
values.push(isActive);
|
|
}
|
|
|
|
const join = await queryOne<any>(
|
|
`UPDATE entity_joins
|
|
SET ${updateFields.join(", ")}
|
|
WHERE join_id = $${paramIndex}
|
|
RETURNING *`,
|
|
[...values, joinId]
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 🔧 기술적 고려사항
|
|
|
|
### 1. 조인 타입 검증
|
|
|
|
```typescript
|
|
const VALID_JOIN_TYPES = ["INNER", "LEFT", "RIGHT", "FULL"];
|
|
if (!VALID_JOIN_TYPES.includes(joinType)) {
|
|
throw new Error("Invalid join type");
|
|
}
|
|
```
|
|
|
|
### 2. 조인 조건 검증
|
|
|
|
```typescript
|
|
// 조인 조건은 SQL 조건식 형태 (예: "source.id = target.parent_id")
|
|
// SQL 인젝션 방지를 위한 검증 필요
|
|
const isValidJoinCondition = /^[\w\s.=<>]+$/.test(joinCondition);
|
|
if (!isValidJoinCondition) {
|
|
throw new Error("Invalid join condition");
|
|
}
|
|
```
|
|
|
|
### 3. 순환 참조 방지
|
|
|
|
```typescript
|
|
// 조인이 순환 참조를 만들지 않는지 검증
|
|
async function checkCircularReference(
|
|
sourceTableId: number,
|
|
targetTableId: number
|
|
): Promise<boolean> {
|
|
// 재귀적으로 조인 관계 확인
|
|
// ...
|
|
}
|
|
```
|
|
|
|
### 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 호출 전환 (✅ 완료)
|
|
|
|
- [ ] `getEntityJoins()` - 목록 조회 (findMany with include)
|
|
- [ ] `getEntityJoin()` - 단건 조회 (findUnique)
|
|
- [ ] `createEntityJoin()` - 생성 (create with validation)
|
|
- [ ] `updateEntityJoin()` - 수정 (update)
|
|
- [ ] `deleteEntityJoin()` - 삭제 (delete)
|
|
|
|
### 2단계: 코드 정리
|
|
|
|
- [ ] import 문 수정 (`prisma` → `query, queryOne`)
|
|
- [ ] 조인 유효성 검증 로직 유지
|
|
- [ ] Prisma import 완전 제거
|
|
|
|
### 3단계: 테스트
|
|
|
|
- [ ] 단위 테스트 작성 (5개)
|
|
- [ ] 조인 유효성 검증 테스트
|
|
- [ ] 순환 참조 방지 테스트
|
|
- [ ] 통합 테스트 작성 (2개)
|
|
|
|
### 4단계: 문서화
|
|
|
|
- [ ] 전환 완료 문서 업데이트
|
|
|
|
---
|
|
|
|
## 🎯 예상 난이도 및 소요 시간
|
|
|
|
- **난이도**: ⭐⭐⭐ (중간)
|
|
- LEFT JOIN 쿼리
|
|
- 조인 유효성 검증
|
|
- 순환 참조 방지
|
|
- **예상 소요 시간**: 1시간
|
|
|
|
---
|
|
|
|
**상태**: ⏳ **대기 중**
|
|
**특이사항**: LEFT JOIN, 조인 유효성 검증, 순환 참조 방지 포함
|