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:
parent
c25405b4de
commit
311811bc0a
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue