feat: Phase 2.1 Stage 4 복잡한 기능 전환 (트랜잭션) - 일부 완료

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 <noreply@anthropic.com>
This commit is contained in:
kjs 2025-09-30 16:42:21 +09:00
parent c25405b4de
commit 311811bc0a
1 changed files with 122 additions and 97 deletions

View File

@ -625,45 +625,37 @@ export class ScreenManagementService {
} }
// 화면 코드 중복 확인 (복원 시 같은 코드가 이미 존재하는지) // 화면 코드 중복 확인 (복원 시 같은 코드가 이미 존재하는지)
const duplicateScreen = await prisma.screen_definitions.findFirst({ const duplicateScreens = await query<{ screen_id: number }>(
where: { `SELECT screen_id FROM screen_definitions
screen_code: existingScreen.screen_code, WHERE screen_code = $1 AND is_active != 'D' AND screen_id != $2
is_active: { not: "D" }, LIMIT 1`,
screen_id: { not: screenId }, [existingScreen.screen_code, screenId]
}, );
});
if (duplicateScreen) { if (duplicateScreens.length > 0) {
throw new Error( throw new Error(
"같은 화면 코드를 가진 활성 화면이 이미 존재합니다. 복원하려면 기존 화면의 코드를 변경하거나 삭제해주세요." "같은 화면 코드를 가진 활성 화면이 이미 존재합니다. 복원하려면 기존 화면의 코드를 변경하거나 삭제해주세요."
); );
} }
// 트랜잭션으로 화면 복원과 메뉴 할당 복원을 함께 처리 // 트랜잭션으로 화면 복원과 메뉴 할당 복원을 함께 처리
await prisma.$transaction(async (tx) => { await transaction(async (client) => {
// 화면 복원 // 화면 복원
await tx.screen_definitions.update({ await client.query(
where: { screen_id: screenId }, `UPDATE screen_definitions
data: { SET is_active = 'Y', deleted_date = NULL, deleted_by = NULL,
is_active: "Y", delete_reason = NULL, updated_date = $1, updated_by = $2
deleted_date: null, WHERE screen_id = $3`,
deleted_by: null, [new Date(), restoredBy, screenId]
delete_reason: null, );
updated_date: new Date(),
updated_by: restoredBy,
},
});
// 메뉴 할당도 다시 활성화 // 메뉴 할당도 다시 활성화
await tx.screen_menu_assignments.updateMany({ await client.query(
where: { `UPDATE screen_menu_assignments
screen_id: screenId, SET is_active = 'Y'
is_active: "N", WHERE screen_id = $1 AND is_active = 'N'`,
}, [screenId]
data: { );
is_active: "Y",
},
});
}); });
} }
@ -810,9 +802,21 @@ export class ScreenManagementService {
whereClause.company_code = userCompanyCode; whereClause.company_code = userCompanyCode;
} }
const screensToDelete = await prisma.screen_definitions.findMany({ // WHERE 절 생성
where: whereClause, 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 deletedCount = 0;
let skippedCount = 0; let skippedCount = 0;
@ -822,7 +826,7 @@ export class ScreenManagementService {
for (const screenId of screenIds) { for (const screenId of screenIds) {
try { try {
const screenToDelete = screensToDelete.find( const screenToDelete = screensToDelete.find(
(s) => s.screen_id === screenId (s: any) => s.screen_id === screenId
); );
if (!screenToDelete) { if (!screenToDelete) {
@ -834,22 +838,25 @@ export class ScreenManagementService {
continue; continue;
} }
// 관련 레이아웃 데이터도 함께 삭제 // 관련 레이아웃 데이터도 함께 삭제 (트랜잭션)
await prisma.$transaction(async (tx) => { await transaction(async (client) => {
// screen_layouts 삭제 // screen_layouts 삭제
await tx.screen_layouts.deleteMany({ await client.query(
where: { screen_id: screenId }, `DELETE FROM screen_layouts WHERE screen_id = $1`,
}); [screenId]
);
// screen_menu_assignments 삭제 // screen_menu_assignments 삭제
await tx.screen_menu_assignments.deleteMany({ await client.query(
where: { screen_id: screenId }, `DELETE FROM screen_menu_assignments WHERE screen_id = $1`,
}); [screenId]
);
// screen_definitions 삭제 // screen_definitions 삭제
await tx.screen_definitions.delete({ await client.query(
where: { screen_id: screenId }, `DELETE FROM screen_definitions WHERE screen_id = $1`,
}); [screenId]
);
}); });
deletedCount++; deletedCount++;
@ -1763,61 +1770,72 @@ export class ScreenManagementService {
} }
/** /**
* ( + ) * ( + ) ( Raw Query )
*/ */
async copyScreen( async copyScreen(
sourceScreenId: number, sourceScreenId: number,
copyData: CopyScreenRequest copyData: CopyScreenRequest
): Promise<ScreenDefinition> { ): Promise<ScreenDefinition> {
// 트랜잭션으로 처리 // 트랜잭션으로 처리
return await prisma.$transaction(async (tx) => { return await transaction(async (client) => {
// 1. 원본 화면 정보 조회 // 1. 원본 화면 정보 조회
const sourceScreen = await tx.screen_definitions.findFirst({ const sourceScreens = await client.query<any>(
where: { `SELECT * FROM screen_definitions
screen_id: sourceScreenId, WHERE screen_id = $1 AND company_code = $2
company_code: copyData.companyCode, LIMIT 1`,
}, [sourceScreenId, copyData.companyCode]
}); );
if (!sourceScreen) { if (sourceScreens.rows.length === 0) {
throw new Error("복사할 화면을 찾을 수 없습니다."); throw new Error("복사할 화면을 찾을 수 없습니다.");
} }
// 2. 화면 코드 중복 체크 const sourceScreen = sourceScreens.rows[0];
const existingScreen = await tx.screen_definitions.findFirst({
where: {
screen_code: copyData.screenCode,
company_code: copyData.companyCode,
},
});
if (existingScreen) { // 2. 화면 코드 중복 체크
const existingScreens = await client.query<any>(
`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("이미 존재하는 화면 코드입니다."); throw new Error("이미 존재하는 화면 코드입니다.");
} }
// 3. 새 화면 생성 // 3. 새 화면 생성
const newScreen = await tx.screen_definitions.create({ const newScreenResult = await client.query<any>(
data: { `INSERT INTO screen_definitions (
screen_code: copyData.screenCode, screen_code, screen_name, description, company_code, table_name,
screen_name: copyData.screenName, is_active, created_by, created_date, updated_by, updated_date
description: copyData.description || sourceScreen.description, ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
company_code: copyData.companyCode, RETURNING *`,
table_name: sourceScreen.table_name, [
is_active: sourceScreen.is_active, copyData.screenCode,
created_by: copyData.createdBy, copyData.screenName,
created_date: new Date(), copyData.description || sourceScreen.description,
updated_by: copyData.createdBy, copyData.companyCode,
updated_date: new Date(), sourceScreen.table_name,
}, sourceScreen.is_active,
}); copyData.createdBy,
new Date(),
copyData.createdBy,
new Date(),
]
);
const newScreen = newScreenResult.rows[0];
// 4. 원본 화면의 레이아웃 정보 조회 // 4. 원본 화면의 레이아웃 정보 조회
const sourceLayouts = await tx.screen_layouts.findMany({ const sourceLayoutsResult = await client.query<any>(
where: { `SELECT * FROM screen_layouts
screen_id: sourceScreenId, WHERE screen_id = $1
}, ORDER BY display_order ASC NULLS LAST`,
orderBy: { display_order: "asc" }, [sourceScreenId]
}); );
const sourceLayouts = sourceLayoutsResult.rows;
// 5. 레이아웃이 있다면 복사 // 5. 레이아웃이 있다면 복사
if (sourceLayouts.length > 0) { if (sourceLayouts.length > 0) {
@ -1826,7 +1844,7 @@ export class ScreenManagementService {
const idMapping: { [oldId: string]: string } = {}; const idMapping: { [oldId: string]: string } = {};
// 새로운 컴포넌트 ID 미리 생성 // 새로운 컴포넌트 ID 미리 생성
sourceLayouts.forEach((layout) => { sourceLayouts.forEach((layout: any) => {
idMapping[layout.component_id] = generateId(); idMapping[layout.component_id] = generateId();
}); });
@ -1837,21 +1855,28 @@ export class ScreenManagementService {
? idMapping[sourceLayout.parent_id] ? idMapping[sourceLayout.parent_id]
: null; : null;
await tx.screen_layouts.create({ await client.query(
data: { `INSERT INTO screen_layouts (
screen_id: newScreen.screen_id, screen_id, component_type, component_id, parent_id,
component_type: sourceLayout.component_type, position_x, position_y, width, height, properties,
component_id: newComponentId, display_order, created_date
parent_id: newParentId, ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
position_x: sourceLayout.position_x, [
position_y: sourceLayout.position_y, newScreen.screen_id,
width: sourceLayout.width, sourceLayout.component_type,
height: sourceLayout.height, newComponentId,
properties: sourceLayout.properties as any, newParentId,
display_order: sourceLayout.display_order, sourceLayout.position_x,
created_date: new Date(), sourceLayout.position_y,
}, sourceLayout.width,
}); sourceLayout.height,
typeof sourceLayout.properties === 'string'
? sourceLayout.properties
: JSON.stringify(sourceLayout.properties),
sourceLayout.display_order,
new Date(),
]
);
} }
} catch (error) { } catch (error) {
console.error("레이아웃 복사 중 오류:", error); console.error("레이아웃 복사 중 오류:", error);