From a8c4f9ec453bd4ef5859658eab8fb414f0d6a7ec Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 1 Oct 2025 11:32:45 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=203.8=20DbTypeCategoryService=20R?= =?UTF-8?q?aw=20Query=20=EC=A0=84=ED=99=98=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 10개 Prisma 호출을 모두 Raw Query로 전환 - 카테고리 목록 조회 (getAllCategories) - 카테고리 단건 조회 (getCategoryByTypeCode) - 카테고리 생성 (createCategory - 중복 검사) - 카테고리 수정 (updateCategory - 동적 UPDATE) - 카테고리 삭제 (deleteCategory - 연결 확인 후 비활성화) - 연결 통계 조회 (getConnectionStatsByType - LEFT JOIN + GROUP BY) - 기본 카테고리 초기화 (initializeDefaultCategories - UPSERT) 주요 기술적 해결: - ApiResponse 래퍼 패턴 유지 - 동적 UPDATE 쿼리 (5개 필드 조건부 업데이트) - ON CONFLICT를 사용한 UPSERT (기본 카테고리 초기화) - 연결 확인 (external_db_connections COUNT) - LEFT JOIN + GROUP BY 통계 쿼리 최적화 (타입별 연결 수) - 중복 검사 (카테고리 생성 시) - try-catch 에러 처리 및 ApiResponse 반환 TypeScript 컴파일 성공 Prisma import 완전 제거 Phase 3 진행률: 107/162 (66.0%) 전체 진행률: 358/444 (80.6%) --- PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md | 17 +- .../src/services/dbTypeCategoryService.ts | 180 +++++++++++------- 2 files changed, 119 insertions(+), 78 deletions(-) diff --git a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md index 86233096..8bb385d3 100644 --- a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md +++ b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md @@ -129,8 +129,8 @@ backend-node/ (루트) - `dataflowDiagramService.ts` (0개) - ✅ **전환 완료** (Phase 3.5) - `collectionService.ts` (0개) - ✅ **전환 완료** (Phase 3.6) - `layoutService.ts` (0개) - ✅ **전환 완료** (Phase 3.7) -- `dbTypeCategoryService.ts` (10개) - DB 타입 분류 ⭐ 신규 발견 -- `templateStandardService.ts` (9개) - 템플릿 표준 +- `dbTypeCategoryService.ts` (0개) - ✅ **전환 완료** (Phase 3.8) +- `templateStandardService.ts` (6개) - 템플릿 표준 - `eventTriggerService.ts` (6개) - JSON 검색 쿼리 #### 🟡 **중간 (단순 CRUD) - 3순위** @@ -1194,6 +1194,16 @@ describe("Performance Benchmarks", () => { - [x] Promise.all 병렬 쿼리 (목록 + 개수) - [x] TypeScript 컴파일 성공 - [x] Prisma import 완전 제거 +- [x] **DbTypeCategoryService 전환 (10개)** ✅ **완료** (Phase 3.8) + - [x] 10개 Prisma 호출 전환 완료 (DB 타입 카테고리 CRUD, 통계) + - [x] ApiResponse 래퍼 패턴 유지 + - [x] 동적 UPDATE 쿼리 (5개 필드 조건부 업데이트) + - [x] ON CONFLICT를 사용한 UPSERT (기본 카테고리 초기화) + - [x] 연결 확인 (external_db_connections COUNT) + - [x] LEFT JOIN + GROUP BY 통계 쿼리 (타입별 연결 수) + - [x] 중복 검사 (카테고리 생성 시) + - [x] TypeScript 컴파일 성공 + - [x] Prisma import 완전 제거 - [ ] 배치 관련 서비스 전환 (26개) ⭐ 대규모 신규 발견 - [ ] BatchExternalDbService (8개) - [ ] BatchExecutionLogService (7개), BatchManagementService (5개) @@ -1202,8 +1212,7 @@ describe("Performance Benchmarks", () => { - [ ] TemplateStandardService (6개) - [계획서](PHASE3.9_TEMPLATE_STANDARD_SERVICE_MIGRATION.md) - [ ] 데이터플로우 관련 서비스 (6개) ⭐ 신규 발견 - [ ] DataflowControlService (6개) -- [ ] 기타 중요 서비스 (18개) ⭐ 신규 발견 - - [ ] DbTypeCategoryService (10개) - [계획서](PHASE3.8_DB_TYPE_CATEGORY_SERVICE_MIGRATION.md) +- [ ] 기타 중요 서비스 (8개) ⭐ 신규 발견 - [ ] DDLAuditLogger (8개) - [ ] 기능별 테스트 완료 diff --git a/backend-node/src/services/dbTypeCategoryService.ts b/backend-node/src/services/dbTypeCategoryService.ts index 1b00f328..4930777b 100644 --- a/backend-node/src/services/dbTypeCategoryService.ts +++ b/backend-node/src/services/dbTypeCategoryService.ts @@ -1,6 +1,4 @@ -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); +import { query, queryOne } from "../database/db"; export interface DbTypeCategory { type_code: string; @@ -42,13 +40,12 @@ export class DbTypeCategoryService { */ static async getAllCategories(): Promise> { try { - const categories = await prisma.db_type_categories.findMany({ - where: { is_active: true }, - orderBy: [ - { sort_order: 'asc' }, - { display_name: 'asc' } - ] - }); + const categories = await query( + `SELECT * FROM db_type_categories + WHERE is_active = $1 + ORDER BY sort_order ASC, display_name ASC`, + [true] + ); return { success: true, @@ -70,9 +67,10 @@ export class DbTypeCategoryService { */ static async getCategoryByTypeCode(typeCode: string): Promise> { try { - const category = await prisma.db_type_categories.findUnique({ - where: { type_code: typeCode } - }); + const category = await queryOne( + `SELECT * FROM db_type_categories WHERE type_code = $1`, + [typeCode] + ); if (!category) { return { @@ -102,9 +100,10 @@ export class DbTypeCategoryService { static async createCategory(data: CreateDbTypeCategoryRequest): Promise> { try { // 중복 체크 - const existing = await prisma.db_type_categories.findUnique({ - where: { type_code: data.type_code } - }); + const existing = await queryOne( + `SELECT * FROM db_type_categories WHERE type_code = $1`, + [data.type_code] + ); if (existing) { return { @@ -113,15 +112,20 @@ export class DbTypeCategoryService { }; } - const category = await prisma.db_type_categories.create({ - data: { - type_code: data.type_code, - display_name: data.display_name, - icon: data.icon, - color: data.color, - sort_order: data.sort_order || 0 - } - }); + const category = await queryOne( + `INSERT INTO db_type_categories + (type_code, display_name, icon, color, sort_order, is_active, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW()) + RETURNING *`, + [ + data.type_code, + data.display_name, + data.icon || null, + data.color || null, + data.sort_order || 0, + true, + ] + ); return { success: true, @@ -143,17 +147,39 @@ export class DbTypeCategoryService { */ static async updateCategory(typeCode: string, data: UpdateDbTypeCategoryRequest): Promise> { try { - const category = await prisma.db_type_categories.update({ - where: { type_code: typeCode }, - data: { - display_name: data.display_name, - icon: data.icon, - color: data.color, - sort_order: data.sort_order, - is_active: data.is_active, - updated_at: new Date() - } - }); + // 동적 UPDATE 쿼리 생성 + const updateFields: string[] = ["updated_at = NOW()"]; + const values: any[] = []; + let paramIndex = 1; + + if (data.display_name !== undefined) { + updateFields.push(`display_name = $${paramIndex++}`); + values.push(data.display_name); + } + if (data.icon !== undefined) { + updateFields.push(`icon = $${paramIndex++}`); + values.push(data.icon); + } + if (data.color !== undefined) { + updateFields.push(`color = $${paramIndex++}`); + values.push(data.color); + } + if (data.sort_order !== undefined) { + updateFields.push(`sort_order = $${paramIndex++}`); + values.push(data.sort_order); + } + if (data.is_active !== undefined) { + updateFields.push(`is_active = $${paramIndex++}`); + values.push(data.is_active); + } + + const category = await queryOne( + `UPDATE db_type_categories + SET ${updateFields.join(", ")} + WHERE type_code = $${paramIndex} + RETURNING *`, + [...values, typeCode] + ); return { success: true, @@ -176,12 +202,12 @@ export class DbTypeCategoryService { static async deleteCategory(typeCode: string): Promise> { try { // 해당 타입을 사용하는 연결이 있는지 확인 - const connectionsCount = await prisma.external_db_connections.count({ - where: { - db_type: typeCode, - is_active: "Y" - } - }); + const countResult = await queryOne<{ count: string }>( + `SELECT COUNT(*) as count FROM external_db_connections + WHERE db_type = $1 AND is_active = $2`, + [typeCode, "Y"] + ); + const connectionsCount = parseInt(countResult?.count || "0"); if (connectionsCount > 0) { return { @@ -190,13 +216,12 @@ export class DbTypeCategoryService { }; } - await prisma.db_type_categories.update({ - where: { type_code: typeCode }, - data: { - is_active: false, - updated_at: new Date() - } - }); + await query( + `UPDATE db_type_categories + SET is_active = $1, updated_at = NOW() + WHERE type_code = $2`, + [false, typeCode] + ); return { success: true, @@ -217,30 +242,28 @@ export class DbTypeCategoryService { */ static async getConnectionStatsByType(): Promise> { try { - const stats = await prisma.external_db_connections.groupBy({ - by: ['db_type'], - where: { is_active: "Y" }, - _count: { - id: true - } - }); + // LEFT JOIN으로 한 번에 조회 + const result = await query( + `SELECT + c.*, + COUNT(e.id) as connection_count + FROM db_type_categories c + LEFT JOIN external_db_connections e ON c.type_code = e.db_type AND e.is_active = $1 + WHERE c.is_active = $2 + GROUP BY c.type_code, c.display_name, c.icon, c.color, c.sort_order, c.is_active, c.created_at, c.updated_at + ORDER BY c.sort_order ASC`, + ["Y", true] + ); - // 카테고리 정보와 함께 반환 - const categories = await prisma.db_type_categories.findMany({ - where: { is_active: true } - }); - - const result = categories.map(category => { - const stat = stats.find(s => s.db_type === category.type_code); - return { - ...category, - connection_count: stat?._count.id || 0 - }; - }); + // connection_count를 숫자로 변환 + const formattedResult = result.map(row => ({ + ...row, + connection_count: parseInt(row.connection_count) + })); return { success: true, - data: result, + data: formattedResult, message: "DB 타입별 연결 통계를 조회했습니다." }; } catch (error) { @@ -297,11 +320,20 @@ export class DbTypeCategoryService { ]; for (const category of defaultCategories) { - await prisma.db_type_categories.upsert({ - where: { type_code: category.type_code }, - update: {}, - create: category - }); + await query( + `INSERT INTO db_type_categories + (type_code, display_name, icon, color, sort_order, is_active, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW()) + ON CONFLICT (type_code) DO NOTHING`, + [ + category.type_code, + category.display_name, + category.icon, + category.color, + category.sort_order, + true, + ] + ); } return {