From 4c20d93c8708bd420ff8579d4a8f3da4a82cc675 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 1 Oct 2025 11:25:08 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=203.7=20LayoutService=20Raw=20Que?= =?UTF-8?q?ry=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로 전환 - 레이아웃 목록 조회 (getLayouts - 복잡한 OR 조건, Promise.all) - 레이아웃 단건 조회 (getLayoutById - OR 조건) - 레이아웃 생성 (createLayout - JSON 필드) - 레이아웃 수정 (updateLayout - 동적 UPDATE, 10개 필드) - 레이아웃 삭제 (deleteLayout - Soft Delete) - 레이아웃 복제 (duplicateLayout - 기존 함수 재사용) - 카테고리별 통계 (getLayoutCountsByCategory - GROUP BY) - 코드 자동 생성 (generateLayoutCode - LIKE 검색) 주요 기술적 해결: - 복잡한 OR 조건 처리 (company_code OR is_public) - 동적 WHERE 조건 생성 (ILIKE 다중 검색) - 동적 UPDATE 쿼리 (10개 필드 조건부 업데이트) - JSON 필드 처리 (default_size, layout_config, zones_config) - GROUP BY 통계 쿼리 (카테고리별 개수) - LIKE 검색 (코드 생성 시 패턴 검색) - Promise.all 병렬 쿼리 (목록 + 개수 동시 조회) - safeJSONStringify 헬퍼 함수 활용 TypeScript 컴파일 성공 Prisma import 완전 제거 Phase 3 진행률: 97/162 (59.9%) 전체 진행률: 348/444 (78.4%) --- PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md | 16 +- backend-node/src/services/layoutService.ts | 275 ++++++++++++--------- 2 files changed, 167 insertions(+), 124 deletions(-) diff --git a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md index 945153dd..86233096 100644 --- a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md +++ b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md @@ -128,7 +128,7 @@ backend-node/ (루트) - `commonCodeService.ts` (0개) - ✅ **전환 완료** (Phase 3.4) - `dataflowDiagramService.ts` (0개) - ✅ **전환 완료** (Phase 3.5) - `collectionService.ts` (0개) - ✅ **전환 완료** (Phase 3.6) -- `layoutService.ts` (10개) - 레이아웃 관리 +- `layoutService.ts` (0개) - ✅ **전환 완료** (Phase 3.7) - `dbTypeCategoryService.ts` (10개) - DB 타입 분류 ⭐ 신규 발견 - `templateStandardService.ts` (9개) - 템플릿 표준 - `eventTriggerService.ts` (6개) - JSON 검색 쿼리 @@ -1183,12 +1183,22 @@ describe("Performance Benchmarks", () => { - [x] 비동기 작업 처리 (setTimeout 내 query 사용) - [x] TypeScript 컴파일 성공 - [x] Prisma import 완전 제거 +- [x] **LayoutService 전환 (10개)** ✅ **완료** (Phase 3.7) + - [x] 10개 Prisma 호출 전환 완료 (레이아웃 CRUD, 통계) + - [x] 복잡한 OR 조건 처리 (company_code OR is_public) + - [x] 동적 WHERE 조건 생성 (ILIKE 다중 검색) + - [x] 동적 UPDATE 쿼리 (10개 필드 조건부 업데이트) + - [x] JSON 필드 처리 (default_size, layout_config, zones_config) + - [x] GROUP BY 통계 쿼리 (카테고리별 개수) + - [x] LIKE 검색 (코드 생성 시 패턴 검색) + - [x] Promise.all 병렬 쿼리 (목록 + 개수) + - [x] TypeScript 컴파일 성공 + - [x] Prisma import 완전 제거 - [ ] 배치 관련 서비스 전환 (26개) ⭐ 대규모 신규 발견 - [ ] BatchExternalDbService (8개) - [ ] BatchExecutionLogService (7개), BatchManagementService (5개) - [ ] BatchSchedulerService (4개) -- [ ] 표준 관리 서비스 전환 (16개) - - [ ] LayoutService (10개) - [계획서](PHASE3.7_LAYOUT_SERVICE_MIGRATION.md) +- [ ] 표준 관리 서비스 전환 (6개) - [ ] TemplateStandardService (6개) - [계획서](PHASE3.9_TEMPLATE_STANDARD_SERVICE_MIGRATION.md) - [ ] 데이터플로우 관련 서비스 (6개) ⭐ 신규 발견 - [ ] DataflowControlService (6개) diff --git a/backend-node/src/services/layoutService.ts b/backend-node/src/services/layoutService.ts index 374e4e96..2aa0788a 100644 --- a/backend-node/src/services/layoutService.ts +++ b/backend-node/src/services/layoutService.ts @@ -1,5 +1,4 @@ -import { PrismaClient } from "@prisma/client"; -import prisma from "../config/database"; +import { query, queryOne } from "../database/db"; import { CreateLayoutRequest, UpdateLayoutRequest, @@ -77,42 +76,59 @@ export class LayoutService { const skip = (page - 1) * size; - // 검색 조건 구성 - const where: any = { - is_active: "Y", - OR: [ - { company_code: companyCode }, - ...(includePublic ? [{ is_public: "Y" }] : []), - ], - }; + // 동적 WHERE 조건 구성 + const whereConditions: string[] = ["is_active = $1"]; + const values: any[] = ["Y"]; + let paramIndex = 2; + + // company_code OR is_public 조건 + if (includePublic) { + whereConditions.push( + `(company_code = $${paramIndex} OR is_public = $${paramIndex + 1})` + ); + values.push(companyCode, "Y"); + paramIndex += 2; + } else { + whereConditions.push(`company_code = $${paramIndex++}`); + values.push(companyCode); + } if (category) { - where.category = category; + whereConditions.push(`category = $${paramIndex++}`); + values.push(category); } if (layoutType) { - where.layout_type = layoutType; + whereConditions.push(`layout_type = $${paramIndex++}`); + values.push(layoutType); } if (searchTerm) { - where.OR = [ - ...where.OR, - { layout_name: { contains: searchTerm, mode: "insensitive" } }, - { layout_name_eng: { contains: searchTerm, mode: "insensitive" } }, - { description: { contains: searchTerm, mode: "insensitive" } }, - ]; + whereConditions.push( + `(layout_name ILIKE $${paramIndex} OR layout_name_eng ILIKE $${paramIndex} OR description ILIKE $${paramIndex})` + ); + values.push(`%${searchTerm}%`); + paramIndex++; } - const [data, total] = await Promise.all([ - prisma.layout_standards.findMany({ - where, - skip, - take: size, - orderBy: [{ sort_order: "asc" }, { created_date: "desc" }], - }), - prisma.layout_standards.count({ where }), + const whereClause = `WHERE ${whereConditions.join(" AND ")}`; + + const [data, countResult] = await Promise.all([ + query( + `SELECT * FROM layout_standards + ${whereClause} + ORDER BY sort_order ASC, created_date DESC + LIMIT $${paramIndex++} OFFSET $${paramIndex++}`, + [...values, size, skip] + ), + queryOne<{ count: string }>( + `SELECT COUNT(*) as count FROM layout_standards ${whereClause}`, + values + ), ]); + const total = parseInt(countResult?.count || "0"); + return { data: data.map( (layout) => @@ -149,13 +165,13 @@ export class LayoutService { layoutCode: string, companyCode: string ): Promise { - const layout = await prisma.layout_standards.findFirst({ - where: { - layout_code: layoutCode, - is_active: "Y", - OR: [{ company_code: companyCode }, { is_public: "Y" }], - }, - }); + const layout = await queryOne( + `SELECT * FROM layout_standards + WHERE layout_code = $1 AND is_active = $2 + AND (company_code = $3 OR is_public = $4) + LIMIT 1`, + [layoutCode, "Y", companyCode, "Y"] + ); if (!layout) return null; @@ -196,24 +212,31 @@ export class LayoutService { companyCode ); - const layout = await prisma.layout_standards.create({ - data: { - layout_code: layoutCode, - layout_name: request.layoutName, - layout_name_eng: request.layoutNameEng, - description: request.description, - layout_type: request.layoutType, - category: request.category, - icon_name: request.iconName, - default_size: safeJSONStringify(request.defaultSize) as any, - layout_config: safeJSONStringify(request.layoutConfig) as any, - zones_config: safeJSONStringify(request.zonesConfig) as any, - is_public: request.isPublic ? "Y" : "N", - company_code: companyCode, - created_by: userId, - updated_by: userId, - }, - }); + const layout = await queryOne( + `INSERT INTO layout_standards + (layout_code, layout_name, layout_name_eng, description, layout_type, category, + icon_name, default_size, layout_config, zones_config, is_public, is_active, + company_code, created_by, updated_by, created_date, updated_date, sort_order) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW(), NOW(), 0) + RETURNING *`, + [ + layoutCode, + request.layoutName, + request.layoutNameEng, + request.description, + request.layoutType, + request.category, + request.iconName, + safeJSONStringify(request.defaultSize), + safeJSONStringify(request.layoutConfig), + safeJSONStringify(request.zonesConfig), + request.isPublic ? "Y" : "N", + "Y", + companyCode, + userId, + userId, + ] + ); return this.mapToLayoutStandard(layout); } @@ -227,47 +250,69 @@ export class LayoutService { userId: string ): Promise { // 수정 권한 확인 - const existing = await prisma.layout_standards.findFirst({ - where: { - layout_code: request.layoutCode, - company_code: companyCode, - is_active: "Y", - }, - }); + const existing = await queryOne( + `SELECT * FROM layout_standards + WHERE layout_code = $1 AND company_code = $2 AND is_active = $3`, + [request.layoutCode, companyCode, "Y"] + ); if (!existing) { throw new Error("레이아웃을 찾을 수 없거나 수정 권한이 없습니다."); } - const updateData: any = { - updated_by: userId, - updated_date: new Date(), - }; + // 동적 UPDATE 쿼리 생성 + const updateFields: string[] = ["updated_by = $1", "updated_date = NOW()"]; + const values: any[] = [userId]; + let paramIndex = 2; - // 수정할 필드만 업데이트 - if (request.layoutName !== undefined) - updateData.layout_name = request.layoutName; - if (request.layoutNameEng !== undefined) - updateData.layout_name_eng = request.layoutNameEng; - if (request.description !== undefined) - updateData.description = request.description; - if (request.layoutType !== undefined) - updateData.layout_type = request.layoutType; - if (request.category !== undefined) updateData.category = request.category; - if (request.iconName !== undefined) updateData.icon_name = request.iconName; - if (request.defaultSize !== undefined) - updateData.default_size = safeJSONStringify(request.defaultSize) as any; - if (request.layoutConfig !== undefined) - updateData.layout_config = safeJSONStringify(request.layoutConfig) as any; - if (request.zonesConfig !== undefined) - updateData.zones_config = safeJSONStringify(request.zonesConfig) as any; - if (request.isPublic !== undefined) - updateData.is_public = request.isPublic ? "Y" : "N"; + if (request.layoutName !== undefined) { + updateFields.push(`layout_name = $${paramIndex++}`); + values.push(request.layoutName); + } + if (request.layoutNameEng !== undefined) { + updateFields.push(`layout_name_eng = $${paramIndex++}`); + values.push(request.layoutNameEng); + } + if (request.description !== undefined) { + updateFields.push(`description = $${paramIndex++}`); + values.push(request.description); + } + if (request.layoutType !== undefined) { + updateFields.push(`layout_type = $${paramIndex++}`); + values.push(request.layoutType); + } + if (request.category !== undefined) { + updateFields.push(`category = $${paramIndex++}`); + values.push(request.category); + } + if (request.iconName !== undefined) { + updateFields.push(`icon_name = $${paramIndex++}`); + values.push(request.iconName); + } + if (request.defaultSize !== undefined) { + updateFields.push(`default_size = $${paramIndex++}`); + values.push(safeJSONStringify(request.defaultSize)); + } + if (request.layoutConfig !== undefined) { + updateFields.push(`layout_config = $${paramIndex++}`); + values.push(safeJSONStringify(request.layoutConfig)); + } + if (request.zonesConfig !== undefined) { + updateFields.push(`zones_config = $${paramIndex++}`); + values.push(safeJSONStringify(request.zonesConfig)); + } + if (request.isPublic !== undefined) { + updateFields.push(`is_public = $${paramIndex++}`); + values.push(request.isPublic ? "Y" : "N"); + } - const updated = await prisma.layout_standards.update({ - where: { layout_code: request.layoutCode }, - data: updateData, - }); + const updated = await queryOne( + `UPDATE layout_standards + SET ${updateFields.join(", ")} + WHERE layout_code = $${paramIndex} + RETURNING *`, + [...values, request.layoutCode] + ); return this.mapToLayoutStandard(updated); } @@ -280,26 +325,22 @@ export class LayoutService { companyCode: string, userId: string ): Promise { - const existing = await prisma.layout_standards.findFirst({ - where: { - layout_code: layoutCode, - company_code: companyCode, - is_active: "Y", - }, - }); + const existing = await queryOne( + `SELECT * FROM layout_standards + WHERE layout_code = $1 AND company_code = $2 AND is_active = $3`, + [layoutCode, companyCode, "Y"] + ); if (!existing) { throw new Error("레이아웃을 찾을 수 없거나 삭제 권한이 없습니다."); } - await prisma.layout_standards.update({ - where: { layout_code: layoutCode }, - data: { - is_active: "N", - updated_by: userId, - updated_date: new Date(), - }, - }); + await query( + `UPDATE layout_standards + SET is_active = $1, updated_by = $2, updated_date = NOW() + WHERE layout_code = $3`, + ["N", userId, layoutCode] + ); return true; } @@ -342,20 +383,17 @@ export class LayoutService { async getLayoutCountsByCategory( companyCode: string ): Promise> { - const counts = await prisma.layout_standards.groupBy({ - by: ["category"], - _count: { - layout_code: true, - }, - where: { - is_active: "Y", - OR: [{ company_code: companyCode }, { is_public: "Y" }], - }, - }); + const counts = await query<{ category: string; count: string }>( + `SELECT category, COUNT(*) as count + FROM layout_standards + WHERE is_active = $1 AND (company_code = $2 OR is_public = $3) + GROUP BY category`, + ["Y", companyCode, "Y"] + ); return counts.reduce( (acc: Record, item: any) => { - acc[item.category] = item._count.layout_code; + acc[item.category] = parseInt(item.count); return acc; }, {} as Record @@ -370,16 +408,11 @@ export class LayoutService { companyCode: string ): Promise { const prefix = `${layoutType.toUpperCase()}_${companyCode}`; - const existingCodes = await prisma.layout_standards.findMany({ - where: { - layout_code: { - startsWith: prefix, - }, - }, - select: { - layout_code: true, - }, - }); + const existingCodes = await query<{ layout_code: string }>( + `SELECT layout_code FROM layout_standards + WHERE layout_code LIKE $1`, + [`${prefix}%`] + ); const maxNumber = existingCodes.reduce((max: number, item: any) => { const match = item.layout_code.match(/_(\d+)$/);