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({
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<ScreenDefinition> {
// 트랜잭션으로 처리
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<any>(
`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<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("이미 존재하는 화면 코드입니다.");
}
// 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<any>(
`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<any>(
`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);