메뉴복사 기능수정(카테고리,코드값 제거)

This commit is contained in:
kjs 2025-11-27 14:53:51 +09:00
parent 51c49f7a3d
commit 30dac204c0
3 changed files with 44 additions and 640 deletions

View File

@ -10,10 +10,6 @@ export interface MenuCopyResult {
copiedMenus: number;
copiedScreens: number;
copiedFlows: number;
copiedCategories: number;
copiedCodes: number;
copiedCategorySettings: number;
copiedNumberingRules: number;
menuIdMap: Record<number, number>;
screenIdMap: Record<number, number>;
flowIdMap: Record<number, number>;
@ -129,35 +125,6 @@ interface FlowStepConnection {
label: string | null;
}
/**
*
*/
interface CodeCategory {
category_code: string;
category_name: string;
category_name_eng: string | null;
description: string | null;
sort_order: number | null;
is_active: string;
company_code: string;
menu_objid: number;
}
/**
*
*/
interface CodeInfo {
code_category: string;
code_value: string;
code_name: string;
code_name_eng: string | null;
description: string | null;
sort_order: number | null;
is_active: string;
company_code: string;
menu_objid: number;
}
/**
*
*/
@ -249,6 +216,24 @@ export class MenuCopyService {
}
}
}
// 3) 탭 컴포넌트 (tabs 배열 내부의 screenId)
if (
props?.componentConfig?.tabs &&
Array.isArray(props.componentConfig.tabs)
) {
for (const tab of props.componentConfig.tabs) {
if (tab.screenId) {
const screenId = tab.screenId;
const numId =
typeof screenId === "number" ? screenId : parseInt(screenId);
if (!isNaN(numId)) {
referenced.push(numId);
logger.debug(` 📑 탭 컴포넌트에서 화면 참조 발견: ${numId} (탭: ${tab.label || tab.id})`);
}
}
}
}
}
return referenced;
@ -355,127 +340,6 @@ export class MenuCopyService {
return flowIds;
}
/**
*
*/
private async collectCodes(
menuObjids: number[],
sourceCompanyCode: string,
client: PoolClient
): Promise<{ categories: CodeCategory[]; codes: CodeInfo[] }> {
logger.info(`📋 코드 수집 시작: ${menuObjids.length}개 메뉴`);
const categories: CodeCategory[] = [];
const codes: CodeInfo[] = [];
for (const menuObjid of menuObjids) {
// 코드 카테고리
const catsResult = await client.query<CodeCategory>(
`SELECT * FROM code_category
WHERE menu_objid = $1 AND company_code = $2`,
[menuObjid, sourceCompanyCode]
);
categories.push(...catsResult.rows);
// 각 카테고리의 코드 정보
for (const cat of catsResult.rows) {
const codesResult = await client.query<CodeInfo>(
`SELECT * FROM code_info
WHERE code_category = $1 AND menu_objid = $2 AND company_code = $3`,
[cat.category_code, menuObjid, sourceCompanyCode]
);
codes.push(...codesResult.rows);
}
}
logger.info(
`✅ 코드 수집 완료: 카테고리 ${categories.length}개, 코드 ${codes.length}`
);
return { categories, codes };
}
/**
*
*/
private async collectCategorySettings(
menuObjids: number[],
sourceCompanyCode: string,
client: PoolClient
): Promise<{
columnMappings: any[];
categoryValues: any[];
}> {
logger.info(`📂 카테고리 설정 수집 시작: ${menuObjids.length}개 메뉴`);
const columnMappings: any[] = [];
const categoryValues: any[] = [];
// 카테고리 컬럼 매핑 (메뉴별 + 공통)
const mappingsResult = await client.query(
`SELECT * FROM category_column_mapping
WHERE (menu_objid = ANY($1) OR menu_objid = 0)
AND company_code = $2`,
[menuObjids, sourceCompanyCode]
);
columnMappings.push(...mappingsResult.rows);
// 테이블 컬럼 카테고리 값 (메뉴별 + 공통)
const valuesResult = await client.query(
`SELECT * FROM table_column_category_values
WHERE (menu_objid = ANY($1) OR menu_objid = 0)
AND company_code = $2`,
[menuObjids, sourceCompanyCode]
);
categoryValues.push(...valuesResult.rows);
logger.info(
`✅ 카테고리 설정 수집 완료: 컬럼 매핑 ${columnMappings.length}개 (공통 포함), 카테고리 값 ${categoryValues.length}개 (공통 포함)`
);
return { columnMappings, categoryValues };
}
/**
*
*/
private async collectNumberingRules(
menuObjids: number[],
sourceCompanyCode: string,
client: PoolClient
): Promise<{
rules: any[];
parts: any[];
}> {
logger.info(`📋 채번 규칙 수집 시작: ${menuObjids.length}개 메뉴`);
const rules: any[] = [];
const parts: any[] = [];
for (const menuObjid of menuObjids) {
// 채번 규칙
const rulesResult = await client.query(
`SELECT * FROM numbering_rules
WHERE menu_objid = $1 AND company_code = $2`,
[menuObjid, sourceCompanyCode]
);
rules.push(...rulesResult.rows);
// 각 규칙의 파트
for (const rule of rulesResult.rows) {
const partsResult = await client.query(
`SELECT * FROM numbering_rule_parts
WHERE rule_id = $1 AND company_code = $2`,
[rule.rule_id, sourceCompanyCode]
);
parts.push(...partsResult.rows);
}
}
logger.info(
`✅ 채번 규칙 수집 완료: 규칙 ${rules.length}개, 파트 ${parts.length}`
);
return { rules, parts };
}
/**
* objid
*/
@ -709,42 +573,8 @@ export class MenuCopyService {
]);
logger.info(` ✅ 메뉴 권한 삭제 완료`);
// 5-5. 채번 규칙 파트 삭제
await client.query(
`DELETE FROM numbering_rule_parts
WHERE rule_id IN (
SELECT rule_id FROM numbering_rules
WHERE menu_objid = ANY($1) AND company_code = $2
)`,
[existingMenuIds, targetCompanyCode]
);
logger.info(` ✅ 채번 규칙 파트 삭제 완료`);
// 5-6. 채번 규칙 삭제
await client.query(
`DELETE FROM numbering_rules
WHERE menu_objid = ANY($1) AND company_code = $2`,
[existingMenuIds, targetCompanyCode]
);
logger.info(` ✅ 채번 규칙 삭제 완료`);
// 5-7. 테이블 컬럼 카테고리 값 삭제
await client.query(
`DELETE FROM table_column_category_values
WHERE menu_objid = ANY($1) AND company_code = $2`,
[existingMenuIds, targetCompanyCode]
);
logger.info(` ✅ 카테고리 값 삭제 완료`);
// 5-8. 카테고리 컬럼 매핑 삭제
await client.query(
`DELETE FROM category_column_mapping
WHERE menu_objid = ANY($1) AND company_code = $2`,
[existingMenuIds, targetCompanyCode]
);
logger.info(` ✅ 카테고리 매핑 삭제 완료`);
// 5-9. 메뉴 삭제 (역순: 하위 메뉴부터)
// 5-5. 메뉴 삭제 (역순: 하위 메뉴부터)
// 주의: 채번 규칙과 카테고리 설정은 회사마다 고유하므로 삭제하지 않음
for (let i = existingMenus.length - 1; i >= 0; i--) {
await client.query(`DELETE FROM menu_info WHERE objid = $1`, [
existingMenus[i].objid,
@ -801,33 +631,11 @@ export class MenuCopyService {
const flowIds = await this.collectFlows(screenIds, client);
const codes = await this.collectCodes(
menus.map((m) => m.objid),
sourceCompanyCode,
client
);
const categorySettings = await this.collectCategorySettings(
menus.map((m) => m.objid),
sourceCompanyCode,
client
);
const numberingRules = await this.collectNumberingRules(
menus.map((m) => m.objid),
sourceCompanyCode,
client
);
logger.info(`
📊 :
- 메뉴: ${menus.length}
- 화면: ${screenIds.size}
- 플로우: ${flowIds.size}
- 카테고리: ${codes.categories.length}
- 코드: ${codes.codes.length}
- 설정: 컬럼 ${categorySettings.columnMappings.length}, ${categorySettings.categoryValues.length}
- 규칙: 규칙 ${numberingRules.rules.length}, ${numberingRules.parts.length}
`);
// === 2단계: 플로우 복사 ===
@ -871,30 +679,6 @@ export class MenuCopyService {
client
);
// === 6단계: 코드 복사 ===
logger.info("\n📋 [6단계] 코드 복사");
await this.copyCodes(codes, menuIdMap, targetCompanyCode, userId, client);
// === 7단계: 카테고리 설정 복사 ===
logger.info("\n📂 [7단계] 카테고리 설정 복사");
await this.copyCategorySettings(
categorySettings,
menuIdMap,
targetCompanyCode,
userId,
client
);
// === 8단계: 채번 규칙 복사 ===
logger.info("\n📋 [8단계] 채번 규칙 복사");
await this.copyNumberingRules(
numberingRules,
menuIdMap,
targetCompanyCode,
userId,
client
);
// 커밋
await client.query("COMMIT");
logger.info("✅ 트랜잭션 커밋 완료");
@ -904,13 +688,6 @@ export class MenuCopyService {
copiedMenus: menuIdMap.size,
copiedScreens: screenIdMap.size,
copiedFlows: flowIdMap.size,
copiedCategories: codes.categories.length,
copiedCodes: codes.codes.length,
copiedCategorySettings:
categorySettings.columnMappings.length +
categorySettings.categoryValues.length,
copiedNumberingRules:
numberingRules.rules.length + numberingRules.parts.length,
menuIdMap: Object.fromEntries(menuIdMap),
screenIdMap: Object.fromEntries(screenIdMap),
flowIdMap: Object.fromEntries(flowIdMap),
@ -923,10 +700,8 @@ export class MenuCopyService {
- 메뉴: ${result.copiedMenus}
- 화면: ${result.copiedScreens}
- 플로우: ${result.copiedFlows}
- 카테고리: ${result.copiedCategories}
- 코드: ${result.copiedCodes}
- 설정: ${result.copiedCategorySettings}
- 규칙: ${result.copiedNumberingRules}
주의: 코드, , .
============================================
`);
@ -1125,13 +900,31 @@ export class MenuCopyService {
const screenDef = screenDefResult.rows[0];
// 2) 새 screen_code 생성
// 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
LIMIT 1`,
[screenDef.screen_code, 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
);
// 2-1) 화면명 변환 적용
// 4) 화면명 변환 적용
let transformedScreenName = screenDef.screen_name;
if (screenNameConfig) {
// 1. 제거할 텍스트 제거
@ -1150,7 +943,7 @@ export class MenuCopyService {
}
}
// 3) screen_definitions 복사 (deleted 필드는 NULL로 설정, 삭제된 화면도 활성화)
// 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,
@ -1479,383 +1272,4 @@ export class MenuCopyService {
logger.info(`✅ 화면-메뉴 할당 완료: ${assignmentCount}`);
}
/**
*
*/
private async checkCodeCategoryExists(
categoryCode: string,
companyCode: string,
menuObjid: number,
client: PoolClient
): Promise<boolean> {
const result = await client.query<{ exists: boolean }>(
`SELECT EXISTS(
SELECT 1 FROM code_category
WHERE category_code = $1 AND company_code = $2 AND menu_objid = $3
) as exists`,
[categoryCode, companyCode, menuObjid]
);
return result.rows[0].exists;
}
/**
*
*/
private async checkCodeInfoExists(
categoryCode: string,
codeValue: string,
companyCode: string,
menuObjid: number,
client: PoolClient
): Promise<boolean> {
const result = await client.query<{ exists: boolean }>(
`SELECT EXISTS(
SELECT 1 FROM code_info
WHERE code_category = $1 AND code_value = $2
AND company_code = $3 AND menu_objid = $4
) as exists`,
[categoryCode, codeValue, companyCode, menuObjid]
);
return result.rows[0].exists;
}
/**
*
*/
private async copyCodes(
codes: { categories: CodeCategory[]; codes: CodeInfo[] },
menuIdMap: Map<number, number>,
targetCompanyCode: string,
userId: string,
client: PoolClient
): Promise<void> {
logger.info(`📋 코드 복사 중...`);
let categoryCount = 0;
let codeCount = 0;
let skippedCategories = 0;
let skippedCodes = 0;
// 1) 코드 카테고리 복사 (중복 체크)
for (const category of codes.categories) {
const newMenuObjid = menuIdMap.get(category.menu_objid);
if (!newMenuObjid) continue;
// 중복 체크
const exists = await this.checkCodeCategoryExists(
category.category_code,
targetCompanyCode,
newMenuObjid,
client
);
if (exists) {
skippedCategories++;
logger.debug(
` ⏭️ 카테고리 이미 존재: ${category.category_code} (menu_objid=${newMenuObjid})`
);
continue;
}
// 카테고리 복사
await client.query(
`INSERT INTO code_category (
category_code, category_name, category_name_eng, description,
sort_order, is_active, company_code, menu_objid, created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
category.category_code,
category.category_name,
category.category_name_eng,
category.description,
category.sort_order,
category.is_active,
targetCompanyCode, // 새 회사 코드
newMenuObjid, // 재매핑
userId,
]
);
categoryCount++;
}
// 2) 코드 정보 복사 (중복 체크)
for (const code of codes.codes) {
const newMenuObjid = menuIdMap.get(code.menu_objid);
if (!newMenuObjid) continue;
// 중복 체크
const exists = await this.checkCodeInfoExists(
code.code_category,
code.code_value,
targetCompanyCode,
newMenuObjid,
client
);
if (exists) {
skippedCodes++;
logger.debug(
` ⏭️ 코드 이미 존재: ${code.code_category}.${code.code_value} (menu_objid=${newMenuObjid})`
);
continue;
}
// 코드 복사
await client.query(
`INSERT INTO code_info (
code_category, code_value, code_name, code_name_eng, description,
sort_order, is_active, company_code, menu_objid, created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
[
code.code_category,
code.code_value,
code.code_name,
code.code_name_eng,
code.description,
code.sort_order,
code.is_active,
targetCompanyCode, // 새 회사 코드
newMenuObjid, // 재매핑
userId,
]
);
codeCount++;
}
logger.info(
`✅ 코드 복사 완료: 카테고리 ${categoryCount}개 (${skippedCategories}개 스킵), 코드 ${codeCount}개 (${skippedCodes}개 스킵)`
);
}
/**
*
*/
private async copyCategorySettings(
settings: { columnMappings: any[]; categoryValues: any[] },
menuIdMap: Map<number, number>,
targetCompanyCode: string,
userId: string,
client: PoolClient
): Promise<void> {
logger.info(`📂 카테고리 설정 복사 중...`);
const valueIdMap = new Map<number, number>(); // 원본 value_id → 새 value_id
let mappingCount = 0;
let valueCount = 0;
// 1) 카테고리 컬럼 매핑 복사 (덮어쓰기 모드)
for (const mapping of settings.columnMappings) {
// menu_objid = 0인 공통 설정은 그대로 0으로 유지
let newMenuObjid: number | undefined;
if (
mapping.menu_objid === 0 ||
mapping.menu_objid === "0" ||
mapping.menu_objid == 0
) {
newMenuObjid = 0; // 공통 설정
} else {
newMenuObjid = menuIdMap.get(mapping.menu_objid);
if (newMenuObjid === undefined) {
logger.debug(
` ⏭️ 매핑할 메뉴가 없음: menu_objid=${mapping.menu_objid}`
);
continue;
}
}
// 기존 매핑 삭제 (덮어쓰기)
await client.query(
`DELETE FROM category_column_mapping
WHERE table_name = $1 AND physical_column_name = $2 AND company_code = $3`,
[mapping.table_name, mapping.physical_column_name, targetCompanyCode]
);
// 새 매핑 추가
await client.query(
`INSERT INTO category_column_mapping (
table_name, logical_column_name, physical_column_name,
menu_objid, company_code, description, created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[
mapping.table_name,
mapping.logical_column_name,
mapping.physical_column_name,
newMenuObjid,
targetCompanyCode,
mapping.description,
userId,
]
);
mappingCount++;
}
// 2) 테이블 컬럼 카테고리 값 복사 (덮어쓰기 모드, 부모-자식 관계 유지)
const sortedValues = settings.categoryValues.sort(
(a, b) => a.depth - b.depth
);
// 먼저 기존 값들을 모두 삭제 (테이블+컬럼 단위)
const uniqueTableColumns = new Set<string>();
for (const value of sortedValues) {
uniqueTableColumns.add(`${value.table_name}:${value.column_name}`);
}
for (const tableColumn of uniqueTableColumns) {
const [tableName, columnName] = tableColumn.split(":");
await client.query(
`DELETE FROM table_column_category_values
WHERE table_name = $1 AND column_name = $2 AND company_code = $3`,
[tableName, columnName, targetCompanyCode]
);
logger.debug(` 🗑️ 기존 카테고리 값 삭제: ${tableName}.${columnName}`);
}
// 새 값 추가
for (const value of sortedValues) {
// menu_objid = 0인 공통 설정은 그대로 0으로 유지
let newMenuObjid: number | undefined;
if (
value.menu_objid === 0 ||
value.menu_objid === "0" ||
value.menu_objid == 0
) {
newMenuObjid = 0; // 공통 설정
} else {
newMenuObjid = menuIdMap.get(value.menu_objid);
if (newMenuObjid === undefined) {
logger.debug(
` ⏭️ 매핑할 메뉴가 없음: menu_objid=${value.menu_objid}`
);
continue;
}
}
// 부모 ID 재매핑
let newParentValueId = null;
if (value.parent_value_id) {
newParentValueId = valueIdMap.get(value.parent_value_id) || null;
}
const result = await client.query(
`INSERT INTO table_column_category_values (
table_name, column_name, value_code, value_label,
value_order, parent_value_id, depth, description,
color, icon, is_active, is_default,
company_code, menu_objid, created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
RETURNING value_id`,
[
value.table_name,
value.column_name,
value.value_code,
value.value_label,
value.value_order,
newParentValueId,
value.depth,
value.description,
value.color,
value.icon,
value.is_active,
value.is_default,
targetCompanyCode,
newMenuObjid,
userId,
]
);
// ID 매핑 저장
const newValueId = result.rows[0].value_id;
valueIdMap.set(value.value_id, newValueId);
valueCount++;
}
logger.info(
`✅ 카테고리 설정 복사 완료: 컬럼 매핑 ${mappingCount}개, 카테고리 값 ${valueCount}개 (덮어쓰기)`
);
}
/**
*
*/
private async copyNumberingRules(
rules: { rules: any[]; parts: any[] },
menuIdMap: Map<number, number>,
targetCompanyCode: string,
userId: string,
client: PoolClient
): Promise<void> {
logger.info(`📋 채번 규칙 복사 중...`);
const ruleIdMap = new Map<string, string>(); // 원본 rule_id → 새 rule_id
let ruleCount = 0;
let partCount = 0;
// 1) 채번 규칙 복사
for (const rule of rules.rules) {
const newMenuObjid = menuIdMap.get(rule.menu_objid);
if (!newMenuObjid) continue;
// 새 rule_id 생성 (타임스탬프 기반)
const newRuleId = `rule-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
ruleIdMap.set(rule.rule_id, newRuleId);
await client.query(
`INSERT INTO numbering_rules (
rule_id, rule_name, description, separator,
reset_period, current_sequence, table_name, column_name,
company_code, menu_objid, created_by, scope_type
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
[
newRuleId,
rule.rule_name,
rule.description,
rule.separator,
rule.reset_period,
1, // 시퀀스 초기화
rule.table_name,
rule.column_name,
targetCompanyCode,
newMenuObjid,
userId,
rule.scope_type,
]
);
ruleCount++;
}
// 2) 채번 규칙 파트 복사
for (const part of rules.parts) {
const newRuleId = ruleIdMap.get(part.rule_id);
if (!newRuleId) continue;
await client.query(
`INSERT INTO numbering_rule_parts (
rule_id, part_order, part_type, generation_method,
auto_config, manual_config, company_code
) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[
newRuleId,
part.part_order,
part.part_type,
part.generation_method,
part.auto_config,
part.manual_config,
targetCompanyCode,
]
);
partCount++;
}
logger.info(
`✅ 채번 규칙 복사 완료: 규칙 ${ruleCount}개, 파트 ${partCount}`
);
}
}

View File

@ -294,18 +294,10 @@ export function MenuCopyDialog({
<span className="text-muted-foreground">:</span>{" "}
<span className="font-medium">{result.copiedScreens}</span>
</div>
<div>
<div className="col-span-2">
<span className="text-muted-foreground">:</span>{" "}
<span className="font-medium">{result.copiedFlows}</span>
</div>
<div>
<span className="text-muted-foreground"> :</span>{" "}
<span className="font-medium">{result.copiedCategories}</span>
</div>
<div className="col-span-2">
<span className="text-muted-foreground">:</span>{" "}
<span className="font-medium">{result.copiedCodes}</span>
</div>
</div>
</div>
)}

View File

@ -199,8 +199,6 @@ export interface MenuCopyResult {
copiedMenus: number;
copiedScreens: number;
copiedFlows: number;
copiedCategories: number;
copiedCodes: number;
menuIdMap: Record<number, number>;
screenIdMap: Record<number, number>;
flowIdMap: Record<number, number>;