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;
  copiedCodeCategories: number;
  copiedCodes: number;
  copiedNumberingRules: number;
  copiedCategoryMappings: number;
  copiedTableTypeColumns: number; // 테이블 타입관리 입력타입 설정
  copiedCascadingRelations: number; // 연쇄관계 설정
  copiedNodeFlows: number; // 노드 플로우 (제어관리)
  copiedDataflowDiagrams: number; // 데이터플로우 다이어그램 (버튼 제어)
  menuIdMap: Record<number, number>;
  screenIdMap: Record<number, number>;
  flowIdMap: Record<number, number>;
  warnings: string[];
}

/**
 * 추가 복사 옵션
 */
export interface AdditionalCopyOptions {
  copyCodeCategory?: boolean;
  copyNumberingRules?: boolean;
  copyCategoryMapping?: boolean;
  copyTableTypeColumns?: boolean; // 테이블 타입관리 입력타입 설정
  copyCascadingRelation?: boolean; // 연쇄관계 설정
}

/**
 * 메뉴 정보
 */
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;
  menu_icon: string | null;
  screen_group_id: number | 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;
  source_screen_id: number | null; // 원본 화면 ID (복사 추적용)
}

/**
 * 화면 레이아웃
 */
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;
}

/**
 * 메뉴 복사 서비스
 */
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) 조건부 컨테이너 (숫자 또는 문자열)
      if (
        props?.componentConfig?.sections &&
        Array.isArray(props.componentConfig.sections)
      ) {
        for (const section of props.componentConfig.sections) {
          if (section.screenId) {
            const screenId = section.screenId;
            const numId =
              typeof screenId === "number" ? screenId : parseInt(screenId);
            if (!isNaN(numId)) {
              referenced.push(numId);
            }
          }
        }
      }

      // 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})`
              );
            }
          }
        }
      }

      // 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}`);
        }
      }

      // 5) 모달 화면 ID (addModalScreenId, editModalScreenId, modalScreenId)
      if (props?.componentConfig?.addModalScreenId) {
        const addModalScreenId = props.componentConfig.addModalScreenId;
        const numId =
          typeof addModalScreenId === "number"
            ? addModalScreenId
            : parseInt(addModalScreenId);
        if (!isNaN(numId) && numId > 0) {
          referenced.push(numId);
          logger.debug(`    📋 추가 모달 화면 참조 발견: ${numId}`);
        }
      }

      if (props?.componentConfig?.editModalScreenId) {
        const editModalScreenId = props.componentConfig.editModalScreenId;
        const numId =
          typeof editModalScreenId === "number"
            ? editModalScreenId
            : parseInt(editModalScreenId);
        if (!isNaN(numId) && numId > 0) {
          referenced.push(numId);
          logger.debug(`    📝 수정 모달 화면 참조 발견: ${numId}`);
        }
      }

      if (props?.componentConfig?.modalScreenId) {
        const modalScreenId = props.componentConfig.modalScreenId;
        const numId =
          typeof modalScreenId === "number"
            ? modalScreenId
            : parseInt(modalScreenId);
        if (!isNaN(numId) && numId > 0) {
          referenced.push(numId);
          logger.debug(`    🔲 모달 화면 참조 발견: ${numId}`);
        }
      }

      // 6) 재귀적으로 모든 properties에서 화면 ID 추출 (깊은 탐색)
      this.extractScreenIdsFromObject(props, referenced);
    }

    return referenced;
  }

  /**
   * 객체 내부에서 화면 ID를 재귀적으로 추출
   */
  private extractScreenIdsFromObject(obj: any, referenced: number[]): void {
    if (!obj || typeof obj !== "object") return;

    if (Array.isArray(obj)) {
      for (const item of obj) {
        this.extractScreenIdsFromObject(item, referenced);
      }
      return;
    }

    for (const key of Object.keys(obj)) {
      const value = obj[key];

      // 화면 ID 키 패턴 확인
      if (
        key === "screenId" ||
        key === "targetScreenId" ||
        key === "leftScreenId" ||
        key === "rightScreenId" ||
        key === "addModalScreenId" ||
        key === "editModalScreenId" ||
        key === "modalScreenId"
      ) {
        const numId = typeof value === "number" ? value : parseInt(value);
        if (!isNaN(numId) && numId > 0 && !referenced.includes(numId)) {
          referenced.push(numId);
        }
      }

      // 재귀 탐색
      if (typeof value === "object" && value !== null) {
        this.extractScreenIdsFromObject(value, referenced);
      }
    }
  }

  /**
   * 화면 수집 (중복 제거, 재귀적 참조 추적)
   */
  private async collectScreens(
    menuObjids: number[],
    sourceCompanyCode: string,
    client: PoolClient,
    menus?: Menu[]
  ): Promise<Set<number>> {
    logger.info(
      `📄 화면 수집 시작: ${menuObjids.length}개 메뉴, company=${sourceCompanyCode}`
    );

    const screenIds = new Set<number>();
    const visited = new Set<number>();

    // 1) 메뉴에 직접 할당된 화면 - 배치 조회
    const assignmentsResult = await client.query<{ screen_id: number }>(
      `SELECT DISTINCT screen_id 
       FROM screen_menu_assignments 
       WHERE menu_objid = ANY($1) AND company_code = $2`,
      [menuObjids, sourceCompanyCode]
    );

    for (const assignment of assignmentsResult.rows) {
      screenIds.add(assignment.screen_id);
    }

    // 1.5) menu_url에서 참조되는 화면 수집 (/screens/{screenId} 패턴)
    if (menus) {
      const screenIdPattern = /\/screens\/(\d+)/;
      for (const menu of menus) {
        if (menu.menu_url) {
          const match = menu.menu_url.match(screenIdPattern);
          if (match) {
            const urlScreenId = parseInt(match[1], 10);
            if (!isNaN(urlScreenId) && urlScreenId > 0) {
              screenIds.add(urlScreenId);
            }
          }
        }
      }
    }

    logger.info(`📌 직접 할당 + menu_url 화면: ${screenIds.size}개`);

    // 2) 화면 내부에서 참조되는 화면 (재귀) - V1 + V2 레이아웃 모두 탐색
    const queue = Array.from(screenIds);

    while (queue.length > 0) {
      const screenId = queue.shift()!;

      if (visited.has(screenId)) continue;
      visited.add(screenId);

      const referencedScreens: number[] = [];

      // V1 레이아웃에서 참조 화면 추출
      const layoutsResult = await client.query<ScreenLayout>(
        `SELECT * FROM screen_layouts WHERE screen_id = $1`,
        [screenId]
      );
      referencedScreens.push(
        ...this.extractReferencedScreens(layoutsResult.rows)
      );

      // V2 레이아웃에서 참조 화면 추출
      const layoutsV2Result = await client.query<{ layout_data: any }>(
        `SELECT layout_data FROM screen_layouts_v2
         WHERE screen_id = $1 AND company_code = $2`,
        [screenId, sourceCompanyCode]
      );
      for (const row of layoutsV2Result.rows) {
        if (row.layout_data) {
          this.extractScreenIdsFromObject(row.layout_data, referencedScreens);
        }
      }

      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;
  }

  /**
   * 플로우 수집
   * - 화면 레이아웃에서 참조된 모든 flowId 수집
   * - dataflowConfig.flowConfig.flowId 및 selectedDiagramId 모두 수집
   */
  private async collectFlows(
    screenIds: Set<number>,
    client: PoolClient
  ): Promise<Set<number>> {
    logger.info(`🔄 플로우 수집 시작: ${screenIds.size}개 화면`);

    const flowIds = new Set<number>();
    const flowDetails: Array<{
      flowId: number;
      flowName: string;
      screenId: number;
    }> = [];

    // 배치 조회: 모든 화면의 레이아웃을 한 번에 조회
    const screenIdArray = Array.from(screenIds);
    if (screenIdArray.length === 0) {
      return flowIds;
    }

    const layoutsResult = await client.query<
      ScreenLayout & { screen_id: number }
    >(
      `SELECT screen_id, properties FROM screen_layouts WHERE screen_id = ANY($1)`,
      [screenIdArray]
    );

    for (const layout of layoutsResult.rows) {
      const props = layout.properties;
      const screenId = layout.screen_id;

      // webTypeConfig.dataflowConfig.flowConfig.flowId
      const flowId = props?.webTypeConfig?.dataflowConfig?.flowConfig?.flowId;
      const flowName =
        props?.webTypeConfig?.dataflowConfig?.flowConfig?.flowName || "Unknown";

      if (flowId && typeof flowId === "number" && flowId > 0) {
        if (!flowIds.has(flowId)) {
          flowIds.add(flowId);
          flowDetails.push({ flowId, flowName, screenId });
          logger.info(
            `    📎 화면 ${screenId}에서 플로우 발견: id=${flowId}, name="${flowName}"`
          );
        }
      }

      // selectedDiagramId도 확인 (flowId와 동일할 수 있지만 다를 수도 있음)
      const selectedDiagramId =
        props?.webTypeConfig?.dataflowConfig?.selectedDiagramId;
      if (
        selectedDiagramId &&
        typeof selectedDiagramId === "number" &&
        selectedDiagramId > 0
      ) {
        if (!flowIds.has(selectedDiagramId)) {
          flowIds.add(selectedDiagramId);
          flowDetails.push({
            flowId: selectedDiagramId,
            flowName: "SelectedDiagram",
            screenId,
          });
          logger.info(
            `    📎 화면 ${screenId}에서 selectedDiagramId 발견: id=${selectedDiagramId}`
          );
        }
      }
    }

    if (flowIds.size > 0) {
      logger.info(`✅ 플로우 수집 완료: ${flowIds.size}개`);
      logger.info(`   📋 수집된 flowIds: [${Array.from(flowIds).join(", ")}]`);
    } else {
      logger.info(`📭 수집된 플로우 없음 (화면에 플로우 참조가 없음)`);
    }

    return flowIds;
  }

  /**
   * 다음 메뉴 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, numberingRuleId 재귀 업데이트
   */
  private updateReferencesInProperties(
    properties: any,
    screenIdMap: Map<number, number>,
    flowIdMap: Map<number, number>,
    numberingRuleIdMap?: Map<string, string>,
    menuIdMap?: Map<number, number>
  ): any {
    if (!properties) return properties;

    // 깊은 복사
    const updated = JSON.parse(JSON.stringify(properties));

    // 재귀적으로 객체/배열 탐색
    this.recursiveUpdateReferences(
      updated,
      screenIdMap,
      flowIdMap,
      "",
      numberingRuleIdMap,
      menuIdMap
    );

    return updated;
  }

  /**
   * 재귀적으로 모든 ID 참조 업데이트
   */
  private recursiveUpdateReferences(
    obj: any,
    screenIdMap: Map<number, number>,
    flowIdMap: Map<number, number>,
    path: string = "",
    numberingRuleIdMap?: Map<string, string>,
    menuIdMap?: Map<number, number>
  ): void {
    if (!obj || typeof obj !== "object") return;

    // 배열인 경우
    if (Array.isArray(obj)) {
      obj.forEach((item, index) => {
        this.recursiveUpdateReferences(
          item,
          screenIdMap,
          flowIdMap,
          `${path}[${index}]`,
          numberingRuleIdMap,
          menuIdMap
        );
      });
      return;
    }

    // 객체인 경우 - 키별로 처리
    for (const key of Object.keys(obj)) {
      const value = obj[key];
      const currentPath = path ? `${path}.${key}` : key;

      // screen_id, screenId, targetScreenId, leftScreenId, rightScreenId, addModalScreenId, editModalScreenId, modalScreenId 매핑 (숫자 또는 숫자 문자열)
      if (
        key === "screen_id" ||
        key === "screenId" ||
        key === "targetScreenId" ||
        key === "leftScreenId" ||
        key === "rightScreenId" ||
        key === "addModalScreenId" ||
        key === "editModalScreenId" ||
        key === "modalScreenId"
      ) {
        const numValue = typeof value === "number" ? value : parseInt(value);
        if (!isNaN(numValue) && numValue > 0) {
          const newId = screenIdMap.get(numValue);
          if (newId) {
            obj[key] = typeof value === "number" ? newId : String(newId); // 원래 타입 유지
            logger.info(
              `    🔗 화면 참조 업데이트 (${currentPath}): ${value} → ${newId}`
            );
          } else {
            // 매핑이 없으면 경고 로그 (복사되지 않은 화면 참조)
            logger.warn(
              `    ⚠️ 화면 매핑 없음 (${currentPath}): ${value} - 원본 화면이 복사되지 않았을 수 있음`
            );
          }
        }
      }

      // flowId, selectedDiagramId 매핑 (숫자 또는 숫자 문자열)
      // selectedDiagramId는 dataflowConfig에서 flowId와 동일한 값을 참조하므로 함께 변환
      if (key === "flowId" || key === "selectedDiagramId") {
        const numValue = typeof value === "number" ? value : parseInt(value);
        if (!isNaN(numValue) && numValue > 0) {
          const newId = flowIdMap.get(numValue);
          if (newId) {
            obj[key] = typeof value === "number" ? newId : String(newId); // 원래 타입 유지
            logger.info(
              `    🔗 플로우 참조 업데이트 (${currentPath}): ${value} → ${newId}`
            );
          } else {
            // 매핑이 없으면 경고 로그
            logger.warn(
              `    ⚠️ 플로우 매핑 없음 (${currentPath}): ${value} - 원본 플로우가 복사되지 않았을 수 있음`
            );
          }
        }
      }

      // numberingRuleId, ruleId 매핑 (문자열) - 채번규칙 참조
      if (
        (key === "numberingRuleId" || key === "ruleId") &&
        numberingRuleIdMap &&
        typeof value === "string" &&
        value
      ) {
        const newRuleId = numberingRuleIdMap.get(value);
        if (newRuleId) {
          obj[key] = newRuleId;
          logger.info(
            `    🔗 채번규칙 참조 업데이트 (${currentPath}): ${value} → ${newRuleId}`
          );
        } else {
          // 매핑이 없는 채번규칙은 빈 값으로 설정 (다른 회사 채번규칙 참조 방지)
          logger.warn(
            `    ⚠️ 채번규칙 매핑 없음 (${currentPath}): ${value} → 빈 값으로 설정`
          );
          obj[key] = "";
        }
      }

      // selectedMenuObjid 매핑 (메뉴 objid 참조)
      if (key === "selectedMenuObjid" && menuIdMap) {
        const numValue = typeof value === "number" ? value : parseInt(value);
        if (!isNaN(numValue) && numValue > 0) {
          const newId = menuIdMap.get(numValue);
          if (newId) {
            obj[key] = typeof value === "number" ? newId : String(newId);
            logger.info(
              `    🔗 메뉴 참조 업데이트 (${currentPath}): ${value} → ${newId}`
            );
          } else {
            // 매핑이 없으면 경고 로그 (복사되지 않은 메뉴 참조)
            logger.warn(
              `    ⚠️ 메뉴 매핑 없음 (${currentPath}): ${value} - 원본 메뉴가 복사되지 않았을 수 있음`
            );
          }
        }
      }

      // 재귀 호출
      if (typeof value === "object" && value !== null) {
        this.recursiveUpdateReferences(
          value,
          screenIdMap,
          flowIdMap,
          currentPath,
          numberingRuleIdMap,
          menuIdMap
        );
      }
    }
  }

  /**
   * 기존 복사본 삭제 (덮어쓰기를 위한 사전 정리)
   *
   * 같은 원본 메뉴에서 복사된 메뉴 구조가 대상 회사에 이미 존재하면 삭제
   * - 최상위 메뉴 복사: 해당 메뉴 트리 전체 삭제
   * - 하위 메뉴 복사: 해당 메뉴와 그 하위만 삭제 (부모는 유지)
   */
  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, parent_obj_id 
       FROM menu_info 
       WHERE objid = $1`,
      [sourceMenuObjid]
    );

    if (sourceMenuResult.rows.length === 0) {
      logger.warn("⚠️ 원본 메뉴를 찾을 수 없습니다");
      return;
    }

    const sourceMenu = sourceMenuResult.rows[0];
    const isRootMenu =
      !sourceMenu.parent_obj_id || sourceMenu.parent_obj_id === 0;

    // 2. 대상 회사에 같은 원본에서 복사된 메뉴 찾기 (source_menu_objid로 정확히 매칭)
    // 최상위/하위 구분 없이 모든 복사본 검색
    const existingMenuResult = await client.query<{
      objid: number;
      parent_obj_id: number | null;
    }>(
      `SELECT objid, parent_obj_id 
       FROM menu_info 
       WHERE source_menu_objid = $1 
         AND company_code = $2`,
      [sourceMenuObjid, targetCompanyCode]
    );

    if (existingMenuResult.rows.length === 0) {
      logger.info("✅ 기존 복사본 없음 - 새로 생성됩니다");
      return;
    }

    const existingMenuObjid = existingMenuResult.rows[0].objid;
    const existingIsRoot =
      !existingMenuResult.rows[0].parent_obj_id ||
      existingMenuResult.rows[0].parent_obj_id === 0;

    logger.info(
      `🔍 기존 복사본 발견: ${sourceMenu.menu_name_kor} (원본: ${sourceMenuObjid}, 복사본: ${existingMenuObjid}, 최상위: ${existingIsRoot})`
    );

    // 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. 화면-메뉴 할당 먼저 삭제 (공유 화면 체크를 위해 먼저 삭제)
    await client.query(
      `DELETE FROM screen_menu_assignments 
       WHERE menu_objid = ANY($1) AND company_code = $2`,
      [existingMenuIds, targetCompanyCode]
    );
    logger.info(`  ✅ 화면-메뉴 할당 삭제 완료`);

    // 5-2. 화면 정의 삭제 (다른 메뉴에서 사용 중인 화면은 제외)
    if (screenIds.length > 0) {
      // 다른 메뉴에서도 사용 중인 화면 ID 조회
      const sharedScreensResult = await client.query<{ screen_id: number }>(
        `SELECT DISTINCT screen_id FROM screen_menu_assignments 
         WHERE screen_id = ANY($1) AND company_code = $2`,
        [screenIds, targetCompanyCode]
      );
      const sharedScreenIds = new Set(
        sharedScreensResult.rows.map((r) => r.screen_id)
      );

      // 공유되지 않은 화면만 삭제
      const screensToDelete = screenIds.filter(
        (id) => !sharedScreenIds.has(id)
      );

      if (screensToDelete.length > 0) {
        // 레이아웃 삭제
        await client.query(
          `DELETE FROM screen_layouts WHERE screen_id = ANY($1)`,
          [screensToDelete]
        );

        // 화면 정의 삭제
        await client.query(
          `DELETE FROM screen_definitions 
         WHERE screen_id = ANY($1) AND company_code = $2`,
          [screensToDelete, targetCompanyCode]
        );
        logger.info(`  ✅ 화면 정의 삭제 완료: ${screensToDelete.length}개`);
      }

      if (sharedScreenIds.size > 0) {
        logger.info(
          `  ♻️ 공유 화면 유지: ${sharedScreenIds.size}개 (다른 메뉴에서 사용 중)`
        );
      }
    }

    // 5-3. 메뉴 권한 삭제
    await client.query(`DELETE FROM rel_menu_auth WHERE menu_objid = ANY($1)`, [
      existingMenuIds,
    ]);
    logger.info(`  ✅ 메뉴 권한 삭제 완료`);

    // 5-4. 채번 규칙 처리 (새 스키마에서는 menu_objid 없음 - 스킵)
    // 새 numbering_rules 스키마: table_name + column_name + company_code 기반
    // 메뉴와 직접 연결되지 않으므로 메뉴 삭제 시 처리 불필요
    logger.info(`  ⏭️ 채번 규칙: 새 스키마에서는 메뉴와 연결되지 않음 (스킵)`);

    // 5-5. 카테고리 매핑 삭제 (menu_objid가 NOT NULL이므로 NULL 설정 불가)
    // 카테고리 매핑은 메뉴와 강하게 연결되어 있으므로 함께 삭제
    const deletedCategoryMappings = await client.query(
      `DELETE FROM category_column_mapping 
       WHERE menu_objid = ANY($1) AND company_code = $2
       RETURNING mapping_id`,
      [existingMenuIds, targetCompanyCode]
    );
    if (
      deletedCategoryMappings.rowCount &&
      deletedCategoryMappings.rowCount > 0
    ) {
      logger.info(
        `  ✅ 카테고리 매핑 삭제 완료: ${deletedCategoryMappings.rowCount}개`
      );
    }

    // 5-6. 메뉴 삭제 (배치 삭제 - 하위 메뉴부터 삭제를 위해 역순 정렬된 ID 사용)
    // 외래키 제약이 해제되었으므로 배치 삭제 가능
    if (existingMenuIds.length > 0) {
      await client.query(`DELETE FROM menu_info WHERE objid = ANY($1)`, [
        existingMenuIds,
      ]);
    }
    logger.info(`  ✅ 메뉴 삭제 완료: ${existingMenus.length}개`);

    logger.info("✅ 기존 복사본 삭제 완료 - 덮어쓰기 준비됨");
  }

  /**
   * 메뉴 복사 (메인 함수)
   */
  async copyMenu(
    sourceMenuObjid: number,
    targetCompanyCode: string,
    userId: string,
    screenNameConfig?: {
      removeText?: string;
      addPrefix?: string;
      replaceFrom?: string;
      replaceTo?: string;
    },
    additionalCopyOptions?: AdditionalCopyOptions
  ): 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!;

      // 같은 회사로 복제하는 경우 경고 (자기 자신의 데이터 손상 위험)
      if (sourceCompanyCode === targetCompanyCode) {
        logger.warn(
          `⚠️ 같은 회사로 메뉴 복제 시도: ${sourceCompanyCode} → ${targetCompanyCode}`
        );
        warnings.push(
          "같은 회사로 복제하면 추가 데이터(카테고리, 채번 등)가 복제되지 않습니다."
        );
      }

      const screenIds = await this.collectScreens(
        menus.map((m) => m.objid),
        sourceCompanyCode,
        client,
        menus
      );

      const flowIds = await this.collectFlows(screenIds, client);

      logger.info(`
📊 수집 완료:
   - 메뉴: ${menus.length}개
   - 화면: ${screenIds.size}개
   - 플로우: ${flowIds.size}개
      `);

      // === 2단계: 플로우 복사 ===
      logger.info("\n🔄 [2단계] 플로우 복사");
      const flowIdMap = await this.copyFlows(
        flowIds,
        targetCompanyCode,
        userId,
        client
      );

      // === 2.1단계: 노드 플로우 복사는 화면 복사에서 처리 ===
      // (screenManagementService.ts의 copyScreen에서 처리)
      const copiedNodeFlows = 0;

      // === 2.2단계: 데이터플로우 다이어그램 복사는 화면 복사에서 처리 ===
      // (screenManagementService.ts의 copyScreen에서 처리)
      const copiedDataflowDiagrams = 0;

      // 변수 초기화
      let copiedCodeCategories = 0;
      let copiedCodes = 0;
      let copiedNumberingRules = 0;
      let copiedCategoryMappings = 0;
      let copiedTableTypeColumns = 0;
      let copiedCascadingRelations = 0;
      let numberingRuleIdMap = new Map<string, string>();

      const menuObjids = menus.map((m) => m.objid);

      // 메뉴 ID 맵을 먼저 생성 (일관된 ID 사용을 위해)
      const tempMenuIdMap = new Map<number, number>();
      let tempObjId = await this.getNextMenuObjid(client);
      for (const menu of menus) {
        tempMenuIdMap.set(menu.objid, tempObjId++);
      }

      // === 3단계: 메뉴 복사 (외래키 의존성 해결을 위해 먼저 실행) ===
      // 채번 규칙, 코드 카테고리 등이 menu_info를 참조하므로 메뉴를 먼저 생성
      logger.info("\n📂 [3단계] 메뉴 복사 (외래키 선행 조건)");
      const menuIdMap = await this.copyMenus(
        menus,
        sourceMenuObjid,
        sourceCompanyCode,
        targetCompanyCode,
        new Map(), // screenIdMap은 아직 없음 (나중에 할당에서 처리)
        userId,
        client,
        tempMenuIdMap
      );

      // === 4단계: 채번 규칙 복사 (메뉴 복사 후, 화면 복사 전) ===
      if (additionalCopyOptions?.copyNumberingRules) {
        logger.info("\n📦 [4단계] 채번 규칙 복사");
        const ruleResult = await this.copyNumberingRulesWithMap(
          menuObjids,
          menuIdMap, // 실제 생성된 메뉴 ID 사용
          targetCompanyCode,
          userId,
          client
        );
        copiedNumberingRules = ruleResult.copiedCount;
        numberingRuleIdMap = ruleResult.ruleIdMap;
      }

      // === 4.1단계: 코드 카테고리 + 코드 복사 ===
      if (additionalCopyOptions?.copyCodeCategory) {
        logger.info("\n📦 [4.1단계] 코드 카테고리 + 코드 복사");
        const codeResult = await this.copyCodeCategoriesAndCodes(
          menuObjids,
          menuIdMap,
          targetCompanyCode,
          userId,
          client
        );
        copiedCodeCategories = codeResult.copiedCategories;
        copiedCodes = codeResult.copiedCodes;
      }

      // === 4.2단계: 카테고리 매핑 + 값 복사 ===
      if (additionalCopyOptions?.copyCategoryMapping) {
        logger.info("\n📦 [4.2단계] 카테고리 매핑 + 값 복사");
        copiedCategoryMappings = await this.copyCategoryMappingsAndValues(
          menuObjids,
          menuIdMap,
          sourceCompanyCode,
          targetCompanyCode,
          Array.from(screenIds),
          userId,
          client
        );
      }

      // === 4.3단계: 연쇄관계 복사 ===
      if (additionalCopyOptions?.copyCascadingRelation) {
        logger.info("\n📦 [4.3단계] 연쇄관계 복사");
        copiedCascadingRelations = await this.copyCascadingRelations(
          sourceCompanyCode,
          targetCompanyCode,
          menuIdMap,
          userId,
          client
        );
      }

      // === 4.9단계: 화면에서 참조하는 채번규칙 매핑 보완 ===
      // 화면 properties에서 참조하는 채번규칙 중 아직 매핑되지 않은 것들을
      // 대상 회사에서 같은 이름의 채번규칙으로 매핑
      if (screenIds.size > 0) {
        logger.info("\n🔗 [4.9단계] 화면 채번규칙 참조 매핑 보완");
        await this.supplementNumberingRuleMapping(
          Array.from(screenIds),
          sourceCompanyCode,
          targetCompanyCode,
          numberingRuleIdMap,
          client
        );
      }

      // === 5단계: 화면 복사 ===
      logger.info("\n📄 [5단계] 화면 복사");
      const screenIdMap = await this.copyScreens(
        screenIds,
        targetCompanyCode,
        flowIdMap,
        userId,
        client,
        screenNameConfig,
        numberingRuleIdMap,
        menuIdMap
      );

      // === 6단계: 화면-메뉴 할당 ===
      logger.info("\n🔗 [6단계] 화면-메뉴 할당");
      await this.createScreenMenuAssignments(
        menus,
        menuIdMap,
        screenIdMap,
        targetCompanyCode,
        client
      );

      // === 6.5단계: 메뉴 URL 업데이트 (화면 ID 재매핑) ===
      logger.info("\n🔄 [6.5단계] 메뉴 URL 화면 ID 재매핑");
      await this.updateMenuUrls(menuIdMap, screenIdMap, client);

      // === 6.7단계: screen_group_screens 복제 ===
      logger.info("\n🏷️ [6.7단계] screen_group_screens 복제");
      await this.copyScreenGroupScreens(
        screenIds,
        screenIdMap,
        sourceCompanyCode,
        targetCompanyCode,
        client
      );

      // === 7단계: 테이블 타입 설정 복사 ===
      if (additionalCopyOptions?.copyTableTypeColumns) {
        logger.info("\n📦 [7단계] 테이블 타입 설정 복사");
        copiedTableTypeColumns = await this.copyTableTypeColumns(
          Array.from(screenIdMap.keys()),
          sourceCompanyCode,
          targetCompanyCode,
          client
        );
      }

      // 커밋
      await client.query("COMMIT");
      logger.info("✅ 트랜잭션 커밋 완료");

      const result: MenuCopyResult = {
        success: true,
        copiedMenus: menuIdMap.size,
        copiedScreens: screenIdMap.size,
        copiedFlows: flowIdMap.size,
        copiedCodeCategories,
        copiedCodes,
        copiedNumberingRules,
        copiedCategoryMappings,
        copiedTableTypeColumns,
        copiedCascadingRelations,
        copiedNodeFlows,
        copiedDataflowDiagrams,
        menuIdMap: Object.fromEntries(menuIdMap),
        screenIdMap: Object.fromEntries(screenIdMap),
        flowIdMap: Object.fromEntries(flowIdMap),
        warnings,
      };

      logger.info(`
🎉 ============================================
   메뉴 복사 완료!
   - 메뉴: ${result.copiedMenus}개
   - 화면: ${result.copiedScreens}개
   - 플로우: ${result.copiedFlows}개
   - 노드 플로우(제어관리): ${copiedNodeFlows}개
   - 데이터플로우 다이어그램(버튼 제어): ${copiedDataflowDiagrams}개
   - 코드 카테고리: ${copiedCodeCategories}개
   - 코드: ${copiedCodes}개
   - 채번규칙: ${copiedNumberingRules}개
   - 카테고리 매핑: ${copiedCategoryMappings}개
   - 테이블 타입 설정: ${copiedTableTypeColumns}개
   - 연쇄관계: ${copiedCascadingRelations}개
============================================
      `);

      return result;
    } catch (error: any) {
      // 롤백
      await client.query("ROLLBACK");
      logger.error("❌ 메뉴 복사 실패, 롤백됨:", error);
      throw error;
    } finally {
      client.release();
    }
  }

  /**
   * 플로우 복사
   * - 대상 회사에 같은 이름+테이블의 플로우가 있으면 재사용 (ID 매핑만)
   * - 없으면 새로 복사
   */
  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;
    }

    const flowIdArray = Array.from(flowIds);
    logger.info(`🔄 플로우 복사 중: ${flowIds.size}개`);
    logger.info(`   📋 복사 대상 flowIds: [${flowIdArray.join(", ")}]`);

    // === 최적화: 배치 조회 ===
    // 1) 모든 원본 플로우 한 번에 조회
    const allFlowDefsResult = await client.query<FlowDefinition>(
      `SELECT * FROM flow_definition WHERE id = ANY($1)`,
      [flowIdArray]
    );
    const flowDefMap = new Map(allFlowDefsResult.rows.map((f) => [f.id, f]));

    // 2) 대상 회사의 기존 플로우 한 번에 조회 (이름+테이블 기준)
    const flowNames = allFlowDefsResult.rows.map((f) => f.name);
    const existingFlowsResult = await client.query<{
      id: number;
      name: string;
      table_name: string;
    }>(
      `SELECT id, name, table_name FROM flow_definition 
       WHERE company_code = $1 AND name = ANY($2)`,
      [targetCompanyCode, flowNames]
    );
    const existingFlowMap = new Map(
      existingFlowsResult.rows.map((f) => [`${f.name}|${f.table_name}`, f.id])
    );

    // 3) 복사가 필요한 플로우 ID 목록
    const flowsToCopy: FlowDefinition[] = [];

    for (const originalFlowId of flowIdArray) {
      const flowDef = flowDefMap.get(originalFlowId);
      if (!flowDef) {
        logger.warn(`⚠️ 플로우를 찾을 수 없음: id=${originalFlowId}`);
        continue;
      }

      const key = `${flowDef.name}|${flowDef.table_name}`;
      const existingId = existingFlowMap.get(key);

      if (existingId) {
        flowIdMap.set(originalFlowId, existingId);
        logger.info(
          `  ♻️ 기존 플로우 재사용: ${originalFlowId} → ${existingId} (${flowDef.name})`
        );
      } else {
        flowsToCopy.push(flowDef);
      }
    }

    // 4) 새 플로우 복사 (배치 처리)
    if (flowsToCopy.length > 0) {
      // 배치 INSERT로 플로우 생성
      const flowValues = flowsToCopy
        .map(
          (f, i) =>
            `($${i * 8 + 1}, $${i * 8 + 2}, $${i * 8 + 3}, $${i * 8 + 4}, $${i * 8 + 5}, $${i * 8 + 6}, $${i * 8 + 7}, $${i * 8 + 8})`
        )
        .join(", ");

      const flowParams = flowsToCopy.flatMap((f) => [
        f.name,
        f.description,
        f.table_name,
        f.is_active,
        targetCompanyCode,
        userId,
        f.db_source_type,
        f.db_connection_id,
      ]);

      const newFlowsResult = 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 ${flowValues}
          RETURNING id`,
        flowParams
      );

      // 새 플로우 ID 매핑
      flowsToCopy.forEach((flowDef, index) => {
        const newFlowId = newFlowsResult.rows[index].id;
        flowIdMap.set(flowDef.id, newFlowId);
        logger.info(
          `  ✅ 플로우 신규 복사: ${flowDef.id} → ${newFlowId} (${flowDef.name})`
        );
      });

      // 5) 스텝 및 연결 복사 (복사된 플로우만)
      const originalFlowIdsToCopy = flowsToCopy.map((f) => f.id);

      // 모든 스텝 한 번에 조회
      const allStepsResult = await client.query<FlowStep>(
        `SELECT * FROM flow_step WHERE flow_definition_id = ANY($1) ORDER BY flow_definition_id, step_order`,
        [originalFlowIdsToCopy]
      );

      // 플로우별 스텝 그룹핑
      const stepsByFlow = new Map<number, FlowStep[]>();
      for (const step of allStepsResult.rows) {
        if (!stepsByFlow.has(step.flow_definition_id)) {
          stepsByFlow.set(step.flow_definition_id, []);
        }
        stepsByFlow.get(step.flow_definition_id)!.push(step);
      }

      // 스텝 복사 (플로우별)
      const allStepIdMaps = new Map<number, Map<number, number>>(); // originalFlowId -> stepIdMap

      for (const originalFlowId of originalFlowIdsToCopy) {
        const newFlowId = flowIdMap.get(originalFlowId)!;
        const steps = stepsByFlow.get(originalFlowId) || [];
        const stepIdMap = new Map<number, number>();

        if (steps.length > 0) {
          // 배치 INSERT로 스텝 생성
          const stepValues = steps
            .map(
              (_, i) =>
                `($${i * 17 + 1}, $${i * 17 + 2}, $${i * 17 + 3}, $${i * 17 + 4}, $${i * 17 + 5}, $${i * 17 + 6}, $${i * 17 + 7}, $${i * 17 + 8}, $${i * 17 + 9}, $${i * 17 + 10}, $${i * 17 + 11}, $${i * 17 + 12}, $${i * 17 + 13}, $${i * 17 + 14}, $${i * 17 + 15}, $${i * 17 + 16}, $${i * 17 + 17})`
            )
            .join(", ");

          const stepParams = steps.flatMap((s) => [
            newFlowId,
            s.step_name,
            s.step_order,
            s.condition_json,
            s.color,
            s.position_x,
            s.position_y,
            s.table_name,
            s.move_type,
            s.status_column,
            s.status_value,
            s.target_table,
            s.field_mappings,
            s.required_fields,
            s.integration_type,
            s.integration_config,
            s.display_config,
          ]);

          const newStepsResult = 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 ${stepValues}
            RETURNING id`,
            stepParams
          );

          steps.forEach((step, index) => {
            stepIdMap.set(step.id, newStepsResult.rows[index].id);
          });

          logger.info(
            `    ↳ 플로우 ${originalFlowId}: 스텝 ${steps.length}개 복사`
          );
        }

        allStepIdMaps.set(originalFlowId, stepIdMap);
      }

      // 모든 연결 한 번에 조회
      const allConnectionsResult = await client.query<FlowStepConnection>(
        `SELECT * FROM flow_step_connection WHERE flow_definition_id = ANY($1)`,
        [originalFlowIdsToCopy]
      );

      // 연결 복사 (배치 INSERT)
      const connectionsToInsert: {
        newFlowId: number;
        newFromStepId: number;
        newToStepId: number;
        label: string;
      }[] = [];

      for (const conn of allConnectionsResult.rows) {
        const stepIdMap = allStepIdMaps.get(conn.flow_definition_id);
        if (!stepIdMap) continue;

        const newFromStepId = stepIdMap.get(conn.from_step_id);
        const newToStepId = stepIdMap.get(conn.to_step_id);
        const newFlowId = flowIdMap.get(conn.flow_definition_id);

        if (newFromStepId && newToStepId && newFlowId) {
          connectionsToInsert.push({
            newFlowId,
            newFromStepId,
            newToStepId,
            label: conn.label || "",
          });
        }
      }

      if (connectionsToInsert.length > 0) {
        const connValues = connectionsToInsert
          .map(
            (_, i) =>
              `($${i * 4 + 1}, $${i * 4 + 2}, $${i * 4 + 3}, $${i * 4 + 4})`
          )
          .join(", ");

        const connParams = connectionsToInsert.flatMap((c) => [
          c.newFlowId,
          c.newFromStepId,
          c.newToStepId,
          c.label,
        ]);

        await client.query(
          `INSERT INTO flow_step_connection (
              flow_definition_id, from_step_id, to_step_id, label
          ) VALUES ${connValues}`,
          connParams
        );

        logger.info(`    ↳ 연결 ${connectionsToInsert.length}개 복사`);
      }
    }

    logger.info(`✅ 플로우 복사 완료: ${flowIdMap.size}개`);
    return flowIdMap;
  }

  /**
   * 화면 복사 (업데이트 또는 신규 생성)
   * - source_screen_id로 기존 복사본 찾기
   * - 변경된 내용이 있으면 업데이트
   * - 없으면 새로 복사
   */
  private async copyScreens(
    screenIds: Set<number>,
    targetCompanyCode: string,
    flowIdMap: Map<number, number>,
    userId: string,
    client: PoolClient,
    screenNameConfig?: {
      removeText?: string;
      addPrefix?: string;
      replaceFrom?: string;
      replaceTo?: string;
    },
    numberingRuleIdMap?: Map<string, string>,
    menuIdMap?: Map<number, number>
  ): Promise<Map<number, number>> {
    const screenIdMap = new Map<number, number>();

    if (screenIds.size === 0) {
      logger.info("📭 복사할 화면 없음");
      return screenIdMap;
    }

    logger.info(`📄 화면 복사/업데이트 중: ${screenIds.size}개`);

    // === 0단계: 원본 화면 정의 배치 조회 ===
    const screenIdArray = Array.from(screenIds);
    const allScreenDefsResult = await client.query<ScreenDefinition>(
      `SELECT * FROM screen_definitions WHERE screen_id = ANY($1)`,
      [screenIdArray]
    );
    const screenDefMap = new Map<number, ScreenDefinition>();
    for (const def of allScreenDefsResult.rows) {
      screenDefMap.set(def.screen_id, def);
    }

    // 대상 회사의 기존 복사본 배치 조회 (source_screen_id 기준)
    const existingCopiesResult = await client.query<{
      screen_id: number;
      screen_name: string;
      source_screen_id: number;
      updated_date: Date;
    }>(
      `SELECT screen_id, screen_name, source_screen_id, updated_date 
       FROM screen_definitions 
       WHERE source_screen_id = ANY($1) AND company_code = $2 AND deleted_date IS NULL`,
      [screenIdArray, targetCompanyCode]
    );
    const existingCopyMap = new Map<
      number,
      { screen_id: number; screen_name: string; updated_date: Date }
    >();
    for (const copy of existingCopiesResult.rows) {
      existingCopyMap.set(copy.source_screen_id, copy);
    }

    // === 1단계: 모든 screen_definitions 처리 (screenIdMap 생성) ===
    const screenDefsToProcess: Array<{
      originalScreenId: number;
      targetScreenId: number;
      screenDef: ScreenDefinition;
      isUpdate: boolean; // 업데이트인지 신규 생성인지
    }> = [];

    for (const originalScreenId of screenIds) {
      try {
        // 1) 원본 screen_definitions 조회 (캐시에서)
        const screenDef = screenDefMap.get(originalScreenId);

        if (!screenDef) {
          logger.warn(`⚠️ 화면을 찾을 수 없음: screen_id=${originalScreenId}`);
          continue;
        }

        // 2) 기존 복사본 찾기: 캐시에서 조회 (source_screen_id 기준)
        let existingCopy = existingCopyMap.get(originalScreenId);

        // 2-1) source_screen_id가 없는 기존 복사본 (이름 + 테이블로 검색) - 호환성 유지
        if (!existingCopy && screenDef.screen_name) {
          const legacyCopyResult = await client.query<{
            screen_id: number;
            screen_name: string;
            updated_date: Date;
          }>(
            `SELECT screen_id, screen_name, updated_date 
             FROM screen_definitions 
             WHERE screen_name = $1 
               AND table_name = $2
               AND company_code = $3 
               AND source_screen_id IS NULL
               AND deleted_date IS NULL
             LIMIT 1`,
            [screenDef.screen_name, screenDef.table_name, targetCompanyCode]
          );

          if (legacyCopyResult.rows.length > 0) {
            existingCopy = legacyCopyResult.rows[0];
            // 기존 복사본에 source_screen_id 업데이트 (마이그레이션)
            await client.query(
              `UPDATE screen_definitions SET source_screen_id = $1 WHERE screen_id = $2`,
              [originalScreenId, existingCopy.screen_id]
            );
            logger.info(
              `  📝 기존 화면에 source_screen_id 추가: ${existingCopy.screen_id} ← ${originalScreenId}`
            );
          }
        }

        // 3) 화면명 변환 적용
        let transformedScreenName = screenDef.screen_name;
        if (screenNameConfig) {
          if (screenNameConfig.replaceFrom?.trim()) {
            transformedScreenName = transformedScreenName.replace(
              new RegExp(screenNameConfig.replaceFrom.trim(), "g"),
              screenNameConfig.replaceTo?.trim() || ""
            );
            transformedScreenName = transformedScreenName.trim();
          }
          if (screenNameConfig.removeText?.trim()) {
            transformedScreenName = transformedScreenName.replace(
              new RegExp(screenNameConfig.removeText.trim(), "g"),
              ""
            );
            transformedScreenName = transformedScreenName.trim();
          }
          if (screenNameConfig.addPrefix?.trim()) {
            transformedScreenName =
              screenNameConfig.addPrefix.trim() + " " + transformedScreenName;
          }
        }

        if (existingCopy) {
          // === 기존 복사본이 있는 경우: 업데이트 ===
          const existingScreenId = existingCopy.screen_id;

          // 원본 V2 레이아웃 조회 (모든 레이어)
          const sourceLayoutV2Result = await client.query<{ layout_data: any }>(
            `SELECT layout_data FROM screen_layouts_v2 WHERE screen_id = $1 ORDER BY layer_id`,
            [originalScreenId]
          );

          // 대상 V2 레이아웃 조회 (모든 레이어)
          const targetLayoutV2Result = await client.query<{ layout_data: any }>(
            `SELECT layout_data FROM screen_layouts_v2 WHERE screen_id = $1 ORDER BY layer_id`,
            [existingScreenId]
          );

          // 변경 여부 확인: 레이어 수가 다르면 무조건 변경됨
          const layerCountDiffers = sourceLayoutV2Result.rows.length !== targetLayoutV2Result.rows.length;
          const hasChanges = layerCountDiffers || this.hasLayoutChangesV2(
            sourceLayoutV2Result.rows[0]?.layout_data,
            targetLayoutV2Result.rows[0]?.layout_data
          );

          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}`, error);
        throw error;
      }
    }

    // === 2단계: screen_conditional_zones + screen_layouts_v2 처리 (멀티 레이어 지원) ===
    logger.info(
      `\n📐 V2 레이아웃 처리 시작 (screenIdMap 완성: ${screenIdMap.size}개)`
    );

    for (const {
      originalScreenId,
      targetScreenId,
      screenDef,
      isUpdate,
    } of screenDefsToProcess) {
      try {
        const sourceCompanyCode = screenDef.company_code;

        // 원본 V2 레이아웃 전체 조회 (모든 레이어)
        const layoutV2Result = await client.query<{
          layout_data: any;
          layer_id: number;
          layer_name: string;
          condition_config: any;
        }>(
          `SELECT layout_data, layer_id, layer_name, condition_config
           FROM screen_layouts_v2
           WHERE screen_id = $1 AND company_code = $2
           ORDER BY layer_id`,
          [originalScreenId, sourceCompanyCode]
        );

        if (layoutV2Result.rows.length === 0) {
          logger.info(`    ↳ V2 레이아웃 없음 (스킵): screen_id=${originalScreenId}`);
          continue;
        }

        // 모든 레이어에서 컴포넌트를 수집하여 componentIdMap 생성
        const componentIdMap = new Map<string, string>();
        const timestamp = Date.now();
        let compIdx = 0;
        for (const layer of layoutV2Result.rows) {
          const components = layer.layout_data?.components || [];
          for (const comp of components) {
            if (!componentIdMap.has(comp.id)) {
              const newId = `comp_${timestamp}_${compIdx++}_${Math.random().toString(36).substr(2, 5)}`;
              componentIdMap.set(comp.id, newId);
            }
          }
        }

        // screen_conditional_zones 복제 + zoneIdMap 생성
        const zoneIdMap = new Map<number, number>();
        const zonesResult = await client.query(
          `SELECT * FROM screen_conditional_zones WHERE screen_id = $1`,
          [originalScreenId]
        );

        if (isUpdate) {
          await client.query(
            `DELETE FROM screen_conditional_zones WHERE screen_id = $1 AND company_code = $2`,
            [targetScreenId, targetCompanyCode]
          );
        }

        for (const zone of zonesResult.rows) {
          const newTriggerCompId = componentIdMap.get(zone.trigger_component_id) || zone.trigger_component_id;
          const newZone = await client.query<{ zone_id: number }>(
            `INSERT INTO screen_conditional_zones
             (screen_id, company_code, zone_name, x, y, width, height,
              trigger_component_id, trigger_operator)
             VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
             RETURNING zone_id`,
            [targetScreenId, targetCompanyCode, zone.zone_name,
             zone.x, zone.y, zone.width, zone.height,
             newTriggerCompId, zone.trigger_operator]
          );
          zoneIdMap.set(zone.zone_id, newZone.rows[0].zone_id);
        }

        if (zonesResult.rows.length > 0) {
          logger.info(`    ↳ 조건부 영역 복사: ${zonesResult.rows.length}개 (zoneIdMap: ${zoneIdMap.size}개)`);
        }

        // 업데이트인 경우 기존 레이아웃 삭제 (레이어 수 변경 대응)
        if (isUpdate) {
          await client.query(
            `DELETE FROM screen_layouts_v2 WHERE screen_id = $1 AND company_code = $2`,
            [targetScreenId, targetCompanyCode]
          );
        }

        // 각 레이어별 처리
        let totalComponents = 0;
        for (const layer of layoutV2Result.rows) {
          const layoutData = layer.layout_data;
          const components = layoutData?.components || [];

          if (!layoutData || components.length === 0) continue;
          totalComponents += components.length;

          // V2 레이아웃 데이터 복사 및 참조 업데이트
          const updatedLayoutData = this.updateReferencesInLayoutDataV2(
            layoutData,
            componentIdMap,
            screenIdMap,
            flowIdMap,
            numberingRuleIdMap,
            menuIdMap
          );

          // condition_config의 zone_id 재매핑
          let updatedConditionConfig = layer.condition_config ? { ...layer.condition_config } : null;
          if (updatedConditionConfig?.zone_id) {
            const newZoneId = zoneIdMap.get(updatedConditionConfig.zone_id);
            if (newZoneId) {
              updatedConditionConfig.zone_id = newZoneId;
            }
          }

          // V2 레이아웃 저장 (레이어별 INSERT)
          await client.query(
            `INSERT INTO screen_layouts_v2 (screen_id, company_code, layer_id, layer_name, layout_data, condition_config, created_at, updated_at)
             VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
             ON CONFLICT (screen_id, company_code, layer_id)
             DO UPDATE SET layout_data = $5, layer_name = $4, condition_config = $6, updated_at = NOW()`,
            [
              targetScreenId,
              targetCompanyCode,
              layer.layer_id,
              layer.layer_name,
              JSON.stringify(updatedLayoutData),
              updatedConditionConfig ? JSON.stringify(updatedConditionConfig) : null,
            ]
          );
        }

        const action = isUpdate ? "업데이트" : "복사";
        logger.info(`    ↳ V2 레이아웃 ${action}: ${layoutV2Result.rows.length}개 레이어, ${totalComponents}개 컴포넌트`);
      } catch (error: any) {
        logger.error(
          `❌ V2 레이아웃 처리 실패: screen_id=${originalScreenId}`,
          error
        );
        throw error;
      }
    }

    // 통계 출력
    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;
  }

  /**
   * V2 레이아웃 변경 여부 확인 (screen_layouts_v2용)
   */
  private hasLayoutChangesV2(
    sourceLayoutData: any,
    targetLayoutData: any
  ): boolean {
    // 1. 둘 다 없으면 변경 없음
    if (!sourceLayoutData && !targetLayoutData) return false;
    
    // 2. 하나만 있으면 변경됨
    if (!sourceLayoutData || !targetLayoutData) return true;
    
    // 3. components 배열 비교
    const sourceComps = sourceLayoutData.components || [];
    const targetComps = targetLayoutData.components || [];
    
    if (sourceComps.length !== targetComps.length) return true;
    
    // 4. 각 컴포넌트 비교 (url, position, size, overrides)
    for (let i = 0; i < sourceComps.length; i++) {
      const s = sourceComps[i];
      const t = targetComps[i];
      
      if (s.url !== t.url) return true;
      if (JSON.stringify(s.position) !== JSON.stringify(t.position)) return true;
      if (JSON.stringify(s.size) !== JSON.stringify(t.size)) return true;
      if (JSON.stringify(s.overrides) !== JSON.stringify(t.overrides)) return true;
    }
    
    return false;
  }

  /**
   * V2 레이아웃 데이터의 참조 ID들을 업데이트 (componentId, flowId, ruleId, screenId, menuId)
   */
  private updateReferencesInLayoutDataV2(
    layoutData: any,
    componentIdMap: Map<string, string>,
    screenIdMap: Map<number, number>,
    flowIdMap: Map<number, number>,
    numberingRuleIdMap?: Map<string, string>,
    menuIdMap?: Map<number, number>
  ): any {
    if (!layoutData?.components) return layoutData;

    const updatedComponents = layoutData.components.map((comp: any) => {
      // 1. componentId 매핑
      const newId = componentIdMap.get(comp.id) || comp.id;

      // 2. overrides 복사 및 재귀적 참조 업데이트
      let overrides = JSON.parse(JSON.stringify(comp.overrides || {}));

      // 재귀적으로 모든 참조 업데이트
      this.recursiveUpdateReferences(
        overrides,
        screenIdMap,
        flowIdMap,
        "",
        numberingRuleIdMap,
        menuIdMap
      );

      return {
        ...comp,
        id: newId,
        overrides,
      };
    });

    return {
      ...layoutData,
      components: updatedComponents,
      updatedAt: new Date().toISOString(),
    };
  }

  /**
   * 메뉴 위상 정렬 (부모 먼저)
   */
  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;
  }

  /**
   * 대상 회사에서 부모 메뉴 찾기
   * - 원본 메뉴의 parent_obj_id를 source_menu_objid로 가진 메뉴를 대상 회사에서 검색
   * - 2레벨 이하 메뉴 복사 시 기존에 복사된 부모 메뉴와 연결하기 위함
   */
  private async findParentMenuInTargetCompany(
    originalParentObjId: number,
    sourceCompanyCode: string,
    targetCompanyCode: string,
    client: PoolClient
  ): Promise<number | null> {
    // 1. 대상 회사에서 source_menu_objid가 원본 부모 ID인 메뉴 찾기
    const result = await client.query<{ objid: number }>(
      `SELECT objid FROM menu_info 
       WHERE source_menu_objid = $1 AND company_code = $2
       LIMIT 1`,
      [originalParentObjId, targetCompanyCode]
    );

    if (result.rows.length > 0) {
      return result.rows[0].objid;
    }

    // 2. source_menu_objid로 못 찾으면, 동일 원본 회사에서 복사된 메뉴 중 같은 이름으로 찾기 (fallback)
    // 원본 부모 메뉴 정보 조회
    const parentMenuResult = await client.query<Menu>(
      `SELECT * FROM menu_info WHERE objid = $1`,
      [originalParentObjId]
    );

    if (parentMenuResult.rows.length === 0) {
      return null;
    }

    const parentMenu = parentMenuResult.rows[0];

    // 대상 회사에서 같은 이름 + 같은 원본 회사에서 복사된 메뉴 찾기
    // source_menu_objid가 있는 메뉴(복사된 메뉴)만 대상으로,
    // 해당 source_menu_objid의 원본 메뉴가 같은 회사(sourceCompanyCode)에 속하는지 확인
    const sameNameResult = await client.query<{ objid: number }>(
      `SELECT m.objid FROM menu_info m
       WHERE m.menu_name_kor = $1 
         AND m.company_code = $2
         AND m.source_menu_objid IS NOT NULL
         AND EXISTS (
           SELECT 1 FROM menu_info orig 
           WHERE orig.objid = m.source_menu_objid 
             AND orig.company_code = $3
         )
       LIMIT 1`,
      [parentMenu.menu_name_kor, targetCompanyCode, sourceCompanyCode]
    );

    if (sameNameResult.rows.length > 0) {
      logger.info(
        `    📎 이름으로 부모 메뉴 찾음: "${parentMenu.menu_name_kor}" → objid: ${sameNameResult.rows[0].objid}`
      );
      return sameNameResult.rows[0].objid;
    }

    return null;
  }

  /**
   * 메뉴 복사
   */
  private async copyMenus(
    menus: Menu[],
    rootMenuObjid: number,
    sourceCompanyCode: string,
    targetCompanyCode: string,
    screenIdMap: Map<number, number>,
    userId: string,
    client: PoolClient,
    preAllocatedMenuIdMap?: Map<number, number> // 미리 할당된 메뉴 ID 맵 (옵션 데이터 복사에 사용된 경우)
  ): Promise<Map<number, number>> {
    const menuIdMap = new Map<number, number>();

    if (menus.length === 0) {
      logger.info("📭 복사할 메뉴 없음");
      return menuIdMap;
    }

    logger.info(`📂 메뉴 복사 중: ${menus.length}개`);

    // screen_group_id 재매핑 맵 생성 (source company → target company)
    const screenGroupIdMap = new Map<number, number>();
    const sourceGroupIds = [...new Set(menus.map(m => m.screen_group_id).filter(Boolean))] as number[];
    if (sourceGroupIds.length > 0) {
      const sourceGroups = await client.query<{ id: number; group_name: string }>(
        `SELECT id, group_name FROM screen_groups WHERE id = ANY($1)`,
        [sourceGroupIds]
      );
      for (const sg of sourceGroups.rows) {
        const targetGroup = await client.query<{ id: number }>(
          `SELECT id FROM screen_groups WHERE group_name = $1 AND company_code = $2 LIMIT 1`,
          [sg.group_name, targetCompanyCode]
        );
        if (targetGroup.rows.length > 0) {
          screenGroupIdMap.set(sg.id, targetGroup.rows[0].id);
        }
      }
      logger.info(`🏷️ screen_group 매핑: ${screenGroupIdMap.size}/${sourceGroupIds.length}개`);
    }

    // 위상 정렬 (부모 먼저 삽입)
    const sortedMenus = this.topologicalSortMenus(menus);

    for (const menu of sortedMenus) {
      try {
        // 0. 이미 복사된 메뉴가 있는지 확인 (고아 메뉴 재연결용)
        // 1차: source_menu_objid로 검색
        let existingCopyResult = await client.query<{
          objid: number;
          parent_obj_id: number | null;
        }>(
          `SELECT objid, parent_obj_id FROM menu_info 
           WHERE source_menu_objid = $1 AND company_code = $2
           LIMIT 1`,
          [menu.objid, targetCompanyCode]
        );

        // 2차: source_menu_objid가 없는 기존 복사본 (이름 + 메뉴타입으로 검색) - 호환성 유지
        if (existingCopyResult.rows.length === 0 && menu.menu_name_kor) {
          existingCopyResult = await client.query<{
            objid: number;
            parent_obj_id: number | null;
          }>(
            `SELECT objid, parent_obj_id FROM menu_info 
             WHERE menu_name_kor = $1 
               AND company_code = $2
               AND menu_type = $3
               AND source_menu_objid IS NULL
             LIMIT 1`,
            [menu.menu_name_kor, targetCompanyCode, menu.menu_type]
          );

          if (existingCopyResult.rows.length > 0) {
            // 기존 복사본에 source_menu_objid 업데이트 (마이그레이션)
            await client.query(
              `UPDATE menu_info SET source_menu_objid = $1 WHERE objid = $2`,
              [menu.objid, existingCopyResult.rows[0].objid]
            );
            logger.info(
              `  📝 기존 메뉴에 source_menu_objid 추가: ${existingCopyResult.rows[0].objid} ← ${menu.objid}`
            );
          }
        }

        // parent_obj_id 계산 (신규/재연결 모두 필요)
        let newParentObjId: number | null;
        if (!menu.parent_obj_id || menu.parent_obj_id === 0) {
          newParentObjId = 0; // 최상위 메뉴는 항상 0
        } else {
          // 1. 현재 복사 세션에서 부모가 이미 복사되었는지 확인
          newParentObjId = menuIdMap.get(menu.parent_obj_id) || null;

          // 2. 현재 세션에서 못 찾으면, 대상 회사에서 기존에 복사된 부모 찾기
          if (!newParentObjId) {
            const existingParent = await this.findParentMenuInTargetCompany(
              menu.parent_obj_id,
              sourceCompanyCode,
              targetCompanyCode,
              client
            );

            if (existingParent) {
              newParentObjId = existingParent;
              logger.info(
                `    🔗 기존 부모 메뉴 연결: 원본 ${menu.parent_obj_id} → 대상 ${existingParent}`
              );
            } else {
              // 3. 부모를 못 찾으면 최상위로 설정 (경고 로그)
              newParentObjId = 0;
              logger.warn(
                `    ⚠️ 부모 메뉴를 찾을 수 없음: ${menu.parent_obj_id} - 최상위로 생성됨`
              );
            }
          }
        }

        if (existingCopyResult.rows.length > 0) {
          // === 이미 복사된 메뉴가 있는 경우: 재연결만 ===
          const existingMenu = existingCopyResult.rows[0];
          const existingObjId = existingMenu.objid;
          const existingParentId = existingMenu.parent_obj_id;

          // 부모가 다르면 업데이트 (고아 메뉴 재연결)
          if (existingParentId !== newParentObjId) {
            await client.query(
              `UPDATE menu_info SET parent_obj_id = $1, writer = $2 WHERE objid = $3`,
              [newParentObjId, userId, existingObjId]
            );
            logger.info(
              `  ♻️ 메뉴 재연결: ${menu.objid} → ${existingObjId} (${menu.menu_name_kor}), parent: ${existingParentId} → ${newParentObjId}`
            );
          } else {
            logger.info(
              `  ⏭️ 메뉴 이미 존재 (스킵): ${menu.objid} → ${existingObjId} (${menu.menu_name_kor})`
            );
          }

          menuIdMap.set(menu.objid, existingObjId);
          continue;
        }

        // === 신규 메뉴 복사 ===
        // 미리 할당된 ID가 있으면 사용, 없으면 새로 생성
        const newObjId =
          preAllocatedMenuIdMap?.get(menu.objid) ??
          (await this.getNextMenuObjid(client));

        // source_menu_objid 저장: 모든 복사된 메뉴에 원본 ID 저장 (추적용)
        const sourceMenuObjid = menu.objid;
        const isRootMenu = String(menu.objid) === String(rootMenuObjid);

        if (isRootMenu) {
          logger.info(
            `    📌 source_menu_objid 저장: ${sourceMenuObjid} (복사 시작 메뉴)`
          );
        }

        // 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, menu_icon, screen_group_id
          ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)`,
          [
            newObjId,
            menu.menu_type,
            newParentObjId,
            menu.menu_name_kor,
            menu.menu_name_eng,
            menu.seq,
            menu.menu_url,
            menu.menu_desc,
            userId,
            menu.status || 'active',
            menu.system_name,
            targetCompanyCode,
            menu.lang_key,
            menu.lang_key_desc,
            menu.screen_code,
            menu.menu_code,
            sourceMenuObjid,
            menu.menu_icon,
            menu.screen_group_id ? (screenGroupIdMap.get(menu.screen_group_id) || menu.screen_group_id) : null,
          ]
        );

        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(`🔗 화면-메뉴 할당 중...`);

    if (menus.length === 0) {
      return;
    }

    // === 최적화: 배치 조회 ===
    // 1. 모든 원본 메뉴의 화면 할당 한 번에 조회
    const menuObjids = menus.map((m) => m.objid);
    const companyCodes = [...new Set(menus.map((m) => m.company_code))];

    const allAssignmentsResult = await client.query<{
      menu_objid: number;
      screen_id: number;
      display_order: number;
      is_active: string;
    }>(
      `SELECT menu_objid, screen_id, display_order, is_active 
         FROM screen_menu_assignments 
       WHERE menu_objid = ANY($1) AND company_code = ANY($2)`,
      [menuObjids, companyCodes]
    );

    if (allAssignmentsResult.rows.length === 0) {
      logger.info(`  📭 화면-메뉴 할당 없음`);
      return;
    }

    // 2. 유효한 할당만 필터링
    const validAssignments: Array<{
      newScreenId: number;
      newMenuObjid: number;
      displayOrder: number;
      isActive: string;
    }> = [];

    for (const assignment of allAssignmentsResult.rows) {
      const newMenuObjid = menuIdMap.get(assignment.menu_objid);
      const newScreenId = screenIdMap.get(assignment.screen_id);

      if (!newMenuObjid || !newScreenId) {
        if (!newScreenId) {
          logger.warn(
            `⚠️ 화면 ID 매핑 없음: screen_id=${assignment.screen_id}`
          );
        }
        continue;
      }

      validAssignments.push({
        newScreenId,
        newMenuObjid,
        displayOrder: assignment.display_order,
        isActive: assignment.is_active,
      });
    }

    // 3. 배치 INSERT
    if (validAssignments.length > 0) {
      const assignmentValues = validAssignments
        .map(
          (_, i) =>
            `($${i * 6 + 1}, $${i * 6 + 2}, $${i * 6 + 3}, $${i * 6 + 4}, $${i * 6 + 5}, $${i * 6 + 6})`
        )
        .join(", ");

      const assignmentParams = validAssignments.flatMap((a) => [
        a.newScreenId,
        a.newMenuObjid,
        targetCompanyCode,
        a.displayOrder,
        a.isActive,
        "system",
      ]);

      const result = await client.query(
        `INSERT INTO screen_menu_assignments (
            screen_id, menu_objid, company_code, display_order, is_active, created_by
        ) VALUES ${assignmentValues}
        ON CONFLICT (screen_id, menu_objid, company_code) DO NOTHING`,
        assignmentParams
      );

      logger.info(
        `✅ 화면-메뉴 할당 완료: ${result.rowCount}개 삽입 (${validAssignments.length - (result.rowCount || 0)}개 중복 무시)`
      );
    } else {
      logger.info(`📭 화면-메뉴 할당할 항목 없음`);
    }
  }

  /**
   * 메뉴 URL + screen_code 업데이트 (화면 ID 재매핑)
   * menu_url에 포함된 /screens/{screenId} 형식의 화면 ID를 복제된 화면 ID로 교체
   * menu_info.screen_code도 복제된 screen_definitions.screen_code로 교체
   */
  private async updateMenuUrls(
    menuIdMap: Map<number, number>,
    screenIdMap: Map<number, number>,
    client: PoolClient
  ): Promise<void> {
    if (menuIdMap.size === 0 || screenIdMap.size === 0) {
      logger.info("📭 메뉴 URL/screen_code 업데이트 대상 없음");
      return;
    }

    const newMenuObjids = Array.from(menuIdMap.values());
    
    // 복제된 메뉴 조회
    const menusToUpdate = await client.query<{
      objid: number;
      menu_url: string | null;
      screen_code: string | null;
    }>(
      `SELECT objid, menu_url, screen_code FROM menu_info 
       WHERE objid = ANY($1)`,
      [newMenuObjids]
    );

    if (menusToUpdate.rows.length === 0) {
      logger.info("📭 메뉴 URL/screen_code 업데이트 대상 없음");
      return;
    }

    // screenIdMap의 역방향: 원본 screen_id → 새 screen_id의 screen_code 조회
    const newScreenIds = Array.from(screenIdMap.values());
    const screenCodeMap = new Map<string, string>();
    if (newScreenIds.length > 0) {
      const screenCodesResult = await client.query<{
        screen_id: number;
        screen_code: string;
        source_screen_id: number;
      }>(
        `SELECT sd_new.screen_id, sd_new.screen_code, sd_new.source_screen_id
         FROM screen_definitions sd_new
         WHERE sd_new.screen_id = ANY($1) AND sd_new.screen_code IS NOT NULL`,
        [newScreenIds]
      );
      for (const row of screenCodesResult.rows) {
        if (row.source_screen_id) {
          // 원본의 screen_code 조회
          const origResult = await client.query<{ screen_code: string }>(
            `SELECT screen_code FROM screen_definitions WHERE screen_id = $1`,
            [row.source_screen_id]
          );
          if (origResult.rows[0]?.screen_code) {
            screenCodeMap.set(origResult.rows[0].screen_code, row.screen_code);
          }
        }
      }
    }

    let updatedUrlCount = 0;
    let updatedCodeCount = 0;
    const screenIdPattern = /\/screens\/(\d+)/;

    for (const menu of menusToUpdate.rows) {
      let newMenuUrl = menu.menu_url;
      let newScreenCode = menu.screen_code;
      let changed = false;

      // menu_url 재매핑
      if (menu.menu_url) {
        const match = menu.menu_url.match(screenIdPattern);
        if (match) {
          const originalScreenId = parseInt(match[1], 10);
          const newScreenId = screenIdMap.get(originalScreenId);
          if (newScreenId && newScreenId !== originalScreenId) {
            newMenuUrl = menu.menu_url.replace(
              `/screens/${originalScreenId}`,
              `/screens/${newScreenId}`
            );
            changed = true;
            updatedUrlCount++;
            logger.info(
              `  🔗 메뉴 URL 업데이트: ${menu.menu_url} → ${newMenuUrl}`
            );
          }
        }
        // /screen/{screen_code} 형식도 처리
        const screenCodeUrlMatch = menu.menu_url.match(/\/screen\/(.+)/);
        if (screenCodeUrlMatch && !menu.menu_url.match(/\/screens\//)) {
          const origCode = screenCodeUrlMatch[1];
          const newCode = screenCodeMap.get(origCode);
          if (newCode && newCode !== origCode) {
            newMenuUrl = `/screen/${newCode}`;
            changed = true;
            updatedUrlCount++;
            logger.info(
              `  🔗 메뉴 URL(코드) 업데이트: ${menu.menu_url} → ${newMenuUrl}`
            );
          }
        }
      }

      // screen_code 재매핑
      if (menu.screen_code) {
        const mappedCode = screenCodeMap.get(menu.screen_code);
        if (mappedCode && mappedCode !== menu.screen_code) {
          newScreenCode = mappedCode;
          changed = true;
          updatedCodeCount++;
          logger.info(
            `  🏷️ screen_code 업데이트: ${menu.screen_code} → ${newScreenCode}`
          );
        }
      }

      if (changed) {
        await client.query(
          `UPDATE menu_info SET menu_url = $1, screen_code = $2 WHERE objid = $3`,
          [newMenuUrl, newScreenCode, menu.objid]
        );
      }
    }

    logger.info(`✅ 메뉴 URL 업데이트: ${updatedUrlCount}개, screen_code 업데이트: ${updatedCodeCount}개`);
  }

  /**
   * screen_group_screens 복제 (화면-스크린그룹 매핑)
   */
  private async copyScreenGroupScreens(
    screenIds: Set<number>,
    screenIdMap: Map<number, number>,
    sourceCompanyCode: string,
    targetCompanyCode: string,
    client: PoolClient
  ): Promise<void> {
    if (screenIds.size === 0 || screenIdMap.size === 0) {
      logger.info("📭 screen_group_screens 복제 대상 없음");
      return;
    }

    // 기존 COMPANY_10의 screen_group_screens 삭제 (깨진 이전 데이터 정리)
    await client.query(
      `DELETE FROM screen_group_screens WHERE company_code = $1`,
      [targetCompanyCode]
    );

    // 소스 회사의 screen_group_screens 조회
    const sourceScreenIds = Array.from(screenIds);
    const sourceResult = await client.query<{
      group_id: number;
      screen_id: number;
      screen_role: string;
      display_order: number;
      is_default: string;
    }>(
      `SELECT group_id, screen_id, screen_role, display_order, is_default
       FROM screen_group_screens
       WHERE company_code = $1 AND screen_id = ANY($2)`,
      [sourceCompanyCode, sourceScreenIds]
    );

    if (sourceResult.rows.length === 0) {
      logger.info("📭 소스에 screen_group_screens 없음");
      return;
    }

    // screen_group ID 매핑 (source group_name → target group_id)
    const sourceGroupIds = [...new Set(sourceResult.rows.map(r => r.group_id))];
    const sourceGroups = await client.query<{ id: number; group_name: string }>(
      `SELECT id, group_name FROM screen_groups WHERE id = ANY($1)`,
      [sourceGroupIds]
    );
    const groupIdMap = new Map<number, number>();
    for (const sg of sourceGroups.rows) {
      const targetGroup = await client.query<{ id: number }>(
        `SELECT id FROM screen_groups WHERE group_name = $1 AND company_code = $2 LIMIT 1`,
        [sg.group_name, targetCompanyCode]
      );
      if (targetGroup.rows.length > 0) {
        groupIdMap.set(sg.id, targetGroup.rows[0].id);
      }
    }

    let insertedCount = 0;
    for (const row of sourceResult.rows) {
      const newGroupId = groupIdMap.get(row.group_id);
      const newScreenId = screenIdMap.get(row.screen_id);
      if (!newGroupId || !newScreenId) continue;

      await client.query(
        `INSERT INTO screen_group_screens (group_id, screen_id, screen_role, display_order, is_default, company_code, writer)
         VALUES ($1, $2, $3, $4, $5, $6, 'system')
         ON CONFLICT DO NOTHING`,
        [newGroupId, newScreenId, row.screen_role, row.display_order, row.is_default, targetCompanyCode]
      );
      insertedCount++;
    }

    logger.info(`✅ screen_group_screens 복제: ${insertedCount}개`);
  }

  /**
   * 코드 카테고리 + 코드 복사 (최적화: 배치 조회/삽입)
   */
  private async copyCodeCategoriesAndCodes(
    menuObjids: number[],
    menuIdMap: Map<number, number>,
    targetCompanyCode: string,
    userId: string,
    client: PoolClient
  ): Promise<{ copiedCategories: number; copiedCodes: number }> {
    let copiedCategories = 0;
    let copiedCodes = 0;

    if (menuObjids.length === 0) {
      return { copiedCategories, copiedCodes };
    }

    // === 최적화: 배치 조회 ===
    // 1. 모든 원본 카테고리 한 번에 조회
    const allCategoriesResult = await client.query(
      `SELECT * FROM code_category WHERE menu_objid = ANY($1)`,
      [menuObjids]
    );

    if (allCategoriesResult.rows.length === 0) {
      logger.info(`  📭 복사할 코드 카테고리 없음`);
      return { copiedCategories, copiedCodes };
    }

    // 2. 대상 회사에 이미 존재하는 카테고리 한 번에 조회
    const categoryCodes = allCategoriesResult.rows.map((c) => c.category_code);
    const existingCategoriesResult = await client.query(
      `SELECT category_code FROM code_category 
       WHERE category_code = ANY($1) AND company_code = $2`,
      [categoryCodes, targetCompanyCode]
    );
    const existingCategoryCodes = new Set(
      existingCategoriesResult.rows.map((c) => c.category_code)
    );

    // 3. 복사할 카테고리 필터링
    const categoriesToCopy = allCategoriesResult.rows.filter(
      (c) => !existingCategoryCodes.has(c.category_code)
    );

    // 4. 배치 INSERT로 카테고리 복사
    if (categoriesToCopy.length > 0) {
      const categoryValues = categoriesToCopy
        .map(
          (_, i) =>
            `($${i * 9 + 1}, $${i * 9 + 2}, $${i * 9 + 3}, $${i * 9 + 4}, $${i * 9 + 5}, $${i * 9 + 6}, NOW(), $${i * 9 + 7}, $${i * 9 + 8}, $${i * 9 + 9})`
        )
        .join(", ");

      const categoryParams = categoriesToCopy.flatMap((c) => {
        const newMenuObjid = menuIdMap.get(c.menu_objid);
        return [
          c.category_code,
          c.category_name,
          c.category_name_eng,
          c.description,
          c.sort_order,
          c.is_active,
          userId,
          targetCompanyCode,
          newMenuObjid,
        ];
      });

      await client.query(
        `INSERT INTO code_category (
          category_code, category_name, category_name_eng, description,
          sort_order, is_active, created_date, created_by, company_code, menu_objid
        ) VALUES ${categoryValues}`,
        categoryParams
      );

      copiedCategories = categoriesToCopy.length;
      logger.info(`  ✅ 코드 카테고리 ${copiedCategories}개 복사`);
    }

    // 5. 모든 원본 코드 한 번에 조회
    const allCodesResult = await client.query(
      `SELECT * FROM code_info WHERE menu_objid = ANY($1)`,
      [menuObjids]
    );

    if (allCodesResult.rows.length === 0) {
      logger.info(`  📭 복사할 코드 없음`);
      return { copiedCategories, copiedCodes };
    }

    // 6. 대상 회사에 이미 존재하는 코드 한 번에 조회
    const existingCodesResult = await client.query(
      `SELECT code_category, code_value FROM code_info 
       WHERE menu_objid = ANY($1) AND company_code = $2`,
      [Array.from(menuIdMap.values()), targetCompanyCode]
    );
    const existingCodeKeys = new Set(
      existingCodesResult.rows.map((c) => `${c.code_category}|${c.code_value}`)
    );

    // 7. 복사할 코드 필터링
    const codesToCopy = allCodesResult.rows.filter(
      (c) => !existingCodeKeys.has(`${c.code_category}|${c.code_value}`)
    );

    // 8. 배치 INSERT로 코드 복사
    if (codesToCopy.length > 0) {
      const codeValues = codesToCopy
        .map(
          (_, i) =>
            `($${i * 10 + 1}, $${i * 10 + 2}, $${i * 10 + 3}, $${i * 10 + 4}, $${i * 10 + 5}, $${i * 10 + 6}, $${i * 10 + 7}, NOW(), $${i * 10 + 8}, $${i * 10 + 9}, $${i * 10 + 10})`
        )
        .join(", ");

      const codeParams = codesToCopy.flatMap((c) => {
        const newMenuObjid = menuIdMap.get(c.menu_objid);
        return [
          c.code_category,
          c.code_value,
          c.code_name,
          c.code_name_eng,
          c.description,
          c.sort_order,
          c.is_active,
          userId,
          targetCompanyCode,
          newMenuObjid,
        ];
      });

      await client.query(
        `INSERT INTO code_info (
          code_category, code_value, code_name, code_name_eng, description,
          sort_order, is_active, created_date, created_by, company_code, menu_objid
        ) VALUES ${codeValues}`,
        codeParams
      );

      copiedCodes = codesToCopy.length;
      logger.info(`  ✅ 코드 ${copiedCodes}개 복사`);
    }

    logger.info(
      `✅ 코드 카테고리 + 코드 복사 완료: 카테고리 ${copiedCategories}개, 코드 ${copiedCodes}개`
    );
    return { copiedCategories, copiedCodes };
  }

  /**
   * 화면에서 참조하는 채번규칙 매핑 보완
   * 화면 properties에서 참조하는 채번규칙 중 아직 매핑되지 않은 것들을
   * 대상 회사에서 같은 이름(rule_name)의 채번규칙으로 매핑
   */
  private async supplementNumberingRuleMapping(
    screenIds: number[],
    sourceCompanyCode: string,
    targetCompanyCode: string,
    numberingRuleIdMap: Map<string, string>,
    client: PoolClient
  ): Promise<void> {
    if (screenIds.length === 0) return;

    // 1. 화면 레이아웃에서 모든 채번규칙 ID 추출
    const layoutsResult = await client.query(
      `SELECT properties::text as props FROM screen_layouts WHERE screen_id = ANY($1)`,
      [screenIds]
    );

    const referencedRuleIds = new Set<string>();
    const ruleIdRegex = /"numberingRuleId"\s*:\s*"([^"]+)"/g;

    for (const row of layoutsResult.rows) {
      if (!row.props) continue;
      let match;
      while ((match = ruleIdRegex.exec(row.props)) !== null) {
        const ruleId = match[1];
        // 이미 매핑된 것은 제외
        if (ruleId && !numberingRuleIdMap.has(ruleId)) {
          referencedRuleIds.add(ruleId);
        }
      }
    }

    if (referencedRuleIds.size === 0) {
      logger.info(`  📭 추가 매핑 필요 없음`);
      return;
    }

    logger.info(`  🔍 매핑 필요한 채번규칙: ${referencedRuleIds.size}개`);

    // 2. 원본 채번규칙 정보 조회 (rule_name으로 대상 회사에서 찾기 위해)
    const sourceRulesResult = await client.query(
      `SELECT rule_id, rule_name, table_name FROM numbering_rules 
       WHERE rule_id = ANY($1)`,
      [Array.from(referencedRuleIds)]
    );

    if (sourceRulesResult.rows.length === 0) {
      logger.warn(
        `  ⚠️ 원본 채번규칙 조회 실패: ${Array.from(referencedRuleIds).join(", ")}`
      );
      return;
    }

    // 3. 대상 회사에서 같은 이름의 채번규칙 찾기
    const ruleNames = sourceRulesResult.rows.map((r) => r.rule_name);
    const targetRulesResult = await client.query(
      `SELECT rule_id, rule_name, table_name FROM numbering_rules 
       WHERE rule_name = ANY($1) AND company_code = $2`,
      [ruleNames, targetCompanyCode]
    );

    // rule_name -> target_rule_id 매핑
    const targetRulesByName = new Map<string, string>();
    for (const r of targetRulesResult.rows) {
      // 같은 이름이 여러 개일 수 있으므로 첫 번째만 사용
      if (!targetRulesByName.has(r.rule_name)) {
        targetRulesByName.set(r.rule_name, r.rule_id);
      }
    }

    // 4. 매핑 추가
    let mappedCount = 0;
    for (const sourceRule of sourceRulesResult.rows) {
      const targetRuleId = targetRulesByName.get(sourceRule.rule_name);
      if (targetRuleId) {
        numberingRuleIdMap.set(sourceRule.rule_id, targetRuleId);
        logger.info(
          `  ✅ 채번규칙 매핑 추가: ${sourceRule.rule_id} (${sourceRule.rule_name}) → ${targetRuleId}`
        );
        mappedCount++;
      } else {
        logger.warn(
          `  ⚠️ 대상 회사에 같은 이름의 채번규칙 없음: ${sourceRule.rule_name}`
        );
      }
    }

    logger.info(
      `  ✅ 채번규칙 매핑 보완 완료: ${mappedCount}/${referencedRuleIds.size}개`
    );
  }

  /**
   * 채번 규칙 복사 (새 스키마: table_name + column_name 기반)
   * 프론트엔드에서 /numbering-rules/copy-for-company API를 별도 호출하므로
   * 이 함수는 ruleIdMap 생성만 담당 (실제 복제는 numberingRuleService에서 처리)
   */
  private async copyNumberingRulesWithMap(
    menuObjids: number[],
    menuIdMap: Map<number, number>,
    targetCompanyCode: string,
    userId: string,
    client: PoolClient
  ): Promise<{ copiedCount: number; ruleIdMap: Map<string, string> }> {
    const ruleIdMap = new Map<string, string>();

    // 새 스키마에서는 채번규칙이 메뉴와 직접 연결되지 않음
    // 프론트엔드에서 /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]
    );

    // 대상 회사의 채번규칙 조회 (이름 기준 매핑)
    const targetRulesResult = await client.query(
      `SELECT rule_id, rule_name FROM numbering_rules WHERE company_code = $1`,
      [targetCompanyCode]
    );

    const targetRulesByName = new Map(
      targetRulesResult.rows.map((r: any) => [r.rule_name, r.rule_id])
    );

    // 이름 기준으로 매핑 생성
    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}`);
      }
    }

    logger.info(`  📋 채번규칙 매핑 완료: ${ruleIdMap.size}개`);
    
    // 실제 복제는 프론트엔드에서 별도 API 호출로 처리됨
    return { copiedCount: 0, ruleIdMap };
  }


  /**
   * 카테고리 매핑 + 값 복사 (최적화: 배치 조회)
   *
   * 화면에서 사용하는 table_name + column_name 조합을 기준으로 카테고리 값 복사
   * menu_objid 기준이 아닌 화면 컴포넌트 기준으로 복사하여 누락 방지
   */
  private async copyCategoryMappingsAndValues(
    menuObjids: number[],
    menuIdMap: Map<number, number>,
    sourceCompanyCode: string,
    targetCompanyCode: string,
    screenIds: number[],
    userId: string,
    client: PoolClient
  ): Promise<number> {
    let copiedCount = 0;

    if (menuObjids.length === 0) {
      return copiedCount;
    }

    // === 최적화: 배치 조회 ===
    // 1. 모든 원본 카테고리 매핑 한 번에 조회
    const allMappingsResult = await client.query(
      `SELECT * FROM category_column_mapping WHERE menu_objid = ANY($1)`,
      [menuObjids]
    );

    if (allMappingsResult.rows.length === 0) {
      logger.info(`  📭 복사할 카테고리 매핑 없음`);
      return copiedCount;
    }

    // 2. 대상 회사에 이미 존재하는 매핑 한 번에 조회
    const existingMappingsResult = await client.query(
      `SELECT mapping_id, table_name, logical_column_name 
       FROM category_column_mapping WHERE company_code = $1`,
      [targetCompanyCode]
    );
    const existingMappingKeys = new Map(
      existingMappingsResult.rows.map((m) => [
        `${m.table_name}|${m.logical_column_name}`,
        m.mapping_id,
      ])
    );

    // 3. 복사할 매핑 필터링 및 기존 매핑 업데이트 대상 분류
    const mappingsToCopy: any[] = [];
    const mappingsToUpdate: Array<{ mappingId: number; newMenuObjid: number }> =
      [];

    for (const m of allMappingsResult.rows) {
      const key = `${m.table_name}|${m.logical_column_name}`;
      if (existingMappingKeys.has(key)) {
        // 기존 매핑은 menu_objid만 업데이트
        const existingMappingId = existingMappingKeys.get(key);
        const newMenuObjid = menuIdMap.get(m.menu_objid);
        if (existingMappingId && newMenuObjid) {
          mappingsToUpdate.push({ mappingId: existingMappingId, newMenuObjid });
        }
      } else {
        mappingsToCopy.push(m);
      }
    }

    // 새 매핑 ID -> 원본 매핑 정보 추적
    const mappingInsertInfo: Array<{ mapping: any; newMenuObjid: number }> = [];

    if (mappingsToCopy.length > 0) {
      const mappingValues = mappingsToCopy
        .map(
          (_, i) =>
            `($${i * 7 + 1}, $${i * 7 + 2}, $${i * 7 + 3}, $${i * 7 + 4}, $${i * 7 + 5}, $${i * 7 + 6}, NOW(), $${i * 7 + 7})`
        )
        .join(", ");

      const mappingParams = mappingsToCopy.flatMap((m) => {
        const newMenuObjid = menuIdMap.get(m.menu_objid) || 0;
        mappingInsertInfo.push({ mapping: m, newMenuObjid });
        return [
          m.table_name,
          m.logical_column_name,
          m.physical_column_name,
          newMenuObjid,
          targetCompanyCode,
          m.description,
          userId,
        ];
      });

      const insertResult = await client.query(
        `INSERT INTO category_column_mapping (
          table_name, logical_column_name, physical_column_name,
          menu_objid, company_code, description, created_at, created_by
        ) VALUES ${mappingValues}
        RETURNING mapping_id`,
        mappingParams
      );

      // 새로 생성된 매핑 ID를 기존 매핑 맵에 추가
      insertResult.rows.forEach((row, index) => {
        const m = mappingsToCopy[index];
        existingMappingKeys.set(
          `${m.table_name}|${m.logical_column_name}`,
          row.mapping_id
        );
      });

      copiedCount = mappingsToCopy.length;
      logger.info(`  ✅ 카테고리 매핑 ${copiedCount}개 복사`);
    }

    // 3-1. 기존 카테고리 매핑의 menu_objid 업데이트 (새 메뉴와 연결) - 배치 처리
    if (mappingsToUpdate.length > 0) {
      // CASE WHEN을 사용한 배치 업데이트
      const caseWhen = mappingsToUpdate
        .map((_, i) => `WHEN mapping_id = $${i * 2 + 1} THEN $${i * 2 + 2}`)
        .join(" ");
      const mappingIdsForUpdate = mappingsToUpdate.map((m) => m.mappingId);
      const params = mappingsToUpdate.flatMap((m) => [
        m.mappingId,
        m.newMenuObjid,
      ]);

      await client.query(
        `UPDATE category_column_mapping 
         SET menu_objid = CASE ${caseWhen} END
         WHERE mapping_id = ANY($${params.length + 1}) AND company_code = $${params.length + 2}`,
        [...params, mappingIdsForUpdate, targetCompanyCode]
      );
      logger.info(
        `  ✅ 기존 카테고리 매핑 ${mappingsToUpdate.length}개 메뉴 연결 갱신`
      );
    }

    // 4. 화면에서 사용하는 카테고리 컬럼 조합 수집
    // 복사된 화면의 레이아웃에서 webType='category'인 컴포넌트의 tableName, columnName 추출
    const categoryColumnsResult = await client.query(
      `SELECT DISTINCT 
         sl.properties->>'tableName' as table_name,
         sl.properties->>'columnName' as column_name
       FROM screen_layouts sl
       JOIN screen_definitions sd ON sl.screen_id = sd.screen_id
       WHERE sd.screen_id = ANY($1)
         AND sl.properties->'componentConfig'->>'webType' = 'category'
         AND sl.properties->>'tableName' IS NOT NULL
         AND sl.properties->>'columnName' IS NOT NULL`,
      [screenIds]
    );

    // 카테고리 매핑에서 사용하는 table_name, column_name도 추가
    const mappingColumnsResult = await client.query(
      `SELECT DISTINCT table_name, logical_column_name as column_name
       FROM category_column_mapping
       WHERE menu_objid = ANY($1)`,
      [menuObjids]
    );

    // 두 결과 합치기
    const categoryColumns = new Set<string>();
    for (const row of categoryColumnsResult.rows) {
      if (row.table_name && row.column_name) {
        categoryColumns.add(`${row.table_name}|${row.column_name}`);
      }
    }
    for (const row of mappingColumnsResult.rows) {
      if (row.table_name && row.column_name) {
        categoryColumns.add(`${row.table_name}|${row.column_name}`);
      }
    }

    logger.info(
      `  📋 화면에서 사용하는 카테고리 컬럼: ${categoryColumns.size}개`
    );

    if (categoryColumns.size === 0) {
      logger.info(`✅ 카테고리 매핑 + 값 복사 완료: ${copiedCount}개`);
      return copiedCount;
    }

    // 5. 원본 회사의 카테고리 값 조회 (table_name + column_name 기준)
    // menu_objid 조건 대신 table_name + column_name + 원본 회사 코드로 조회
    const columnConditions = Array.from(categoryColumns).map((col, i) => {
      const [tableName, columnName] = col.split("|");
      return `(table_name = $${i * 2 + 2} AND column_name = $${i * 2 + 3})`;
    });

    const columnParams: string[] = [];
    for (const col of categoryColumns) {
      const [tableName, columnName] = col.split("|");
      columnParams.push(tableName, columnName);
    }

    const allValuesResult = await client.query(
      `SELECT * FROM category_values 
       WHERE company_code = $1
         AND (${columnConditions.join(" OR ")})
       ORDER BY depth NULLS FIRST, parent_value_id NULLS FIRST, value_order`,
      [sourceCompanyCode, ...columnParams]
    );

    if (allValuesResult.rows.length === 0) {
      logger.info(`✅ 카테고리 매핑 + 값 복사 완료: ${copiedCount}개`);
      return copiedCount;
    }

    logger.info(`  📋 원본 카테고리 값: ${allValuesResult.rows.length}개 발견`);

    // 5. 대상 회사에 이미 존재하는 값 한 번에 조회
    const existingValuesResult = await client.query(
      `SELECT value_id, table_name, column_name, value_code 
       FROM category_values WHERE company_code = $1`,
      [targetCompanyCode]
    );
    const existingValueKeys = new Map(
      existingValuesResult.rows.map((v) => [
        `${v.table_name}|${v.column_name}|${v.value_code}`,
        v.value_id,
      ])
    );

    // 6. 값 복사 (부모-자식 관계 유지를 위해 depth 순서로 처리)
    const valueIdMap = new Map<number, number>();
    let copiedValues = 0;

    // 이미 존재하는 값들의 ID 매핑
    for (const value of allValuesResult.rows) {
      const key = `${value.table_name}|${value.column_name}|${value.value_code}`;
      const existingId = existingValueKeys.get(key);
      if (existingId) {
        valueIdMap.set(value.value_id, existingId);
      }
    }

    // depth별로 그룹핑하여 배치 처리 (부모가 먼저 삽입되어야 함)
    const valuesByDepth = new Map<number, any[]>();
    for (const value of allValuesResult.rows) {
      const key = `${value.table_name}|${value.column_name}|${value.value_code}`;
      if (existingValueKeys.has(key)) continue; // 이미 존재하면 스킵

      const depth = value.depth ?? 0;
      if (!valuesByDepth.has(depth)) {
        valuesByDepth.set(depth, []);
      }
      valuesByDepth.get(depth)!.push(value);
    }

    // depth 순서대로 처리
    const sortedDepths = Array.from(valuesByDepth.keys()).sort((a, b) => a - b);

    for (const depth of sortedDepths) {
      const values = valuesByDepth.get(depth)!;
      if (values.length === 0) continue;

      const valueStrings = values
        .map(
          (_, i) =>
            `($${i * 15 + 1}, $${i * 15 + 2}, $${i * 15 + 3}, $${i * 15 + 4}, $${i * 15 + 5}, $${i * 15 + 6}, $${i * 15 + 7}, $${i * 15 + 8}, $${i * 15 + 9}, $${i * 15 + 10}, $${i * 15 + 11}, $${i * 15 + 12}, NOW(), $${i * 15 + 13}, $${i * 15 + 14}, $${i * 15 + 15})`
        )
        .join(", ");

      // 기본 menu_objid: 매핑이 없을 경우 첫 번째 복사된 메뉴 사용
      const defaultMenuObjid = menuIdMap.values().next().value || 0;

      const valueParams = values.flatMap((v) => {
        // 원본 menu_objid가 매핑에 있으면 사용, 없으면 기본값 사용
        const newMenuObjid = menuIdMap.get(v.menu_objid) ?? defaultMenuObjid;
        const newParentId = v.parent_value_id
          ? valueIdMap.get(v.parent_value_id) || null
          : null;
        return [
          v.table_name,
          v.column_name,
          v.value_code,
          v.value_label,
          v.value_order,
          newParentId,
          v.depth,
          v.description,
          v.color,
          v.icon,
          v.is_active,
          v.is_default,
          userId,
          targetCompanyCode,
          newMenuObjid,
        ];
      });

      const insertResult = await client.query(
        `INSERT INTO category_values (
          table_name, column_name, value_code, value_label, value_order,
          parent_value_id, depth, description, color, icon,
          is_active, is_default, created_at, created_by, company_code, menu_objid
        ) VALUES ${valueStrings}
        RETURNING value_id`,
        valueParams
      );

      // 새 value_id 매핑
      insertResult.rows.forEach((row, index) => {
        valueIdMap.set(values[index].value_id, row.value_id);
      });

      copiedValues += values.length;
    }

    if (copiedValues > 0) {
      logger.info(`  ✅ 카테고리 값 ${copiedValues}개 복사`);
    }

    logger.info(`✅ 카테고리 매핑 + 값 복사 완료: ${copiedCount}개`);
    return copiedCount;
  }

  /**
   * 테이블 타입관리 입력타입 설정 복사 (최적화: 배치 조회/삽입)
   * - 복사된 화면에서 사용하는 테이블들의 table_type_columns 설정을 대상 회사로 복사
   */
  private async copyTableTypeColumns(
    screenIds: number[],
    sourceCompanyCode: string,
    targetCompanyCode: string,
    client: PoolClient
  ): Promise<number> {
    if (screenIds.length === 0) {
      return 0;
    }

    logger.info(`📋 테이블 타입 설정 복사 시작`);

    // === 최적화: 배치 조회 ===
    // 1. 복사된 화면에서 사용하는 테이블 목록 조회
    const tablesResult = await client.query<{ table_name: string }>(
      `SELECT DISTINCT table_name FROM screen_definitions 
       WHERE screen_id = ANY($1) AND table_name IS NOT NULL AND table_name != ''`,
      [screenIds]
    );

    if (tablesResult.rows.length === 0) {
      logger.info("  ⚠️ 복사된 화면에 테이블이 없음");
      return 0;
    }

    const tableNames = tablesResult.rows.map((r) => r.table_name);
    logger.info(`  사용 테이블: ${tableNames.join(", ")}`);

    // 2. 원본 회사의 모든 테이블 타입 설정 한 번에 조회
    const sourceSettingsResult = await client.query(
      `SELECT * FROM table_type_columns 
       WHERE table_name = ANY($1) AND company_code = $2`,
      [tableNames, sourceCompanyCode]
    );

    if (sourceSettingsResult.rows.length === 0) {
      logger.info(`  ⚠️ 원본 회사 설정 없음`);
      return 0;
    }

    // 3. 대상 회사의 기존 설정 한 번에 조회
    const existingSettingsResult = await client.query(
      `SELECT table_name, column_name FROM table_type_columns 
       WHERE table_name = ANY($1) AND company_code = $2`,
      [tableNames, targetCompanyCode]
    );
    const existingKeys = new Set(
      existingSettingsResult.rows.map((s) => `${s.table_name}|${s.column_name}`)
    );

    // 4. 복사할 설정 필터링
    const settingsToCopy = sourceSettingsResult.rows.filter(
      (s) => !existingKeys.has(`${s.table_name}|${s.column_name}`)
    );

    logger.info(
      `  원본 설정: ${sourceSettingsResult.rows.length}개, 복사 대상: ${settingsToCopy.length}개`
    );

    // 5. 배치 INSERT
    if (settingsToCopy.length > 0) {
      const settingValues = settingsToCopy
        .map(
          (_, i) =>
            `($${i * 7 + 1}, $${i * 7 + 2}, $${i * 7 + 3}, $${i * 7 + 4}, $${i * 7 + 5}, $${i * 7 + 6}, NOW(), NOW(), $${i * 7 + 7})`
        )
        .join(", ");

      const settingParams = settingsToCopy.flatMap((s) => [
        s.table_name,
        s.column_name,
        s.input_type,
        s.detail_settings,
        s.is_nullable,
        s.display_order,
        targetCompanyCode,
      ]);

      await client.query(
        `INSERT INTO table_type_columns (
          table_name, column_name, input_type, detail_settings, 
          is_nullable, display_order, created_date, updated_date, company_code
        ) VALUES ${settingValues}`,
        settingParams
      );
    }

    logger.info(`✅ 테이블 타입 설정 복사 완료: ${settingsToCopy.length}개`);
    return settingsToCopy.length;
  }

  /**
   * 연쇄관계 복사 (최적화: 배치 조회/삽입)
   * - category_value_cascading_group + category_value_cascading_mapping
   * - cascading_relation (테이블 기반)
   */
  private async copyCascadingRelations(
    sourceCompanyCode: string,
    targetCompanyCode: string,
    menuIdMap: Map<number, number>,
    userId: string,
    client: PoolClient
  ): Promise<number> {
    logger.info(`📋 연쇄관계 복사 시작`);
    let copiedCount = 0;

    // === 1. category_value_cascading_group 복사 ===
    const groupsResult = await client.query(
      `SELECT * FROM category_value_cascading_group 
       WHERE company_code = $1 AND is_active = 'Y'`,
      [sourceCompanyCode]
    );

    if (groupsResult.rows.length === 0) {
      logger.info(`  카테고리 값 연쇄 그룹: 0개`);
    } else {
      logger.info(`  카테고리 값 연쇄 그룹: ${groupsResult.rows.length}개`);

      // 대상 회사의 기존 그룹 한 번에 조회
      const existingGroupsResult = await client.query(
        `SELECT group_id, relation_code FROM category_value_cascading_group 
         WHERE company_code = $1`,
        [targetCompanyCode]
      );
      const existingGroupsByCode = new Map(
        existingGroupsResult.rows.map((g) => [g.relation_code, g.group_id])
      );

      // group_id 매핑
      const groupIdMap = new Map<number, number>();
      const groupsToCopy: any[] = [];

      for (const group of groupsResult.rows) {
        const existingGroupId = existingGroupsByCode.get(group.relation_code);
        if (existingGroupId) {
          groupIdMap.set(group.group_id, existingGroupId);
        } else {
          groupsToCopy.push(group);
        }
      }

      logger.info(
        `    기존: ${groupsResult.rows.length - groupsToCopy.length}개, 신규: ${groupsToCopy.length}개`
      );

      // 그룹별로 삽입하고 매핑 저장 (RETURNING이 필요해서 배치 불가)
      for (const group of groupsToCopy) {
        const newParentMenuObjid = group.parent_menu_objid
          ? menuIdMap.get(Number(group.parent_menu_objid)) || null
          : null;
        const newChildMenuObjid = group.child_menu_objid
          ? menuIdMap.get(Number(group.child_menu_objid)) || null
          : null;

        const insertResult = await client.query(
          `INSERT INTO category_value_cascading_group (
            relation_code, relation_name, description,
            parent_table_name, parent_column_name, parent_menu_objid,
            child_table_name, child_column_name, child_menu_objid,
            clear_on_parent_change, show_group_label,
            empty_parent_message, no_options_message,
            company_code, is_active, created_by, created_date
          ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, NOW())
          RETURNING group_id`,
          [
            group.relation_code,
            group.relation_name,
            group.description,
            group.parent_table_name,
            group.parent_column_name,
            newParentMenuObjid,
            group.child_table_name,
            group.child_column_name,
            newChildMenuObjid,
            group.clear_on_parent_change,
            group.show_group_label,
            group.empty_parent_message,
            group.no_options_message,
            targetCompanyCode,
            "Y",
            userId,
          ]
        );

        const newGroupId = insertResult.rows[0].group_id;
        groupIdMap.set(group.group_id, newGroupId);
        copiedCount++;
      }

      // 모든 매핑 한 번에 조회 (복사할 그룹만)
      const groupIdsToCopy = groupsToCopy.map((g) => g.group_id);
      if (groupIdsToCopy.length > 0) {
        const allMappingsResult = await client.query(
          `SELECT * FROM category_value_cascading_mapping 
           WHERE group_id = ANY($1) AND company_code = $2
           ORDER BY group_id, display_order`,
          [groupIdsToCopy, sourceCompanyCode]
        );

        // 배치 INSERT
        if (allMappingsResult.rows.length > 0) {
          const mappingValues = allMappingsResult.rows
            .map(
              (_, i) =>
                `($${i * 8 + 1}, $${i * 8 + 2}, $${i * 8 + 3}, $${i * 8 + 4}, $${i * 8 + 5}, $${i * 8 + 6}, $${i * 8 + 7}, $${i * 8 + 8}, NOW())`
            )
            .join(", ");

          const mappingParams = allMappingsResult.rows.flatMap((m) => {
            const newGroupId = groupIdMap.get(m.group_id);
            return [
              newGroupId,
              m.parent_value_code,
              m.parent_value_label,
              m.child_value_code,
              m.child_value_label,
              m.display_order,
              targetCompanyCode,
              "Y",
            ];
          });

          await client.query(
            `INSERT INTO category_value_cascading_mapping (
              group_id, parent_value_code, parent_value_label,
              child_value_code, child_value_label, display_order,
              company_code, is_active, created_date
            ) VALUES ${mappingValues}`,
            mappingParams
          );

          logger.info(`    ↳ 매핑 ${allMappingsResult.rows.length}개 복사`);
        }
      }
    }

    // === 2. cascading_relation 복사 (테이블 기반) ===
    const relationsResult = await client.query(
      `SELECT * FROM cascading_relation 
       WHERE company_code = $1 AND is_active = 'Y'`,
      [sourceCompanyCode]
    );

    if (relationsResult.rows.length === 0) {
      logger.info(`  기본 연쇄관계: 0개`);
    } else {
      logger.info(`  기본 연쇄관계: ${relationsResult.rows.length}개`);

      // 대상 회사의 기존 관계 한 번에 조회
      const existingRelationsResult = await client.query(
        `SELECT relation_code FROM cascading_relation 
         WHERE company_code = $1`,
        [targetCompanyCode]
      );
      const existingRelationCodes = new Set(
        existingRelationsResult.rows.map((r) => r.relation_code)
      );

      // 복사할 관계 필터링
      const relationsToCopy = relationsResult.rows.filter(
        (r) => !existingRelationCodes.has(r.relation_code)
      );

      logger.info(
        `    기존: ${relationsResult.rows.length - relationsToCopy.length}개, 신규: ${relationsToCopy.length}개`
      );

      // 배치 INSERT
      if (relationsToCopy.length > 0) {
        const relationValues = relationsToCopy
          .map(
            (_, i) =>
              `($${i * 19 + 1}, $${i * 19 + 2}, $${i * 19 + 3}, $${i * 19 + 4}, $${i * 19 + 5}, $${i * 19 + 6}, $${i * 19 + 7}, $${i * 19 + 8}, $${i * 19 + 9}, $${i * 19 + 10}, $${i * 19 + 11}, $${i * 19 + 12}, $${i * 19 + 13}, $${i * 19 + 14}, $${i * 19 + 15}, $${i * 19 + 16}, $${i * 19 + 17}, $${i * 19 + 18}, $${i * 19 + 19}, NOW())`
          )
          .join(", ");

        const relationParams = relationsToCopy.flatMap((r) => [
          r.relation_code,
          r.relation_name,
          r.description,
          r.parent_table,
          r.parent_value_column,
          r.parent_label_column,
          r.child_table,
          r.child_filter_column,
          r.child_value_column,
          r.child_label_column,
          r.child_order_column,
          r.child_order_direction,
          r.empty_parent_message,
          r.no_options_message,
          r.loading_message,
          r.clear_on_parent_change,
          targetCompanyCode,
          "Y",
          userId,
        ]);

        await client.query(
          `INSERT INTO cascading_relation (
            relation_code, relation_name, description,
            parent_table, parent_value_column, parent_label_column,
            child_table, child_filter_column, child_value_column, child_label_column,
            child_order_column, child_order_direction,
            empty_parent_message, no_options_message, loading_message,
            clear_on_parent_change, company_code, is_active, created_by, created_date
          ) VALUES ${relationValues}`,
          relationParams
        );

        copiedCount += relationsToCopy.length;
      }
    }

    logger.info(`✅ 연쇄관계 복사 완료: ${copiedCount}개`);
    return copiedCount;
  }

  /**
   * 노드 플로우 복사 (node_flows 테이블 - 제어관리에서 사용)
   * - 원본 회사의 모든 node_flows를 대상 회사로 복사
   * - 대상 회사에 같은 이름의 노드 플로우가 있으면 재사용
   * - 없으면 새로 복사 (flow_data 포함)
   * - 원본 ID → 새 ID 매핑 반환 (버튼의 flowId, selectedDiagramId 매핑용)
   */
  private async copyNodeFlows(
    sourceCompanyCode: string,
    targetCompanyCode: string,
    client: PoolClient
  ): Promise<{ copiedCount: number; nodeFlowIdMap: Map<number, number> }> {
    logger.info(`📋 노드 플로우(제어관리) 복사 시작`);
    const nodeFlowIdMap = new Map<number, number>();
    let copiedCount = 0;

    // 1. 원본 회사의 모든 node_flows 조회
    const sourceFlowsResult = await client.query(
      `SELECT * FROM node_flows WHERE company_code = $1`,
      [sourceCompanyCode]
    );

    if (sourceFlowsResult.rows.length === 0) {
      logger.info(`  📭 원본 회사에 노드 플로우 없음`);
      return { copiedCount: 0, nodeFlowIdMap };
    }

    logger.info(`  📋 원본 노드 플로우: ${sourceFlowsResult.rows.length}개`);

    // 2. 대상 회사의 기존 노드 플로우 조회 (이름 기준)
    const existingFlowsResult = await client.query(
      `SELECT flow_id, flow_name FROM node_flows WHERE company_code = $1`,
      [targetCompanyCode]
    );
    const existingFlowsByName = new Map<string, number>(
      existingFlowsResult.rows.map((f) => [f.flow_name, f.flow_id])
    );

    // 3. 복사할 플로우 필터링 + 기존 플로우 매핑
    const flowsToCopy: any[] = [];
    for (const flow of sourceFlowsResult.rows) {
      const existingId = existingFlowsByName.get(flow.flow_name);
      if (existingId) {
        // 기존 플로우 재사용 - ID 매핑 추가
        nodeFlowIdMap.set(flow.flow_id, existingId);
        logger.info(`    ♻️ 기존 노드 플로우 재사용: ${flow.flow_name} (${flow.flow_id} → ${existingId})`);
      } else {
        flowsToCopy.push(flow);
      }
    }

    if (flowsToCopy.length === 0) {
      logger.info(`  📭 모든 노드 플로우가 이미 존재함 (매핑 ${nodeFlowIdMap.size}개)`);
      return { copiedCount: 0, nodeFlowIdMap };
    }

    logger.info(`  🔄 복사할 노드 플로우: ${flowsToCopy.length}개`);

    // 4. 개별 INSERT (RETURNING으로 새 ID 획득)
    for (const flow of flowsToCopy) {
      const insertResult = await client.query(
        `INSERT INTO node_flows (flow_name, flow_description, flow_data, company_code)
         VALUES ($1, $2, $3, $4)
         RETURNING flow_id`,
        [
          flow.flow_name,
          flow.flow_description,
          JSON.stringify(flow.flow_data),
          targetCompanyCode,
        ]
      );

      const newFlowId = insertResult.rows[0].flow_id;
      nodeFlowIdMap.set(flow.flow_id, newFlowId);
      logger.info(`    ➕ 노드 플로우 복사: ${flow.flow_name} (${flow.flow_id} → ${newFlowId})`);
      copiedCount++;
    }

    logger.info(`  ✅ 노드 플로우 복사 완료: ${copiedCount}개, 매핑 ${nodeFlowIdMap.size}개`);

    return { copiedCount, nodeFlowIdMap };
  }

  /**
   * 데이터플로우 다이어그램 복사 (dataflow_diagrams 테이블 - 버튼 제어 설정에서 사용)
   * - 원본 회사의 모든 dataflow_diagrams를 대상 회사로 복사
   * - 대상 회사에 같은 이름의 다이어그램이 있으면 재사용
   * - 없으면 새로 복사 (relationships, node_positions, control, plan, category 포함)
   * - 원본 ID → 새 ID 매핑 반환
   */
  private async copyDataflowDiagrams(
    sourceCompanyCode: string,
    targetCompanyCode: string,
    userId: string,
    client: PoolClient
  ): Promise<{ copiedCount: number; diagramIdMap: Map<number, number> }> {
    logger.info(`📋 데이터플로우 다이어그램(버튼 제어) 복사 시작`);
    const diagramIdMap = new Map<number, number>();
    let copiedCount = 0;

    // 1. 원본 회사의 모든 dataflow_diagrams 조회
    const sourceDiagramsResult = await client.query(
      `SELECT * FROM dataflow_diagrams WHERE company_code = $1`,
      [sourceCompanyCode]
    );

    if (sourceDiagramsResult.rows.length === 0) {
      logger.info(`  📭 원본 회사에 데이터플로우 다이어그램 없음`);
      return { copiedCount: 0, diagramIdMap };
    }

    logger.info(`  📋 원본 데이터플로우 다이어그램: ${sourceDiagramsResult.rows.length}개`);

    // 2. 대상 회사의 기존 다이어그램 조회 (이름 기준)
    const existingDiagramsResult = await client.query(
      `SELECT diagram_id, diagram_name FROM dataflow_diagrams WHERE company_code = $1`,
      [targetCompanyCode]
    );
    const existingDiagramsByName = new Map<string, number>(
      existingDiagramsResult.rows.map((d) => [d.diagram_name, d.diagram_id])
    );

    // 3. 복사할 다이어그램 필터링 + 기존 다이어그램 매핑
    const diagramsToCopy: any[] = [];
    for (const diagram of sourceDiagramsResult.rows) {
      const existingId = existingDiagramsByName.get(diagram.diagram_name);
      if (existingId) {
        // 기존 다이어그램 재사용 - ID 매핑 추가
        diagramIdMap.set(diagram.diagram_id, existingId);
        logger.info(`    ♻️ 기존 다이어그램 재사용: ${diagram.diagram_name} (${diagram.diagram_id} → ${existingId})`);
      } else {
        diagramsToCopy.push(diagram);
      }
    }

    if (diagramsToCopy.length === 0) {
      logger.info(`  📭 모든 다이어그램이 이미 존재함 (매핑 ${diagramIdMap.size}개)`);
      return { copiedCount: 0, diagramIdMap };
    }

    logger.info(`  🔄 복사할 다이어그램: ${diagramsToCopy.length}개`);

    // 4. 개별 INSERT (RETURNING으로 새 ID 획득)
    for (const diagram of diagramsToCopy) {
      const insertResult = await client.query(
        `INSERT INTO dataflow_diagrams (diagram_name, relationships, company_code, created_by, node_positions, control, plan, category)
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
         RETURNING diagram_id`,
        [
          diagram.diagram_name,
          JSON.stringify(diagram.relationships),
          targetCompanyCode,
          userId,
          diagram.node_positions ? JSON.stringify(diagram.node_positions) : null,
          diagram.control ? JSON.stringify(diagram.control) : null,
          diagram.plan ? JSON.stringify(diagram.plan) : null,
          diagram.category ? JSON.stringify(diagram.category) : null,
        ]
      );

      const newDiagramId = insertResult.rows[0].diagram_id;
      diagramIdMap.set(diagram.diagram_id, newDiagramId);
      logger.info(`    ➕ 다이어그램 복사: ${diagram.diagram_name} (${diagram.diagram_id} → ${newDiagramId})`);
      copiedCount++;
    }

    logger.info(`  ✅ 데이터플로우 다이어그램 복사 완료: ${copiedCount}개, 매핑 ${diagramIdMap.size}개`);

    return { copiedCount, diagramIdMap };
  }
}
