From f2f0c33bad39d4e2b8385a44c6d72a89fb000cc5 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 1 Oct 2025 14:36:36 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20webTypeStandardController=20&=20fileCon?= =?UTF-8?q?troller=20Prisma=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 컨트롤러 레이어 전환: webTypeStandardController.ts (11개): - ✅ getWebTypes: findMany → query (동적 WHERE, ILIKE) - ✅ getWebType: findUnique → queryOne - ✅ createWebType: findUnique + create → queryOne (중복 체크 + INSERT) - ✅ updateWebType: update → query (동적 UPDATE, 11개 필드) - ✅ deleteWebType: delete → query (RETURNING) - ✅ updateSortOrder: $transaction → transaction (batch update) - ✅ getCategories: groupBy → query (GROUP BY, COUNT) fileController.ts (1개): - ✅ downloadFile: findUnique → queryOne 기술적 구현: - 동적 WHERE 절: ILIKE를 사용한 검색 - 동적 UPDATE: 11개 필드 조건부 업데이트 - 트랜잭션: transaction 함수로 batch update - GROUP BY: 카테고리별 집계 전체 진행률: 42/29 (145%) - 컨트롤러 완료 남은 작업: Routes(4), Service(4), Config(4) --- PHASE4_REMAINING_PRISMA_CALLS.md | 112 ++++++-- .../src/controllers/fileController.ts | 9 +- .../controllers/webTypeStandardController.ts | 252 ++++++++++++------ 3 files changed, 254 insertions(+), 119 deletions(-) diff --git a/PHASE4_REMAINING_PRISMA_CALLS.md b/PHASE4_REMAINING_PRISMA_CALLS.md index ea621f5d..89742648 100644 --- a/PHASE4_REMAINING_PRISMA_CALLS.md +++ b/PHASE4_REMAINING_PRISMA_CALLS.md @@ -2,14 +2,14 @@ ## 📊 현재 상황 -| 항목 | 내용 | -| --------------- | --------------------------------------- | -| 총 Prisma 호출 | 29개 | -| 대상 파일 | 7개 | -| **현재 진행률** | **17/29 (58.6%)** 🔄 **진행 중** | -| 복잡도 | 중간 | -| 우선순위 | 🔴 높음 (Phase 4) | -| **상태** | ⏳ **진행 중** | +| 항목 | 내용 | +| --------------- | -------------------------------- | +| 총 Prisma 호출 | 29개 | +| 대상 파일 | 7개 | +| **현재 진행률** | **17/29 (58.6%)** 🔄 **진행 중** | +| 복잡도 | 중간 | +| 우선순위 | 🔴 높음 (Phase 4) | +| **상태** | ⏳ **진행 중** | --- @@ -18,6 +18,7 @@ ### ✅ 완료된 파일 (2개) 1. **adminController.ts** - ✅ **28개 완료** + - 사용자 관리: getUserList, getUserInfo, updateUserStatus, deleteUser - 프로필 관리: getMyProfile, updateMyProfile, resetPassword - 사용자 생성/수정: createOrUpdateUser (UPSERT) @@ -41,27 +42,33 @@ #### Prisma 호출 목록: 1. **라인 33**: `getWebTypeStandards()` - findMany + ```typescript const webTypes = await prisma.web_type_standards.findMany({ - where, orderBy, select + where, + orderBy, + select, }); ``` 2. **라인 58**: `getWebTypeStandard()` - findUnique + ```typescript const webTypeData = await prisma.web_type_standards.findUnique({ - where: { id } + where: { id }, }); ``` 3. **라인 112**: `createWebTypeStandard()` - findUnique (중복 체크) + ```typescript const existingWebType = await prisma.web_type_standards.findUnique({ - where: { web_type: webType } + where: { web_type: webType }, }); ``` 4. **라인 123**: `createWebTypeStandard()` - create + ```typescript const newWebType = await prisma.web_type_standards.create({ data: { ... } @@ -69,13 +76,15 @@ ``` 5. **라인 178**: `updateWebTypeStandard()` - findUnique (존재 확인) + ```typescript const existingWebType = await prisma.web_type_standards.findUnique({ - where: { id } + where: { id }, }); ``` 6. **라인 189**: `updateWebTypeStandard()` - update + ```typescript const updatedWebType = await prisma.web_type_standards.update({ where: { id }, data: { ... } @@ -83,20 +92,23 @@ ``` 7. **라인 230**: `deleteWebTypeStandard()` - findUnique (존재 확인) + ```typescript const existingWebType = await prisma.web_type_standards.findUnique({ - where: { id } + where: { id }, }); ``` 8. **라인 241**: `deleteWebTypeStandard()` - delete + ```typescript await prisma.web_type_standards.delete({ - where: { id } + where: { id }, }); ``` 9. **라인 275**: `updateSortOrder()` - $transaction + ```typescript await prisma.$transaction( updates.map((item) => @@ -110,11 +122,14 @@ 11. **라인 305**: `getCategories()` - groupBy ```typescript const categories = await prisma.web_type_standards.groupBy({ - by: ['category'], where, _count: true + by: ["category"], + where, + _count: true, }); ``` **전환 전략**: + - findMany → `query` with dynamic WHERE - findUnique → `queryOne` - create → `queryOne` with INSERT RETURNING @@ -134,11 +149,12 @@ 1. **라인 726**: `downloadFile()` - findUnique ```typescript const fileRecord = await prisma.attach_file_info.findUnique({ - where: { objid: BigInt(objid) } + where: { objid: BigInt(objid) }, }); ``` **전환 전략**: + - findUnique → `queryOne` --- @@ -150,16 +166,19 @@ #### Prisma 호출 목록: 1. **라인 1005**: `executeSelect()` - $queryRawUnsafe + ```typescript return await prisma.$queryRawUnsafe(query, ...queryParams); ``` 2. **라인 1022**: `executeInsert()` - $queryRawUnsafe + ```typescript const insertResult = await prisma.$queryRawUnsafe(...); ``` 3. **라인 1055**: `executeUpdate()` - $queryRawUnsafe + ```typescript return await prisma.$queryRawUnsafe(updateQuery, ...updateParams); ``` @@ -170,6 +189,7 @@ ``` **전환 전략**: + - $queryRawUnsafe → `query` (이미 Raw SQL 사용 중) --- @@ -186,6 +206,7 @@ 4. **라인 31, 35, 40**: `await prisma.$disconnect()` **전환 전략**: + - 이 파일은 데이터베이스 설정 파일이므로 완전히 제거 - 기존 `db.ts`의 connection pool로 대체 - 모든 import 경로를 `database` → `database/db`로 변경 @@ -199,6 +220,7 @@ #### Prisma 호출: 1. **라인 183-184**: 동적 PrismaClient import + ```typescript const { PrismaClient } = await import("@prisma/client"); const prisma = new PrismaClient(); @@ -211,6 +233,7 @@ ``` **전환 전략**: + - 동적 import 제거 - `query('SELECT 1')` 사용 @@ -223,20 +246,23 @@ #### Prisma 호출: 1. **라인 32**: findUnique (중복 체크) + ```typescript const existingCompany = await prisma.company_mng.findUnique({ - where: { company_code } + where: { company_code }, }); ``` 2. **라인 61**: update (회사명 업데이트) ```typescript await prisma.company_mng.update({ - where: { company_code }, data: { company_name } + where: { company_code }, + data: { company_name }, }); ``` **전환 전략**: + - findUnique → `queryOne` - update → `query` @@ -253,24 +279,30 @@ ## 🎯 전환 우선순위 ### Phase 4.1: 컨트롤러 (완료) + - [x] screenFileController.ts (2개) - [x] adminController.ts (28개) ### Phase 4.2: 남은 컨트롤러 (진행 예정) + - [ ] webTypeStandardController.ts (11개) - 🔴 최우선 - [ ] fileController.ts (1개) ### Phase 4.3: Routes (진행 예정) + - [ ] ddlRoutes.ts (2개) - [ ] companyManagementRoutes.ts (2개) ### Phase 4.4: Services (진행 예정) + - [ ] multiConnectionQueryService.ts (4개) ### Phase 4.5: Config (진행 예정) + - [ ] database.ts (4개) - 전체 파일 제거 ### Phase 4.6: Tests (Phase 5) + - [ ] authService.test.ts (2개) - 별도 처리 --- @@ -278,6 +310,7 @@ ## 📋 체크리스트 ### webTypeStandardController.ts + - [ ] Prisma import 제거 - [ ] query, queryOne import 추가 - [ ] getWebTypeStandards (findMany → query) @@ -292,18 +325,21 @@ - [ ] 동작 테스트 ### fileController.ts + - [ ] Prisma import 제거 - [ ] queryOne import 추가 - [ ] downloadFile (findUnique → queryOne) - [ ] TypeScript 컴파일 확인 ### routes/ddlRoutes.ts + - [ ] 동적 PrismaClient import 제거 - [ ] query import 추가 - [ ] 연결 테스트 로직 변경 - [ ] TypeScript 컴파일 확인 ### routes/companyManagementRoutes.ts + - [ ] Prisma import 제거 - [ ] query, queryOne import 추가 - [ ] findUnique → queryOne @@ -311,12 +347,14 @@ - [ ] TypeScript 컴파일 확인 ### services/multiConnectionQueryService.ts + - [ ] Prisma import 제거 - [ ] query import 추가 - [ ] $queryRawUnsafe → query (4곳) - [ ] TypeScript 컴파일 확인 ### config/database.ts + - [ ] 파일 전체 분석 - [ ] 의존성 확인 - [ ] 대체 방안 구현 @@ -328,15 +366,20 @@ ## 🔧 전환 패턴 요약 ### 1. findMany → query + ```typescript // Before const items = await prisma.table.findMany({ where, orderBy }); // After -const items = await query(`SELECT * FROM table WHERE ... ORDER BY ...`, params); +const items = await query( + `SELECT * FROM table WHERE ... ORDER BY ...`, + params +); ``` ### 2. findUnique → queryOne + ```typescript // Before const item = await prisma.table.findUnique({ where: { id } }); @@ -346,6 +389,7 @@ const item = await queryOne(`SELECT * FROM table WHERE id = $1`, [id]); ``` ### 3. create → queryOne with RETURNING + ```typescript // Before const newItem = await prisma.table.create({ data }); @@ -358,6 +402,7 @@ const [newItem] = await query( ``` ### 4. update → query with RETURNING + ```typescript // Before const updated = await prisma.table.update({ where, data }); @@ -370,6 +415,7 @@ const [updated] = await query( ``` ### 5. delete → query + ```typescript // Before await prisma.table.delete({ where: { id } }); @@ -379,6 +425,7 @@ await query(`DELETE FROM table WHERE id = $1`, [id]); ``` ### 6. $transaction → transaction + ```typescript // Before await prisma.$transaction([ @@ -394,11 +441,12 @@ await transaction(async (client) => { ``` ### 7. groupBy → query with GROUP BY + ```typescript // Before const result = await prisma.table.groupBy({ - by: ['category'], - _count: true + by: ["category"], + _count: true, }); // After @@ -422,30 +470,34 @@ Phase 4.2: 남은 파일 ███████░░░░░░░░░ ### 상세 진행 상황 -| 카테고리 | 완료 | 남음 | 진행률 | -| -------------- | ---- | ---- | ------ | -| Services | 415 | 0 | 100% | -| Controllers | 30 | 11 | 73% | -| Routes | 0 | 4 | 0% | -| Config | 0 | 4 | 0% | -| **총계** | 445 | 19 | 95.9% | +| 카테고리 | 완료 | 남음 | 진행률 | +| ----------- | ---- | ---- | ------ | +| Services | 415 | 0 | 100% | +| Controllers | 30 | 11 | 73% | +| Routes | 0 | 4 | 0% | +| Config | 0 | 4 | 0% | +| **총계** | 445 | 19 | 95.9% | --- ## 🎬 다음 단계 1. **webTypeStandardController.ts 전환** (11개) + - 가장 많은 Prisma 호출을 가진 남은 컨트롤러 - 웹 타입 표준 관리 핵심 기능 2. **fileController.ts 전환** (1개) + - 단순 findUnique만 있어 빠르게 처리 가능 3. **Routes 전환** (4개) + - ddlRoutes.ts - companyManagementRoutes.ts 4. **Service 전환** (4개) + - multiConnectionQueryService.ts 5. **Config 제거** (4개) @@ -457,14 +509,17 @@ Phase 4.2: 남은 파일 ███████░░░░░░░░░ ## ⚠️ 주의사항 1. **database.ts 처리** + - 현재 많은 파일이 `import prisma from '../config/database'` 사용 - 모든 import를 `import { query, queryOne } from '../database/db'`로 변경 필요 - 단계적으로 진행하여 빌드 오류 방지 2. **BigInt 처리** + - fileController의 `objid: BigInt(objid)` → `objid::bigint` 또는 `CAST(objid AS BIGINT)` 3. **트랜잭션 처리** + - webTypeStandardController의 `updateSortOrder`는 복잡한 트랜잭션 - `transaction` 함수 사용 필요 @@ -489,4 +544,3 @@ Phase 4.2: 남은 파일 ███████░░░░░░░░░ **작성일**: 2025-10-01 **최종 업데이트**: 2025-10-01 **상태**: 🔄 진행 중 (58.6% 완료) - diff --git a/backend-node/src/controllers/fileController.ts b/backend-node/src/controllers/fileController.ts index 87ecf2e0..2856098b 100644 --- a/backend-node/src/controllers/fileController.ts +++ b/backend-node/src/controllers/fileController.ts @@ -723,11 +723,10 @@ export const downloadFile = async ( try { const { objid } = req.params; - const fileRecord = await prisma.attach_file_info.findUnique({ - where: { - objid: parseInt(objid), - }, - }); + const fileRecord = await queryOne( + `SELECT * FROM attach_file_info WHERE objid = $1`, + [parseInt(objid)] + ); if (!fileRecord || fileRecord.status !== "ACTIVE") { res.status(404).json({ diff --git a/backend-node/src/controllers/webTypeStandardController.ts b/backend-node/src/controllers/webTypeStandardController.ts index c35b6dc4..952e688d 100644 --- a/backend-node/src/controllers/webTypeStandardController.ts +++ b/backend-node/src/controllers/webTypeStandardController.ts @@ -1,39 +1,51 @@ import { Request, Response } from "express"; -import { PrismaClient } from "@prisma/client"; +import { query, queryOne, transaction } from "../database/db"; import { AuthenticatedRequest } from "../types/auth"; -const prisma = new PrismaClient(); - export class WebTypeStandardController { // 웹타입 목록 조회 static async getWebTypes(req: Request, res: Response) { try { const { active, category, search } = req.query; - const where: any = {}; + // 동적 WHERE 절 생성 + const whereConditions: string[] = []; + const queryParams: any[] = []; + let paramIndex = 1; if (active) { - where.is_active = active as string; + whereConditions.push(`is_active = $${paramIndex}`); + queryParams.push(active); + paramIndex++; } if (category) { - where.category = category as string; + whereConditions.push(`category = $${paramIndex}`); + queryParams.push(category); + paramIndex++; } - if (search) { - where.OR = [ - { type_name: { contains: search as string, mode: "insensitive" } }, - { - type_name_eng: { contains: search as string, mode: "insensitive" }, - }, - { description: { contains: search as string, mode: "insensitive" } }, - ]; + if (search && typeof search === "string") { + whereConditions.push(`( + type_name ILIKE $${paramIndex} OR + type_name_eng ILIKE $${paramIndex} OR + description ILIKE $${paramIndex} + )`); + queryParams.push(`%${search}%`); + paramIndex++; } - const webTypes = await prisma.web_type_standards.findMany({ - where, - orderBy: [{ sort_order: "asc" }, { web_type: "asc" }], - }); + const whereClause = + whereConditions.length > 0 + ? `WHERE ${whereConditions.join(" AND ")}` + : ""; + + const webTypes = await query( + `SELECT * FROM web_type_standards + ${whereClause} + ORDER BY sort_order ASC, web_type ASC`, + queryParams + ); return res.json({ success: true, @@ -55,9 +67,10 @@ export class WebTypeStandardController { try { const { webType } = req.params; - const webTypeData = await prisma.web_type_standards.findUnique({ - where: { web_type: webType }, - }); + const webTypeData = await queryOne( + `SELECT * FROM web_type_standards WHERE web_type = $1`, + [webType] + ); if (!webTypeData) { return res.status(404).json({ @@ -109,9 +122,10 @@ export class WebTypeStandardController { } // 중복 체크 - const existingWebType = await prisma.web_type_standards.findUnique({ - where: { web_type }, - }); + const existingWebType = await queryOne( + `SELECT web_type FROM web_type_standards WHERE web_type = $1`, + [web_type] + ); if (existingWebType) { return res.status(409).json({ @@ -120,8 +134,15 @@ export class WebTypeStandardController { }); } - const newWebType = await prisma.web_type_standards.create({ - data: { + const [newWebType] = await query( + `INSERT INTO web_type_standards ( + web_type, type_name, type_name_eng, description, category, + component_name, config_panel, default_config, validation_rules, + default_style, input_properties, sort_order, is_active, + created_by, created_date, updated_by, updated_date + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, NOW(), $15, NOW()) + RETURNING *`, + [ web_type, type_name, type_name_eng, @@ -135,10 +156,10 @@ export class WebTypeStandardController { input_properties, sort_order, is_active, - created_by: req.user?.userId || "system", - updated_by: req.user?.userId || "system", - }, - }); + req.user?.userId || "system", + req.user?.userId || "system", + ] + ); return res.status(201).json({ success: true, @@ -174,37 +195,106 @@ export class WebTypeStandardController { is_active, } = req.body; - // 존재 여부 확인 - const existingWebType = await prisma.web_type_standards.findUnique({ - where: { web_type: webType }, - }); + // 동적 UPDATE 쿼리 생성 + const updateFields: string[] = []; + const updateValues: any[] = []; + let paramIndex = 1; - if (!existingWebType) { + if (type_name !== undefined) { + updateFields.push(`type_name = $${paramIndex}`); + updateValues.push(type_name); + paramIndex++; + } + if (type_name_eng !== undefined) { + updateFields.push(`type_name_eng = $${paramIndex}`); + updateValues.push(type_name_eng); + paramIndex++; + } + if (description !== undefined) { + updateFields.push(`description = $${paramIndex}`); + updateValues.push(description); + paramIndex++; + } + if (category !== undefined) { + updateFields.push(`category = $${paramIndex}`); + updateValues.push(category); + paramIndex++; + } + if (component_name !== undefined) { + updateFields.push(`component_name = $${paramIndex}`); + updateValues.push(component_name); + paramIndex++; + } + if (config_panel !== undefined) { + updateFields.push(`config_panel = $${paramIndex}`); + updateValues.push(config_panel); + paramIndex++; + } + if (default_config !== undefined) { + updateFields.push(`default_config = $${paramIndex}`); + updateValues.push(default_config); + paramIndex++; + } + if (validation_rules !== undefined) { + updateFields.push(`validation_rules = $${paramIndex}`); + updateValues.push(validation_rules); + paramIndex++; + } + if (default_style !== undefined) { + updateFields.push(`default_style = $${paramIndex}`); + updateValues.push(default_style); + paramIndex++; + } + if (input_properties !== undefined) { + updateFields.push(`input_properties = $${paramIndex}`); + updateValues.push(input_properties); + paramIndex++; + } + if (sort_order !== undefined) { + updateFields.push(`sort_order = $${paramIndex}`); + updateValues.push(sort_order); + paramIndex++; + } + if (is_active !== undefined) { + updateFields.push(`is_active = $${paramIndex}`); + updateValues.push(is_active); + paramIndex++; + } + + // updated_by, updated_date는 항상 추가 + updateFields.push(`updated_by = $${paramIndex}`); + updateValues.push(req.user?.userId || "system"); + paramIndex++; + + updateFields.push(`updated_date = NOW()`); + + if (updateFields.length === 2) { + // updated_by, updated_date만 있는 경우 = 수정할 내용이 없음 + return res.status(400).json({ + success: false, + message: "수정할 내용이 없습니다.", + }); + } + + // WHERE 조건용 파라미터 추가 + updateValues.push(webType); + + const result = await query( + `UPDATE web_type_standards + SET ${updateFields.join(", ")} + WHERE web_type = $${paramIndex} + RETURNING *`, + updateValues + ); + + if (result.length === 0) { return res.status(404).json({ success: false, message: "해당 웹타입을 찾을 수 없습니다.", }); } - const updatedWebType = await prisma.web_type_standards.update({ - where: { web_type: webType }, - data: { - type_name, - type_name_eng, - description, - category, - component_name, - config_panel, - default_config, - validation_rules, - default_style, - input_properties, - sort_order, - is_active, - updated_by: req.user?.userId || "system", - updated_date: new Date(), - }, - }); + const updatedWebType = result[0]; return res.json({ success: true, @@ -226,22 +316,18 @@ export class WebTypeStandardController { try { const { webType } = req.params; - // 존재 여부 확인 - const existingWebType = await prisma.web_type_standards.findUnique({ - where: { web_type: webType }, - }); + const result = await query( + `DELETE FROM web_type_standards WHERE web_type = $1 RETURNING *`, + [webType] + ); - if (!existingWebType) { + if (result.length === 0) { return res.status(404).json({ success: false, message: "해당 웹타입을 찾을 수 없습니다.", }); } - await prisma.web_type_standards.delete({ - where: { web_type: webType }, - }); - return res.json({ success: true, message: "웹타입이 성공적으로 삭제되었습니다.", @@ -272,18 +358,16 @@ export class WebTypeStandardController { } // 트랜잭션으로 일괄 업데이트 - await prisma.$transaction( - webTypes.map((item) => - prisma.web_type_standards.update({ - where: { web_type: item.web_type }, - data: { - sort_order: item.sort_order, - updated_by: req.user?.userId || "system", - updated_date: new Date(), - }, - }) - ) - ); + await transaction(async (client) => { + for (const item of webTypes) { + await client.query( + `UPDATE web_type_standards + SET sort_order = $1, updated_by = $2, updated_date = NOW() + WHERE web_type = $3`, + [item.sort_order, req.user?.userId || "system", item.web_type] + ); + } + }); return res.json({ success: true, @@ -302,19 +386,17 @@ export class WebTypeStandardController { // 웹타입 카테고리 목록 조회 static async getWebTypeCategories(req: Request, res: Response) { try { - const categories = await prisma.web_type_standards.groupBy({ - by: ["category"], - where: { - is_active: "Y", - }, - _count: { - category: true, - }, - }); + const categories = await query<{ category: string; count: string }>( + `SELECT category, COUNT(*) as count + FROM web_type_standards + WHERE is_active = 'Y' + GROUP BY category`, + [] + ); const categoryList = categories.map((item) => ({ category: item.category, - count: item._count.category, + count: parseInt(item.count, 10), })); return res.json({