화면 일괄삭제기능
This commit is contained in:
@@ -1428,10 +1428,51 @@ export async function deleteMenu(
|
||||
}
|
||||
}
|
||||
|
||||
// 외래키 제약 조건이 있는 관련 테이블 데이터 먼저 정리
|
||||
const menuObjid = Number(menuId);
|
||||
|
||||
// 1. category_column_mapping에서 menu_objid를 NULL로 설정
|
||||
await query(
|
||||
`UPDATE category_column_mapping SET menu_objid = NULL WHERE menu_objid = $1`,
|
||||
[menuObjid]
|
||||
);
|
||||
|
||||
// 2. code_category에서 menu_objid를 NULL로 설정
|
||||
await query(
|
||||
`UPDATE code_category SET menu_objid = NULL WHERE menu_objid = $1`,
|
||||
[menuObjid]
|
||||
);
|
||||
|
||||
// 3. code_info에서 menu_objid를 NULL로 설정
|
||||
await query(
|
||||
`UPDATE code_info SET menu_objid = NULL WHERE menu_objid = $1`,
|
||||
[menuObjid]
|
||||
);
|
||||
|
||||
// 4. numbering_rules에서 menu_objid를 NULL로 설정
|
||||
await query(
|
||||
`UPDATE numbering_rules SET menu_objid = NULL WHERE menu_objid = $1`,
|
||||
[menuObjid]
|
||||
);
|
||||
|
||||
// 5. rel_menu_auth에서 관련 권한 삭제
|
||||
await query(
|
||||
`DELETE FROM rel_menu_auth WHERE menu_objid = $1`,
|
||||
[menuObjid]
|
||||
);
|
||||
|
||||
// 6. screen_menu_assignments에서 관련 할당 삭제
|
||||
await query(
|
||||
`DELETE FROM screen_menu_assignments WHERE menu_objid = $1`,
|
||||
[menuObjid]
|
||||
);
|
||||
|
||||
logger.info("메뉴 관련 데이터 정리 완료", { menuObjid });
|
||||
|
||||
// Raw Query를 사용한 메뉴 삭제
|
||||
const [deletedMenu] = await query<any>(
|
||||
`DELETE FROM menu_info WHERE objid = $1 RETURNING *`,
|
||||
[Number(menuId)]
|
||||
[menuObjid]
|
||||
);
|
||||
|
||||
logger.info("메뉴 삭제 성공", { deletedMenu });
|
||||
|
||||
@@ -325,6 +325,53 @@ export const getDeletedScreens = async (
|
||||
}
|
||||
};
|
||||
|
||||
// 활성 화면 일괄 삭제 (휴지통으로 이동)
|
||||
export const bulkDeleteScreens = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { companyCode, userId } = req.user as any;
|
||||
const { screenIds, deleteReason, force } = req.body;
|
||||
|
||||
if (!Array.isArray(screenIds) || screenIds.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "삭제할 화면 ID 목록이 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await screenManagementService.bulkDeleteScreens(
|
||||
screenIds,
|
||||
companyCode,
|
||||
userId,
|
||||
deleteReason,
|
||||
force || false
|
||||
);
|
||||
|
||||
let message = `${result.deletedCount}개 화면이 휴지통으로 이동되었습니다.`;
|
||||
if (result.skippedCount > 0) {
|
||||
message += ` (${result.skippedCount}개 화면은 삭제되지 않았습니다.)`;
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message,
|
||||
result: {
|
||||
deletedCount: result.deletedCount,
|
||||
skippedCount: result.skippedCount,
|
||||
errors: result.errors,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("활성 화면 일괄 삭제 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "일괄 삭제에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 휴지통 화면 일괄 영구 삭제
|
||||
export const bulkPermanentDeleteScreens = async (
|
||||
req: AuthenticatedRequest,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
updateScreen,
|
||||
updateScreenInfo,
|
||||
deleteScreen,
|
||||
bulkDeleteScreens,
|
||||
checkScreenDependencies,
|
||||
restoreScreen,
|
||||
permanentDeleteScreen,
|
||||
@@ -44,6 +45,7 @@ router.put("/screens/:id", updateScreen);
|
||||
router.put("/screens/:id/info", updateScreenInfo); // 화면 정보만 수정
|
||||
router.get("/screens/:id/dependencies", checkScreenDependencies); // 의존성 체크
|
||||
router.delete("/screens/:id", deleteScreen); // 휴지통으로 이동
|
||||
router.delete("/screens/bulk/delete", bulkDeleteScreens); // 활성 화면 일괄 삭제 (휴지통으로 이동)
|
||||
router.get("/screens/:id/linked-modals", detectLinkedScreens); // 연결된 모달 화면 감지
|
||||
router.post("/screens/check-duplicate-name", checkDuplicateScreenName); // 화면명 중복 체크
|
||||
router.post("/screens/:id/copy", copyScreen); // 단일 화면 복사 (하위 호환용)
|
||||
|
||||
@@ -53,6 +53,7 @@ interface ScreenDefinition {
|
||||
layout_metadata: any;
|
||||
db_source_type: string | null;
|
||||
db_connection_id: number | null;
|
||||
source_screen_id: number | null; // 원본 화면 ID (복사 추적용)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,6 +235,27 @@ export class MenuCopyService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 화면 분할 패널 (screen-split-panel: leftScreenId, rightScreenId)
|
||||
if (props?.componentConfig?.leftScreenId) {
|
||||
const leftScreenId = props.componentConfig.leftScreenId;
|
||||
const numId =
|
||||
typeof leftScreenId === "number" ? leftScreenId : parseInt(leftScreenId);
|
||||
if (!isNaN(numId) && numId > 0) {
|
||||
referenced.push(numId);
|
||||
logger.debug(` 📐 분할 패널 좌측 화면 참조 발견: ${numId}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (props?.componentConfig?.rightScreenId) {
|
||||
const rightScreenId = props.componentConfig.rightScreenId;
|
||||
const numId =
|
||||
typeof rightScreenId === "number" ? rightScreenId : parseInt(rightScreenId);
|
||||
if (!isNaN(numId) && numId > 0) {
|
||||
referenced.push(numId);
|
||||
logger.debug(` 📐 분할 패널 우측 화면 참조 발견: ${numId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return referenced;
|
||||
@@ -431,14 +453,16 @@ export class MenuCopyService {
|
||||
const value = obj[key];
|
||||
const currentPath = path ? `${path}.${key}` : key;
|
||||
|
||||
// screen_id, screenId, targetScreenId 매핑 (숫자 또는 숫자 문자열)
|
||||
// screen_id, screenId, targetScreenId, leftScreenId, rightScreenId 매핑 (숫자 또는 숫자 문자열)
|
||||
if (
|
||||
key === "screen_id" ||
|
||||
key === "screenId" ||
|
||||
key === "targetScreenId"
|
||||
key === "targetScreenId" ||
|
||||
key === "leftScreenId" ||
|
||||
key === "rightScreenId"
|
||||
) {
|
||||
const numValue = typeof value === "number" ? value : parseInt(value);
|
||||
if (!isNaN(numValue)) {
|
||||
if (!isNaN(numValue) && numValue > 0) {
|
||||
const newId = screenIdMap.get(numValue);
|
||||
if (newId) {
|
||||
obj[key] = typeof value === "number" ? newId : String(newId); // 원래 타입 유지
|
||||
@@ -856,7 +880,10 @@ export class MenuCopyService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면 복사
|
||||
* 화면 복사 (업데이트 또는 신규 생성)
|
||||
* - source_screen_id로 기존 복사본 찾기
|
||||
* - 변경된 내용이 있으면 업데이트
|
||||
* - 없으면 새로 복사
|
||||
*/
|
||||
private async copyScreens(
|
||||
screenIds: Set<number>,
|
||||
@@ -876,18 +903,19 @@ export class MenuCopyService {
|
||||
return screenIdMap;
|
||||
}
|
||||
|
||||
logger.info(`📄 화면 복사 중: ${screenIds.size}개`);
|
||||
logger.info(`📄 화면 복사/업데이트 중: ${screenIds.size}개`);
|
||||
|
||||
// === 1단계: 모든 screen_definitions 먼저 복사 (screenIdMap 생성) ===
|
||||
// === 1단계: 모든 screen_definitions 처리 (screenIdMap 생성) ===
|
||||
const screenDefsToProcess: Array<{
|
||||
originalScreenId: number;
|
||||
newScreenId: number;
|
||||
targetScreenId: number;
|
||||
screenDef: ScreenDefinition;
|
||||
isUpdate: boolean; // 업데이트인지 신규 생성인지
|
||||
}> = [];
|
||||
|
||||
for (const originalScreenId of screenIds) {
|
||||
try {
|
||||
// 1) screen_definitions 조회
|
||||
// 1) 원본 screen_definitions 조회
|
||||
const screenDefResult = await client.query<ScreenDefinition>(
|
||||
`SELECT * FROM screen_definitions WHERE screen_id = $1`,
|
||||
[originalScreenId]
|
||||
@@ -900,122 +928,198 @@ export class MenuCopyService {
|
||||
|
||||
const screenDef = screenDefResult.rows[0];
|
||||
|
||||
// 2) 중복 체크: 같은 screen_code가 대상 회사에 이미 있는지 확인
|
||||
const existingScreenResult = await client.query<{ screen_id: number }>(
|
||||
`SELECT screen_id FROM screen_definitions
|
||||
WHERE screen_code = $1 AND company_code = $2 AND deleted_date IS NULL
|
||||
// 2) 기존 복사본 찾기: source_screen_id로 검색
|
||||
const existingCopyResult = await client.query<{
|
||||
screen_id: number;
|
||||
screen_name: string;
|
||||
updated_date: Date;
|
||||
}>(
|
||||
`SELECT screen_id, screen_name, updated_date
|
||||
FROM screen_definitions
|
||||
WHERE source_screen_id = $1 AND company_code = $2 AND deleted_date IS NULL
|
||||
LIMIT 1`,
|
||||
[screenDef.screen_code, targetCompanyCode]
|
||||
[originalScreenId, targetCompanyCode]
|
||||
);
|
||||
|
||||
if (existingScreenResult.rows.length > 0) {
|
||||
// 이미 존재하는 화면 - 복사하지 않고 기존 ID 매핑
|
||||
const existingScreenId = existingScreenResult.rows[0].screen_id;
|
||||
screenIdMap.set(originalScreenId, existingScreenId);
|
||||
logger.info(
|
||||
` ⏭️ 화면 이미 존재 (스킵): ${originalScreenId} → ${existingScreenId} (${screenDef.screen_code})`
|
||||
);
|
||||
continue; // 레이아웃 복사도 스킵
|
||||
}
|
||||
|
||||
// 3) 새 screen_code 생성
|
||||
const newScreenCode = await this.generateUniqueScreenCode(
|
||||
targetCompanyCode,
|
||||
client
|
||||
);
|
||||
|
||||
// 4) 화면명 변환 적용
|
||||
// 3) 화면명 변환 적용
|
||||
let transformedScreenName = screenDef.screen_name;
|
||||
if (screenNameConfig) {
|
||||
// 1. 제거할 텍스트 제거
|
||||
if (screenNameConfig.removeText?.trim()) {
|
||||
transformedScreenName = transformedScreenName.replace(
|
||||
new RegExp(screenNameConfig.removeText.trim(), "g"),
|
||||
""
|
||||
);
|
||||
transformedScreenName = transformedScreenName.trim(); // 앞뒤 공백 제거
|
||||
transformedScreenName = transformedScreenName.trim();
|
||||
}
|
||||
|
||||
// 2. 접두사 추가
|
||||
if (screenNameConfig.addPrefix?.trim()) {
|
||||
transformedScreenName =
|
||||
screenNameConfig.addPrefix.trim() + " " + transformedScreenName;
|
||||
}
|
||||
}
|
||||
|
||||
// 5) screen_definitions 복사 (deleted 필드는 NULL로 설정, 삭제된 화면도 활성화)
|
||||
const newScreenResult = await client.query<{ screen_id: number }>(
|
||||
`INSERT INTO screen_definitions (
|
||||
screen_name, screen_code, table_name, company_code,
|
||||
description, is_active, layout_metadata,
|
||||
db_source_type, db_connection_id, created_by,
|
||||
deleted_date, deleted_by, delete_reason
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
RETURNING screen_id`,
|
||||
[
|
||||
transformedScreenName, // 변환된 화면명
|
||||
newScreenCode, // 새 화면 코드
|
||||
screenDef.table_name,
|
||||
targetCompanyCode, // 새 회사 코드
|
||||
screenDef.description,
|
||||
screenDef.is_active === "D" ? "Y" : screenDef.is_active, // 삭제된 화면은 활성화
|
||||
screenDef.layout_metadata,
|
||||
screenDef.db_source_type,
|
||||
screenDef.db_connection_id,
|
||||
userId,
|
||||
null, // deleted_date: NULL (새 화면은 삭제되지 않음)
|
||||
null, // deleted_by: NULL
|
||||
null, // delete_reason: NULL
|
||||
]
|
||||
);
|
||||
if (existingCopyResult.rows.length > 0) {
|
||||
// === 기존 복사본이 있는 경우: 업데이트 ===
|
||||
const existingScreen = existingCopyResult.rows[0];
|
||||
const existingScreenId = existingScreen.screen_id;
|
||||
|
||||
const newScreenId = newScreenResult.rows[0].screen_id;
|
||||
screenIdMap.set(originalScreenId, newScreenId);
|
||||
// 원본 레이아웃 조회
|
||||
const sourceLayoutsResult = await client.query<ScreenLayout>(
|
||||
`SELECT * FROM screen_layouts WHERE screen_id = $1 ORDER BY display_order`,
|
||||
[originalScreenId]
|
||||
);
|
||||
|
||||
logger.info(
|
||||
` ✅ 화면 정의 복사: ${originalScreenId} → ${newScreenId} (${screenDef.screen_name})`
|
||||
);
|
||||
// 대상 레이아웃 조회
|
||||
const targetLayoutsResult = await client.query<ScreenLayout>(
|
||||
`SELECT * FROM screen_layouts WHERE screen_id = $1 ORDER BY display_order`,
|
||||
[existingScreenId]
|
||||
);
|
||||
|
||||
// 저장해서 2단계에서 처리
|
||||
screenDefsToProcess.push({ originalScreenId, newScreenId, screenDef });
|
||||
// 변경 여부 확인 (레이아웃 개수 또는 내용 비교)
|
||||
const hasChanges = this.hasLayoutChanges(
|
||||
sourceLayoutsResult.rows,
|
||||
targetLayoutsResult.rows
|
||||
);
|
||||
|
||||
if (hasChanges) {
|
||||
// 변경 사항이 있으면 업데이트
|
||||
logger.info(
|
||||
` 🔄 화면 업데이트 필요: ${originalScreenId} → ${existingScreenId} (${screenDef.screen_name})`
|
||||
);
|
||||
|
||||
// screen_definitions 업데이트
|
||||
await client.query(
|
||||
`UPDATE screen_definitions SET
|
||||
screen_name = $1,
|
||||
table_name = $2,
|
||||
description = $3,
|
||||
is_active = $4,
|
||||
layout_metadata = $5,
|
||||
db_source_type = $6,
|
||||
db_connection_id = $7,
|
||||
updated_by = $8,
|
||||
updated_date = NOW()
|
||||
WHERE screen_id = $9`,
|
||||
[
|
||||
transformedScreenName,
|
||||
screenDef.table_name,
|
||||
screenDef.description,
|
||||
screenDef.is_active === "D" ? "Y" : screenDef.is_active,
|
||||
screenDef.layout_metadata,
|
||||
screenDef.db_source_type,
|
||||
screenDef.db_connection_id,
|
||||
userId,
|
||||
existingScreenId,
|
||||
]
|
||||
);
|
||||
|
||||
screenIdMap.set(originalScreenId, existingScreenId);
|
||||
screenDefsToProcess.push({
|
||||
originalScreenId,
|
||||
targetScreenId: existingScreenId,
|
||||
screenDef,
|
||||
isUpdate: true,
|
||||
});
|
||||
} else {
|
||||
// 변경 사항이 없으면 스킵
|
||||
screenIdMap.set(originalScreenId, existingScreenId);
|
||||
logger.info(
|
||||
` ⏭️ 화면 변경 없음 (스킵): ${originalScreenId} → ${existingScreenId} (${screenDef.screen_name})`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// === 기존 복사본이 없는 경우: 신규 생성 ===
|
||||
const newScreenCode = await this.generateUniqueScreenCode(
|
||||
targetCompanyCode,
|
||||
client
|
||||
);
|
||||
|
||||
const newScreenResult = await client.query<{ screen_id: number }>(
|
||||
`INSERT INTO screen_definitions (
|
||||
screen_name, screen_code, table_name, company_code,
|
||||
description, is_active, layout_metadata,
|
||||
db_source_type, db_connection_id, created_by,
|
||||
deleted_date, deleted_by, delete_reason, source_screen_id
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
RETURNING screen_id`,
|
||||
[
|
||||
transformedScreenName,
|
||||
newScreenCode,
|
||||
screenDef.table_name,
|
||||
targetCompanyCode,
|
||||
screenDef.description,
|
||||
screenDef.is_active === "D" ? "Y" : screenDef.is_active,
|
||||
screenDef.layout_metadata,
|
||||
screenDef.db_source_type,
|
||||
screenDef.db_connection_id,
|
||||
userId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
originalScreenId, // source_screen_id 저장
|
||||
]
|
||||
);
|
||||
|
||||
const newScreenId = newScreenResult.rows[0].screen_id;
|
||||
screenIdMap.set(originalScreenId, newScreenId);
|
||||
|
||||
logger.info(
|
||||
` ✅ 화면 신규 복사: ${originalScreenId} → ${newScreenId} (${screenDef.screen_name})`
|
||||
);
|
||||
|
||||
screenDefsToProcess.push({
|
||||
originalScreenId,
|
||||
targetScreenId: newScreenId,
|
||||
screenDef,
|
||||
isUpdate: false,
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`❌ 화면 정의 복사 실패: screen_id=${originalScreenId}`,
|
||||
`❌ 화면 처리 실패: screen_id=${originalScreenId}`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// === 2단계: screen_layouts 복사 (이제 screenIdMap이 완성됨) ===
|
||||
// === 2단계: screen_layouts 처리 (이제 screenIdMap이 완성됨) ===
|
||||
logger.info(
|
||||
`\n📐 레이아웃 복사 시작 (screenIdMap 완성: ${screenIdMap.size}개)`
|
||||
`\n📐 레이아웃 처리 시작 (screenIdMap 완성: ${screenIdMap.size}개)`
|
||||
);
|
||||
|
||||
for (const {
|
||||
originalScreenId,
|
||||
newScreenId,
|
||||
targetScreenId,
|
||||
screenDef,
|
||||
isUpdate,
|
||||
} of screenDefsToProcess) {
|
||||
try {
|
||||
// screen_layouts 복사
|
||||
// 원본 레이아웃 조회
|
||||
const layoutsResult = await client.query<ScreenLayout>(
|
||||
`SELECT * FROM screen_layouts WHERE screen_id = $1 ORDER BY display_order`,
|
||||
[originalScreenId]
|
||||
);
|
||||
|
||||
// 1단계: component_id 매핑 생성 (원본 → 새 ID)
|
||||
if (isUpdate) {
|
||||
// 업데이트: 기존 레이아웃 삭제 후 새로 삽입
|
||||
await client.query(
|
||||
`DELETE FROM screen_layouts WHERE screen_id = $1`,
|
||||
[targetScreenId]
|
||||
);
|
||||
logger.info(` ↳ 기존 레이아웃 삭제 (업데이트 준비)`);
|
||||
}
|
||||
|
||||
// component_id 매핑 생성 (원본 → 새 ID)
|
||||
const componentIdMap = new Map<string, string>();
|
||||
for (const layout of layoutsResult.rows) {
|
||||
const newComponentId = `comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
componentIdMap.set(layout.component_id, newComponentId);
|
||||
}
|
||||
|
||||
// 2단계: screen_layouts 복사 (parent_id, zone_id도 매핑)
|
||||
// 레이아웃 삽입
|
||||
for (const layout of layoutsResult.rows) {
|
||||
const newComponentId = componentIdMap.get(layout.component_id)!;
|
||||
|
||||
// parent_id와 zone_id 매핑 (다른 컴포넌트를 참조하는 경우)
|
||||
const newParentId = layout.parent_id
|
||||
? componentIdMap.get(layout.parent_id) || layout.parent_id
|
||||
: null;
|
||||
@@ -1023,7 +1127,6 @@ export class MenuCopyService {
|
||||
? componentIdMap.get(layout.zone_id) || layout.zone_id
|
||||
: null;
|
||||
|
||||
// properties 내부 참조 업데이트
|
||||
const updatedProperties = this.updateReferencesInProperties(
|
||||
layout.properties,
|
||||
screenIdMap,
|
||||
@@ -1037,38 +1140,94 @@ export class MenuCopyService {
|
||||
display_order, layout_type, layout_config, zones_config, zone_id
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)`,
|
||||
[
|
||||
newScreenId, // 새 화면 ID
|
||||
targetScreenId,
|
||||
layout.component_type,
|
||||
newComponentId, // 새 컴포넌트 ID
|
||||
newParentId, // 매핑된 parent_id
|
||||
newComponentId,
|
||||
newParentId,
|
||||
layout.position_x,
|
||||
layout.position_y,
|
||||
layout.width,
|
||||
layout.height,
|
||||
updatedProperties, // 업데이트된 속성
|
||||
updatedProperties,
|
||||
layout.display_order,
|
||||
layout.layout_type,
|
||||
layout.layout_config,
|
||||
layout.zones_config,
|
||||
newZoneId, // 매핑된 zone_id
|
||||
newZoneId,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(` ↳ 레이아웃 복사: ${layoutsResult.rows.length}개`);
|
||||
const action = isUpdate ? "업데이트" : "복사";
|
||||
logger.info(` ↳ 레이아웃 ${action}: ${layoutsResult.rows.length}개`);
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`❌ 레이아웃 복사 실패: screen_id=${originalScreenId}`,
|
||||
`❌ 레이아웃 처리 실패: screen_id=${originalScreenId}`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`\n✅ 화면 복사 완료: ${screenIdMap.size}개`);
|
||||
// 통계 출력
|
||||
const newCount = screenDefsToProcess.filter((s) => !s.isUpdate).length;
|
||||
const updateCount = screenDefsToProcess.filter((s) => s.isUpdate).length;
|
||||
const skipCount = screenIds.size - screenDefsToProcess.length;
|
||||
|
||||
logger.info(`
|
||||
✅ 화면 처리 완료:
|
||||
- 신규 복사: ${newCount}개
|
||||
- 업데이트: ${updateCount}개
|
||||
- 스킵 (변경 없음): ${skipCount}개
|
||||
- 총 매핑: ${screenIdMap.size}개
|
||||
`);
|
||||
|
||||
return screenIdMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 변경 여부 확인
|
||||
*/
|
||||
private hasLayoutChanges(
|
||||
sourceLayouts: ScreenLayout[],
|
||||
targetLayouts: ScreenLayout[]
|
||||
): boolean {
|
||||
// 1. 레이아웃 개수가 다르면 변경됨
|
||||
if (sourceLayouts.length !== targetLayouts.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. 각 레이아웃의 주요 속성 비교
|
||||
for (let i = 0; i < sourceLayouts.length; i++) {
|
||||
const source = sourceLayouts[i];
|
||||
const target = targetLayouts[i];
|
||||
|
||||
// component_type이 다르면 변경됨
|
||||
if (source.component_type !== target.component_type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 위치/크기가 다르면 변경됨
|
||||
if (
|
||||
source.position_x !== target.position_x ||
|
||||
source.position_y !== target.position_y ||
|
||||
source.width !== target.width ||
|
||||
source.height !== target.height
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// properties의 JSON 문자열 비교 (깊은 비교)
|
||||
const sourceProps = JSON.stringify(source.properties || {});
|
||||
const targetProps = JSON.stringify(target.properties || {});
|
||||
if (sourceProps !== targetProps) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 위상 정렬 (부모 먼저)
|
||||
*/
|
||||
|
||||
@@ -892,6 +892,134 @@ export class ScreenManagementService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 화면 일괄 삭제 (휴지통으로 이동)
|
||||
*/
|
||||
async bulkDeleteScreens(
|
||||
screenIds: number[],
|
||||
userCompanyCode: string,
|
||||
deletedBy: string,
|
||||
deleteReason?: string,
|
||||
force: boolean = false
|
||||
): Promise<{
|
||||
deletedCount: number;
|
||||
skippedCount: number;
|
||||
errors: Array<{ screenId: number; error: string }>;
|
||||
}> {
|
||||
if (screenIds.length === 0) {
|
||||
throw new Error("삭제할 화면을 선택해주세요.");
|
||||
}
|
||||
|
||||
let deletedCount = 0;
|
||||
let skippedCount = 0;
|
||||
const errors: Array<{ screenId: number; error: string }> = [];
|
||||
|
||||
// 각 화면을 개별적으로 삭제 처리
|
||||
for (const screenId of screenIds) {
|
||||
try {
|
||||
// 권한 확인 (Raw Query)
|
||||
const existingResult = await query<{
|
||||
company_code: string | null;
|
||||
is_active: string;
|
||||
screen_name: string;
|
||||
}>(
|
||||
`SELECT company_code, is_active, screen_name FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
|
||||
[screenId]
|
||||
);
|
||||
|
||||
if (existingResult.length === 0) {
|
||||
skippedCount++;
|
||||
errors.push({
|
||||
screenId,
|
||||
error: "화면을 찾을 수 없습니다.",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingScreen = existingResult[0];
|
||||
|
||||
// 권한 확인
|
||||
if (
|
||||
userCompanyCode !== "*" &&
|
||||
existingScreen.company_code !== userCompanyCode
|
||||
) {
|
||||
skippedCount++;
|
||||
errors.push({
|
||||
screenId,
|
||||
error: "이 화면을 삭제할 권한이 없습니다.",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 이미 삭제된 화면인지 확인
|
||||
if (existingScreen.is_active === "D") {
|
||||
skippedCount++;
|
||||
errors.push({
|
||||
screenId,
|
||||
error: "이미 삭제된 화면입니다.",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 강제 삭제가 아닌 경우 의존성 체크
|
||||
if (!force) {
|
||||
const dependencyCheck = await this.checkScreenDependencies(
|
||||
screenId,
|
||||
userCompanyCode
|
||||
);
|
||||
if (dependencyCheck.hasDependencies) {
|
||||
skippedCount++;
|
||||
errors.push({
|
||||
screenId,
|
||||
error: `다른 화면에서 사용 중 (${dependencyCheck.dependencies.length}개 참조)`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 트랜잭션으로 화면 삭제와 메뉴 할당 정리를 함께 처리
|
||||
await transaction(async (client) => {
|
||||
const now = new Date();
|
||||
|
||||
// 소프트 삭제 (휴지통으로 이동)
|
||||
await client.query(
|
||||
`UPDATE screen_definitions
|
||||
SET is_active = 'D',
|
||||
deleted_date = $1,
|
||||
deleted_by = $2,
|
||||
delete_reason = $3,
|
||||
updated_date = $4,
|
||||
updated_by = $5
|
||||
WHERE screen_id = $6`,
|
||||
[now, deletedBy, deleteReason || null, now, deletedBy, screenId]
|
||||
);
|
||||
|
||||
// 메뉴 할당 정리 (삭제된 화면의 메뉴 할당 제거)
|
||||
await client.query(
|
||||
`DELETE FROM screen_menu_assignments WHERE screen_id = $1`,
|
||||
[screenId]
|
||||
);
|
||||
});
|
||||
|
||||
deletedCount++;
|
||||
logger.info(`화면 삭제 완료: ${screenId} (${existingScreen.screen_name})`);
|
||||
} catch (error) {
|
||||
skippedCount++;
|
||||
errors.push({
|
||||
screenId,
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
logger.error(`화면 삭제 실패: ${screenId}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`일괄 삭제 완료: 성공 ${deletedCount}개, 실패 ${skippedCount}개`
|
||||
);
|
||||
|
||||
return { deletedCount, skippedCount, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴지통 화면 일괄 영구 삭제
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user