ERP-node/PHASE2.3_DATAFLOW_SERVICE_M...

737 lines
20 KiB
Markdown

# 📊 Phase 2.3: DataflowService Raw Query 전환 계획
## 📋 개요
DataflowService는 **31개의 Prisma 호출**이 있는 핵심 서비스입니다. 테이블 간 관계 관리, 데이터플로우 다이어그램, 데이터 연결 브리지 등 복잡한 기능을 포함합니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ---------------------------------------------- |
| 파일 위치 | `backend-node/src/services/dataflowService.ts` |
| 파일 크기 | 1,170+ 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| **현재 진행률** | **31/31 (100%)****완료** |
| 복잡도 | 매우 높음 (트랜잭션 + 복잡한 관계 관리) |
| 우선순위 | 🔴 최우선 (Phase 2.3) |
| **상태** | ✅ **전환 완료 및 컴파일 성공** |
### 🎯 전환 목표
- ✅ 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 () => { ... });
});
```
---
## 📋 체크리스트
### 1단계: 기본 CRUD (8개 함수) ✅ **완료**
- [x] `createTableRelationship()` - 관계 생성
- [x] `getTableRelationships()` - 관계 목록 조회
- [x] `getTableRelationship()` - 단일 관계 조회
- [x] `updateTableRelationship()` - 관계 수정
- [x] `deleteTableRelationship()` - 관계 삭제 (소프트)
- [x] `getRelationshipsByTable()` - 테이블별 조회
- [x] `getRelationshipsByConnectionType()` - 연결타입별 조회
- [x] `getDataFlowDiagrams()` - diagram_id별 그룹 조회
### 2단계: 브리지 관리 (6개 함수) ✅ **완료**
- [x] `createDataLink()` - 데이터 연결 생성
- [x] `getLinkedDataByRelationship()` - 관계별 연결 데이터 조회
- [x] `getLinkedDataByTable()` - 테이블별 연결 데이터 조회
- [x] `updateDataLink()` - 연결 수정
- [x] `deleteDataLink()` - 연결 삭제 (소프트)
- [x] `deleteAllLinkedDataByRelationship()` - 관계별 모든 연결 삭제
### 3단계: 통계 & 복잡한 조회 (4개 함수) ✅ **완료**
- [x] `getRelationshipStats()` - 통계 조회
- [x] count 쿼리 전환
- [x] groupBy 쿼리 전환 (관계 타입별)
- [x] groupBy 쿼리 전환 (연결 타입별)
- [x] `getTableData()` - 테이블 데이터 조회 (페이징)
- [x] `getDiagramRelationships()` - 관계도 관계 조회
- [x] `getDiagramRelationshipsByDiagramId()` - diagram_id별 관계 조회
### 4단계: 복잡한 기능 (3개 함수) ✅ **완료**
- [x] `copyDiagram()` - 관계도 복사 (트랜잭션)
- [x] `deleteDiagram()` - 관계도 완전 삭제
- [x] `getDiagramRelationshipsByRelationshipId()` - relationship_id로 조회
### 5단계: 테스트 & 검증 ⏳ **진행 필요**
- [ ] 단위 테스트 작성 (20개 이상)
- createTableRelationship, updateTableRelationship, deleteTableRelationship
- getTableRelationships, getTableRelationship
- createDataLink, getLinkedDataByRelationship
- getRelationshipStats
- copyDiagram
- [ ] 통합 테스트 작성 (7개 시나리오)
- 관계 생명주기 테스트
- 관계도 복사 테스트
- 데이터 브리지 테스트
- 통계 조회 테스트
- [x] Prisma import 완전 제거 확인
- [ ] 성능 테스트
---
## 🎯 완료 기준
- [x] **31개 Prisma 호출 모두 Raw Query로 전환 완료**
- [x] **모든 TypeScript 컴파일 오류 해결**
- [x] **트랜잭션 정상 동작 확인**
- [x] **에러 처리 및 롤백 정상 동작**
- [ ] **모든 단위 테스트 통과 (20개 이상)**
- [ ] **모든 통합 테스트 작성 완료 (7개 시나리오)**
- [x] **Prisma import 완전 제거**
- [ ] **성능 저하 없음 (기존 대비 ±10% 이내)**
---
## 🎯 주요 기술적 도전 과제
### 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 ")}` : "";
```
---
## 📊 전환 완료 요약
### ✅ 성공적으로 전환된 항목
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
**완료일**: 2025-10-01
**소요 시간**: 1일
**담당자**: 백엔드 개발팀
**우선순위**: 🔴 최우선 (Phase 2.3)
**상태**: ✅ **전환 완료** (테스트 필요)