2025-09-30 17:40:21 +09:00
|
|
|
# 📊 Phase 2.3: DataflowService Raw Query 전환 계획
|
|
|
|
|
|
|
|
|
|
## 📋 개요
|
|
|
|
|
|
|
|
|
|
DataflowService는 **31개의 Prisma 호출**이 있는 핵심 서비스입니다. 테이블 간 관계 관리, 데이터플로우 다이어그램, 데이터 연결 브리지 등 복잡한 기능을 포함합니다.
|
|
|
|
|
|
|
|
|
|
### 📊 기본 정보
|
|
|
|
|
|
|
|
|
|
| 항목 | 내용 |
|
|
|
|
|
| --------------- | ---------------------------------------------- |
|
|
|
|
|
| 파일 위치 | `backend-node/src/services/dataflowService.ts` |
|
2025-10-01 10:03:41 +09:00
|
|
|
| 파일 크기 | 1,170+ 라인 |
|
|
|
|
|
| Prisma 호출 | 0개 (전환 완료) |
|
|
|
|
|
| **현재 진행률** | **31/31 (100%)** ✅ **완료** |
|
2025-09-30 17:40:21 +09:00
|
|
|
| 복잡도 | 매우 높음 (트랜잭션 + 복잡한 관계 관리) |
|
|
|
|
|
| 우선순위 | 🔴 최우선 (Phase 2.3) |
|
2025-10-01 10:03:41 +09:00
|
|
|
| **상태** | ✅ **전환 완료 및 컴파일 성공** |
|
2025-09-30 17:40:21 +09:00
|
|
|
|
|
|
|
|
### 🎯 전환 목표
|
|
|
|
|
|
|
|
|
|
- ✅ 31개 Prisma 호출을 모두 Raw Query로 전환
|
|
|
|
|
- ✅ 트랜잭션 처리 정상 동작 확인
|
|
|
|
|
- ✅ 에러 처리 및 롤백 정상 동작
|
|
|
|
|
- ✅ 모든 단위 테스트 통과 (20개 이상)
|
|
|
|
|
- ✅ 통합 테스트 작성 완료
|
|
|
|
|
- ✅ Prisma import 완전 제거
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🔍 Prisma 사용 현황 분석
|
|
|
|
|
|
|
|
|
|
### 1. 테이블 관계 관리 (Table Relationships) - 22개
|
|
|
|
|
|
|
|
|
|
#### 1.1 관계 생성 (3개)
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Line 48: 최대 diagram_id 조회
|
|
|
|
|
await prisma.table_relationships.findFirst({
|
|
|
|
|
where: { company_code },
|
|
|
|
|
orderBy: { diagram_id: 'desc' }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 64: 중복 관계 확인
|
|
|
|
|
await prisma.table_relationships.findFirst({
|
|
|
|
|
where: { diagram_id, source_table, target_table, relationship_type }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 83: 새 관계 생성
|
|
|
|
|
await prisma.table_relationships.create({
|
|
|
|
|
data: { diagram_id, source_table, target_table, ... }
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 1.2 관계 조회 (6개)
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Line 128: 관계 목록 조회
|
|
|
|
|
await prisma.table_relationships.findMany({
|
|
|
|
|
where: whereCondition,
|
|
|
|
|
orderBy: { created_at: 'desc' }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 164: 단일 관계 조회
|
|
|
|
|
await prisma.table_relationships.findFirst({
|
|
|
|
|
where: whereCondition
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 287: 회사별 관계 조회
|
|
|
|
|
await prisma.table_relationships.findMany({
|
|
|
|
|
where: { company_code, is_active: 'Y' },
|
|
|
|
|
orderBy: { diagram_id: 'asc' }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 326: 테이블별 관계 조회
|
|
|
|
|
await prisma.table_relationships.findMany({
|
|
|
|
|
where: whereCondition,
|
|
|
|
|
orderBy: { relationship_type: 'asc' }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 784: diagram_id별 관계 조회
|
|
|
|
|
await prisma.table_relationships.findMany({
|
|
|
|
|
where: whereCondition,
|
|
|
|
|
select: { diagram_id, diagram_name, source_table, ... }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 883: 회사 코드로 전체 조회
|
|
|
|
|
await prisma.table_relationships.findMany({
|
|
|
|
|
where: { company_code, is_active: 'Y' }
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 1.3 통계 조회 (3개)
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Line 362: 전체 관계 수
|
|
|
|
|
await prisma.table_relationships.count({
|
|
|
|
|
where: whereCondition,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 367: 관계 타입별 통계
|
|
|
|
|
await prisma.table_relationships.groupBy({
|
|
|
|
|
by: ["relationship_type"],
|
|
|
|
|
where: whereCondition,
|
|
|
|
|
_count: { relationship_id: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 376: 연결 타입별 통계
|
|
|
|
|
await prisma.table_relationships.groupBy({
|
|
|
|
|
by: ["connection_type"],
|
|
|
|
|
where: whereCondition,
|
|
|
|
|
_count: { relationship_id: true },
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 1.4 관계 수정/삭제 (5개)
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Line 209: 관계 수정
|
|
|
|
|
await prisma.table_relationships.update({
|
|
|
|
|
where: { relationship_id },
|
|
|
|
|
data: { source_table, target_table, ... }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 248: 소프트 삭제
|
|
|
|
|
await prisma.table_relationships.update({
|
|
|
|
|
where: { relationship_id },
|
|
|
|
|
data: { is_active: 'N', updated_at: new Date() }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 936: 중복 diagram_name 확인
|
|
|
|
|
await prisma.table_relationships.findFirst({
|
|
|
|
|
where: { company_code, diagram_name, is_active: 'Y' }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 953: 최대 diagram_id 조회 (복사용)
|
|
|
|
|
await prisma.table_relationships.findFirst({
|
|
|
|
|
where: { company_code },
|
|
|
|
|
orderBy: { diagram_id: 'desc' }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 1015: 관계도 완전 삭제
|
|
|
|
|
await prisma.table_relationships.deleteMany({
|
|
|
|
|
where: { company_code, diagram_id, is_active: 'Y' }
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 1.5 복잡한 조회 (5개)
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Line 919: 원본 관계도 조회
|
|
|
|
|
await prisma.table_relationships.findMany({
|
|
|
|
|
where: { company_code, diagram_id: sourceDiagramId, is_active: "Y" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 1046: diagram_id로 모든 관계 조회
|
|
|
|
|
await prisma.table_relationships.findMany({
|
|
|
|
|
where: { diagram_id, is_active: "Y" },
|
|
|
|
|
orderBy: { created_at: "asc" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 1085: 특정 relationship_id의 diagram_id 찾기
|
|
|
|
|
await prisma.table_relationships.findFirst({
|
|
|
|
|
where: { relationship_id, company_code },
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2. 데이터 연결 브리지 (Data Relationship Bridge) - 8개
|
|
|
|
|
|
|
|
|
|
#### 2.1 브리지 생성/수정 (4개)
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Line 425: 브리지 생성
|
|
|
|
|
await prisma.data_relationship_bridge.create({
|
|
|
|
|
data: {
|
|
|
|
|
relationship_id,
|
|
|
|
|
source_record_id,
|
|
|
|
|
target_record_id,
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 554: 브리지 수정
|
|
|
|
|
await prisma.data_relationship_bridge.update({
|
|
|
|
|
where: whereCondition,
|
|
|
|
|
data: { target_record_id, ... }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 595: 브리지 소프트 삭제
|
|
|
|
|
await prisma.data_relationship_bridge.update({
|
|
|
|
|
where: whereCondition,
|
|
|
|
|
data: { is_active: 'N', updated_at: new Date() }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 637: 브리지 일괄 삭제
|
|
|
|
|
await prisma.data_relationship_bridge.updateMany({
|
|
|
|
|
where: whereCondition,
|
|
|
|
|
data: { is_active: 'N', updated_at: new Date() }
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 2.2 브리지 조회 (4개)
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Line 471: relationship_id로 브리지 조회
|
|
|
|
|
await prisma.data_relationship_bridge.findMany({
|
|
|
|
|
where: whereCondition,
|
|
|
|
|
orderBy: { created_at: "desc" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 512: 레코드별 브리지 조회
|
|
|
|
|
await prisma.data_relationship_bridge.findMany({
|
|
|
|
|
where: whereCondition,
|
|
|
|
|
orderBy: { created_at: "desc" },
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3. Raw Query 사용 (이미 있음) - 1개
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Line 673: 테이블 존재 확인
|
|
|
|
|
await prisma.$queryRaw`
|
|
|
|
|
SELECT table_name
|
|
|
|
|
FROM information_schema.tables
|
|
|
|
|
WHERE table_schema = 'public'
|
|
|
|
|
AND table_name = ${tableName}
|
|
|
|
|
`;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 4. 트랜잭션 사용 - 1개
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Line 968: 관계도 복사 트랜잭션
|
|
|
|
|
await prisma.$transaction(
|
|
|
|
|
originalRelationships.map((rel) =>
|
|
|
|
|
prisma.table_relationships.create({
|
|
|
|
|
data: {
|
|
|
|
|
diagram_id: newDiagramId,
|
|
|
|
|
company_code: companyCode,
|
|
|
|
|
source_table: rel.source_table,
|
|
|
|
|
target_table: rel.target_table,
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🛠️ 전환 전략
|
|
|
|
|
|
|
|
|
|
### 전략 1: 단계적 전환
|
|
|
|
|
|
|
|
|
|
1. **1단계**: 단순 CRUD 전환 (findFirst, findMany, create, update, delete)
|
|
|
|
|
2. **2단계**: 복잡한 조회 전환 (groupBy, count, 조건부 조회)
|
|
|
|
|
3. **3단계**: 트랜잭션 전환
|
|
|
|
|
4. **4단계**: Raw Query 개선
|
|
|
|
|
|
|
|
|
|
### 전략 2: 함수별 전환 우선순위
|
|
|
|
|
|
|
|
|
|
#### 🔴 최우선 (기본 CRUD)
|
|
|
|
|
|
|
|
|
|
- `createRelationship()` - Line 83
|
|
|
|
|
- `getRelationships()` - Line 128
|
|
|
|
|
- `getRelationshipById()` - Line 164
|
|
|
|
|
- `updateRelationship()` - Line 209
|
|
|
|
|
- `deleteRelationship()` - Line 248
|
|
|
|
|
|
|
|
|
|
#### 🟡 2순위 (브리지 관리)
|
|
|
|
|
|
|
|
|
|
- `createDataLink()` - Line 425
|
|
|
|
|
- `getLinkedData()` - Line 471
|
|
|
|
|
- `getLinkedDataByRecord()` - Line 512
|
|
|
|
|
- `updateDataLink()` - Line 554
|
|
|
|
|
- `deleteDataLink()` - Line 595
|
|
|
|
|
|
|
|
|
|
#### 🟢 3순위 (통계 & 조회)
|
|
|
|
|
|
|
|
|
|
- `getRelationshipStats()` - Line 362-376
|
|
|
|
|
- `getAllRelationshipsByCompany()` - Line 287
|
|
|
|
|
- `getRelationshipsByTable()` - Line 326
|
|
|
|
|
- `getDiagrams()` - Line 784
|
|
|
|
|
|
|
|
|
|
#### 🔵 4순위 (복잡한 기능)
|
|
|
|
|
|
|
|
|
|
- `copyDiagram()` - Line 968 (트랜잭션)
|
|
|
|
|
- `deleteDiagram()` - Line 1015
|
|
|
|
|
- `getRelationshipsForDiagram()` - Line 1046
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 📝 전환 예시
|
|
|
|
|
|
|
|
|
|
### 예시 1: createRelationship() 전환
|
|
|
|
|
|
|
|
|
|
**기존 Prisma 코드:**
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Line 48: 최대 diagram_id 조회
|
|
|
|
|
const maxDiagramId = await prisma.table_relationships.findFirst({
|
|
|
|
|
where: { company_code: data.companyCode },
|
|
|
|
|
orderBy: { diagram_id: 'desc' }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 64: 중복 관계 확인
|
|
|
|
|
const existingRelationship = await prisma.table_relationships.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
diagram_id: diagramId,
|
|
|
|
|
source_table: data.sourceTable,
|
|
|
|
|
target_table: data.targetTable,
|
|
|
|
|
relationship_type: data.relationshipType
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 83: 새 관계 생성
|
|
|
|
|
const relationship = await prisma.table_relationships.create({
|
|
|
|
|
data: {
|
|
|
|
|
diagram_id: diagramId,
|
|
|
|
|
company_code: data.companyCode,
|
|
|
|
|
diagram_name: data.diagramName,
|
|
|
|
|
source_table: data.sourceTable,
|
|
|
|
|
target_table: data.targetTable,
|
|
|
|
|
relationship_type: data.relationshipType,
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**새로운 Raw Query 코드:**
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import { query } from "../database/db";
|
|
|
|
|
|
|
|
|
|
// 최대 diagram_id 조회
|
|
|
|
|
const maxDiagramResult = await query<{ diagram_id: number }>(
|
|
|
|
|
`SELECT diagram_id FROM table_relationships
|
|
|
|
|
WHERE company_code = $1
|
|
|
|
|
ORDER BY diagram_id DESC
|
|
|
|
|
LIMIT 1`,
|
|
|
|
|
[data.companyCode]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const diagramId =
|
|
|
|
|
data.diagramId ||
|
|
|
|
|
(maxDiagramResult.length > 0 ? maxDiagramResult[0].diagram_id + 1 : 1);
|
|
|
|
|
|
|
|
|
|
// 중복 관계 확인
|
|
|
|
|
const existingResult = await query<{ relationship_id: number }>(
|
|
|
|
|
`SELECT relationship_id FROM table_relationships
|
|
|
|
|
WHERE diagram_id = $1
|
|
|
|
|
AND source_table = $2
|
|
|
|
|
AND target_table = $3
|
|
|
|
|
AND relationship_type = $4
|
|
|
|
|
LIMIT 1`,
|
|
|
|
|
[diagramId, data.sourceTable, data.targetTable, data.relationshipType]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (existingResult.length > 0) {
|
|
|
|
|
throw new Error("이미 존재하는 관계입니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 새 관계 생성
|
|
|
|
|
const [relationship] = await query<TableRelationship>(
|
|
|
|
|
`INSERT INTO table_relationships (
|
|
|
|
|
diagram_id, company_code, diagram_name, source_table, target_table,
|
|
|
|
|
relationship_type, connection_type, source_column, target_column,
|
|
|
|
|
is_active, created_at, updated_at
|
|
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'Y', NOW(), NOW())
|
|
|
|
|
RETURNING *`,
|
|
|
|
|
[
|
|
|
|
|
diagramId,
|
|
|
|
|
data.companyCode,
|
|
|
|
|
data.diagramName,
|
|
|
|
|
data.sourceTable,
|
|
|
|
|
data.targetTable,
|
|
|
|
|
data.relationshipType,
|
|
|
|
|
data.connectionType,
|
|
|
|
|
data.sourceColumn,
|
|
|
|
|
data.targetColumn,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 예시 2: getRelationshipStats() 전환 (통계 조회)
|
|
|
|
|
|
|
|
|
|
**기존 Prisma 코드:**
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Line 362: 전체 관계 수
|
|
|
|
|
const totalCount = await prisma.table_relationships.count({
|
|
|
|
|
where: whereCondition,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 367: 관계 타입별 통계
|
|
|
|
|
const relationshipTypeStats = await prisma.table_relationships.groupBy({
|
|
|
|
|
by: ["relationship_type"],
|
|
|
|
|
where: whereCondition,
|
|
|
|
|
_count: { relationship_id: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Line 376: 연결 타입별 통계
|
|
|
|
|
const connectionTypeStats = await prisma.table_relationships.groupBy({
|
|
|
|
|
by: ["connection_type"],
|
|
|
|
|
where: whereCondition,
|
|
|
|
|
_count: { relationship_id: true },
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**새로운 Raw Query 코드:**
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// WHERE 조건 동적 생성
|
|
|
|
|
const whereParams: any[] = [];
|
|
|
|
|
let whereSQL = "";
|
|
|
|
|
let paramIndex = 1;
|
|
|
|
|
|
|
|
|
|
if (companyCode) {
|
|
|
|
|
whereSQL += `WHERE company_code = $${paramIndex}`;
|
|
|
|
|
whereParams.push(companyCode);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
|
|
|
|
|
if (isActive !== undefined) {
|
|
|
|
|
whereSQL += ` AND is_active = $${paramIndex}`;
|
|
|
|
|
whereParams.push(isActive ? "Y" : "N");
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 전체 관계 수
|
|
|
|
|
const [totalResult] = await query<{ count: number }>(
|
|
|
|
|
`SELECT COUNT(*) as count
|
|
|
|
|
FROM table_relationships ${whereSQL}`,
|
|
|
|
|
whereParams
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const totalCount = totalResult?.count || 0;
|
|
|
|
|
|
|
|
|
|
// 관계 타입별 통계
|
|
|
|
|
const relationshipTypeStats = await query<{
|
|
|
|
|
relationship_type: string;
|
|
|
|
|
count: number;
|
|
|
|
|
}>(
|
|
|
|
|
`SELECT relationship_type, COUNT(*) as count
|
|
|
|
|
FROM table_relationships ${whereSQL}
|
|
|
|
|
GROUP BY relationship_type
|
|
|
|
|
ORDER BY count DESC`,
|
|
|
|
|
whereParams
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 연결 타입별 통계
|
|
|
|
|
const connectionTypeStats = await query<{
|
|
|
|
|
connection_type: string;
|
|
|
|
|
count: number;
|
|
|
|
|
}>(
|
|
|
|
|
`SELECT connection_type, COUNT(*) as count
|
|
|
|
|
FROM table_relationships ${whereSQL}
|
|
|
|
|
GROUP BY connection_type
|
|
|
|
|
ORDER BY count DESC`,
|
|
|
|
|
whereParams
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 예시 3: copyDiagram() 트랜잭션 전환
|
|
|
|
|
|
|
|
|
|
**기존 Prisma 코드:**
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Line 968: 트랜잭션으로 모든 관계 복사
|
|
|
|
|
const copiedRelationships = await prisma.$transaction(
|
|
|
|
|
originalRelationships.map((rel) =>
|
|
|
|
|
prisma.table_relationships.create({
|
|
|
|
|
data: {
|
|
|
|
|
diagram_id: newDiagramId,
|
|
|
|
|
company_code: companyCode,
|
|
|
|
|
diagram_name: newDiagramName,
|
|
|
|
|
source_table: rel.source_table,
|
|
|
|
|
target_table: rel.target_table,
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**새로운 Raw Query 코드:**
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import { transaction } from "../database/db";
|
|
|
|
|
|
|
|
|
|
const copiedRelationships = await transaction(async (client) => {
|
|
|
|
|
const results: TableRelationship[] = [];
|
|
|
|
|
|
|
|
|
|
for (const rel of originalRelationships) {
|
|
|
|
|
const [copiedRel] = await client.query<TableRelationship>(
|
|
|
|
|
`INSERT INTO table_relationships (
|
|
|
|
|
diagram_id, company_code, diagram_name, source_table, target_table,
|
|
|
|
|
relationship_type, connection_type, source_column, target_column,
|
|
|
|
|
is_active, created_at, updated_at
|
|
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'Y', NOW(), NOW())
|
|
|
|
|
RETURNING *`,
|
|
|
|
|
[
|
|
|
|
|
newDiagramId,
|
|
|
|
|
companyCode,
|
|
|
|
|
newDiagramName,
|
|
|
|
|
rel.source_table,
|
|
|
|
|
rel.target_table,
|
|
|
|
|
rel.relationship_type,
|
|
|
|
|
rel.connection_type,
|
|
|
|
|
rel.source_column,
|
|
|
|
|
rel.target_column,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
results.push(copiedRel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return results;
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🧪 테스트 계획
|
|
|
|
|
|
|
|
|
|
### 단위 테스트 (20개 이상)
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
describe('DataflowService Raw Query 전환 테스트', () => {
|
|
|
|
|
describe('createRelationship', () => {
|
|
|
|
|
test('관계 생성 성공', async () => { ... });
|
|
|
|
|
test('중복 관계 에러', async () => { ... });
|
|
|
|
|
test('diagram_id 자동 생성', async () => { ... });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getRelationships', () => {
|
|
|
|
|
test('전체 관계 조회 성공', async () => { ... });
|
|
|
|
|
test('회사별 필터링', async () => { ... });
|
|
|
|
|
test('diagram_id별 필터링', async () => { ... });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getRelationshipStats', () => {
|
|
|
|
|
test('통계 조회 성공', async () => { ... });
|
|
|
|
|
test('관계 타입별 그룹화', async () => { ... });
|
|
|
|
|
test('연결 타입별 그룹화', async () => { ... });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('copyDiagram', () => {
|
|
|
|
|
test('관계도 복사 성공 (트랜잭션)', async () => { ... });
|
|
|
|
|
test('diagram_name 중복 에러', async () => { ... });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('createDataLink', () => {
|
|
|
|
|
test('데이터 연결 생성 성공', async () => { ... });
|
|
|
|
|
test('브리지 레코드 저장', async () => { ... });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getLinkedData', () => {
|
|
|
|
|
test('연결된 데이터 조회', async () => { ... });
|
|
|
|
|
test('relationship_id별 필터링', async () => { ... });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 통합 테스트 (7개 시나리오)
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
describe('Dataflow 관리 통합 테스트', () => {
|
|
|
|
|
test('관계 생명주기 (생성 → 조회 → 수정 → 삭제)', async () => { ... });
|
|
|
|
|
test('관계도 복사 및 검증', async () => { ... });
|
|
|
|
|
test('데이터 연결 브리지 생성 및 조회', async () => { ... });
|
|
|
|
|
test('통계 정보 조회', async () => { ... });
|
|
|
|
|
test('테이블별 관계 조회', async () => { ... });
|
|
|
|
|
test('diagram_id별 관계 조회', async () => { ... });
|
|
|
|
|
test('관계도 완전 삭제', async () => { ... });
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 📋 체크리스트
|
|
|
|
|
|
2025-10-01 10:03:41 +09:00
|
|
|
### 1단계: 기본 CRUD (8개 함수) ✅ **완료**
|
2025-09-30 17:40:21 +09:00
|
|
|
|
2025-10-01 10:03:41 +09:00
|
|
|
- [x] `createTableRelationship()` - 관계 생성
|
|
|
|
|
- [x] `getTableRelationships()` - 관계 목록 조회
|
|
|
|
|
- [x] `getTableRelationship()` - 단일 관계 조회
|
|
|
|
|
- [x] `updateTableRelationship()` - 관계 수정
|
|
|
|
|
- [x] `deleteTableRelationship()` - 관계 삭제 (소프트)
|
|
|
|
|
- [x] `getRelationshipsByTable()` - 테이블별 조회
|
|
|
|
|
- [x] `getRelationshipsByConnectionType()` - 연결타입별 조회
|
|
|
|
|
- [x] `getDataFlowDiagrams()` - diagram_id별 그룹 조회
|
2025-09-30 17:40:21 +09:00
|
|
|
|
2025-10-01 10:03:41 +09:00
|
|
|
### 2단계: 브리지 관리 (6개 함수) ✅ **완료**
|
2025-09-30 17:40:21 +09:00
|
|
|
|
2025-10-01 10:03:41 +09:00
|
|
|
- [x] `createDataLink()` - 데이터 연결 생성
|
|
|
|
|
- [x] `getLinkedDataByRelationship()` - 관계별 연결 데이터 조회
|
|
|
|
|
- [x] `getLinkedDataByTable()` - 테이블별 연결 데이터 조회
|
|
|
|
|
- [x] `updateDataLink()` - 연결 수정
|
|
|
|
|
- [x] `deleteDataLink()` - 연결 삭제 (소프트)
|
|
|
|
|
- [x] `deleteAllLinkedDataByRelationship()` - 관계별 모든 연결 삭제
|
2025-09-30 17:40:21 +09:00
|
|
|
|
2025-10-01 10:03:41 +09:00
|
|
|
### 3단계: 통계 & 복잡한 조회 (4개 함수) ✅ **완료**
|
2025-09-30 17:40:21 +09:00
|
|
|
|
2025-10-01 10:03:41 +09:00
|
|
|
- [x] `getRelationshipStats()` - 통계 조회
|
|
|
|
|
- [x] count 쿼리 전환
|
|
|
|
|
- [x] groupBy 쿼리 전환 (관계 타입별)
|
|
|
|
|
- [x] groupBy 쿼리 전환 (연결 타입별)
|
|
|
|
|
- [x] `getTableData()` - 테이블 데이터 조회 (페이징)
|
|
|
|
|
- [x] `getDiagramRelationships()` - 관계도 관계 조회
|
|
|
|
|
- [x] `getDiagramRelationshipsByDiagramId()` - diagram_id별 관계 조회
|
2025-09-30 17:40:21 +09:00
|
|
|
|
2025-10-01 10:03:41 +09:00
|
|
|
### 4단계: 복잡한 기능 (3개 함수) ✅ **완료**
|
2025-09-30 17:40:21 +09:00
|
|
|
|
2025-10-01 10:03:41 +09:00
|
|
|
- [x] `copyDiagram()` - 관계도 복사 (트랜잭션)
|
|
|
|
|
- [x] `deleteDiagram()` - 관계도 완전 삭제
|
|
|
|
|
- [x] `getDiagramRelationshipsByRelationshipId()` - relationship_id로 조회
|
2025-09-30 17:40:21 +09:00
|
|
|
|
2025-10-01 10:03:41 +09:00
|
|
|
### 5단계: 테스트 & 검증 ⏳ **진행 필요**
|
2025-09-30 17:40:21 +09:00
|
|
|
|
|
|
|
|
- [ ] 단위 테스트 작성 (20개 이상)
|
2025-10-01 10:03:41 +09:00
|
|
|
- createTableRelationship, updateTableRelationship, deleteTableRelationship
|
|
|
|
|
- getTableRelationships, getTableRelationship
|
|
|
|
|
- createDataLink, getLinkedDataByRelationship
|
2025-09-30 17:40:21 +09:00
|
|
|
- getRelationshipStats
|
|
|
|
|
- copyDiagram
|
|
|
|
|
- [ ] 통합 테스트 작성 (7개 시나리오)
|
|
|
|
|
- 관계 생명주기 테스트
|
|
|
|
|
- 관계도 복사 테스트
|
|
|
|
|
- 데이터 브리지 테스트
|
|
|
|
|
- 통계 조회 테스트
|
2025-10-01 10:03:41 +09:00
|
|
|
- [x] Prisma import 완전 제거 확인
|
2025-09-30 17:40:21 +09:00
|
|
|
- [ ] 성능 테스트
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🎯 완료 기준
|
|
|
|
|
|
2025-10-01 10:03:41 +09:00
|
|
|
- [x] **31개 Prisma 호출 모두 Raw Query로 전환 완료** ✅
|
|
|
|
|
- [x] **모든 TypeScript 컴파일 오류 해결** ✅
|
|
|
|
|
- [x] **트랜잭션 정상 동작 확인** ✅
|
|
|
|
|
- [x] **에러 처리 및 롤백 정상 동작** ✅
|
|
|
|
|
- [ ] **모든 단위 테스트 통과 (20개 이상)** ⏳
|
|
|
|
|
- [ ] **모든 통합 테스트 작성 완료 (7개 시나리오)** ⏳
|
|
|
|
|
- [x] **Prisma import 완전 제거** ✅
|
|
|
|
|
- [ ] **성능 저하 없음 (기존 대비 ±10% 이내)** ⏳
|
2025-09-30 17:40:21 +09:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🎯 주요 기술적 도전 과제
|
|
|
|
|
|
|
|
|
|
### 1. groupBy 쿼리 전환
|
|
|
|
|
|
|
|
|
|
**문제**: Prisma의 `groupBy`를 Raw Query로 전환
|
|
|
|
|
**해결**: PostgreSQL의 `GROUP BY` 및 집계 함수 사용
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
SELECT relationship_type, COUNT(*) as count
|
|
|
|
|
FROM table_relationships
|
|
|
|
|
WHERE company_code = $1 AND is_active = 'Y'
|
|
|
|
|
GROUP BY relationship_type
|
|
|
|
|
ORDER BY count DESC
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2. 트랜잭션 배열 처리
|
|
|
|
|
|
|
|
|
|
**문제**: Prisma의 `$transaction([...])` 배열 방식을 Raw Query로 전환
|
|
|
|
|
**해결**: `transaction` 함수 내에서 순차 실행
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
await transaction(async (client) => {
|
|
|
|
|
const results = [];
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
const result = await client.query(...);
|
|
|
|
|
results.push(result);
|
|
|
|
|
}
|
|
|
|
|
return results;
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3. 동적 WHERE 조건 생성
|
|
|
|
|
|
|
|
|
|
**문제**: 다양한 필터 조건을 동적으로 구성
|
|
|
|
|
**해결**: 조건부 파라미터 인덱스 관리
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const whereParams: any[] = [];
|
|
|
|
|
const whereConditions: string[] = [];
|
|
|
|
|
let paramIndex = 1;
|
|
|
|
|
|
|
|
|
|
if (companyCode) {
|
|
|
|
|
whereConditions.push(`company_code = $${paramIndex++}`);
|
|
|
|
|
whereParams.push(companyCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (diagramId) {
|
|
|
|
|
whereConditions.push(`diagram_id = $${paramIndex++}`);
|
|
|
|
|
whereParams.push(diagramId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const whereSQL =
|
|
|
|
|
whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2025-10-01 10:03:41 +09:00
|
|
|
## 📊 전환 완료 요약
|
|
|
|
|
|
|
|
|
|
### ✅ 성공적으로 전환된 항목
|
|
|
|
|
|
|
|
|
|
1. **기본 CRUD (8개)**: 모든 테이블 관계 CRUD 작업을 Raw Query로 전환
|
|
|
|
|
2. **브리지 관리 (6개)**: 데이터 연결 브리지의 모든 작업 전환
|
|
|
|
|
3. **통계 & 조회 (4개)**: COUNT, GROUP BY 등 복잡한 통계 쿼리 전환
|
|
|
|
|
4. **복잡한 기능 (3개)**: 트랜잭션 기반 관계도 복사 등 고급 기능 전환
|
|
|
|
|
|
|
|
|
|
### 🔧 주요 기술적 해결 사항
|
|
|
|
|
|
|
|
|
|
1. **트랜잭션 처리**: `transaction()` 함수 내에서 `client.query().rows` 사용
|
|
|
|
|
2. **동적 WHERE 조건**: 파라미터 인덱스를 동적으로 관리하여 유연한 쿼리 생성
|
|
|
|
|
3. **GROUP BY 전환**: Prisma의 `groupBy`를 PostgreSQL의 네이티브 GROUP BY로 전환
|
|
|
|
|
4. **타입 안전성**: 모든 쿼리 결과에 TypeScript 타입 지정
|
|
|
|
|
|
|
|
|
|
### 📈 다음 단계
|
|
|
|
|
|
|
|
|
|
- [ ] 단위 테스트 작성 및 실행
|
|
|
|
|
- [ ] 통합 테스트 시나리오 구현
|
|
|
|
|
- [ ] 성능 벤치마크 테스트
|
|
|
|
|
- [ ] 프로덕션 배포 준비
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2025-09-30 17:40:21 +09:00
|
|
|
**작성일**: 2025-09-30
|
2025-10-01 10:03:41 +09:00
|
|
|
**완료일**: 2025-10-01
|
|
|
|
|
**소요 시간**: 1일
|
2025-09-30 17:40:21 +09:00
|
|
|
**담당자**: 백엔드 개발팀
|
|
|
|
|
**우선순위**: 🔴 최우선 (Phase 2.3)
|
2025-10-01 10:03:41 +09:00
|
|
|
**상태**: ✅ **전환 완료** (테스트 필요)
|