2025-11-21 14:37:09 +09:00
|
|
|
|
import { PoolClient } from "pg";
|
|
|
|
|
|
import { query, pool } from "../database/db";
|
|
|
|
|
|
import logger from "../utils/logger";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 메뉴 복사 결과
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface MenuCopyResult {
|
|
|
|
|
|
success: boolean;
|
|
|
|
|
|
copiedMenus: number;
|
|
|
|
|
|
copiedScreens: number;
|
|
|
|
|
|
copiedFlows: number;
|
|
|
|
|
|
copiedCategories: number;
|
|
|
|
|
|
copiedCodes: number;
|
2025-11-21 15:27:54 +09:00
|
|
|
|
copiedCategorySettings: number;
|
|
|
|
|
|
copiedNumberingRules: number;
|
2025-11-21 14:37:09 +09:00
|
|
|
|
menuIdMap: Record<number, number>;
|
|
|
|
|
|
screenIdMap: Record<number, number>;
|
|
|
|
|
|
flowIdMap: Record<number, number>;
|
|
|
|
|
|
warnings: string[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 메뉴 정보
|
|
|
|
|
|
*/
|
|
|
|
|
|
interface Menu {
|
|
|
|
|
|
objid: number;
|
|
|
|
|
|
menu_type: number | null;
|
|
|
|
|
|
parent_obj_id: number | null;
|
|
|
|
|
|
menu_name_kor: string | null;
|
|
|
|
|
|
menu_name_eng: string | null;
|
|
|
|
|
|
seq: number | null;
|
|
|
|
|
|
menu_url: string | null;
|
|
|
|
|
|
menu_desc: string | null;
|
|
|
|
|
|
writer: string | null;
|
|
|
|
|
|
regdate: Date | null;
|
|
|
|
|
|
status: string | null;
|
|
|
|
|
|
system_name: string | null;
|
|
|
|
|
|
company_code: string | null;
|
|
|
|
|
|
lang_key: string | null;
|
|
|
|
|
|
lang_key_desc: string | null;
|
|
|
|
|
|
screen_code: string | null;
|
|
|
|
|
|
menu_code: string | null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 화면 정의
|
|
|
|
|
|
*/
|
|
|
|
|
|
interface ScreenDefinition {
|
|
|
|
|
|
screen_id: number;
|
|
|
|
|
|
screen_name: string;
|
|
|
|
|
|
screen_code: string;
|
|
|
|
|
|
table_name: string;
|
|
|
|
|
|
company_code: string;
|
|
|
|
|
|
description: string | null;
|
|
|
|
|
|
is_active: string;
|
|
|
|
|
|
layout_metadata: any;
|
|
|
|
|
|
db_source_type: string | null;
|
|
|
|
|
|
db_connection_id: number | null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 화면 레이아웃
|
|
|
|
|
|
*/
|
|
|
|
|
|
interface ScreenLayout {
|
|
|
|
|
|
layout_id: number;
|
|
|
|
|
|
screen_id: number;
|
|
|
|
|
|
component_type: string;
|
|
|
|
|
|
component_id: string;
|
|
|
|
|
|
parent_id: string | null;
|
|
|
|
|
|
position_x: number;
|
|
|
|
|
|
position_y: number;
|
|
|
|
|
|
width: number;
|
|
|
|
|
|
height: number;
|
|
|
|
|
|
properties: any;
|
|
|
|
|
|
display_order: number;
|
|
|
|
|
|
layout_type: string | null;
|
|
|
|
|
|
layout_config: any;
|
|
|
|
|
|
zones_config: any;
|
|
|
|
|
|
zone_id: string | null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 플로우 정의
|
|
|
|
|
|
*/
|
|
|
|
|
|
interface FlowDefinition {
|
|
|
|
|
|
id: number;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
description: string | null;
|
|
|
|
|
|
table_name: string;
|
|
|
|
|
|
is_active: boolean;
|
|
|
|
|
|
company_code: string;
|
|
|
|
|
|
db_source_type: string | null;
|
|
|
|
|
|
db_connection_id: number | null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 플로우 스텝
|
|
|
|
|
|
*/
|
|
|
|
|
|
interface FlowStep {
|
|
|
|
|
|
id: number;
|
|
|
|
|
|
flow_definition_id: number;
|
|
|
|
|
|
step_name: string;
|
|
|
|
|
|
step_order: number;
|
|
|
|
|
|
condition_json: any;
|
|
|
|
|
|
color: string | null;
|
|
|
|
|
|
position_x: number | null;
|
|
|
|
|
|
position_y: number | null;
|
|
|
|
|
|
table_name: string | null;
|
|
|
|
|
|
move_type: string | null;
|
|
|
|
|
|
status_column: string | null;
|
|
|
|
|
|
status_value: string | null;
|
|
|
|
|
|
target_table: string | null;
|
|
|
|
|
|
field_mappings: any;
|
|
|
|
|
|
required_fields: any;
|
|
|
|
|
|
integration_type: string | null;
|
|
|
|
|
|
integration_config: any;
|
|
|
|
|
|
display_config: any;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 플로우 스텝 연결
|
|
|
|
|
|
*/
|
|
|
|
|
|
interface FlowStepConnection {
|
|
|
|
|
|
id: number;
|
|
|
|
|
|
flow_definition_id: number;
|
|
|
|
|
|
from_step_id: number;
|
|
|
|
|
|
to_step_id: number;
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 메뉴 복사 서비스
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class MenuCopyService {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 메뉴 트리 수집 (재귀)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async collectMenuTree(
|
|
|
|
|
|
rootMenuObjid: number,
|
|
|
|
|
|
client: PoolClient
|
|
|
|
|
|
): Promise<Menu[]> {
|
|
|
|
|
|
logger.info(`📂 메뉴 트리 수집 시작: rootMenuObjid=${rootMenuObjid}`);
|
|
|
|
|
|
|
|
|
|
|
|
const result: Menu[] = [];
|
|
|
|
|
|
const visited = new Set<number>();
|
|
|
|
|
|
const stack: number[] = [rootMenuObjid];
|
|
|
|
|
|
|
|
|
|
|
|
while (stack.length > 0) {
|
|
|
|
|
|
const currentObjid = stack.pop()!;
|
|
|
|
|
|
|
|
|
|
|
|
if (visited.has(currentObjid)) continue;
|
|
|
|
|
|
visited.add(currentObjid);
|
|
|
|
|
|
|
|
|
|
|
|
// 현재 메뉴 조회
|
|
|
|
|
|
const menuResult = await client.query<Menu>(
|
|
|
|
|
|
`SELECT * FROM menu_info WHERE objid = $1`,
|
|
|
|
|
|
[currentObjid]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (menuResult.rows.length === 0) {
|
|
|
|
|
|
logger.warn(`⚠️ 메뉴를 찾을 수 없음: objid=${currentObjid}`);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const menu = menuResult.rows[0];
|
|
|
|
|
|
result.push(menu);
|
|
|
|
|
|
|
|
|
|
|
|
// 자식 메뉴 조회
|
|
|
|
|
|
const childrenResult = await client.query<Menu>(
|
|
|
|
|
|
`SELECT * FROM menu_info WHERE parent_obj_id = $1 ORDER BY seq`,
|
|
|
|
|
|
[currentObjid]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
for (const child of childrenResult.rows) {
|
|
|
|
|
|
if (!visited.has(child.objid)) {
|
|
|
|
|
|
stack.push(child.objid);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`✅ 메뉴 트리 수집 완료: ${result.length}개`);
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 화면 레이아웃에서 참조 화면 추출
|
|
|
|
|
|
*/
|
|
|
|
|
|
private extractReferencedScreens(layouts: ScreenLayout[]): number[] {
|
|
|
|
|
|
const referenced: number[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const layout of layouts) {
|
|
|
|
|
|
const props = layout.properties;
|
|
|
|
|
|
|
|
|
|
|
|
if (!props) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// 1) 모달 버튼 (숫자 또는 문자열)
|
|
|
|
|
|
if (props?.componentConfig?.action?.targetScreenId) {
|
|
|
|
|
|
const targetId = props.componentConfig.action.targetScreenId;
|
|
|
|
|
|
const numId =
|
|
|
|
|
|
typeof targetId === "number" ? targetId : parseInt(targetId);
|
|
|
|
|
|
if (!isNaN(numId)) {
|
|
|
|
|
|
referenced.push(numId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2) 조건부 컨테이너 (숫자 또는 문자열)
|
2025-11-21 15:17:38 +09:00
|
|
|
|
if (
|
|
|
|
|
|
props?.componentConfig?.sections &&
|
|
|
|
|
|
Array.isArray(props.componentConfig.sections)
|
|
|
|
|
|
) {
|
|
|
|
|
|
for (const section of props.componentConfig.sections) {
|
2025-11-21 14:37:09 +09:00
|
|
|
|
if (section.screenId) {
|
|
|
|
|
|
const screenId = section.screenId;
|
|
|
|
|
|
const numId =
|
|
|
|
|
|
typeof screenId === "number" ? screenId : parseInt(screenId);
|
|
|
|
|
|
if (!isNaN(numId)) {
|
|
|
|
|
|
referenced.push(numId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return referenced;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 화면 수집 (중복 제거, 재귀적 참조 추적)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async collectScreens(
|
|
|
|
|
|
menuObjids: number[],
|
|
|
|
|
|
sourceCompanyCode: string,
|
|
|
|
|
|
client: PoolClient
|
|
|
|
|
|
): Promise<Set<number>> {
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
`📄 화면 수집 시작: ${menuObjids.length}개 메뉴, company=${sourceCompanyCode}`
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const screenIds = new Set<number>();
|
|
|
|
|
|
const visited = new Set<number>();
|
|
|
|
|
|
|
|
|
|
|
|
// 1) 메뉴에 직접 할당된 화면
|
|
|
|
|
|
for (const menuObjid of menuObjids) {
|
|
|
|
|
|
const assignmentsResult = await client.query<{ screen_id: number }>(
|
|
|
|
|
|
`SELECT DISTINCT screen_id
|
|
|
|
|
|
FROM screen_menu_assignments
|
|
|
|
|
|
WHERE menu_objid = $1 AND company_code = $2`,
|
|
|
|
|
|
[menuObjid, sourceCompanyCode]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
for (const assignment of assignmentsResult.rows) {
|
|
|
|
|
|
screenIds.add(assignment.screen_id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`📌 직접 할당 화면: ${screenIds.size}개`);
|
|
|
|
|
|
|
|
|
|
|
|
// 2) 화면 내부에서 참조되는 화면 (재귀)
|
|
|
|
|
|
const queue = Array.from(screenIds);
|
|
|
|
|
|
|
|
|
|
|
|
while (queue.length > 0) {
|
|
|
|
|
|
const screenId = queue.shift()!;
|
|
|
|
|
|
|
|
|
|
|
|
if (visited.has(screenId)) continue;
|
|
|
|
|
|
visited.add(screenId);
|
|
|
|
|
|
|
|
|
|
|
|
// 화면 레이아웃 조회
|
|
|
|
|
|
const layoutsResult = await client.query<ScreenLayout>(
|
|
|
|
|
|
`SELECT * FROM screen_layouts WHERE screen_id = $1`,
|
|
|
|
|
|
[screenId]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 참조 화면 추출
|
|
|
|
|
|
const referencedScreens = this.extractReferencedScreens(
|
|
|
|
|
|
layoutsResult.rows
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (referencedScreens.length > 0) {
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
` 📎 화면 ${screenId}에서 참조 화면 발견: ${referencedScreens.join(", ")}`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const refId of referencedScreens) {
|
|
|
|
|
|
if (!screenIds.has(refId)) {
|
|
|
|
|
|
screenIds.add(refId);
|
|
|
|
|
|
queue.push(refId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`✅ 화면 수집 완료: ${screenIds.size}개 (참조 포함)`);
|
|
|
|
|
|
return screenIds;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 플로우 수집
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async collectFlows(
|
|
|
|
|
|
screenIds: Set<number>,
|
|
|
|
|
|
client: PoolClient
|
|
|
|
|
|
): Promise<Set<number>> {
|
|
|
|
|
|
logger.info(`🔄 플로우 수집 시작: ${screenIds.size}개 화면`);
|
|
|
|
|
|
|
|
|
|
|
|
const flowIds = new Set<number>();
|
|
|
|
|
|
|
|
|
|
|
|
for (const screenId of screenIds) {
|
|
|
|
|
|
const layoutsResult = await client.query<ScreenLayout>(
|
|
|
|
|
|
`SELECT properties FROM screen_layouts WHERE screen_id = $1`,
|
|
|
|
|
|
[screenId]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
for (const layout of layoutsResult.rows) {
|
|
|
|
|
|
const props = layout.properties;
|
|
|
|
|
|
|
|
|
|
|
|
// webTypeConfig.dataflowConfig.flowConfig.flowId
|
|
|
|
|
|
const flowId = props?.webTypeConfig?.dataflowConfig?.flowConfig?.flowId;
|
|
|
|
|
|
if (flowId) {
|
|
|
|
|
|
flowIds.add(flowId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`✅ 플로우 수집 완료: ${flowIds.size}개`);
|
|
|
|
|
|
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 };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-21 15:27:54 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 카테고리 설정 수집
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async collectCategorySettings(
|
|
|
|
|
|
menuObjids: number[],
|
|
|
|
|
|
sourceCompanyCode: string,
|
|
|
|
|
|
client: PoolClient
|
|
|
|
|
|
): Promise<{
|
|
|
|
|
|
columnMappings: any[];
|
|
|
|
|
|
categoryValues: any[];
|
|
|
|
|
|
}> {
|
|
|
|
|
|
logger.info(`📂 카테고리 설정 수집 시작: ${menuObjids.length}개 메뉴`);
|
|
|
|
|
|
|
|
|
|
|
|
const columnMappings: any[] = [];
|
|
|
|
|
|
const categoryValues: any[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const menuObjid of menuObjids) {
|
|
|
|
|
|
// 카테고리 컬럼 매핑
|
|
|
|
|
|
const mappingsResult = await client.query(
|
|
|
|
|
|
`SELECT * FROM category_column_mapping
|
|
|
|
|
|
WHERE menu_objid = $1 AND company_code = $2`,
|
|
|
|
|
|
[menuObjid, sourceCompanyCode]
|
|
|
|
|
|
);
|
|
|
|
|
|
columnMappings.push(...mappingsResult.rows);
|
|
|
|
|
|
|
|
|
|
|
|
// 테이블 컬럼 카테고리 값
|
|
|
|
|
|
const valuesResult = await client.query(
|
|
|
|
|
|
`SELECT * FROM table_column_category_values
|
|
|
|
|
|
WHERE menu_objid = $1 AND company_code = $2`,
|
|
|
|
|
|
[menuObjid, 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 };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-21 14:37:09 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 다음 메뉴 objid 생성
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async getNextMenuObjid(client: PoolClient): Promise<number> {
|
|
|
|
|
|
const result = await client.query<{ max_objid: string }>(
|
|
|
|
|
|
`SELECT COALESCE(MAX(objid), 0)::text as max_objid FROM menu_info`
|
|
|
|
|
|
);
|
|
|
|
|
|
return parseInt(result.rows[0].max_objid, 10) + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 고유 화면 코드 생성
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async generateUniqueScreenCode(
|
|
|
|
|
|
targetCompanyCode: string,
|
|
|
|
|
|
client: PoolClient
|
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
|
// {company_code}_{순번} 형식
|
|
|
|
|
|
const prefix = targetCompanyCode === "*" ? "*" : targetCompanyCode;
|
|
|
|
|
|
|
|
|
|
|
|
const result = await client.query<{ max_num: string }>(
|
|
|
|
|
|
`SELECT COALESCE(
|
|
|
|
|
|
MAX(
|
|
|
|
|
|
CASE
|
|
|
|
|
|
WHEN screen_code ~ '^${prefix}_[0-9]+$'
|
|
|
|
|
|
THEN CAST(SUBSTRING(screen_code FROM '${prefix}_([0-9]+)') AS INTEGER)
|
|
|
|
|
|
ELSE 0
|
|
|
|
|
|
END
|
|
|
|
|
|
), 0
|
|
|
|
|
|
)::text as max_num
|
|
|
|
|
|
FROM screen_definitions
|
|
|
|
|
|
WHERE company_code = $1`,
|
|
|
|
|
|
[targetCompanyCode]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const maxNum = parseInt(result.rows[0].max_num, 10);
|
|
|
|
|
|
const newNum = maxNum + 1;
|
|
|
|
|
|
return `${prefix}_${String(newNum).padStart(3, "0")}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* properties 내부 참조 업데이트
|
|
|
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
|
|
|
* properties 내부의 모든 screen_id, screenId, targetScreenId, flowId 재귀 업데이트
|
|
|
|
|
|
*/
|
|
|
|
|
|
private updateReferencesInProperties(
|
|
|
|
|
|
properties: any,
|
|
|
|
|
|
screenIdMap: Map<number, number>,
|
|
|
|
|
|
flowIdMap: Map<number, number>
|
|
|
|
|
|
): any {
|
|
|
|
|
|
if (!properties) return properties;
|
|
|
|
|
|
|
|
|
|
|
|
// 깊은 복사
|
|
|
|
|
|
const updated = JSON.parse(JSON.stringify(properties));
|
|
|
|
|
|
|
|
|
|
|
|
// 재귀적으로 객체/배열 탐색
|
|
|
|
|
|
this.recursiveUpdateReferences(updated, screenIdMap, flowIdMap);
|
|
|
|
|
|
|
|
|
|
|
|
return updated;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 재귀적으로 모든 ID 참조 업데이트
|
|
|
|
|
|
*/
|
|
|
|
|
|
private recursiveUpdateReferences(
|
|
|
|
|
|
obj: any,
|
|
|
|
|
|
screenIdMap: Map<number, number>,
|
|
|
|
|
|
flowIdMap: Map<number, number>,
|
|
|
|
|
|
path: string = ""
|
|
|
|
|
|
): void {
|
|
|
|
|
|
if (!obj || typeof obj !== "object") return;
|
|
|
|
|
|
|
|
|
|
|
|
// 배열인 경우
|
|
|
|
|
|
if (Array.isArray(obj)) {
|
|
|
|
|
|
obj.forEach((item, index) => {
|
|
|
|
|
|
this.recursiveUpdateReferences(
|
|
|
|
|
|
item,
|
|
|
|
|
|
screenIdMap,
|
|
|
|
|
|
flowIdMap,
|
|
|
|
|
|
`${path}[${index}]`
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 객체인 경우 - 키별로 처리
|
|
|
|
|
|
for (const key of Object.keys(obj)) {
|
|
|
|
|
|
const value = obj[key];
|
|
|
|
|
|
const currentPath = path ? `${path}.${key}` : key;
|
|
|
|
|
|
|
|
|
|
|
|
// screen_id, screenId, targetScreenId 매핑 (숫자 또는 숫자 문자열)
|
|
|
|
|
|
if (
|
|
|
|
|
|
key === "screen_id" ||
|
|
|
|
|
|
key === "screenId" ||
|
|
|
|
|
|
key === "targetScreenId"
|
|
|
|
|
|
) {
|
|
|
|
|
|
const numValue = typeof value === "number" ? value : parseInt(value);
|
|
|
|
|
|
if (!isNaN(numValue)) {
|
|
|
|
|
|
const newId = screenIdMap.get(numValue);
|
|
|
|
|
|
if (newId) {
|
|
|
|
|
|
obj[key] = typeof value === "number" ? newId : String(newId); // 원래 타입 유지
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
` 🔗 화면 참조 업데이트 (${currentPath}): ${value} → ${newId}`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// flowId 매핑 (숫자 또는 숫자 문자열)
|
|
|
|
|
|
if (key === "flowId") {
|
|
|
|
|
|
const numValue = typeof value === "number" ? value : parseInt(value);
|
|
|
|
|
|
if (!isNaN(numValue)) {
|
|
|
|
|
|
const newId = flowIdMap.get(numValue);
|
|
|
|
|
|
if (newId) {
|
|
|
|
|
|
obj[key] = typeof value === "number" ? newId : String(newId); // 원래 타입 유지
|
|
|
|
|
|
logger.debug(
|
|
|
|
|
|
` 🔗 플로우 참조 업데이트 (${currentPath}): ${value} → ${newId}`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 재귀 호출
|
|
|
|
|
|
if (typeof value === "object" && value !== null) {
|
|
|
|
|
|
this.recursiveUpdateReferences(
|
|
|
|
|
|
value,
|
|
|
|
|
|
screenIdMap,
|
|
|
|
|
|
flowIdMap,
|
|
|
|
|
|
currentPath
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 기존 복사본 삭제 (덮어쓰기를 위한 사전 정리)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 같은 원본 메뉴에서 복사된 메뉴 구조가 대상 회사에 이미 존재하면 삭제
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async deleteExistingCopy(
|
|
|
|
|
|
sourceMenuObjid: number,
|
|
|
|
|
|
targetCompanyCode: string,
|
|
|
|
|
|
client: PoolClient
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
logger.info("\n🗑️ [0단계] 기존 복사본 확인 및 삭제");
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 대상 회사에 같은 이름의 최상위 메뉴가 있는지 확인
|
|
|
|
|
|
const sourceMenuResult = await client.query<Menu>(
|
|
|
|
|
|
`SELECT menu_name_kor, menu_name_eng
|
|
|
|
|
|
FROM menu_info
|
|
|
|
|
|
WHERE objid = $1`,
|
|
|
|
|
|
[sourceMenuObjid]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (sourceMenuResult.rows.length === 0) {
|
|
|
|
|
|
logger.warn("⚠️ 원본 메뉴를 찾을 수 없습니다");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sourceMenu = sourceMenuResult.rows[0];
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 대상 회사에 같은 원본에서 복사된 메뉴 찾기 (source_menu_objid로 정확히 매칭)
|
|
|
|
|
|
const existingMenuResult = await client.query<{ objid: number }>(
|
|
|
|
|
|
`SELECT objid
|
|
|
|
|
|
FROM menu_info
|
|
|
|
|
|
WHERE source_menu_objid = $1
|
|
|
|
|
|
AND company_code = $2
|
|
|
|
|
|
AND (parent_obj_id = 0 OR parent_obj_id IS NULL)`,
|
|
|
|
|
|
[sourceMenuObjid, targetCompanyCode]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (existingMenuResult.rows.length === 0) {
|
|
|
|
|
|
logger.info("✅ 기존 복사본 없음 - 새로 생성됩니다");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const existingMenuObjid = existingMenuResult.rows[0].objid;
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
`🔍 기존 복사본 발견: ${sourceMenu.menu_name_kor} (원본: ${sourceMenuObjid}, 복사본: ${existingMenuObjid})`
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 기존 메뉴 트리 수집
|
|
|
|
|
|
const existingMenus = await this.collectMenuTree(existingMenuObjid, client);
|
|
|
|
|
|
const existingMenuIds = existingMenus.map((m) => m.objid);
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`📊 삭제 대상: 메뉴 ${existingMenus.length}개`);
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 관련 화면 ID 수집
|
|
|
|
|
|
const existingScreenIds = await client.query<{ screen_id: number }>(
|
|
|
|
|
|
`SELECT DISTINCT screen_id
|
|
|
|
|
|
FROM screen_menu_assignments
|
|
|
|
|
|
WHERE menu_objid = ANY($1) AND company_code = $2`,
|
|
|
|
|
|
[existingMenuIds, targetCompanyCode]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const screenIds = existingScreenIds.rows.map((r) => r.screen_id);
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 삭제 순서 (외래키 제약 고려)
|
|
|
|
|
|
|
|
|
|
|
|
// 5-1. 화면 레이아웃 삭제
|
|
|
|
|
|
if (screenIds.length > 0) {
|
|
|
|
|
|
await client.query(
|
|
|
|
|
|
`DELETE FROM screen_layouts WHERE screen_id = ANY($1)`,
|
|
|
|
|
|
[screenIds]
|
|
|
|
|
|
);
|
|
|
|
|
|
logger.info(` ✅ 화면 레이아웃 삭제 완료`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5-2. 화면-메뉴 할당 삭제
|
|
|
|
|
|
await client.query(
|
|
|
|
|
|
`DELETE FROM screen_menu_assignments
|
|
|
|
|
|
WHERE menu_objid = ANY($1) AND company_code = $2`,
|
|
|
|
|
|
[existingMenuIds, targetCompanyCode]
|
|
|
|
|
|
);
|
|
|
|
|
|
logger.info(` ✅ 화면-메뉴 할당 삭제 완료`);
|
|
|
|
|
|
|
|
|
|
|
|
// 5-3. 화면 정의 삭제
|
|
|
|
|
|
if (screenIds.length > 0) {
|
|
|
|
|
|
await client.query(
|
|
|
|
|
|
`DELETE FROM screen_definitions
|
|
|
|
|
|
WHERE screen_id = ANY($1) AND company_code = $2`,
|
|
|
|
|
|
[screenIds, targetCompanyCode]
|
|
|
|
|
|
);
|
|
|
|
|
|
logger.info(` ✅ 화면 정의 삭제 완료`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5-4. 메뉴 권한 삭제
|
|
|
|
|
|
await client.query(`DELETE FROM rel_menu_auth WHERE menu_objid = ANY($1)`, [
|
|
|
|
|
|
existingMenuIds,
|
|
|
|
|
|
]);
|
|
|
|
|
|
logger.info(` ✅ 메뉴 권한 삭제 완료`);
|
|
|
|
|
|
|
|
|
|
|
|
// 5-5. 메뉴 삭제 (역순: 하위 메뉴부터)
|
|
|
|
|
|
for (let i = existingMenus.length - 1; i >= 0; i--) {
|
|
|
|
|
|
await client.query(`DELETE FROM menu_info WHERE objid = $1`, [
|
|
|
|
|
|
existingMenus[i].objid,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
logger.info(` ✅ 메뉴 삭제 완료: ${existingMenus.length}개`);
|
|
|
|
|
|
|
|
|
|
|
|
logger.info("✅ 기존 복사본 삭제 완료 - 덮어쓰기 준비됨");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 메뉴 복사 (메인 함수)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async copyMenu(
|
|
|
|
|
|
sourceMenuObjid: number,
|
|
|
|
|
|
targetCompanyCode: string,
|
2025-11-21 15:38:59 +09:00
|
|
|
|
userId: string,
|
|
|
|
|
|
screenNameConfig?: {
|
|
|
|
|
|
removeText?: string;
|
|
|
|
|
|
addPrefix?: string;
|
|
|
|
|
|
}
|
2025-11-21 14:37:09 +09:00
|
|
|
|
): Promise<MenuCopyResult> {
|
|
|
|
|
|
logger.info(`
|
|
|
|
|
|
🚀 ============================================
|
|
|
|
|
|
메뉴 복사 시작
|
|
|
|
|
|
원본 메뉴: ${sourceMenuObjid}
|
|
|
|
|
|
대상 회사: ${targetCompanyCode}
|
|
|
|
|
|
사용자: ${userId}
|
|
|
|
|
|
============================================
|
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
|
|
const warnings: string[] = [];
|
|
|
|
|
|
const client = await pool.connect();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 트랜잭션 시작
|
|
|
|
|
|
await client.query("BEGIN");
|
|
|
|
|
|
logger.info("📦 트랜잭션 시작");
|
|
|
|
|
|
|
|
|
|
|
|
// === 0단계: 기존 복사본 삭제 (덮어쓰기) ===
|
|
|
|
|
|
await this.deleteExistingCopy(sourceMenuObjid, targetCompanyCode, client);
|
|
|
|
|
|
|
|
|
|
|
|
// === 1단계: 수집 (Collection Phase) ===
|
|
|
|
|
|
logger.info("\n📂 [1단계] 데이터 수집");
|
|
|
|
|
|
|
|
|
|
|
|
const menus = await this.collectMenuTree(sourceMenuObjid, client);
|
|
|
|
|
|
const sourceCompanyCode = menus[0].company_code!;
|
|
|
|
|
|
|
|
|
|
|
|
const screenIds = await this.collectScreens(
|
|
|
|
|
|
menus.map((m) => m.objid),
|
|
|
|
|
|
sourceCompanyCode,
|
|
|
|
|
|
client
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const flowIds = await this.collectFlows(screenIds, client);
|
|
|
|
|
|
|
|
|
|
|
|
const codes = await this.collectCodes(
|
|
|
|
|
|
menus.map((m) => m.objid),
|
|
|
|
|
|
sourceCompanyCode,
|
|
|
|
|
|
client
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-11-21 15:27:54 +09:00
|
|
|
|
const categorySettings = await this.collectCategorySettings(
|
|
|
|
|
|
menus.map((m) => m.objid),
|
|
|
|
|
|
sourceCompanyCode,
|
|
|
|
|
|
client
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const numberingRules = await this.collectNumberingRules(
|
|
|
|
|
|
menus.map((m) => m.objid),
|
|
|
|
|
|
sourceCompanyCode,
|
|
|
|
|
|
client
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-11-21 14:37:09 +09:00
|
|
|
|
logger.info(`
|
|
|
|
|
|
📊 수집 완료:
|
|
|
|
|
|
- 메뉴: ${menus.length}개
|
|
|
|
|
|
- 화면: ${screenIds.size}개
|
|
|
|
|
|
- 플로우: ${flowIds.size}개
|
|
|
|
|
|
- 코드 카테고리: ${codes.categories.length}개
|
|
|
|
|
|
- 코드: ${codes.codes.length}개
|
2025-11-21 15:27:54 +09:00
|
|
|
|
- 카테고리 설정: 컬럼 매핑 ${categorySettings.columnMappings.length}개, 카테고리 값 ${categorySettings.categoryValues.length}개
|
|
|
|
|
|
- 채번 규칙: 규칙 ${numberingRules.rules.length}개, 파트 ${numberingRules.parts.length}개
|
2025-11-21 14:37:09 +09:00
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
|
|
// === 2단계: 플로우 복사 ===
|
|
|
|
|
|
logger.info("\n🔄 [2단계] 플로우 복사");
|
|
|
|
|
|
const flowIdMap = await this.copyFlows(
|
|
|
|
|
|
flowIds,
|
|
|
|
|
|
targetCompanyCode,
|
|
|
|
|
|
userId,
|
|
|
|
|
|
client
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// === 3단계: 화면 복사 ===
|
|
|
|
|
|
logger.info("\n📄 [3단계] 화면 복사");
|
|
|
|
|
|
const screenIdMap = await this.copyScreens(
|
|
|
|
|
|
screenIds,
|
|
|
|
|
|
targetCompanyCode,
|
|
|
|
|
|
flowIdMap,
|
|
|
|
|
|
userId,
|
2025-11-21 15:38:59 +09:00
|
|
|
|
client,
|
|
|
|
|
|
screenNameConfig
|
2025-11-21 14:37:09 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// === 4단계: 메뉴 복사 ===
|
|
|
|
|
|
logger.info("\n📂 [4단계] 메뉴 복사");
|
|
|
|
|
|
const menuIdMap = await this.copyMenus(
|
|
|
|
|
|
menus,
|
2025-11-21 14:58:57 +09:00
|
|
|
|
sourceMenuObjid, // 원본 최상위 메뉴 ID 전달
|
2025-11-21 14:37:09 +09:00
|
|
|
|
targetCompanyCode,
|
|
|
|
|
|
screenIdMap,
|
|
|
|
|
|
userId,
|
|
|
|
|
|
client
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// === 5단계: 화면-메뉴 할당 ===
|
|
|
|
|
|
logger.info("\n🔗 [5단계] 화면-메뉴 할당");
|
|
|
|
|
|
await this.createScreenMenuAssignments(
|
|
|
|
|
|
menus,
|
|
|
|
|
|
menuIdMap,
|
|
|
|
|
|
screenIdMap,
|
|
|
|
|
|
targetCompanyCode,
|
|
|
|
|
|
client
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// === 6단계: 코드 복사 ===
|
|
|
|
|
|
logger.info("\n📋 [6단계] 코드 복사");
|
|
|
|
|
|
await this.copyCodes(codes, menuIdMap, targetCompanyCode, userId, client);
|
|
|
|
|
|
|
2025-11-21 15:27:54 +09:00
|
|
|
|
// === 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
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-11-21 14:37:09 +09:00
|
|
|
|
// 커밋
|
|
|
|
|
|
await client.query("COMMIT");
|
|
|
|
|
|
logger.info("✅ 트랜잭션 커밋 완료");
|
|
|
|
|
|
|
|
|
|
|
|
const result: MenuCopyResult = {
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
copiedMenus: menuIdMap.size,
|
|
|
|
|
|
copiedScreens: screenIdMap.size,
|
|
|
|
|
|
copiedFlows: flowIdMap.size,
|
|
|
|
|
|
copiedCategories: codes.categories.length,
|
|
|
|
|
|
copiedCodes: codes.codes.length,
|
2025-11-21 15:27:54 +09:00
|
|
|
|
copiedCategorySettings:
|
|
|
|
|
|
categorySettings.columnMappings.length +
|
|
|
|
|
|
categorySettings.categoryValues.length,
|
|
|
|
|
|
copiedNumberingRules:
|
|
|
|
|
|
numberingRules.rules.length + numberingRules.parts.length,
|
2025-11-21 14:37:09 +09:00
|
|
|
|
menuIdMap: Object.fromEntries(menuIdMap),
|
|
|
|
|
|
screenIdMap: Object.fromEntries(screenIdMap),
|
|
|
|
|
|
flowIdMap: Object.fromEntries(flowIdMap),
|
|
|
|
|
|
warnings,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`
|
|
|
|
|
|
🎉 ============================================
|
|
|
|
|
|
메뉴 복사 완료!
|
|
|
|
|
|
- 메뉴: ${result.copiedMenus}개
|
|
|
|
|
|
- 화면: ${result.copiedScreens}개
|
|
|
|
|
|
- 플로우: ${result.copiedFlows}개
|
|
|
|
|
|
- 코드 카테고리: ${result.copiedCategories}개
|
|
|
|
|
|
- 코드: ${result.copiedCodes}개
|
2025-11-21 15:27:54 +09:00
|
|
|
|
- 카테고리 설정: ${result.copiedCategorySettings}개
|
|
|
|
|
|
- 채번 규칙: ${result.copiedNumberingRules}개
|
2025-11-21 14:37:09 +09:00
|
|
|
|
============================================
|
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
// 롤백
|
|
|
|
|
|
await client.query("ROLLBACK");
|
|
|
|
|
|
logger.error("❌ 메뉴 복사 실패, 롤백됨:", error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
client.release();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 플로우 복사
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async copyFlows(
|
|
|
|
|
|
flowIds: Set<number>,
|
|
|
|
|
|
targetCompanyCode: string,
|
|
|
|
|
|
userId: string,
|
|
|
|
|
|
client: PoolClient
|
|
|
|
|
|
): Promise<Map<number, number>> {
|
|
|
|
|
|
const flowIdMap = new Map<number, number>();
|
|
|
|
|
|
|
|
|
|
|
|
if (flowIds.size === 0) {
|
|
|
|
|
|
logger.info("📭 복사할 플로우 없음");
|
|
|
|
|
|
return flowIdMap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`🔄 플로우 복사 중: ${flowIds.size}개`);
|
|
|
|
|
|
|
|
|
|
|
|
for (const originalFlowId of flowIds) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1) flow_definition 조회
|
|
|
|
|
|
const flowDefResult = await client.query<FlowDefinition>(
|
|
|
|
|
|
`SELECT * FROM flow_definition WHERE id = $1`,
|
|
|
|
|
|
[originalFlowId]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (flowDefResult.rows.length === 0) {
|
|
|
|
|
|
logger.warn(`⚠️ 플로우를 찾을 수 없음: id=${originalFlowId}`);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const flowDef = flowDefResult.rows[0];
|
|
|
|
|
|
|
|
|
|
|
|
// 2) flow_definition 복사
|
|
|
|
|
|
const newFlowResult = await client.query<{ id: number }>(
|
|
|
|
|
|
`INSERT INTO flow_definition (
|
|
|
|
|
|
name, description, table_name, is_active,
|
|
|
|
|
|
company_code, created_by, db_source_type, db_connection_id
|
|
|
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
|
|
|
|
RETURNING id`,
|
|
|
|
|
|
[
|
|
|
|
|
|
flowDef.name,
|
|
|
|
|
|
flowDef.description,
|
|
|
|
|
|
flowDef.table_name,
|
|
|
|
|
|
flowDef.is_active,
|
|
|
|
|
|
targetCompanyCode, // 새 회사 코드
|
|
|
|
|
|
userId,
|
|
|
|
|
|
flowDef.db_source_type,
|
|
|
|
|
|
flowDef.db_connection_id,
|
|
|
|
|
|
]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const newFlowId = newFlowResult.rows[0].id;
|
|
|
|
|
|
flowIdMap.set(originalFlowId, newFlowId);
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
` ✅ 플로우 복사: ${originalFlowId} → ${newFlowId} (${flowDef.name})`
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 3) flow_step 복사
|
|
|
|
|
|
const stepsResult = await client.query<FlowStep>(
|
|
|
|
|
|
`SELECT * FROM flow_step WHERE flow_definition_id = $1 ORDER BY step_order`,
|
|
|
|
|
|
[originalFlowId]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const stepIdMap = new Map<number, number>();
|
|
|
|
|
|
|
|
|
|
|
|
for (const step of stepsResult.rows) {
|
|
|
|
|
|
const newStepResult = await client.query<{ id: number }>(
|
|
|
|
|
|
`INSERT INTO flow_step (
|
|
|
|
|
|
flow_definition_id, step_name, step_order, condition_json,
|
|
|
|
|
|
color, position_x, position_y, table_name, move_type,
|
|
|
|
|
|
status_column, status_value, target_table, field_mappings,
|
|
|
|
|
|
required_fields, integration_type, integration_config, display_config
|
|
|
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
|
|
|
|
|
RETURNING id`,
|
|
|
|
|
|
[
|
|
|
|
|
|
newFlowId, // 새 플로우 ID
|
|
|
|
|
|
step.step_name,
|
|
|
|
|
|
step.step_order,
|
|
|
|
|
|
step.condition_json,
|
|
|
|
|
|
step.color,
|
|
|
|
|
|
step.position_x,
|
|
|
|
|
|
step.position_y,
|
|
|
|
|
|
step.table_name,
|
|
|
|
|
|
step.move_type,
|
|
|
|
|
|
step.status_column,
|
|
|
|
|
|
step.status_value,
|
|
|
|
|
|
step.target_table,
|
|
|
|
|
|
step.field_mappings,
|
|
|
|
|
|
step.required_fields,
|
|
|
|
|
|
step.integration_type,
|
|
|
|
|
|
step.integration_config,
|
|
|
|
|
|
step.display_config,
|
|
|
|
|
|
]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const newStepId = newStepResult.rows[0].id;
|
|
|
|
|
|
stepIdMap.set(step.id, newStepId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(` ↳ 스텝 복사: ${stepIdMap.size}개`);
|
|
|
|
|
|
|
|
|
|
|
|
// 4) flow_step_connection 복사 (스텝 ID 재매핑)
|
|
|
|
|
|
const connectionsResult = await client.query<FlowStepConnection>(
|
|
|
|
|
|
`SELECT * FROM flow_step_connection WHERE flow_definition_id = $1`,
|
|
|
|
|
|
[originalFlowId]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
for (const conn of connectionsResult.rows) {
|
|
|
|
|
|
const newFromStepId = stepIdMap.get(conn.from_step_id);
|
|
|
|
|
|
const newToStepId = stepIdMap.get(conn.to_step_id);
|
|
|
|
|
|
|
|
|
|
|
|
if (!newFromStepId || !newToStepId) {
|
|
|
|
|
|
logger.warn(
|
|
|
|
|
|
`⚠️ 스텝 ID 매핑 실패: ${conn.from_step_id} → ${conn.to_step_id}`
|
|
|
|
|
|
);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await client.query(
|
|
|
|
|
|
`INSERT INTO flow_step_connection (
|
|
|
|
|
|
flow_definition_id, from_step_id, to_step_id, label
|
|
|
|
|
|
) VALUES ($1, $2, $3, $4)`,
|
|
|
|
|
|
[newFlowId, newFromStepId, newToStepId, conn.label]
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(` ↳ 연결 복사: ${connectionsResult.rows.length}개`);
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
logger.error(`❌ 플로우 복사 실패: id=${originalFlowId}`, error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`✅ 플로우 복사 완료: ${flowIdMap.size}개`);
|
|
|
|
|
|
return flowIdMap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 화면 복사
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async copyScreens(
|
|
|
|
|
|
screenIds: Set<number>,
|
|
|
|
|
|
targetCompanyCode: string,
|
|
|
|
|
|
flowIdMap: Map<number, number>,
|
|
|
|
|
|
userId: string,
|
2025-11-21 15:38:59 +09:00
|
|
|
|
client: PoolClient,
|
|
|
|
|
|
screenNameConfig?: {
|
|
|
|
|
|
removeText?: string;
|
|
|
|
|
|
addPrefix?: string;
|
|
|
|
|
|
}
|
2025-11-21 14:37:09 +09:00
|
|
|
|
): Promise<Map<number, number>> {
|
|
|
|
|
|
const screenIdMap = new Map<number, number>();
|
|
|
|
|
|
|
|
|
|
|
|
if (screenIds.size === 0) {
|
|
|
|
|
|
logger.info("📭 복사할 화면 없음");
|
|
|
|
|
|
return screenIdMap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`📄 화면 복사 중: ${screenIds.size}개`);
|
|
|
|
|
|
|
|
|
|
|
|
// === 1단계: 모든 screen_definitions 먼저 복사 (screenIdMap 생성) ===
|
|
|
|
|
|
const screenDefsToProcess: Array<{
|
|
|
|
|
|
originalScreenId: number;
|
|
|
|
|
|
newScreenId: number;
|
|
|
|
|
|
screenDef: ScreenDefinition;
|
|
|
|
|
|
}> = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const originalScreenId of screenIds) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1) screen_definitions 조회
|
|
|
|
|
|
const screenDefResult = await client.query<ScreenDefinition>(
|
|
|
|
|
|
`SELECT * FROM screen_definitions WHERE screen_id = $1`,
|
|
|
|
|
|
[originalScreenId]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (screenDefResult.rows.length === 0) {
|
|
|
|
|
|
logger.warn(`⚠️ 화면을 찾을 수 없음: screen_id=${originalScreenId}`);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const screenDef = screenDefResult.rows[0];
|
|
|
|
|
|
|
|
|
|
|
|
// 2) 새 screen_code 생성
|
|
|
|
|
|
const newScreenCode = await this.generateUniqueScreenCode(
|
|
|
|
|
|
targetCompanyCode,
|
|
|
|
|
|
client
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-11-21 15:38:59 +09:00
|
|
|
|
// 2-1) 화면명 변환 적용
|
|
|
|
|
|
let transformedScreenName = screenDef.screen_name;
|
|
|
|
|
|
if (screenNameConfig) {
|
|
|
|
|
|
// 1. 제거할 텍스트 제거
|
|
|
|
|
|
if (screenNameConfig.removeText?.trim()) {
|
|
|
|
|
|
transformedScreenName = transformedScreenName.replace(
|
|
|
|
|
|
new RegExp(screenNameConfig.removeText.trim(), "g"),
|
|
|
|
|
|
""
|
|
|
|
|
|
);
|
|
|
|
|
|
transformedScreenName = transformedScreenName.trim(); // 앞뒤 공백 제거
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 접두사 추가
|
|
|
|
|
|
if (screenNameConfig.addPrefix?.trim()) {
|
|
|
|
|
|
transformedScreenName =
|
|
|
|
|
|
screenNameConfig.addPrefix.trim() + " " + transformedScreenName;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-21 14:37:09 +09:00
|
|
|
|
// 3) 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`,
|
|
|
|
|
|
[
|
2025-11-21 15:38:59 +09:00
|
|
|
|
transformedScreenName, // 변환된 화면명
|
2025-11-21 14:37:09 +09:00
|
|
|
|
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
|
|
|
|
|
|
]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const newScreenId = newScreenResult.rows[0].screen_id;
|
|
|
|
|
|
screenIdMap.set(originalScreenId, newScreenId);
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
` ✅ 화면 정의 복사: ${originalScreenId} → ${newScreenId} (${screenDef.screen_name})`
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 저장해서 2단계에서 처리
|
|
|
|
|
|
screenDefsToProcess.push({ originalScreenId, newScreenId, screenDef });
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
logger.error(
|
|
|
|
|
|
`❌ 화면 정의 복사 실패: screen_id=${originalScreenId}`,
|
|
|
|
|
|
error
|
|
|
|
|
|
);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === 2단계: screen_layouts 복사 (이제 screenIdMap이 완성됨) ===
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
`\n📐 레이아웃 복사 시작 (screenIdMap 완성: ${screenIdMap.size}개)`
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
for (const {
|
|
|
|
|
|
originalScreenId,
|
|
|
|
|
|
newScreenId,
|
|
|
|
|
|
screenDef,
|
|
|
|
|
|
} 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)
|
|
|
|
|
|
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;
|
|
|
|
|
|
const newZoneId = layout.zone_id
|
|
|
|
|
|
? componentIdMap.get(layout.zone_id) || layout.zone_id
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
// properties 내부 참조 업데이트
|
|
|
|
|
|
const updatedProperties = this.updateReferencesInProperties(
|
|
|
|
|
|
layout.properties,
|
|
|
|
|
|
screenIdMap,
|
|
|
|
|
|
flowIdMap
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
await client.query(
|
|
|
|
|
|
`INSERT INTO screen_layouts (
|
|
|
|
|
|
screen_id, component_type, component_id, parent_id,
|
|
|
|
|
|
position_x, position_y, width, height, properties,
|
|
|
|
|
|
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
|
|
|
|
|
|
layout.component_type,
|
|
|
|
|
|
newComponentId, // 새 컴포넌트 ID
|
|
|
|
|
|
newParentId, // 매핑된 parent_id
|
|
|
|
|
|
layout.position_x,
|
|
|
|
|
|
layout.position_y,
|
|
|
|
|
|
layout.width,
|
|
|
|
|
|
layout.height,
|
|
|
|
|
|
updatedProperties, // 업데이트된 속성
|
|
|
|
|
|
layout.display_order,
|
|
|
|
|
|
layout.layout_type,
|
|
|
|
|
|
layout.layout_config,
|
|
|
|
|
|
layout.zones_config,
|
|
|
|
|
|
newZoneId, // 매핑된 zone_id
|
|
|
|
|
|
]
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(` ↳ 레이아웃 복사: ${layoutsResult.rows.length}개`);
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
logger.error(
|
|
|
|
|
|
`❌ 레이아웃 복사 실패: screen_id=${originalScreenId}`,
|
|
|
|
|
|
error
|
|
|
|
|
|
);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`\n✅ 화면 복사 완료: ${screenIdMap.size}개`);
|
|
|
|
|
|
return screenIdMap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 메뉴 위상 정렬 (부모 먼저)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private topologicalSortMenus(menus: Menu[]): Menu[] {
|
|
|
|
|
|
const result: Menu[] = [];
|
|
|
|
|
|
const visited = new Set<number>();
|
|
|
|
|
|
const menuMap = new Map<number, Menu>();
|
|
|
|
|
|
|
|
|
|
|
|
for (const menu of menus) {
|
|
|
|
|
|
menuMap.set(menu.objid, menu);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const visit = (menu: Menu) => {
|
|
|
|
|
|
if (visited.has(menu.objid)) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 부모 먼저 방문
|
|
|
|
|
|
if (menu.parent_obj_id) {
|
|
|
|
|
|
const parent = menuMap.get(menu.parent_obj_id);
|
|
|
|
|
|
if (parent) {
|
|
|
|
|
|
visit(parent);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
visited.add(menu.objid);
|
|
|
|
|
|
result.push(menu);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
for (const menu of menus) {
|
|
|
|
|
|
visit(menu);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* screen_code 재매핑
|
|
|
|
|
|
*/
|
|
|
|
|
|
private getNewScreenCode(
|
|
|
|
|
|
screenIdMap: Map<number, number>,
|
|
|
|
|
|
screenCode: string | null,
|
|
|
|
|
|
client: PoolClient
|
|
|
|
|
|
): string | null {
|
|
|
|
|
|
if (!screenCode) return null;
|
|
|
|
|
|
|
|
|
|
|
|
// screen_code로 screen_id 조회 (원본 회사)
|
|
|
|
|
|
// 간단하게 처리: 새 화면 코드는 이미 생성됨
|
|
|
|
|
|
return screenCode;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 메뉴 복사
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async copyMenus(
|
|
|
|
|
|
menus: Menu[],
|
2025-11-21 14:58:57 +09:00
|
|
|
|
rootMenuObjid: number,
|
2025-11-21 14:37:09 +09:00
|
|
|
|
targetCompanyCode: string,
|
|
|
|
|
|
screenIdMap: Map<number, number>,
|
|
|
|
|
|
userId: string,
|
|
|
|
|
|
client: PoolClient
|
|
|
|
|
|
): Promise<Map<number, number>> {
|
|
|
|
|
|
const menuIdMap = new Map<number, number>();
|
|
|
|
|
|
|
|
|
|
|
|
if (menus.length === 0) {
|
|
|
|
|
|
logger.info("📭 복사할 메뉴 없음");
|
|
|
|
|
|
return menuIdMap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`📂 메뉴 복사 중: ${menus.length}개`);
|
|
|
|
|
|
|
|
|
|
|
|
// 위상 정렬 (부모 먼저 삽입)
|
|
|
|
|
|
const sortedMenus = this.topologicalSortMenus(menus);
|
|
|
|
|
|
|
|
|
|
|
|
for (const menu of sortedMenus) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 새 objid 생성
|
|
|
|
|
|
const newObjId = await this.getNextMenuObjid(client);
|
|
|
|
|
|
|
|
|
|
|
|
// parent_obj_id 재매핑
|
|
|
|
|
|
// NULL이나 0은 최상위 메뉴를 의미하므로 0으로 통일
|
|
|
|
|
|
let newParentObjId: number | null;
|
|
|
|
|
|
if (!menu.parent_obj_id || menu.parent_obj_id === 0) {
|
|
|
|
|
|
newParentObjId = 0; // 최상위 메뉴는 항상 0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newParentObjId =
|
|
|
|
|
|
menuIdMap.get(menu.parent_obj_id) || menu.parent_obj_id;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-21 14:58:57 +09:00
|
|
|
|
// source_menu_objid 저장: 원본 최상위 메뉴만 저장 (덮어쓰기 식별용)
|
|
|
|
|
|
// BigInt 타입이 문자열로 반환될 수 있으므로 문자열로 변환 후 비교
|
|
|
|
|
|
const isRootMenu = String(menu.objid) === String(rootMenuObjid);
|
|
|
|
|
|
const sourceMenuObjid = isRootMenu ? menu.objid : null;
|
2025-11-21 14:37:09 +09:00
|
|
|
|
|
|
|
|
|
|
if (sourceMenuObjid) {
|
|
|
|
|
|
logger.info(
|
2025-11-21 14:58:57 +09:00
|
|
|
|
` 📌 source_menu_objid 저장: ${sourceMenuObjid} (원본 최상위 메뉴)`
|
2025-11-21 14:37:09 +09:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// screen_code는 그대로 유지 (화면-메뉴 할당에서 처리)
|
|
|
|
|
|
await client.query(
|
|
|
|
|
|
`INSERT INTO menu_info (
|
|
|
|
|
|
objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng,
|
|
|
|
|
|
seq, menu_url, menu_desc, writer, status, system_name,
|
|
|
|
|
|
company_code, lang_key, lang_key_desc, screen_code, menu_code,
|
|
|
|
|
|
source_menu_objid
|
|
|
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)`,
|
|
|
|
|
|
[
|
|
|
|
|
|
newObjId,
|
|
|
|
|
|
menu.menu_type,
|
|
|
|
|
|
newParentObjId, // 재매핑
|
|
|
|
|
|
menu.menu_name_kor,
|
|
|
|
|
|
menu.menu_name_eng,
|
|
|
|
|
|
menu.seq,
|
|
|
|
|
|
menu.menu_url,
|
|
|
|
|
|
menu.menu_desc,
|
|
|
|
|
|
userId,
|
|
|
|
|
|
menu.status,
|
|
|
|
|
|
menu.system_name,
|
|
|
|
|
|
targetCompanyCode, // 새 회사 코드
|
|
|
|
|
|
menu.lang_key,
|
|
|
|
|
|
menu.lang_key_desc,
|
|
|
|
|
|
menu.screen_code, // 그대로 유지
|
|
|
|
|
|
menu.menu_code,
|
|
|
|
|
|
sourceMenuObjid, // 원본 메뉴 ID (최상위만)
|
|
|
|
|
|
]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
menuIdMap.set(menu.objid, newObjId);
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
` ✅ 메뉴 복사: ${menu.objid} → ${newObjId} (${menu.menu_name_kor})`
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
logger.error(`❌ 메뉴 복사 실패: objid=${menu.objid}`, error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`✅ 메뉴 복사 완료: ${menuIdMap.size}개`);
|
|
|
|
|
|
return menuIdMap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 화면-메뉴 할당
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async createScreenMenuAssignments(
|
|
|
|
|
|
menus: Menu[],
|
|
|
|
|
|
menuIdMap: Map<number, number>,
|
|
|
|
|
|
screenIdMap: Map<number, number>,
|
|
|
|
|
|
targetCompanyCode: string,
|
|
|
|
|
|
client: PoolClient
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
logger.info(`🔗 화면-메뉴 할당 중...`);
|
|
|
|
|
|
|
|
|
|
|
|
let assignmentCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (const menu of menus) {
|
|
|
|
|
|
const newMenuObjid = menuIdMap.get(menu.objid);
|
|
|
|
|
|
if (!newMenuObjid) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// 원본 메뉴에 할당된 화면 조회
|
|
|
|
|
|
const assignmentsResult = await client.query<{
|
|
|
|
|
|
screen_id: number;
|
|
|
|
|
|
display_order: number;
|
|
|
|
|
|
is_active: string;
|
|
|
|
|
|
}>(
|
|
|
|
|
|
`SELECT screen_id, display_order, is_active
|
|
|
|
|
|
FROM screen_menu_assignments
|
|
|
|
|
|
WHERE menu_objid = $1 AND company_code = $2`,
|
|
|
|
|
|
[menu.objid, menu.company_code]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
for (const assignment of assignmentsResult.rows) {
|
|
|
|
|
|
const newScreenId = screenIdMap.get(assignment.screen_id);
|
|
|
|
|
|
if (!newScreenId) {
|
|
|
|
|
|
logger.warn(
|
|
|
|
|
|
`⚠️ 화면 ID 매핑 없음: screen_id=${assignment.screen_id}`
|
|
|
|
|
|
);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 새 할당 생성
|
|
|
|
|
|
await client.query(
|
|
|
|
|
|
`INSERT INTO screen_menu_assignments (
|
|
|
|
|
|
screen_id, menu_objid, company_code, display_order, is_active, created_by
|
|
|
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
|
|
|
|
[
|
|
|
|
|
|
newScreenId, // 재매핑
|
|
|
|
|
|
newMenuObjid, // 재매핑
|
|
|
|
|
|
targetCompanyCode,
|
|
|
|
|
|
assignment.display_order,
|
|
|
|
|
|
assignment.is_active,
|
|
|
|
|
|
"system",
|
|
|
|
|
|
]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assignmentCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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}개 스킵)`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-11-21 15:27:54 +09:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 카테고리 설정 복사
|
|
|
|
|
|
*/
|
|
|
|
|
|
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) {
|
|
|
|
|
|
const newMenuObjid = menuIdMap.get(mapping.menu_objid);
|
|
|
|
|
|
if (!newMenuObjid) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// 중복 체크
|
|
|
|
|
|
const existsResult = await client.query(
|
|
|
|
|
|
`SELECT mapping_id 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]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (existsResult.rows.length > 0) {
|
|
|
|
|
|
logger.debug(
|
|
|
|
|
|
` ⏭️ 카테고리 매핑 이미 존재: ${mapping.table_name}.${mapping.physical_column_name}`
|
|
|
|
|
|
);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
);
|
|
|
|
|
|
let skippedValues = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (const value of sortedValues) {
|
|
|
|
|
|
const newMenuObjid = menuIdMap.get(value.menu_objid);
|
|
|
|
|
|
if (!newMenuObjid) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// 중복 체크
|
|
|
|
|
|
const existsResult = await client.query(
|
|
|
|
|
|
`SELECT value_id FROM table_column_category_values
|
|
|
|
|
|
WHERE table_name = $1 AND column_name = $2 AND value_code = $3 AND company_code = $4`,
|
2025-11-21 15:38:59 +09:00
|
|
|
|
[
|
|
|
|
|
|
value.table_name,
|
|
|
|
|
|
value.column_name,
|
|
|
|
|
|
value.value_code,
|
|
|
|
|
|
targetCompanyCode,
|
|
|
|
|
|
]
|
2025-11-21 15:27:54 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (existsResult.rows.length > 0) {
|
|
|
|
|
|
skippedValues++;
|
|
|
|
|
|
logger.debug(
|
|
|
|
|
|
` ⏭️ 카테고리 값 이미 존재: ${value.table_name}.${value.column_name}.${value.value_code}`
|
|
|
|
|
|
);
|
|
|
|
|
|
// 기존 값의 ID를 매핑에 저장 (자식 항목의 parent_id 재매핑용)
|
|
|
|
|
|
valueIdMap.set(value.value_id, existsResult.rows[0].value_id);
|
|
|
|
|
|
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}개 (${skippedValues}개 스킵)`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 채번 규칙 복사
|
|
|
|
|
|
*/
|
|
|
|
|
|
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}개`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-11-21 14:37:09 +09:00
|
|
|
|
}
|