feat: 화면 그룹 삭제 시 메뉴 및 플로우 데이터 정리 로직 개선

- 화면 그룹 삭제 시 연결된 메뉴를 정리하는 로직을 추가하여, 삭제될 그룹에 연결된 메뉴를 자동으로 삭제하도록 하였습니다.
- 메뉴 삭제 시 관련된 화면 및 플로우 데이터도 함께 정리하여 데이터 일관성을 유지하였습니다.
- 복제 화면 모달에서 원본 회사와 동일한 회사 선택 시 자동으로 다른 회사로 변경하는 기능을 추가하였습니다.
- 삭제 확인 다이얼로그에 경고 메시지를 추가하여 사용자에게 삭제 작업의 영향을 명확히 안내하였습니다.
This commit is contained in:
DDD1542
2026-02-02 20:18:47 +09:00
parent 8c96b2d185
commit 257174d0c6
16 changed files with 472 additions and 442 deletions

View File

@@ -851,47 +851,10 @@ export class MenuCopyService {
]);
logger.info(` ✅ 메뉴 권한 삭제 완료`);
// 5-4. 채번 규칙 처리 (체크 제약조건 고려)
// scope_type = 'menu'인 채번 규칙: 메뉴 전용이므로 삭제 (파트 포함)
// check_menu_scope_requires_menu_objid 제약: scope_type='menu'이면 menu_objid NOT NULL 필수
const menuScopedRulesResult = await client.query(
`SELECT rule_id FROM numbering_rules
WHERE menu_objid = ANY($1) AND company_code = $2 AND scope_type = 'menu'`,
[existingMenuIds, targetCompanyCode]
);
if (menuScopedRulesResult.rows.length > 0) {
const menuScopedRuleIds = menuScopedRulesResult.rows.map(
(r) => r.rule_id
);
// 채번 규칙 파트 먼저 삭제
await client.query(
`DELETE FROM numbering_rule_parts WHERE rule_id = ANY($1)`,
[menuScopedRuleIds]
);
// 채번 규칙 삭제
await client.query(
`DELETE FROM numbering_rules WHERE rule_id = ANY($1)`,
[menuScopedRuleIds]
);
logger.info(
` ✅ 메뉴 전용 채번 규칙 삭제: ${menuScopedRuleIds.length}`
);
}
// scope_type != 'menu'인 채번 규칙: menu_objid만 NULL로 설정 (규칙 보존)
const updatedNumberingRules = await client.query(
`UPDATE numbering_rules
SET menu_objid = NULL
WHERE menu_objid = ANY($1) AND company_code = $2
AND (scope_type IS NULL OR scope_type != 'menu')
RETURNING rule_id`,
[existingMenuIds, targetCompanyCode]
);
if (updatedNumberingRules.rowCount && updatedNumberingRules.rowCount > 0) {
logger.info(
` ✅ 테이블 스코프 채번 규칙 연결 해제: ${updatedNumberingRules.rowCount}개 (데이터 보존됨)`
);
}
// 5-4. 채번 규칙 처리 (새 스키마에서는 menu_objid 없음 - 스킵)
// 새 numbering_rules 스키마: table_name + column_name + company_code 기반
// 메뉴와 직접 연결되지 않으므로 메뉴 삭제 시 처리 불필요
logger.info(` ⏭️ 채번 규칙: 새 스키마에서는 메뉴와 연결되지 않음 (스킵)`);
// 5-5. 카테고리 매핑 삭제 (menu_objid가 NOT NULL이므로 NULL 설정 불가)
// 카테고리 매핑은 메뉴와 강하게 연결되어 있으므로 함께 삭제
@@ -2590,8 +2553,9 @@ export class MenuCopyService {
}
/**
* 채번 규칙 복사 (최적화: 배치 조회/삽입)
* 화면 복사 전에 호출되어 numberingRuleId 참조 업데이트에 사용됨
* 채번 규칙 복사 (새 스키마: table_name + column_name 기반)
* 프론트엔드에서 /numbering-rules/copy-for-company API를 별도 호출하므로
* 이 함수는 ruleIdMap 생성만 담당 (실제 복제는 numberingRuleService에서 처리)
*/
private async copyNumberingRulesWithMap(
menuObjids: number[],
@@ -2600,222 +2564,47 @@ export class MenuCopyService {
userId: string,
client: PoolClient
): Promise<{ copiedCount: number; ruleIdMap: Map<string, string> }> {
let copiedCount = 0;
const ruleIdMap = new Map<string, string>();
if (menuObjids.length === 0) {
return { copiedCount, ruleIdMap };
}
// === 최적화: 배치 조회 ===
// 1. 모든 원본 채번 규칙 한 번에 조회
const allRulesResult = await client.query(
`SELECT * FROM numbering_rules WHERE menu_objid = ANY($1)`,
[menuObjids]
// 새 스키마에서는 채번규칙이 메뉴와 직접 연결되지 않음
// 프론트엔드에서 /numbering-rules/copy-for-company API를 별도 호출
// 여기서는 기존 규칙 ID를 그대로 매핑 (화면 레이아웃의 numberingRuleId 참조용)
// 원본 회사의 채번규칙 조회 (company_code 기반)
const sourceRulesResult = await client.query(
`SELECT rule_id, rule_name FROM numbering_rules WHERE company_code = $1`,
[menuObjids.length > 0 ? (await client.query(
`SELECT company_code FROM menu_info WHERE objid = $1`,
[menuObjids[0]]
)).rows[0]?.company_code : null]
);
if (allRulesResult.rows.length === 0) {
logger.info(` 📭 복사할 채번 규칙 없음`);
return { copiedCount, ruleIdMap };
}
// 2. 대상 회사에 이미 존재하는 모든 채번 규칙 조회 (원본 ID + 새로 생성될 ID 모두 체크 필요)
const existingRulesResult = await client.query(
`SELECT rule_id FROM numbering_rules WHERE company_code = $1`,
// 대상 회사의 채번규칙 조회 (이름 기준 매핑)
const targetRulesResult = await client.query(
`SELECT rule_id, rule_name FROM numbering_rules WHERE company_code = $1`,
[targetCompanyCode]
);
const existingRuleIds = new Set(
existingRulesResult.rows.map((r) => r.rule_id)
const targetRulesByName = new Map(
targetRulesResult.rows.map((r: any) => [r.rule_name, r.rule_id])
);
// 3. 복사할 규칙과 스킵할 규칙 분류
const rulesToCopy: any[] = [];
const originalToNewRuleMap: Array<{ original: string; new: string }> = [];
// 기존 규칙 중 menu_objid 업데이트가 필요한 규칙들
const rulesToUpdate: Array<{ ruleId: string; newMenuObjid: number }> = [];
for (const rule of allRulesResult.rows) {
// 새 rule_id 계산: 회사코드 접두사 제거 후 대상 회사코드 추가
// 예: COMPANY_10_rule-123 -> rule-123 -> COMPANY_16_rule-123
// 예: rule-123 -> rule-123 -> COMPANY_16_rule-123
// 예: WACE_품목코드 -> 품목코드 -> COMPANY_16_품목코드
let baseName = rule.rule_id;
// 회사코드 접두사 패턴들을 순서대로 제거 시도
// 1. COMPANY_숫자_ 패턴 (예: COMPANY_10_)
// 2. 일반 접두사_ 패턴 (예: WACE_)
if (baseName.match(/^COMPANY_\d+_/)) {
baseName = baseName.replace(/^COMPANY_\d+_/, "");
} else if (baseName.includes("_")) {
baseName = baseName.replace(/^[^_]+_/, "");
}
const newRuleId = `${targetCompanyCode}_${baseName}`;
if (existingRuleIds.has(rule.rule_id)) {
// 원본 ID가 이미 존재 (동일한 ID로 매핑)
ruleIdMap.set(rule.rule_id, rule.rule_id);
const newMenuObjid = menuIdMap.get(rule.menu_objid);
if (newMenuObjid) {
rulesToUpdate.push({ ruleId: rule.rule_id, newMenuObjid });
}
logger.info(` ♻️ 채번규칙 이미 존재 (원본 ID): ${rule.rule_id}`);
} else if (existingRuleIds.has(newRuleId)) {
// 새로 생성될 ID가 이미 존재 (기존 규칙으로 매핑)
ruleIdMap.set(rule.rule_id, newRuleId);
const newMenuObjid = menuIdMap.get(rule.menu_objid);
if (newMenuObjid) {
rulesToUpdate.push({ ruleId: newRuleId, newMenuObjid });
}
logger.info(
` ♻️ 채번규칙 이미 존재 (대상 ID): ${rule.rule_id} -> ${newRuleId}`
);
} else {
// 새로 복사 필요
ruleIdMap.set(rule.rule_id, newRuleId);
originalToNewRuleMap.push({ original: rule.rule_id, new: newRuleId });
rulesToCopy.push({ ...rule, newRuleId });
logger.info(` 📋 채번규칙 복사 예정: ${rule.rule_id} -> ${newRuleId}`);
// 이름 기준으로 매핑 생성
for (const sourceRule of sourceRulesResult.rows) {
const targetRuleId = targetRulesByName.get(sourceRule.rule_name);
if (targetRuleId) {
ruleIdMap.set(sourceRule.rule_id, targetRuleId);
logger.info(` 🔗 채번규칙 매핑: ${sourceRule.rule_id} -> ${targetRuleId}`);
}
}
// 4. 배치 INSERT로 채번 규칙 복사
// menu 스코프인데 menu_objid 매핑이 없는 규칙은 제외 (연결 없이 복제하지 않음)
const validRulesToCopy = rulesToCopy.filter((r) => {
if (r.scope_type === "menu") {
const newMenuObjid = menuIdMap.get(r.menu_objid);
if (newMenuObjid === undefined) {
logger.info(` ⏭️ 채번규칙 "${r.rule_name}" 건너뜀: 메뉴 연결 없음 (원본 menu_objid: ${r.menu_objid})`);
// ruleIdMap에서도 제거
ruleIdMap.delete(r.rule_id);
return false; // 복제 대상에서 제외
}
}
return true;
});
if (validRulesToCopy.length > 0) {
const ruleValues = validRulesToCopy
.map(
(_, i) =>
`($${i * 13 + 1}, $${i * 13 + 2}, $${i * 13 + 3}, $${i * 13 + 4}, $${i * 13 + 5}, $${i * 13 + 6}, $${i * 13 + 7}, $${i * 13 + 8}, $${i * 13 + 9}, NOW(), $${i * 13 + 10}, $${i * 13 + 11}, $${i * 13 + 12}, $${i * 13 + 13})`
)
.join(", ");
const ruleParams = validRulesToCopy.flatMap((r) => {
const newMenuObjid = menuIdMap.get(r.menu_objid);
// menu 스코프인 경우 반드시 menu_objid가 있음 (위에서 필터링됨)
const finalMenuObjid = newMenuObjid !== undefined ? newMenuObjid : null;
// scope_type은 원본 유지 (menu 스코프는 반드시 menu_objid가 있으므로)
const finalScopeType = r.scope_type;
return [
r.newRuleId,
r.rule_name,
r.description,
r.separator,
r.reset_period,
0,
r.table_name,
r.column_name,
targetCompanyCode,
userId,
finalMenuObjid,
finalScopeType,
null,
];
});
await client.query(
`INSERT INTO numbering_rules (
rule_id, rule_name, description, separator, reset_period,
current_sequence, table_name, column_name, company_code,
created_at, created_by, menu_objid, scope_type, last_generated_date
) VALUES ${ruleValues}`,
ruleParams
);
copiedCount = validRulesToCopy.length;
logger.info(` ✅ 채번 규칙 ${copiedCount}개 복사 (${rulesToCopy.length - validRulesToCopy.length}개 건너뜀)`);
}
// 4-1. 기존 채번 규칙의 menu_objid 업데이트 (새 메뉴와 연결) - 배치 처리
if (rulesToUpdate.length > 0) {
// CASE WHEN을 사용한 배치 업데이트
// menu_objid는 numeric 타입이므로 ::numeric 캐스팅 필요
const caseWhen = rulesToUpdate
.map(
(_, i) => `WHEN rule_id = $${i * 2 + 1} THEN $${i * 2 + 2}::numeric`
)
.join(" ");
const ruleIdsForUpdate = rulesToUpdate.map((r) => r.ruleId);
const params = rulesToUpdate.flatMap((r) => [r.ruleId, r.newMenuObjid]);
await client.query(
`UPDATE numbering_rules
SET menu_objid = CASE ${caseWhen} END, updated_at = NOW()
WHERE rule_id = ANY($${params.length + 1}) AND company_code = $${params.length + 2}`,
[...params, ruleIdsForUpdate, targetCompanyCode]
);
logger.info(
` ✅ 기존 채번 규칙 ${rulesToUpdate.length}개 메뉴 연결 갱신`
);
}
// 5. 모든 원본 파트 한 번에 조회 (새로 복사한 규칙만 대상)
if (rulesToCopy.length > 0) {
const originalRuleIds = rulesToCopy.map((r) => r.rule_id);
const allPartsResult = await client.query(
`SELECT * FROM numbering_rule_parts
WHERE rule_id = ANY($1) ORDER BY rule_id, part_order`,
[originalRuleIds]
);
// 6. 배치 INSERT로 채번 규칙 파트 복사
if (allPartsResult.rows.length > 0) {
// 원본 rule_id -> 새 rule_id 매핑
const ruleMapping = new Map(
originalToNewRuleMap.map((m) => [m.original, m.new])
);
const partValues = allPartsResult.rows
.map(
(_, i) =>
`($${i * 7 + 1}, $${i * 7 + 2}, $${i * 7 + 3}, $${i * 7 + 4}, $${i * 7 + 5}, $${i * 7 + 6}, $${i * 7 + 7}, NOW())`
)
.join(", ");
const partParams = allPartsResult.rows.flatMap((p) => [
ruleMapping.get(p.rule_id),
p.part_order,
p.part_type,
p.generation_method,
p.auto_config,
p.manual_config,
targetCompanyCode,
]);
await client.query(
`INSERT INTO numbering_rule_parts (
rule_id, part_order, part_type, generation_method,
auto_config, manual_config, company_code, created_at
) VALUES ${partValues}`,
partParams
);
logger.info(` ✅ 채번 규칙 파트 ${allPartsResult.rows.length}개 복사`);
}
}
logger.info(
`✅ 채번 규칙 복사 완료: ${copiedCount}개, 매핑: ${ruleIdMap.size}`
);
return { copiedCount, ruleIdMap };
logger.info(` 📋 채번규칙 매핑 완료: ${ruleIdMap.size}`);
// 실제 복제는 프론트엔드에서 별도 API 호출로 처리됨
return { copiedCount: 0, ruleIdMap };
}
/**
* 카테고리 매핑 + 값 복사 (최적화: 배치 조회)
*