feat: Phase 3.5 DataflowDiagramService Raw Query 전환 완료

12개 Prisma 호출을 모두 Raw Query로 전환
- 관계도 목록 조회 (getDataflowDiagrams - 페이지네이션, ILIKE 검색)
- 관계도 단건 조회 (getDataflowDiagramById - 동적 WHERE)
- 관계도 생성 (createDataflowDiagram - JSON 필드)
- 관계도 수정 (updateDataflowDiagram - 동적 UPDATE, JSON 필드)
- 관계도 삭제 (deleteDataflowDiagram)
- 관계도 복제 (copyDataflowDiagram - LIKE 검색, 번호 증가)
- 버튼 제어용 조회 (getAllRelationshipsForButtonControl)

주요 기술적 해결:
- 동적 WHERE 조건 생성 (company_code 필터링)
- 동적 UPDATE 쿼리 (변경된 필드만 업데이트)
- JSON 필드 처리 (relationships, node_positions, control, category, plan)
- LIKE 검색 (복제 시 이름 패턴 검색)
- 복잡한 복제 로직 (자동 번호 증가)

TypeScript 컴파일 성공
Prisma import 완전 제거

Phase 3 진행률: 76/162 (46.9%)
전체 진행률: 327/444 (73.6%)
This commit is contained in:
kjs 2025-10-01 11:12:41 +09:00
parent 34295d6afa
commit 7fb2ce582c
2 changed files with 181 additions and 178 deletions

View File

