From 311811bc0ad05c50f77c94cba00ed9ca84034765 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 30 Sep 2025 16:42:21 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=202.1=20Stage=204=20=EB=B3=B5?= =?UTF-8?q?=EC=9E=A1=ED=95=9C=20=EA=B8=B0=EB=8A=A5=20=EC=A0=84=ED=99=98=20?= =?UTF-8?q?(=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98)=20-=20=EC=9D=BC=EB=B6=80?= =?UTF-8?q?=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 4: 트랜잭션 기반 복잡한 기능 Raw Query 전환 ✅ 전환 완료 (3개 트랜잭션 함수): **트랜잭션 함수들:** 1. copyScreen() - 화면 복사 (화면 + 레이아웃 전체 복사) - 원본 화면 조회 (SELECT) - 화면 코드 중복 체크 (SELECT) - 새 화면 생성 (INSERT RETURNING) - 원본 레이아웃 조회 (SELECT with ORDER BY) - ID 매핑 후 레이아웃 복사 (반복 INSERT) - PoolClient 기반 트랜잭션 사용 2. restoreScreen() - 삭제된 화면 복원 - 화면 코드 중복 체크 (SELECT with multiple conditions) - 화면 복원 (UPDATE with NULL 설정) - 메뉴 할당 활성화 (UPDATE) - 트랜잭션으로 원자성 보장 3. bulkDeletePermanently() - 일괄 영구 삭제 - 삭제 대상 조회 (SELECT with dynamic WHERE) - 레이아웃 삭제 (DELETE) - 메뉴 할당 삭제 (DELETE) - 화면 정의 삭제 (DELETE) - 각 화면마다 개별 트랜잭션으로 롤백 격리 📊 진행률: 20+/46 (43%+) 🎯 다음: 나머지 Prisma 호출 전환 (조회, UPSERT 등) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/services/screenManagementService.ts | 219 ++++++++++-------- 1 file changed, 122 insertions(+), 97 deletions(-) diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 02cd5ab7..7b33e0f9 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -625,45 +625,37 @@ export class ScreenManagementService { } // 화면 코드 중복 확인 (복원 시 같은 코드가 이미 존재하는지) - const duplicateScreen = await prisma.screen_definitions.findFirst({ - where: { - screen_code: existingScreen.screen_code, - is_active: { not: "D" }, - screen_id: { not: screenId }, - }, - }); + const duplicateScreens = await query<{ screen_id: number }>( + `SELECT screen_id FROM screen_definitions + WHERE screen_code = $1 AND is_active != 'D' AND screen_id != $2 + LIMIT 1`, + [existingScreen.screen_code, screenId] + ); - if (duplicateScreen) { + if (duplicateScreens.length > 0) { throw new Error( "같은 화면 코드를 가진 활성 화면이 이미 존재합니다. 복원하려면 기존 화면의 코드를 변경하거나 삭제해주세요." ); } // 트랜잭션으로 화면 복원과 메뉴 할당 복원을 함께 처리 - await prisma.$transaction(async (tx) => { + await transaction(async (client) => { // 화면 복원 - await tx.screen_definitions.update({ - where: { screen_id: screenId }, - data: { - is_active: "Y", - deleted_date: null, - deleted_by: null, - delete_reason: null, - updated_date: new Date(), - updated_by: restoredBy, - }, - }); + await client.query( + `UPDATE screen_definitions + SET is_active = 'Y', deleted_date = NULL, deleted_by = NULL, + delete_reason = NULL, updated_date = $1, updated_by = $2 + WHERE screen_id = $3`, + [new Date(), restoredBy, screenId] + ); // 메뉴 할당도 다시 활성화 - await tx.screen_menu_assignments.updateMany({ - where: { - screen_id: screenId, - is_active: "N", - }, - data: { - is_active: "Y", - }, - }); + await client.query( + `UPDATE screen_menu_assignments + SET is_active = 'Y' + WHERE screen_id = $1 AND is_active = 'N'`, + [screenId] + ); }); } @@ -810,9 +802,21 @@ export class ScreenManagementService { whereClause.company_code = userCompanyCode; } - const screensToDelete = await prisma.screen_definitions.findMany({ - where: whereClause, - }); + // WHERE 절 생성 + const whereConditions: string[] = ["is_active = 'D'"]; + const params: any[] = []; + + if (userCompanyCode !== "*") { + whereConditions.push(`company_code = $${params.length + 1}`); + params.push(userCompanyCode); + } + + const whereSQL = whereConditions.join(" AND "); + + const screensToDelete = await query<{ screen_id: number }>( + `SELECT screen_id FROM screen_definitions WHERE ${whereSQL}`, + params + ); let deletedCount = 0; let skippedCount = 0; @@ -822,7 +826,7 @@ export class ScreenManagementService { for (const screenId of screenIds) { try { const screenToDelete = screensToDelete.find( - (s) => s.screen_id === screenId + (s: any) => s.screen_id === screenId ); if (!screenToDelete) { @@ -834,22 +838,25 @@ export class ScreenManagementService { continue; } - // 관련 레이아웃 데이터도 함께 삭제 - await prisma.$transaction(async (tx) => { + // 관련 레이아웃 데이터도 함께 삭제 (트랜잭션) + await transaction(async (client) => { // screen_layouts 삭제 - await tx.screen_layouts.deleteMany({ - where: { screen_id: screenId }, - }); + await client.query( + `DELETE FROM screen_layouts WHERE screen_id = $1`, + [screenId] + ); // screen_menu_assignments 삭제 - await tx.screen_menu_assignments.deleteMany({ - where: { screen_id: screenId }, - }); + await client.query( + `DELETE FROM screen_menu_assignments WHERE screen_id = $1`, + [screenId] + ); // screen_definitions 삭제 - await tx.screen_definitions.delete({ - where: { screen_id: screenId }, - }); + await client.query( + `DELETE FROM screen_definitions WHERE screen_id = $1`, + [screenId] + ); }); deletedCount++; @@ -1763,61 +1770,72 @@ export class ScreenManagementService { } /** - * 화면 복사 (화면 정보 + 레이아웃 모두 복사) + * 화면 복사 (화면 정보 + 레이아웃 모두 복사) (✅ Raw Query 전환 완료) */ async copyScreen( sourceScreenId: number, copyData: CopyScreenRequest ): Promise { // 트랜잭션으로 처리 - return await prisma.$transaction(async (tx) => { + return await transaction(async (client) => { // 1. 원본 화면 정보 조회 - const sourceScreen = await tx.screen_definitions.findFirst({ - where: { - screen_id: sourceScreenId, - company_code: copyData.companyCode, - }, - }); + const sourceScreens = await client.query( + `SELECT * FROM screen_definitions + WHERE screen_id = $1 AND company_code = $2 + LIMIT 1`, + [sourceScreenId, copyData.companyCode] + ); - if (!sourceScreen) { + if (sourceScreens.rows.length === 0) { throw new Error("복사할 화면을 찾을 수 없습니다."); } - // 2. 화면 코드 중복 체크 - const existingScreen = await tx.screen_definitions.findFirst({ - where: { - screen_code: copyData.screenCode, - company_code: copyData.companyCode, - }, - }); + const sourceScreen = sourceScreens.rows[0]; - if (existingScreen) { + // 2. 화면 코드 중복 체크 + const existingScreens = await client.query( + `SELECT screen_id FROM screen_definitions + WHERE screen_code = $1 AND company_code = $2 + LIMIT 1`, + [copyData.screenCode, copyData.companyCode] + ); + + if (existingScreens.rows.length > 0) { throw new Error("이미 존재하는 화면 코드입니다."); } // 3. 새 화면 생성 - const newScreen = await tx.screen_definitions.create({ - data: { - screen_code: copyData.screenCode, - screen_name: copyData.screenName, - description: copyData.description || sourceScreen.description, - company_code: copyData.companyCode, - table_name: sourceScreen.table_name, - is_active: sourceScreen.is_active, - created_by: copyData.createdBy, - created_date: new Date(), - updated_by: copyData.createdBy, - updated_date: new Date(), - }, - }); + const newScreenResult = await client.query( + `INSERT INTO screen_definitions ( + screen_code, screen_name, description, company_code, table_name, + is_active, created_by, created_date, updated_by, updated_date + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + RETURNING *`, + [ + copyData.screenCode, + copyData.screenName, + copyData.description || sourceScreen.description, + copyData.companyCode, + sourceScreen.table_name, + sourceScreen.is_active, + copyData.createdBy, + new Date(), + copyData.createdBy, + new Date(), + ] + ); + + const newScreen = newScreenResult.rows[0]; // 4. 원본 화면의 레이아웃 정보 조회 - const sourceLayouts = await tx.screen_layouts.findMany({ - where: { - screen_id: sourceScreenId, - }, - orderBy: { display_order: "asc" }, - }); + const sourceLayoutsResult = await client.query( + `SELECT * FROM screen_layouts + WHERE screen_id = $1 + ORDER BY display_order ASC NULLS LAST`, + [sourceScreenId] + ); + + const sourceLayouts = sourceLayoutsResult.rows; // 5. 레이아웃이 있다면 복사 if (sourceLayouts.length > 0) { @@ -1826,7 +1844,7 @@ export class ScreenManagementService { const idMapping: { [oldId: string]: string } = {}; // 새로운 컴포넌트 ID 미리 생성 - sourceLayouts.forEach((layout) => { + sourceLayouts.forEach((layout: any) => { idMapping[layout.component_id] = generateId(); }); @@ -1837,21 +1855,28 @@ export class ScreenManagementService { ? idMapping[sourceLayout.parent_id] : null; - await tx.screen_layouts.create({ - data: { - screen_id: newScreen.screen_id, - component_type: sourceLayout.component_type, - component_id: newComponentId, - parent_id: newParentId, - position_x: sourceLayout.position_x, - position_y: sourceLayout.position_y, - width: sourceLayout.width, - height: sourceLayout.height, - properties: sourceLayout.properties as any, - display_order: sourceLayout.display_order, - created_date: new Date(), - }, - }); + await client.query( + `INSERT INTO screen_layouts ( + screen_id, component_type, component_id, parent_id, + position_x, position_y, width, height, properties, + display_order, created_date + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, + [ + newScreen.screen_id, + sourceLayout.component_type, + newComponentId, + newParentId, + sourceLayout.position_x, + sourceLayout.position_y, + sourceLayout.width, + sourceLayout.height, + typeof sourceLayout.properties === 'string' + ? sourceLayout.properties + : JSON.stringify(sourceLayout.properties), + sourceLayout.display_order, + new Date(), + ] + ); } } catch (error) { console.error("레이아웃 복사 중 오류:", error);