@ -126,7 +126,7 @@ backend-node/ (루트)
- `batchService.ts` (0개) - ✅ **전환 완료** (Phase 3.2) - `batchService.ts` (0개) - ✅ **전환 완료** (Phase 3.2)
- `componentStandardService.ts` (0개) - ✅ **전환 완료** (Phase 3.3) - `componentStandardService.ts` (0개) - ✅ **전환 완료** (Phase 3.3)
- `commonCodeService.ts` (0개) - ✅ **전환 완료** (Phase 3.4) - `commonCodeService.ts` (0개) - ✅ **전환 완료** (Phase 3.4)
- `dataflowDiagramService.ts` (12개) - 다이어그램 관리 ⭐ 신규 발견 - `dataflowDiagramService.ts` (0개) - ✅ **전환 완료** (Phase 3.5)
- `collectionService.ts` (11개) - 컬렉션 관리 - `collectionService.ts` (11개) - 컬렉션 관리
- `layoutService.ts` (10개) - 레이아웃 관리 - `layoutService.ts` (10개) - 레이아웃 관리
- `dbTypeCategoryService.ts` (10개) - DB 타입 분류 ⭐ 신규 발견 - `dbTypeCategoryService.ts` (10개) - DB 타입 분류 ⭐ 신규 발견
@ -1165,14 +1165,23 @@ describe("Performance Benchmarks", () => {
- [x] 동적 SQL 쿼리 생성 (중복 검사) - [x] 동적 SQL 쿼리 생성 (중복 검사)
- [x] TypeScript 컴파일 성공 - [x] TypeScript 컴파일 성공
- [x] Prisma import 완전 제거 - [x] Prisma import 완전 제거
- [x] **DataflowDiagramService 전환 (12개)****완료** (Phase 3.5)
- [x] 12개 Prisma 호출 전환 완료 (관계도 CRUD, 복제)
- [x] 동적 WHERE 조건 생성 (company_code 필터링)
- [x] 동적 UPDATE 쿼리 (JSON 필드 포함)
- [x] JSON 필드 처리 (relationships, node_positions, control, category, plan)
- [x] LIKE 검색 (복제 시 이름 패턴 검색)
- [x] 복잡한 복제 로직 (이름 번호 증가)
- [x] TypeScript 컴파일 성공
- [x] Prisma import 완전 제거
- [ ] 배치 관련 서비스 전환 (26개) ⭐ 대규모 신규 발견 - [ ] 배치 관련 서비스 전환 (26개) ⭐ 대규모 신규 발견
- [ ] BatchExternalDbService (8개) - [ ] BatchExternalDbService (8개)
- [ ] BatchExecutionLogService (7개), BatchManagementService (5개) - [ ] BatchExecutionLogService (7개), BatchManagementService (5개)
- [ ] BatchSchedulerService (4개) - [ ] BatchSchedulerService (4개)
- [ ] 표준 관리 서비스 전환 (10개) - [ ] 표준 관리 서비스 전환 (10개)
- [ ] LayoutService (10개) - [ ] LayoutService (10개)
- [ ] 데이터플로우 관련 서비스 (18개) ⭐ 신규 발견 - [ ] 데이터플로우 관련 서비스 (6개) ⭐ 신규 발견
- [ ] DataflowDiagramService (12개), DataflowControlService (6개) - [ ] DataflowControlService (6개)
- [ ] 기타 중요 서비스 (38개) ⭐ 신규 발견 - [ ] 기타 중요 서비스 (38개) ⭐ 신규 발견
- [ ] CollectionService (11개), DbTypeCategoryService (10개) - [ ] CollectionService (11개), DbTypeCategoryService (10개)
- [ ] TemplateStandardService (9개), DDLAuditLogger (8개) - [ ] TemplateStandardService (9개), DDLAuditLogger (8개)

View File

@ -1,5 +1,4 @@
import { Prisma } from "@prisma/client"; import { query, queryOne, transaction } from "../database/db";
import prisma from "../config/database";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
// 타입 정의 // 타입 정의
@ -43,41 +42,41 @@ export const getDataflowDiagrams = async (
try { try {
const offset = (page - 1) * size; const offset = (page - 1) * size;
// 검색 조건 구성 const whereConditions: string[] = [];
const whereClause: { const values: any[] = [];
company_code?: string; let paramIndex = 1;
diagram_name?: {
contains: string;
mode: "insensitive";
};
} = {};
// company_code가 '*'가 아닌 경우에만 필터링 // company_code가 '*'가 아닌 경우에만 필터링
if (companyCode !== "*") { if (companyCode !== "*") {
whereClause.company_code = companyCode; whereConditions.push(`company_code = $${paramIndex++}`);
values.push(companyCode);
} }
if (searchTerm) { if (searchTerm) {
whereClause.diagram_name = { whereConditions.push(`diagram_name ILIKE $${paramIndex++}`);
contains: searchTerm, values.push(`%${searchTerm}%`);
mode: "insensitive",
};
} }
const whereClause =
whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
// 총 개수 조회 // 총 개수 조회
const total = await prisma.dataflow_diagrams.count({ const countResult = await queryOne<{ count: string }>(
where: whereClause, `SELECT COUNT(*) as count FROM dataflow_diagrams ${whereClause}`,
}); values
);
const total = parseInt(countResult?.count || "0");
// 데이터 조회 // 데이터 조회
const diagrams = await prisma.dataflow_diagrams.findMany({ const diagrams = await query<any>(
where: whereClause, `SELECT * FROM dataflow_diagrams
orderBy: { ${whereClause}
updated_at: "desc", ORDER BY updated_at DESC
}, LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
skip: offset, [...values, size, offset]
take: size, );
});
const totalPages = Math.ceil(total / size); const totalPages = Math.ceil(total / size);
@ -104,21 +103,21 @@ export const getDataflowDiagramById = async (
companyCode: string companyCode: string
) => { ) => {
try { try {
const whereClause: { const whereConditions: string[] = ["diagram_id = $1"];
diagram_id: number; const values: any[] = [diagramId];
company_code?: string;
} = {
diagram_id: diagramId,
};
// company_code가 '*'가 아닌 경우에만 필터링 // company_code가 '*'가 아닌 경우에만 필터링
if (companyCode !== "*") { if (companyCode !== "*") {
whereClause.company_code = companyCode; whereConditions.push("company_code = $2");
values.push(companyCode);
} }
const diagram = await prisma.dataflow_diagrams.findFirst({ const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
where: whereClause,
}); const diagram = await queryOne<any>(
`SELECT * FROM dataflow_diagrams ${whereClause} LIMIT 1`,
values
);
return diagram; return diagram;
} catch (error) { } catch (error) {
@ -134,23 +133,24 @@ export const createDataflowDiagram = async (
data: CreateDataflowDiagramData data: CreateDataflowDiagramData
) => { ) => {
try { try {
const newDiagram = await prisma.dataflow_diagrams.create({ const newDiagram = await queryOne<any>(
data: { `INSERT INTO dataflow_diagrams
diagram_name: data.diagram_name, (diagram_name, relationships, node_positions, category, control, plan,
relationships: data.relationships as Prisma.InputJsonValue, company_code, created_by, updated_by, created_at, updated_at)
node_positions: data.node_positions as VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
| Prisma.InputJsonValue RETURNING *`,
| undefined, [
category: data.category data.diagram_name,
? (data.category as Prisma.InputJsonValue) JSON.stringify(data.relationships),
: undefined, data.node_positions ? JSON.stringify(data.node_positions) : null,
control: data.control as Prisma.InputJsonValue | undefined, data.category ? JSON.stringify(data.category) : null,
plan: data.plan as Prisma.InputJsonValue | undefined, data.control ? JSON.stringify(data.control) : null,
company_code: data.company_code, data.plan ? JSON.stringify(data.plan) : null,
created_by: data.created_by, data.company_code,
updated_by: data.updated_by, data.created_by,
}, data.updated_by,
}); ]
);
return newDiagram; return newDiagram;
} catch (error) { } catch (error) {
@ -173,21 +173,18 @@ export const updateDataflowDiagram = async (
); );
// 먼저 해당 관계도가 존재하는지 확인 // 먼저 해당 관계도가 존재하는지 확인
const whereClause: { const whereConditions: string[] = ["diagram_id = $1"];
diagram_id: number; const checkValues: any[] = [diagramId];
company_code?: string;
} = {
diagram_id: diagramId,
};
// company_code가 '*'가 아닌 경우에만 필터링
if (companyCode !== "*") { if (companyCode !== "*") {
whereClause.company_code = companyCode; whereConditions.push("company_code = $2");
checkValues.push(companyCode);
} }
const existingDiagram = await prisma.dataflow_diagrams.findFirst({ const existingDiagram = await queryOne<any>(
where: whereClause, `SELECT * FROM dataflow_diagrams WHERE ${whereConditions.join(" AND ")} LIMIT 1`,
}); checkValues
);
logger.info( logger.info(
`기존 관계도 조회 결과:`, `기존 관계도 조회 결과:`,
@ -201,36 +198,45 @@ export const updateDataflowDiagram = async (
return null; return null;
} }
// 업데이트 실행 // 동적 UPDATE 쿼리 생성
const updatedDiagram = await prisma.dataflow_diagrams.update({ const updateFields: string[] = ["updated_by = $1", "updated_at = NOW()"];
where: { const values: any[] = [data.updated_by];
diagram_id: diagramId, let paramIndex = 2;
},
data: { if (data.diagram_name) {
...(data.diagram_name && { diagram_name: data.diagram_name }), updateFields.push(`diagram_name = $${paramIndex++}`);
...(data.relationships && { values.push(data.diagram_name);
relationships: data.relationships as Prisma.InputJsonValue, }
}), if (data.relationships) {
...(data.node_positions !== undefined && { updateFields.push(`relationships = $${paramIndex++}`);
node_positions: data.node_positions values.push(JSON.stringify(data.relationships));
? (data.node_positions as Prisma.InputJsonValue) }
: Prisma.JsonNull, if (data.node_positions !== undefined) {
}), updateFields.push(`node_positions = $${paramIndex++}`);
...(data.category !== undefined && { values.push(
category: data.category data.node_positions ? JSON.stringify(data.node_positions) : null
? (data.category as Prisma.InputJsonValue) );
: undefined, }
}), if (data.category !== undefined) {
...(data.control !== undefined && { updateFields.push(`category = $${paramIndex++}`);
control: data.control as Prisma.InputJsonValue | undefined, values.push(data.category ? JSON.stringify(data.category) : null);
}), }
...(data.plan !== undefined && { if (data.control !== undefined) {
plan: data.plan as Prisma.InputJsonValue | undefined, updateFields.push(`control = $${paramIndex++}`);
}), values.push(data.control ? JSON.stringify(data.control) : null);
updated_by: data.updated_by, }
updated_at: new Date(), if (data.plan !== undefined) {
}, updateFields.push(`plan = $${paramIndex++}`);
}); values.push(data.plan ? JSON.stringify(data.plan) : null);
}
const updatedDiagram = await queryOne<any>(
`UPDATE dataflow_diagrams
SET ${updateFields.join(", ")}
WHERE diagram_id = $${paramIndex}
RETURNING *`,
[...values, diagramId]
);
return updatedDiagram; return updatedDiagram;
} catch (error) { } catch (error) {
@ -248,32 +254,27 @@ export const deleteDataflowDiagram = async (
) => { ) => {
try { try {
// 먼저 해당 관계도가 존재하는지 확인 // 먼저 해당 관계도가 존재하는지 확인
const whereClause: { const whereConditions: string[] = ["diagram_id = $1"];
diagram_id: number; const values: any[] = [diagramId];
company_code?: string;
} = {
diagram_id: diagramId,
};
// company_code가 '*'가 아닌 경우에만 필터링
if (companyCode !== "*") { if (companyCode !== "*") {
whereClause.company_code = companyCode; whereConditions.push("company_code = $2");
values.push(companyCode);
} }
const existingDiagram = await prisma.dataflow_diagrams.findFirst({ const existingDiagram = await queryOne<any>(
where: whereClause, `SELECT * FROM dataflow_diagrams WHERE ${whereConditions.join(" AND ")} LIMIT 1`,
}); values
);
if (!existingDiagram) { if (!existingDiagram) {
return false; return false;
} }
// 삭제 실행 // 삭제 실행
await prisma.dataflow_diagrams.delete({ await query(`DELETE FROM dataflow_diagrams WHERE diagram_id = $1`, [
where: { diagramId,
diagram_id: diagramId, ]);
},
});
return true; return true;
} catch (error) { } catch (error) {
@ -293,21 +294,18 @@ export const copyDataflowDiagram = async (
) => { ) => {
try { try {
// 원본 관계도 조회 // 원본 관계도 조회
const whereClause: { const whereConditions: string[] = ["diagram_id = $1"];
diagram_id: number; const values: any[] = [diagramId];
company_code?: string;
} = {
diagram_id: diagramId,
};
// company_code가 '*'가 아닌 경우에만 필터링
if (companyCode !== "*") { if (companyCode !== "*") {
whereClause.company_code = companyCode; whereConditions.push("company_code = $2");
values.push(companyCode);
} }
const originalDiagram = await prisma.dataflow_diagrams.findFirst({ const originalDiagram = await queryOne<any>(
where: whereClause, `SELECT * FROM dataflow_diagrams WHERE ${whereConditions.join(" AND ")} LIMIT 1`,
}); values
);
if (!originalDiagram) { if (!originalDiagram) {
return null; return null;
@ -325,28 +323,19 @@ export const copyDataflowDiagram = async (
: originalDiagram.diagram_name; : originalDiagram.diagram_name;
// 같은 패턴의 이름들을 찾아서 가장 큰 번호 찾기 // 같은 패턴의 이름들을 찾아서 가장 큰 번호 찾기
const copyWhereClause: { const copyWhereConditions: string[] = ["diagram_name LIKE $1"];
diagram_name: { const copyValues: any[] = [`${baseName}%`];
startsWith: string;
};
company_code?: string;
} = {
diagram_name: {
startsWith: baseName,
},
};
// company_code가 '*'가 아닌 경우에만 필터링
if (companyCode !== "*") { if (companyCode !== "*") {
copyWhereClause.company_code = companyCode; copyWhereConditions.push("company_code = $2");
copyValues.push(companyCode);
} }
const existingCopies = await prisma.dataflow_diagrams.findMany({ const existingCopies = await query<{ diagram_name: string }>(
where: copyWhereClause, `SELECT diagram_name FROM dataflow_diagrams
select: { WHERE ${copyWhereConditions.join(" AND ")}`,
diagram_name: true, copyValues
}, );
});
let maxNumber = 0; let maxNumber = 0;
existingCopies.forEach((copy) => { existingCopies.forEach((copy) => {
@ -363,19 +352,24 @@ export const copyDataflowDiagram = async (
} }
// 새로운 관계도 생성 // 새로운 관계도 생성
const copiedDiagram = await prisma.dataflow_diagrams.create({ const copiedDiagram = await queryOne<any>(
data: { `INSERT INTO dataflow_diagrams
diagram_name: copyName, (diagram_name, relationships, node_positions, category, company_code,
relationships: originalDiagram.relationships as Prisma.InputJsonValue, created_by, updated_by, created_at, updated_at)
node_positions: originalDiagram.node_positions VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
? (originalDiagram.node_positions as Prisma.InputJsonValue) RETURNING *`,
: Prisma.JsonNull, [
category: originalDiagram.category || undefined, copyName,
company_code: companyCode, JSON.stringify(originalDiagram.relationships),
created_by: userId, originalDiagram.node_positions
updated_by: userId, ? JSON.stringify(originalDiagram.node_positions)
}, : null,
}); originalDiagram.category || null,
companyCode,
userId,
userId,
]
);
return copiedDiagram; return copiedDiagram;
} catch (error) { } catch (error) {
@ -390,39 +384,39 @@ export const copyDataflowDiagram = async (
*/ */
export const getAllRelationshipsForButtonControl = async ( export const getAllRelationshipsForButtonControl = async (
companyCode: string companyCode: string
): Promise<Array<{ ): Promise<
id: string; Array<{
name: string; id: string;
sourceTable: string; name: string;
targetTable: string; sourceTable: string;
category: string; targetTable: string;
}>> => { category: string;
}>
> => {
try { try {
logger.info(`전체 관계 목록 조회 시작 - companyCode: ${companyCode}`); logger.info(`전체 관계 목록 조회 시작 - companyCode: ${companyCode}`);
// dataflow_diagrams 테이블에서 관계도들을 조회 // dataflow_diagrams 테이블에서 관계도들을 조회
const diagrams = await prisma.dataflow_diagrams.findMany({ const diagrams = await query<{
where: { diagram_id: number;
company_code: companyCode, diagram_name: string;
}, relationships: any;
select: { }>(
diagram_id: true, `SELECT diagram_id, diagram_name, relationships
diagram_name: true, FROM dataflow_diagrams
relationships: true, WHERE company_code = $1
}, ORDER BY updated_at DESC`,
orderBy: { [companyCode]
updated_at: "desc", );
},
});
const allRelationships = diagrams.map((diagram) => { const allRelationships = diagrams.map((diagram) => {
// relationships 구조에서 테이블 정보 추출 // relationships 구조에서 테이블 정보 추출
const relationships = diagram.relationships as any || {}; const relationships = (diagram.relationships as any) || {};
// 테이블 정보 추출 // 테이블 정보 추출
let sourceTable = ""; let sourceTable = "";
let targetTable = ""; let targetTable = "";
if (relationships.fromTable?.tableName) { if (relationships.fromTable?.tableName) {
sourceTable = relationships.fromTable.tableName; sourceTable = relationships.fromTable.tableName;
} }