// ✅ Prisma → Raw Query 전환 (Phase 2.1)
import { query, queryOne, transaction } from "../database/db";
import {
  ScreenDefinition,
  CreateScreenRequest,
  UpdateScreenRequest,
  LayoutData,
  SaveLayoutRequest,
  ScreenTemplate,
  MenuAssignmentRequest,
  PaginatedResponse,
  ComponentData,
  ColumnInfo,
  ColumnWebTypeSetting,
  WebType,
  WidgetData,
} from "../types/screen";

import { generateId } from "../utils/generateId";
import logger from "../utils/logger";
import {
  reconstructConfig,
  extractConfigDiff,
} from "../utils/componentDefaults";

// 화면 복사 요청 인터페이스
interface CopyScreenRequest {
  screenName: string;
  screenCode: string;
  description?: string;
  companyCode: string; // 요청한 사용자의 회사 코드 (인증용)
  createdBy?: string; // 생성자 ID
  targetCompanyCode?: string; // 복사 대상 회사 코드 (최고 관리자 전용)
}

// 백엔드에서 사용할 테이블 정보 타입
interface TableInfo {
  tableName: string;
  tableLabel: string;
  columns: ColumnInfo[];
}

export class ScreenManagementService {
  // ========================================
  // 화면 정의 관리
  // ========================================

  /**
   * 화면 정의 생성 (✅ Raw Query 전환 완료)
   */
  async createScreen(
    screenData: CreateScreenRequest,
    userCompanyCode: string,
  ): Promise<ScreenDefinition> {
    console.log(`=== 화면 생성 요청 ===`);
    console.log(`요청 데이터:`, screenData);
    console.log(`사용자 회사 코드:`, userCompanyCode);

    // 화면 코드 중복 확인 (Raw Query)
    const existingResult = await query<{ screen_id: number }>(
      `SELECT screen_id FROM screen_definitions
       WHERE screen_code = $1 AND is_active != 'D'
       LIMIT 1`,
      [screenData.screenCode],
    );

    console.log(
      `화면 코드 '${screenData.screenCode}' 중복 검사 결과:`,
      existingResult.length > 0 ? "중복됨" : "사용 가능",
    );

    if (existingResult.length > 0) {
      console.log(`기존 화면 정보:`, existingResult[0]);
      throw new Error("이미 존재하는 화면 코드입니다.");
    }

    // 화면 생성 (Raw Query) - REST API 지원 추가
    const [screen] = await query<any>(
      `INSERT INTO screen_definitions (
        screen_name, screen_code, table_name, company_code, description, created_by,
        db_source_type, db_connection_id, data_source_type, rest_api_connection_id, 
        rest_api_endpoint, rest_api_json_path
      ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
      RETURNING *`,
      [
        screenData.screenName,
        screenData.screenCode,
        screenData.tableName,
        screenData.companyCode,
        screenData.description || null,
        screenData.createdBy,
        screenData.dbSourceType || "internal",
        screenData.dbConnectionId || null,
        (screenData as any).dataSourceType || "database",
        (screenData as any).restApiConnectionId || null,
        (screenData as any).restApiEndpoint || null,
        (screenData as any).restApiJsonPath || "data",
      ],
    );

    return this.mapToScreenDefinition(screen);
  }

  /**
   * 회사별 화면 목록 조회 (페이징 지원) - 활성 화면만 (✅ Raw Query 전환 완료)
   */
  async getScreensByCompany(
    companyCode: string,
    page: number = 1,
    size: number = 20,
    searchTerm?: string,
    options?: { excludePop?: boolean },
  ): Promise<PaginatedResponse<ScreenDefinition>> {
    const offset = (page - 1) * size;

    // WHERE 절 동적 생성
    const whereConditions: string[] = ["sd.is_active != 'D'"];
    const params: any[] = [];

    if (companyCode !== "*") {
      whereConditions.push(`sd.company_code = $${params.length + 1}`);
      params.push(companyCode);
    }

    if (searchTerm && searchTerm.trim() !== "") {
      whereConditions.push(`(
        sd.screen_name ILIKE $${params.length + 1} OR 
        sd.screen_code ILIKE $${params.length + 1} OR 
        sd.table_name ILIKE $${params.length + 1}
      )`);
      params.push(`%${searchTerm.trim()}%`);
    }

    // POP 화면 제외 필터: screen_layouts_pop에 레이아웃이 있는 화면 제외
    if (options?.excludePop) {
      whereConditions.push(
        `NOT EXISTS (SELECT 1 FROM screen_layouts_pop slp WHERE slp.screen_id = sd.screen_id)`
      );
    }

    const whereSQL = whereConditions.join(" AND ");

    // 페이징 쿼리 (Raw Query)
    const [screens, totalResult] = await Promise.all([
      query<any>(
        `SELECT sd.* FROM screen_definitions sd
         WHERE ${whereSQL}
         ORDER BY sd.created_date DESC
         LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
        [...params, size, offset],
      ),
      query<{ count: string }>(
        `SELECT COUNT(*)::text as count FROM screen_definitions sd
         WHERE ${whereSQL}`,
        params,
      ),
    ]);

    const total = parseInt(totalResult[0]?.count || "0", 10);

    // 테이블 라벨 정보를 한 번에 조회 (Raw Query)
    const tableNames = Array.from(
      new Set(screens.map((s: any) => s.table_name).filter(Boolean)),
    );

    let tableLabelMap = new Map<string, string>();

    if (tableNames.length > 0) {
      try {
        const placeholders = tableNames.map((_, i) => `$${i + 1}`).join(", ");
        const tableLabels = await query<{
          table_name: string;
          table_label: string | null;
        }>(
          `SELECT table_name, table_label FROM table_labels
           WHERE table_name IN (${placeholders})`,
          tableNames,
        );

        tableLabelMap = new Map(
          tableLabels.map((tl) => [
            tl.table_name,
            tl.table_label || tl.table_name,
          ]),
        );

        // 테스트: company_mng 라벨 직접 확인
        if (tableLabelMap.has("company_mng")) {
          console.log(
            "✅ company_mng 라벨 찾음:",
            tableLabelMap.get("company_mng"),
          );
        } else {
          console.log("❌ company_mng 라벨 없음");
        }
      } catch (error) {
        console.error("테이블 라벨 조회 오류:", error);
      }
    }

    return {
      data: screens.map((screen) =>
        this.mapToScreenDefinition(screen, tableLabelMap),
      ),
      pagination: {
        page,
        size,
        total,
        totalPages: Math.ceil(total / size),
      },
    };
  }

  /**
   * 화면 목록 조회 (간단 버전) - 활성 화면만 (✅ Raw Query 전환 완료)
   */
  async getScreens(companyCode: string): Promise<ScreenDefinition[]> {
    // 동적 WHERE 절 생성
    const whereConditions: string[] = ["is_active != 'D'"];
    const params: any[] = [];

    if (companyCode !== "*") {
      whereConditions.push(`company_code = $${params.length + 1}`);
      params.push(companyCode);
    }

    const whereSQL = whereConditions.join(" AND ");

    const screens = await query<any>(
      `SELECT * FROM screen_definitions
       WHERE ${whereSQL}
       ORDER BY created_date DESC`,
      params,
    );

    return screens.map((screen) => this.mapToScreenDefinition(screen));
  }

  /**
   * 화면 정의 조회 (활성 화면만) (✅ Raw Query 전환 완료)
   */
  async getScreenById(screenId: number): Promise<ScreenDefinition | null> {
    const screens = await query<any>(
      `SELECT * FROM screen_definitions
       WHERE screen_id = $1 AND is_active != 'D'
       LIMIT 1`,
      [screenId],
    );

    return screens.length > 0 ? this.mapToScreenDefinition(screens[0]) : null;
  }

  /**
   * 화면 정의 조회 (회사 코드 검증 포함, 활성 화면만) (✅ Raw Query 전환 완료)
   */
  async getScreen(
    screenId: number,
    companyCode: string,
  ): Promise<ScreenDefinition | null> {
    // 동적 WHERE 절 생성
    const whereConditions: string[] = [
      "screen_id = $1",
      "is_active != 'D'", // 삭제된 화면 제외
    ];
    const params: any[] = [screenId];

    // 회사 코드가 '*'가 아닌 경우 회사별 필터링
    if (companyCode !== "*") {
      whereConditions.push(`company_code = $${params.length + 1}`);
      params.push(companyCode);
    }

    const whereSQL = whereConditions.join(" AND ");

    const screens = await query<any>(
      `SELECT * FROM screen_definitions
       WHERE ${whereSQL}
       LIMIT 1`,
      params,
    );

    return screens.length > 0 ? this.mapToScreenDefinition(screens[0]) : null;
  }

  /**
   * 화면 정의 수정 (✅ Raw Query 전환 완료)
   */
  async updateScreen(
    screenId: number,
    updateData: UpdateScreenRequest,
    userCompanyCode: string,
  ): Promise<ScreenDefinition> {
    // 권한 확인 (Raw Query)
    const existingResult = await query<{ company_code: string | null }>(
      `SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
      [screenId],
    );

    if (existingResult.length === 0) {
      throw new Error("화면을 찾을 수 없습니다.");
    }

    const existingScreen = existingResult[0];

    if (
      userCompanyCode !== "*" &&
      existingScreen.company_code !== userCompanyCode
    ) {
      throw new Error("이 화면을 수정할 권한이 없습니다.");
    }

    // 화면 업데이트 (Raw Query)
    const [screen] = await query<any>(
      `UPDATE screen_definitions
       SET screen_name = $1,
           description = $2,
           is_active = $3,
           updated_by = $4,
           updated_date = $5
       WHERE screen_id = $6
       RETURNING *`,
      [
        updateData.screenName,
        updateData.description || null,
        updateData.isActive ? "Y" : "N",
        updateData.updatedBy,
        new Date(),
        screenId,
      ],
    );

    return this.mapToScreenDefinition(screen);
  }

  /**
   * 화면 정보 수정 (메타데이터만) - 편집 기능용
   */
  async updateScreenInfo(
    screenId: number,
    updateData: {
      screenName: string;
      tableName?: string;
      description?: string;
      isActive: string;
      // REST API 관련 필드 추가
      dataSourceType?: string;
      dbSourceType?: string;
      dbConnectionId?: number;
      restApiConnectionId?: number;
      restApiEndpoint?: string;
      restApiJsonPath?: string;
    },
    userCompanyCode: string,
  ): Promise<void> {
    // 권한 확인
    const existingResult = await query<{ company_code: string | null }>(
      `SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
      [screenId],
    );

    if (existingResult.length === 0) {
      throw new Error("화면을 찾을 수 없습니다.");
    }

    const existingScreen = existingResult[0];

    if (
      userCompanyCode !== "*" &&
      existingScreen.company_code !== userCompanyCode
    ) {
      throw new Error("이 화면을 수정할 권한이 없습니다.");
    }

    // 화면 정보 업데이트 (REST API 필드 포함)
    await query(
      `UPDATE screen_definitions
       SET screen_name = $1,
           table_name = $2,
           description = $3,
           is_active = $4,
           updated_date = $5,
           data_source_type = $6,
           db_source_type = $7,
           db_connection_id = $8,
           rest_api_connection_id = $9,
           rest_api_endpoint = $10,
           rest_api_json_path = $11
       WHERE screen_id = $12`,
      [
        updateData.screenName,
        updateData.tableName || null,
        updateData.description || null,
        updateData.isActive,
        new Date(),
        updateData.dataSourceType || "database",
        updateData.dbSourceType || "internal",
        updateData.dbConnectionId || null,
        updateData.restApiConnectionId || null,
        updateData.restApiEndpoint || null,
        updateData.restApiJsonPath || null,
        screenId,
      ],
    );

    console.log(`화면 정보 업데이트 완료: screenId=${screenId}`, {
      dataSourceType: updateData.dataSourceType,
      restApiConnectionId: updateData.restApiConnectionId,
      restApiEndpoint: updateData.restApiEndpoint,
      restApiJsonPath: updateData.restApiJsonPath,
    });
  }

  /**
   * 화면의 메인 테이블명만 업데이트
   */
  async updateScreenTableName(
    screenId: number,
    tableName: string,
    userCompanyCode: string,
  ): Promise<void> {
    const existingResult = await query<{ company_code: string | null }>(
      `SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
      [screenId],
    );

    if (existingResult.length === 0) {
      throw new Error("화면을 찾을 수 없습니다.");
    }

    if (
      userCompanyCode !== "*" &&
      existingResult[0].company_code !== userCompanyCode
    ) {
      throw new Error("이 화면을 수정할 권한이 없습니다.");
    }

    await query(
      `UPDATE screen_definitions SET table_name = $1, updated_date = NOW() WHERE screen_id = $2`,
      [tableName, screenId],
    );

    console.log(`화면 테이블명 업데이트 완료: screenId=${screenId}, tableName=${tableName}`);
  }

  /**
   * 화면 의존성 체크 - 다른 화면에서 이 화면을 참조하는지 확인
   */
  async checkScreenDependencies(
    screenId: number,
    userCompanyCode: string,
  ): Promise<{
    hasDependencies: boolean;
    dependencies: Array<{
      screenId: number;
      screenName: string;
      screenCode: string;
      componentId: string;
      componentType: string;
      referenceType: string; // 'popup', 'navigate', 'targetScreen' 등
    }>;
  }> {
    // 권한 확인
    const targetScreens = await query<{ company_code: string | null }>(
      `SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
      [screenId],
    );

    if (targetScreens.length === 0) {
      throw new Error("화면을 찾을 수 없습니다.");
    }

    const targetScreen = targetScreens[0];

    if (
      userCompanyCode !== "*" &&
      targetScreen.company_code !== "*" &&
      targetScreen.company_code !== userCompanyCode
    ) {
      throw new Error("이 화면에 접근할 권한이 없습니다.");
    }

    // 같은 회사의 모든 활성 화면에서 이 화면을 참조하는지 확인
    const whereConditions: string[] = ["sd.is_active != 'D'"];
    const params: any[] = [];

    if (userCompanyCode !== "*") {
      whereConditions.push(
        `sd.company_code IN ($${params.length + 1}, $${params.length + 2})`,
      );
      params.push(userCompanyCode, "*");
    }

    const whereSQL = whereConditions.join(" AND ");

    // 화면과 레이아웃을 JOIN해서 조회
    const allScreens = await query<any>(
      `SELECT
        sd.screen_id, sd.screen_name, sd.screen_code, sd.company_code,
        sl.layout_id, sl.component_id, sl.component_type, sl.properties
       FROM screen_definitions sd
       LEFT JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
       WHERE ${whereSQL}
       ORDER BY sd.screen_id, sl.layout_id`,
      params,
    );

    const dependencies: Array<{
      screenId: number;
      screenName: string;
      screenCode: string;
      componentId: string;
      componentType: string;
      referenceType: string;
    }> = [];

    // 각 화면의 레이아웃에서 버튼 컴포넌트들을 검사
    for (const screen of allScreens) {
      if (screen.screen_id === screenId) continue; // 자기 자신은 제외

      try {
        // screen_layouts 테이블에서 버튼 컴포넌트 확인 (위젯 타입만)
        if (screen.component_type === "widget") {
          const properties = screen.properties as any;

          // 버튼 컴포넌트인지 확인
          if (properties?.widgetType === "button") {
            const config = properties.webTypeConfig;
            if (!config) continue;

            // popup 액션에서 popupScreenId 확인
            if (
              config.actionType === "popup" &&
              config.popupScreenId === screenId
            ) {
              dependencies.push({
                screenId: screen.screen_id,
                screenName: screen.screen_name,
                screenCode: screen.screen_code,
                componentId: screen.component_id,
                componentType: "button",
                referenceType: "popup",
              });
            }

            // navigate 액션에서 navigateScreenId 확인
            if (
              config.actionType === "navigate" &&
              config.navigateScreenId === screenId
            ) {
              dependencies.push({
                screenId: screen.screen_id,
                screenName: screen.screen_name,
                screenCode: screen.screen_code,
                componentId: screen.component_id,
                componentType: "button",
                referenceType: "navigate",
              });
            }

            // navigateUrl에서 화면 ID 패턴 확인 (예: /screens/123)
            if (
              config.navigateUrl &&
              config.navigateUrl.includes(`/screens/${screenId}`)
            ) {
              dependencies.push({
                screenId: screen.screen_id,
                screenName: screen.screen_name,
                screenCode: screen.screen_code,
                componentId: screen.component_id,
                componentType: "button",
                referenceType: "url",
              });
            }
          }
        }

        // 기존 layout_metadata도 확인 (하위 호환성) - 현재는 사용하지 않음
        // 실제 데이터는 screen_layouts 테이블에서 개별적으로 조회해야 함
      } catch (error) {
        console.error(
          `화면 ${screen.screen_id}의 레이아웃 분석 중 오류:`,
          error,
        );
        continue;
      }
    }

    // 메뉴 할당 확인 (Raw Query)
    try {
      const menuAssignments = await query<{
        assignment_id: number;
        menu_objid: number;
        menu_name_kor?: string;
      }>(
        `SELECT sma.assignment_id, sma.menu_objid, mi.menu_name_kor
         FROM screen_menu_assignments sma
         LEFT JOIN menu_info mi ON sma.menu_objid = mi.objid
         WHERE sma.screen_id = $1 AND sma.is_active = 'Y'`,
        [screenId],
      );

      // 메뉴에 할당된 경우 의존성에 추가
      for (const assignment of menuAssignments) {
        dependencies.push({
          screenId: 0, // 메뉴는 화면이 아니므로 0으로 설정
          screenName: assignment.menu_name_kor || "알 수 없는 메뉴",
          screenCode: `MENU_${assignment.menu_objid}`,
          componentId: `menu_${assignment.assignment_id}`,
          componentType: "menu",
          referenceType: "menu_assignment",
        });
      }
    } catch (error) {
      console.error("메뉴 할당 확인 중 오류:", error);
      // 메뉴 할당 확인 실패해도 다른 의존성 체크는 계속 진행
    }

    return {
      hasDependencies: dependencies.length > 0,
      dependencies,
    };
  }

  /**
   * 화면 정의 삭제 (휴지통으로 이동 - 소프트 삭제) (✅ Raw Query 전환 완료)
   */
  async deleteScreen(
    screenId: number,
    userCompanyCode: string,
    deletedBy: string,
    deleteReason?: string,
    force: boolean = false,
  ): Promise<void> {
    // 권한 확인 (Raw Query)
    const existingResult = await query<{
      company_code: string | null;
      is_active: string;
    }>(
      `SELECT company_code, is_active FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
      [screenId],
    );

    if (existingResult.length === 0) {
      throw new Error("화면을 찾을 수 없습니다.");
    }

    const existingScreen = existingResult[0];

    if (
      userCompanyCode !== "*" &&
      existingScreen.company_code !== userCompanyCode
    ) {
      throw new Error("이 화면을 삭제할 권한이 없습니다.");
    }

    // 이미 삭제된 화면인지 확인
    if (existingScreen.is_active === "D") {
      throw new Error("이미 삭제된 화면입니다.");
    }

    // 강제 삭제가 아닌 경우 의존성 체크
    if (!force) {
      const dependencyCheck = await this.checkScreenDependencies(
        screenId,
        userCompanyCode,
      );
      if (dependencyCheck.hasDependencies) {
        const error = new Error("다른 화면에서 사용 중인 화면입니다.") as any;
        error.code = "SCREEN_HAS_DEPENDENCIES";
        error.dependencies = dependencyCheck.dependencies;
        throw error;
      }
    }

    // 트랜잭션으로 화면 삭제와 메뉴 할당 정리를 함께 처리 (Raw Query)
    await transaction(async (client) => {
      // 1. 화면에서 사용하는 flowId 수집 (V2 레이아웃)
      const layoutResult = await client.query<{ layout_data: any }>(
        `SELECT layout_data FROM screen_layouts_v2 
         WHERE screen_id = $1 AND (company_code = $2 OR company_code = '*')
         ORDER BY CASE WHEN company_code = $2 THEN 0 ELSE 1 END
         LIMIT 1`,
        [screenId, userCompanyCode],
      );
      
      const layoutData = layoutResult.rows[0]?.layout_data;
      const flowIds = this.collectFlowIdsFromLayoutData(layoutData);
      
      // 2. 각 flowId가 다른 화면에서도 사용되는지 체크 후 삭제
      if (flowIds.size > 0) {
        for (const flowId of flowIds) {
          // 다른 화면에서 사용 중인지 확인 (같은 회사 내, 삭제되지 않은 화면 기준)
          const companyFilterForCheck = userCompanyCode === "*" ? "" : " AND sd.company_code = $3";
          const checkParams = userCompanyCode === "*" 
            ? [screenId, flowId] 
            : [screenId, flowId, userCompanyCode];
          
          const otherUsageResult = await client.query<{ count: string }>(
            `SELECT COUNT(*) as count FROM screen_layouts_v2 slv
             JOIN screen_definitions sd ON slv.screen_id = sd.screen_id
             WHERE slv.screen_id != $1 
             AND sd.is_active != 'D'
             ${companyFilterForCheck}
             AND (
               slv.layout_data::text LIKE '%"flowId":' || $2 || '%'
               OR slv.layout_data::text LIKE '%"flowId":"' || $2 || '"%'
             )`,
            checkParams,
          );
          
          const otherUsageCount = parseInt(otherUsageResult.rows[0]?.count || "0");
          
          // 다른 화면에서 사용하지 않는 경우에만 플로우 삭제
          if (otherUsageCount === 0) {
            // 해당 회사의 플로우만 삭제 (멀티테넌시)
            const companyFilter = userCompanyCode === "*" ? "" : " AND company_code = $2";
            const flowParams = userCompanyCode === "*" ? [flowId] : [flowId, userCompanyCode];
            
            // 1. flow_definition 관련 데이터 먼저 삭제 (외래키 순서)
            await client.query(
              `DELETE FROM flow_step_connection WHERE flow_definition_id = $1`,
              [flowId],
            );
            await client.query(
              `DELETE FROM flow_step WHERE flow_definition_id = $1`,
              [flowId],
            );
            await client.query(
              `DELETE FROM flow_definition WHERE id = $1${companyFilter}`,
              flowParams,
            );
            
            // 2. node_flows 테이블에서도 삭제 (제어플로우)
            await client.query(
              `DELETE FROM node_flows WHERE flow_id = $1${companyFilter}`,
              flowParams,
            );
            
            logger.info("화면 삭제 시 플로우 삭제 (flow_definition + node_flows)", { screenId, flowId, companyCode: userCompanyCode });
          } else {
            logger.debug("플로우가 다른 화면에서 사용 중 - 삭제 스킵", { screenId, flowId, otherUsageCount });
          }
        }
      }

      // 3. 소프트 삭제 (휴지통으로 이동)
      await client.query(
        `UPDATE screen_definitions
         SET is_active = 'D',
             deleted_date = $1,
             deleted_by = $2,
             delete_reason = $3,
             updated_date = $4,
             updated_by = $5
         WHERE screen_id = $6`,
        [
          new Date(),
          deletedBy,
          deleteReason || null,
          new Date(),
          deletedBy,
          screenId,
        ],
      );

      // 4. 메뉴 할당도 비활성화
      await client.query(
        `UPDATE screen_menu_assignments
         SET is_active = 'N'
         WHERE screen_id = $1 AND is_active = 'Y'`,
        [screenId],
      );
      
      // 5. 화면 그룹 연결 삭제 (screen_group_screens)
      await client.query(
        `DELETE FROM screen_group_screens WHERE screen_id = $1`,
        [screenId],
      );
      
      logger.info("화면 삭제 시 그룹 연결 해제", { screenId });
    });
  }

  /**
   * 화면 복원 (휴지통에서 복원) (✅ Raw Query 전환 완료)
   */
  async restoreScreen(
    screenId: number,
    userCompanyCode: string,
    restoredBy: string,
  ): Promise<void> {
    // 권한 확인
    const screens = await query<{
      company_code: string | null;
      is_active: string;
      screen_code: string;
    }>(
      `SELECT company_code, is_active, screen_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
      [screenId],
    );

    if (screens.length === 0) {
      throw new Error("화면을 찾을 수 없습니다.");
    }

    const existingScreen = screens[0];

    if (
      userCompanyCode !== "*" &&
      existingScreen.company_code !== userCompanyCode
    ) {
      throw new Error("이 화면을 복원할 권한이 없습니다.");
    }

    // 삭제된 화면이 아닌 경우
    if (existingScreen.is_active !== "D") {
      throw new Error("삭제된 화면이 아닙니다.");
    }

    // 화면 코드 중복 확인 (복원 시 같은 코드가 이미 존재하는지)
    const duplicateScreens = await query<{ screen_id: number }>(
      `SELECT screen_id FROM screen_definitions
       WHERE screen_code = $1 AND is_active != 'D' AND screen_id != $2
       LIMIT 1`,
      [existingScreen.screen_code, screenId],
    );

    if (duplicateScreens.length > 0) {
      throw new Error(
        "같은 화면 코드를 가진 활성 화면이 이미 존재합니다. 복원하려면 기존 화면의 코드를 변경하거나 삭제해주세요.",
      );
    }

    // 트랜잭션으로 화면 복원과 메뉴 할당 복원을 함께 처리
    await transaction(async (client) => {
      // 화면 복원
      await client.query(
        `UPDATE screen_definitions
         SET is_active = 'Y', deleted_date = NULL, deleted_by = NULL,
             delete_reason = NULL, updated_date = $1, updated_by = $2
         WHERE screen_id = $3`,
        [new Date(), restoredBy, screenId],
      );

      // 메뉴 할당도 다시 활성화
      await client.query(
        `UPDATE screen_menu_assignments
         SET is_active = 'Y'
         WHERE screen_id = $1 AND is_active = 'N'`,
        [screenId],
      );
    });
  }

  /**
   * 휴지통 화면들의 메뉴 할당 정리 (관리자용) (✅ Raw Query 전환 완료)
   */
  async cleanupDeletedScreenMenuAssignments(): Promise<{
    updatedCount: number;
    message: string;
  }> {
    const result = await query(
      `UPDATE screen_menu_assignments
       SET is_active = 'N'
       WHERE screen_id IN (
         SELECT screen_id
         FROM screen_definitions
         WHERE is_active = 'D'
       ) AND is_active = 'Y'`,
      [],
    );

    const updatedCount = result.length;

    return {
      updatedCount,
      message: `${updatedCount}개의 메뉴 할당이 정리되었습니다.`,
    };
  }

  /**
   * 화면 영구 삭제 (휴지통에서 완전 삭제) (✅ Raw Query 전환 완료)
   */
  async permanentDeleteScreen(
    screenId: number,
    userCompanyCode: string,
  ): Promise<void> {
    // 권한 확인
    const screens = await query<{
      company_code: string | null;
      is_active: string;
    }>(
      `SELECT company_code, is_active FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
      [screenId],
    );

    if (screens.length === 0) {
      throw new Error("화면을 찾을 수 없습니다.");
    }

    const existingScreen = screens[0];

    if (
      userCompanyCode !== "*" &&
      existingScreen.company_code !== userCompanyCode
    ) {
      throw new Error("이 화면을 영구 삭제할 권한이 없습니다.");
    }

    // 삭제된 화면이 아닌 경우 영구 삭제 불가
    if (existingScreen.is_active !== "D") {
      throw new Error("휴지통에 있는 화면만 영구 삭제할 수 있습니다.");
    }

    // 물리적 삭제 (수동으로 관련 데이터 삭제)
    await transaction(async (client) => {
      await client.query(`DELETE FROM screen_layouts WHERE screen_id = $1`, [
        screenId,
      ]);
      await client.query(
        `DELETE FROM screen_menu_assignments WHERE screen_id = $1`,
        [screenId],
      );
      await client.query(
        `DELETE FROM screen_definitions WHERE screen_id = $1`,
        [screenId],
      );
    });
  }

  /**
   * 휴지통 화면 목록 조회 (✅ Raw Query 전환 완료)
   */
  async getDeletedScreens(
    companyCode: string,
    page: number = 1,
    size: number = 20,
  ): Promise<
    PaginatedResponse<
      ScreenDefinition & {
        deletedDate?: Date;
        deletedBy?: string;
        deleteReason?: string;
      }
    >
  > {
    const offset = (page - 1) * size;
    const whereConditions: string[] = ["is_active = 'D'"];
    const params: any[] = [];

    if (companyCode !== "*") {
      whereConditions.push(`company_code = $${params.length + 1}`);
      params.push(companyCode);
    }

    const whereSQL = whereConditions.join(" AND ");

    const [screens, totalResult] = await Promise.all([
      query<any>(
        `SELECT * FROM screen_definitions
         WHERE ${whereSQL}
         ORDER BY deleted_date DESC NULLS LAST
         LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
        [...params, size, offset],
      ),
      query<{ count: string }>(
        `SELECT COUNT(*)::text as count FROM screen_definitions WHERE ${whereSQL}`,
        params,
      ),
    ]);

    const total = parseInt(totalResult[0]?.count || "0", 10);

    // 테이블 라벨 정보를 한 번에 조회
    const tableNames = Array.from(
      new Set(screens.map((s: any) => s.table_name).filter(Boolean)),
    );

    let tableLabelMap = new Map<string, string>();

    if (tableNames.length > 0) {
      const placeholders = tableNames.map((_, i) => `$${i + 1}`).join(", ");
      const tableLabels = await query<{
        table_name: string;
        table_label: string | null;
      }>(
        `SELECT table_name, table_label FROM table_labels WHERE table_name IN (${placeholders})`,
        tableNames,
      );

      tableLabelMap = new Map(
        tableLabels.map((tl: any) => [
          tl.table_name,
          tl.table_label || tl.table_name,
        ]),
      );
    }

    return {
      data: screens.map((screen: any) => ({
        ...this.mapToScreenDefinition(screen, tableLabelMap),
        deletedDate: screen.deleted_date || undefined,
        deletedBy: screen.deleted_by || undefined,
        deleteReason: screen.delete_reason || undefined,
      })),
      pagination: {
        page,
        size,
        total,
        totalPages: Math.ceil(total / size),
      },
    };
  }

  /**
   * 활성 화면 일괄 삭제 (휴지통으로 이동)
   */
  async bulkDeleteScreens(
    screenIds: number[],
    userCompanyCode: string,
    deletedBy: string,
    deleteReason?: string,
    force: boolean = false,
  ): Promise<{
    deletedCount: number;
    skippedCount: number;
    errors: Array<{ screenId: number; error: string }>;
  }> {
    if (screenIds.length === 0) {
      throw new Error("삭제할 화면을 선택해주세요.");
    }

    let deletedCount = 0;
    let skippedCount = 0;
    const errors: Array<{ screenId: number; error: string }> = [];

    // 각 화면을 개별적으로 삭제 처리
    for (const screenId of screenIds) {
      try {
        // 권한 확인 (Raw Query)
        const existingResult = await query<{
          company_code: string | null;
          is_active: string;
          screen_name: string;
        }>(
          `SELECT company_code, is_active, screen_name FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
          [screenId],
        );

        if (existingResult.length === 0) {
          skippedCount++;
          errors.push({
            screenId,
            error: "화면을 찾을 수 없습니다.",
          });
          continue;
        }

        const existingScreen = existingResult[0];

        // 권한 확인
        if (
          userCompanyCode !== "*" &&
          existingScreen.company_code !== userCompanyCode
        ) {
          skippedCount++;
          errors.push({
            screenId,
            error: "이 화면을 삭제할 권한이 없습니다.",
          });
          continue;
        }

        // 이미 삭제된 화면인지 확인
        if (existingScreen.is_active === "D") {
          skippedCount++;
          errors.push({
            screenId,
            error: "이미 삭제된 화면입니다.",
          });
          continue;
        }

        // 강제 삭제가 아닌 경우 의존성 체크
        if (!force) {
          const dependencyCheck = await this.checkScreenDependencies(
            screenId,
            userCompanyCode,
          );
          if (dependencyCheck.hasDependencies) {
            skippedCount++;
            errors.push({
              screenId,
              error: `다른 화면에서 사용 중 (${dependencyCheck.dependencies.length}개 참조)`,
            });
            continue;
          }
        }

        // 트랜잭션으로 화면 삭제와 메뉴 할당 정리를 함께 처리
        await transaction(async (client) => {
          const now = new Date();

          // 소프트 삭제 (휴지통으로 이동)
          await client.query(
            `UPDATE screen_definitions
             SET is_active = 'D',
                 deleted_date = $1,
                 deleted_by = $2,
                 delete_reason = $3,
                 updated_date = $4,
                 updated_by = $5
             WHERE screen_id = $6`,
            [now, deletedBy, deleteReason || null, now, deletedBy, screenId],
          );

          // 메뉴 할당 정리 (삭제된 화면의 메뉴 할당 제거)
          await client.query(
            `DELETE FROM screen_menu_assignments WHERE screen_id = $1`,
            [screenId],
          );
        });

        deletedCount++;
        logger.info(
          `화면 삭제 완료: ${screenId} (${existingScreen.screen_name})`,
        );
      } catch (error) {
        skippedCount++;
        errors.push({
          screenId,
          error: error instanceof Error ? error.message : "알 수 없는 오류",
        });
        logger.error(`화면 삭제 실패: ${screenId}`, error);
      }
    }

    logger.info(
      `일괄 삭제 완료: 성공 ${deletedCount}개, 실패 ${skippedCount}개`,
    );

    return { deletedCount, skippedCount, errors };
  }

  /**
   * 휴지통 화면 일괄 영구 삭제
   */
  async bulkPermanentDeleteScreens(
    screenIds: number[],
    userCompanyCode: string,
  ): Promise<{
    deletedCount: number;
    skippedCount: number;
    errors: Array<{ screenId: number; error: string }>;
  }> {
    if (screenIds.length === 0) {
      throw new Error("삭제할 화면을 선택해주세요.");
    }

    // 권한 확인 - 해당 회사의 휴지통 화면들만 조회
    const whereClause: any = {
      screen_id: { in: screenIds },
      is_active: "D", // 휴지통에 있는 화면만
    };

    if (userCompanyCode !== "*") {
      whereClause.company_code = userCompanyCode;
    }

    // WHERE 절 생성
    const whereConditions: string[] = ["is_active = 'D'"];
    const params: any[] = [];

    if (userCompanyCode !== "*") {
      whereConditions.push(`company_code = $${params.length + 1}`);
      params.push(userCompanyCode);
    }

    const whereSQL = whereConditions.join(" AND ");

    const screensToDelete = await query<{ screen_id: number }>(
      `SELECT screen_id FROM screen_definitions WHERE ${whereSQL}`,
      params,
    );

    let deletedCount = 0;
    let skippedCount = 0;
    const errors: Array<{ screenId: number; error: string }> = [];

    // 각 화면을 개별적으로 삭제 처리
    for (const screenId of screenIds) {
      try {
        const screenToDelete = screensToDelete.find(
          (s: any) => s.screen_id === screenId,
        );

        if (!screenToDelete) {
          skippedCount++;
          errors.push({
            screenId,
            error: "화면을 찾을 수 없거나 삭제 권한이 없습니다.",
          });
          continue;
        }

        // 관련 레이아웃 데이터도 함께 삭제 (트랜잭션)
        await transaction(async (client) => {
          // screen_layouts 삭제
          await client.query(
            `DELETE FROM screen_layouts WHERE screen_id = $1`,
            [screenId],
          );

          // screen_menu_assignments 삭제
          await client.query(
            `DELETE FROM screen_menu_assignments WHERE screen_id = $1`,
            [screenId],
          );

          // screen_definitions 삭제
          await client.query(
            `DELETE FROM screen_definitions WHERE screen_id = $1`,
            [screenId],
          );
        });

        deletedCount++;
      } catch (error) {
        skippedCount++;
        errors.push({
          screenId,
          error: error instanceof Error ? error.message : "알 수 없는 오류",
        });
        console.error(`화면 ${screenId} 영구 삭제 실패:`, error);
      }
    }

    return {
      deletedCount,
      skippedCount,
      errors,
    };
  }

  // ========================================
  // 테이블 관리
  // ========================================

  /**
   * 테이블 목록 조회 (모든 테이블) (✅ Raw Query 전환 완료)
   */
  async getTables(companyCode: string): Promise<TableInfo[]> {
    try {
      // PostgreSQL에서 사용 가능한 테이블 목록 조회
      const tables = await query<{ table_name: string }>(
        `SELECT table_name
         FROM information_schema.tables
         WHERE table_schema = 'public'
           AND table_type = 'BASE TABLE'
         ORDER BY table_name`,
        [],
      );

      // 각 테이블의 컬럼 정보도 함께 조회
      const tableInfos: TableInfo[] = [];

      for (const table of tables) {
        const columns = await this.getTableColumns(
          table.table_name,
          companyCode,
        );
        if (columns.length > 0) {
          tableInfos.push({
            tableName: table.table_name,
            tableLabel: this.getTableLabel(table.table_name),
            columns: columns,
          });
        }
      }

      return tableInfos;
    } catch (error) {
      console.error("테이블 목록 조회 실패:", error);
      throw new Error("테이블 목록을 조회할 수 없습니다.");
    }
  }

  /**
   * 특정 테이블 정보 조회 (최적화된 단일 테이블 조회)
   */
  async getTableInfo(
    tableName: string,
    companyCode: string,
  ): Promise<TableInfo | null> {
    try {
      console.log(`=== 단일 테이블 조회 시작: ${tableName} ===`);

      // 테이블 존재 여부 확인
      const tableExists = await query<{ table_name: string }>(
        `SELECT table_name
         FROM information_schema.tables
         WHERE table_schema = 'public'
           AND table_type = 'BASE TABLE'
           AND table_name = $1`,
        [tableName],
      );

      if (tableExists.length === 0) {
        console.log(`테이블 ${tableName}이 존재하지 않습니다.`);
        return null;
      }

      // 해당 테이블의 컬럼 정보 조회
      const columns = await this.getTableColumns(tableName, companyCode);

      if (columns.length === 0) {
        console.log(`테이블 ${tableName}의 컬럼 정보가 없습니다.`);
        return null;
      }

      const tableInfo: TableInfo = {
        tableName: tableName,
        tableLabel: this.getTableLabel(tableName),
        columns: columns,
      };

      console.log(
        `단일 테이블 조회 완료: ${tableName}, 컬럼 ${columns.length}개`,
      );
      return tableInfo;
    } catch (error) {
      console.error(`테이블 ${tableName} 조회 실패:`, error);
      throw new Error(`테이블 ${tableName} 정보를 조회할 수 없습니다.`);
    }
  }

  /**
   * 테이블 컬럼 정보 조회 (✅ Raw Query 전환 완료)
   */
  async getTableColumns(
    tableName: string,
    companyCode: string,
  ): Promise<ColumnInfo[]> {
    try {
      // 테이블 컬럼 정보 조회
      const columns = await query<{
        column_name: string;
        data_type: string;
        is_nullable: string;
        column_default: string | null;
        character_maximum_length: number | null;
        numeric_precision: number | null;
        numeric_scale: number | null;
      }>(
        `SELECT
          column_name,
          data_type,
          is_nullable,
          column_default,
          character_maximum_length,
          numeric_precision,
          numeric_scale
         FROM information_schema.columns
         WHERE table_schema = 'public'
           AND table_name = $1
         ORDER BY ordinal_position`,
        [tableName],
      );

      // 🆕 table_type_columns에서 입력타입 정보 조회 (회사별만, fallback 없음)
      // 멀티테넌시: 각 회사는 자신의 설정만 사용, 최고관리자 설정은 별도 관리
      console.log(
        `🔍 [getTableColumns] 시작: table=${tableName}, company=${companyCode}`,
      );

      const typeInfo = await query<{
        column_name: string;
        input_type: string | null;
        detail_settings: any;
      }>(
        `SELECT column_name, input_type, detail_settings
         FROM table_type_columns
         WHERE table_name = $1
           AND company_code = $2
         ORDER BY id DESC`, // 최신 레코드 우선 (중복 방지)
        [tableName, companyCode],
      );

      console.log(
        `📊 [getTableColumns] typeInfo 조회 완료: ${typeInfo.length}개`,
      );
      const currencyCodeType = typeInfo.find(
        (t) => t.column_name === "currency_code",
      );
      if (currencyCodeType) {
        console.log(
          `💰 [getTableColumns] currency_code 발견:`,
          currencyCodeType,
        );
      } else {
        console.log(`⚠️ [getTableColumns] currency_code 없음`);
      }

      // table_type_columns 테이블에서 라벨 정보 조회 (우선순위 2)
      const labelInfo = await query<{
        column_name: string;
        column_label: string | null;
      }>(
        `SELECT column_name, column_label
         FROM table_type_columns
         WHERE table_name = $1 AND company_code = '*'`,
        [tableName],
      );

      // 🆕 category_column_mapping에서 코드 카테고리 정보 조회
      const categoryInfo = await query<{
        physical_column_name: string;
        logical_column_name: string;
      }>(
        `SELECT physical_column_name, logical_column_name
         FROM category_column_mapping
         WHERE table_name = $1
           AND company_code = $2`,
        [tableName, companyCode],
      );

      // 컬럼 정보 매핑
      const columnMap = new Map<string, any>();

      // 먼저 information_schema에서 가져온 컬럼들로 기본 맵 생성
      columns.forEach((column: any) => {
        columnMap.set(column.column_name, {
          tableName: tableName,
          columnName: column.column_name,
          dataType: column.data_type,
          isNullable: column.is_nullable,
          columnDefault: column.column_default || undefined,
          characterMaximumLength: column.character_maximum_length || undefined,
          numericPrecision: column.numeric_precision || undefined,
          numericScale: column.numeric_scale || undefined,
        });
      });

      console.log(
        `🗺️ [getTableColumns] 기본 columnMap 생성: ${columnMap.size}개`,
      );

      // table_type_columns에서 input_type 추가 (중복 시 최신 것만)
      const addedTypes = new Set<string>();
      typeInfo.forEach((type) => {
        const colName = type.column_name;
        if (!addedTypes.has(colName) && columnMap.has(colName)) {
          const col = columnMap.get(colName);
          col.inputType = type.input_type;
          col.webType = type.input_type; // webType도 동일하게 설정
          col.detailSettings = type.detail_settings;
          addedTypes.add(colName);

          if (colName === "currency_code") {
            console.log(
              `✅ [getTableColumns] currency_code inputType 설정됨: ${type.input_type}`,
            );
          }
        }
      });

      console.log(
        `🏷️ [getTableColumns] inputType 추가 완료: ${addedTypes.size}개`,
      );

      // table_type_columns에서 라벨 추가
      labelInfo.forEach((label) => {
        const col = columnMap.get(label.column_name);
        if (col) {
          col.columnLabel =
            label.column_label || this.getColumnLabel(label.column_name);
        }
      });

      // category_column_mapping에서 코드 카테고리 추가
      categoryInfo.forEach((cat) => {
        const col = columnMap.get(cat.physical_column_name);
        if (col) {
          col.codeCategory = cat.logical_column_name;
        }
      });

      // 최종 결과 생성
      const result = Array.from(columnMap.values()).map((col) => ({
        ...col,
        // 기본값 설정
        columnLabel: col.columnLabel || this.getColumnLabel(col.columnName),
        inputType: col.inputType || this.inferWebType(col.dataType),
        webType: col.webType || this.inferWebType(col.dataType),
        detailSettings: col.detailSettings || undefined,
        codeCategory: col.codeCategory || undefined,
      }));

      // 디버깅: currency_code의 최종 inputType 확인
      const currencyCodeResult = result.find(
        (r) => r.columnName === "currency_code",
      );
      if (currencyCodeResult) {
        console.log(`🎯 [getTableColumns] 최종 currency_code:`, {
          inputType: currencyCodeResult.inputType,
          webType: currencyCodeResult.webType,
          dataType: currencyCodeResult.dataType,
        });
      }

      console.log(`✅ [getTableColumns] 반환: ${result.length}개 컬럼`);
      return result;
    } catch (error) {
      console.error("테이블 컬럼 조회 실패:", error);
      throw new Error("테이블 컬럼 정보를 조회할 수 없습니다.");
    }
  }

  /**
   * 테이블 라벨 생성
   */
  private getTableLabel(tableName: string): string {
    // snake_case를 읽기 쉬운 형태로 변환
    return tableName
      .replace(/_/g, " ")
      .replace(/\b\w/g, (l) => l.toUpperCase())
      .replace(/\s+/g, " ")
      .trim();
  }

  /**
   * 컬럼 라벨 생성
   */
  private getColumnLabel(columnName: string): string {
    // snake_case를 읽기 쉬운 형태로 변환
    return columnName
      .replace(/_/g, " ")
      .replace(/\b\w/g, (l) => l.toUpperCase())
      .replace(/\s+/g, " ")
      .trim();
  }

  /**
   * 데이터 타입으로부터 웹타입 추론
   */
  private inferWebType(dataType: string): WebType {
    // 통합 타입 매핑에서 import
    const { DB_TYPE_TO_WEB_TYPE } = require("../types/v2-web-types");

    const lowerType = dataType.toLowerCase();

    // 정확한 매핑 우선 확인
    if (DB_TYPE_TO_WEB_TYPE[lowerType]) {
      return DB_TYPE_TO_WEB_TYPE[lowerType];
    }

    // 부분 문자열 매칭 (더 정교한 규칙)
    for (const [dbType, webType] of Object.entries(DB_TYPE_TO_WEB_TYPE)) {
      if (
        lowerType.includes(dbType.toLowerCase()) ||
        dbType.toLowerCase().includes(lowerType)
      ) {
        return webType as WebType;
      }
    }

    // 추가 정밀 매핑
    if (lowerType.includes("int") && !lowerType.includes("point")) {
      return "number";
    } else if (lowerType.includes("numeric") || lowerType.includes("decimal")) {
      return "decimal";
    } else if (
      lowerType.includes("timestamp") ||
      lowerType.includes("datetime")
    ) {
      return "datetime";
    } else if (lowerType.includes("date")) {
      return "date";
    } else if (lowerType.includes("time")) {
      return "datetime";
    } else if (lowerType.includes("bool")) {
      return "checkbox";
    } else if (
      lowerType.includes("char") ||
      lowerType.includes("text") ||
      lowerType.includes("varchar")
    ) {
      return lowerType.includes("text") ? "textarea" : "text";
    }

    // 기본값
    return "text";
  }

  // ========================================
  // 레이아웃 관리
  // ========================================

  /**
   * 레이아웃 저장 (✅ Raw Query 전환 완료)
   */
  async saveLayout(
    screenId: number,
    layoutData: LayoutData,
    companyCode: string,
  ): Promise<void> {
    console.log(`=== 레이아웃 저장 시작 ===`);
    console.log(`화면 ID: ${screenId}`);
    console.log(`컴포넌트 수: ${layoutData.components.length}`);
    console.log(`격자 설정:`, layoutData.gridSettings);
    console.log(`해상도 설정:`, layoutData.screenResolution);
    console.log(`기본 테이블:`, (layoutData as any).mainTableName);

    // 권한 확인
    const screens = await query<{ company_code: string | null }>(
      `SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
      [screenId],
    );

    if (screens.length === 0) {
      throw new Error("화면을 찾을 수 없습니다.");
    }

    const existingScreen = screens[0];

    if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
      throw new Error("이 화면의 레이아웃을 저장할 권한이 없습니다.");
    }

    // 🆕 화면의 기본 테이블 업데이트 (테이블이 선택된 경우)
    const mainTableName = (layoutData as any).mainTableName;
    if (mainTableName) {
      await query(
        `UPDATE screen_definitions SET table_name = $1, updated_date = NOW() WHERE screen_id = $2`,
        [mainTableName, screenId],
      );
      console.log(`✅ 화면 기본 테이블 업데이트: ${mainTableName}`);
    }

    // 기존 레이아웃 삭제 (컴포넌트와 메타데이터 모두)
    await query(`DELETE FROM screen_layouts WHERE screen_id = $1`, [screenId]);

    // 1. 메타데이터 저장 (격자 설정과 해상도 정보)
    if (layoutData.gridSettings || layoutData.screenResolution) {
      const metadata: any = {
        gridSettings: layoutData.gridSettings,
        screenResolution: layoutData.screenResolution,
      };

      await query(
        `INSERT INTO screen_layouts (
          screen_id, component_type, component_id, parent_id,
          position_x, position_y, width, height, properties, display_order
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
        [
          screenId,
          "_metadata", // 특별한 타입으로 메타데이터 식별
          `_metadata_${screenId}`,
          null,
          0,
          0,
          0,
          0,
          JSON.stringify(metadata),
          -1, // 메타데이터는 맨 앞에 배치
        ],
      );

      console.log(`메타데이터 저장 완료:`, metadata);
    }

    // 2. 컴포넌트 저장
    for (const component of layoutData.components) {
      const { id, ...componentData } = component;

      console.log(`저장 중인 컴포넌트:`, {
        id: component.id,
        type: component.type,
        position: component.position,
        size: component.size,
        parentId: component.parentId,
        title: (component as any).title,
      });

      // JSON 필드에 맞는 타입으로 변환
      const properties: any = {
        ...componentData,
        position: {
          x: component.position.x,
          y: component.position.y,
          z: component.position.z || 1, // z 값 포함
        },
        size: {
          width: component.size.width,
          height: component.size.height,
        },
      };

      // 🔍 디버깅: webTypeConfig.dataflowConfig 확인
      if ((component as any).webTypeConfig?.dataflowConfig) {
        console.log(
          `🔍 컴포넌트 ${component.id}의 dataflowConfig:`,
          JSON.stringify(
            (component as any).webTypeConfig.dataflowConfig,
            null,
            2,
          ),
        );
      }

      await query(
        `INSERT INTO screen_layouts (
          screen_id, component_type, component_id, parent_id,
          position_x, position_y, width, height, properties
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
        [
          screenId,
          component.type,
          component.id,
          component.parentId || null,
          Math.round(component.position.x), // 정수로 반올림
          Math.round(component.position.y), // 정수로 반올림
          Math.round(component.size.width), // 정수로 반올림
          Math.round(component.size.height), // 정수로 반올림
          JSON.stringify(properties),
        ],
      );
    }

    console.log(`=== 레이아웃 저장 완료 ===`);
  }

  /**
   * 레이아웃 조회 (✅ Raw Query 전환 완료)
   * V2 테이블 우선 조회 → 없으면 V1 테이블 조회
   */
  async getLayout(
    screenId: number,
    companyCode: string,
  ): Promise<LayoutData | null> {
    console.log(`=== 레이아웃 로드 시작 ===`);
    console.log(`화면 ID: ${screenId}`);

    // 권한 확인 및 테이블명 조회
    const screens = await query<{
      company_code: string | null;
      table_name: string | null;
    }>(
      `SELECT company_code, table_name FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
      [screenId],
    );

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

    const existingScreen = screens[0];

    if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
      throw new Error("이 화면의 레이아웃을 조회할 권한이 없습니다.");
    }

    // V2 테이블 우선 조회: 기본 레이어(layer_id=1)만 가져옴
    // layer_id 필터 없이 queryOne 하면 조건부 레이어가 반환될 수 있음
    let v2Layout = await queryOne<{ layout_data: any }>(
      `SELECT layout_data FROM screen_layouts_v2
       WHERE screen_id = $1 AND company_code = $2 AND layer_id = 1`,
      [screenId, companyCode],
    );

    // 최고관리자(*): 화면 정의의 company_code로 재조회
    if (!v2Layout && companyCode === "*" && existingScreen.company_code && existingScreen.company_code !== "*") {
      v2Layout = await queryOne<{ layout_data: any }>(
        `SELECT layout_data FROM screen_layouts_v2
         WHERE screen_id = $1 AND company_code = $2 AND layer_id = 1`,
        [screenId, existingScreen.company_code],
      );
    }

    // 일반 사용자: 회사별 레이아웃 없으면 공통(*) 조회
    if (!v2Layout && companyCode !== "*") {
      v2Layout = await queryOne<{ layout_data: any }>(
        `SELECT layout_data FROM screen_layouts_v2
         WHERE screen_id = $1 AND company_code = '*' AND layer_id = 1`,
        [screenId],
      );
    }

    // V2 레이아웃이 있으면 V2 형식으로 반환
    if (v2Layout && v2Layout.layout_data) {
      
      const layoutData = v2Layout.layout_data;

      // URL에서 컴포넌트 타입 추출하는 헬퍼 함수
      const getTypeFromUrl = (url: string | undefined): string => {
        if (!url) return "component";
        const parts = url.split("/");
        return parts[parts.length - 1] || "component";
      };

      // V2 형식의 components를 LayoutData 형식으로 변환
      const components = (layoutData.components || []).map((comp: any) => {
        const componentType = getTypeFromUrl(comp.url);
        return {
          id: comp.id,
          type: componentType,
          position: comp.position || { x: 0, y: 0, z: 1 },
          size: comp.size || { width: 200, height: 100 },
          componentUrl: comp.url,
          componentType: componentType,
          componentConfig: comp.overrides || {},
          displayOrder: comp.displayOrder || 0,
          ...comp.overrides,
        };
      });

      // screenResolution이 없으면 컴포넌트 위치 기반으로 자동 계산
      let screenResolution = layoutData.screenResolution;
      if (!screenResolution && components.length > 0) {
        let maxRight = 0;
        let maxBottom = 0;

        for (const comp of layoutData.components || []) {
          const right = (comp.position?.x || 0) + (comp.size?.width || 200);
          const bottom = (comp.position?.y || 0) + (comp.size?.height || 100);
          maxRight = Math.max(maxRight, right);
          maxBottom = Math.max(maxBottom, bottom);
        }

        // 여백 100px 추가, 최소 1200x800 보장
        screenResolution = {
          width: Math.max(1200, maxRight + 100),
          height: Math.max(800, maxBottom + 100),
        };
        console.log(`screenResolution 자동 계산:`, screenResolution);
      }

      return {
        components,
        gridSettings: layoutData.gridSettings || {
          columns: 12,
          gap: 16,
          padding: 16,
          snapToGrid: true,
          showGrid: true,
        },
        screenResolution,
      };
    }

    

    const layouts = await query<any>(
      `SELECT * FROM screen_layouts
       WHERE screen_id = $1
       ORDER BY display_order ASC NULLS LAST, layout_id ASC`,
      [screenId],
    );

    console.log(`DB에서 조회된 레이아웃 수: ${layouts.length}`);

    // 메타데이터와 컴포넌트 분리
    const metadataLayout = layouts.find(
      (layout) => layout.component_type === "_metadata",
    );
    const componentLayouts = layouts.filter(
      (layout) => layout.component_type !== "_metadata",
    );

    // 기본 메타데이터 설정
    let gridSettings = {
      columns: 12,
      gap: 16,
      padding: 16,
      snapToGrid: true,
      showGrid: true,
    };
    let screenResolution = null;

    // 저장된 메타데이터가 있으면 적용
    if (metadataLayout && metadataLayout.properties) {
      const metadata = metadataLayout.properties as any;
      if (metadata.gridSettings) {
        gridSettings = { ...gridSettings, ...metadata.gridSettings };
      }
      if (metadata.screenResolution) {
        screenResolution = metadata.screenResolution;
      }
      console.log(`메타데이터 로드:`, { gridSettings, screenResolution });
    }

    if (componentLayouts.length === 0) {
      return {
        components: [],
        gridSettings,
        screenResolution,
      };
    }

    // 🔥 최신 inputType 정보 조회 (table_type_columns에서)
    const inputTypeMap = await this.getLatestInputTypes(
      componentLayouts,
      companyCode,
    );

    const components: ComponentData[] = componentLayouts.map((layout) => {
      const properties = layout.properties as any;

      // 🔥 최신 inputType으로 widgetType 및 componentType 업데이트
      const tableName = properties?.tableName;
      const columnName = properties?.columnName;
      const latestTypeInfo =
        tableName && columnName
          ? inputTypeMap.get(`${tableName}.${columnName}`)
          : null;

      // 🆕 V2 컴포넌트는 덮어쓰지 않음 (새로운 컴포넌트 시스템 보호)
      const savedComponentType = properties?.componentType;
      const isV2Component = savedComponentType?.startsWith("v2-");

      const component = {
        id: layout.component_id,
        // 🔥 최신 componentType이 있으면 type 덮어쓰기 (단, V2 컴포넌트는 제외)
        type: isV2Component
          ? (layout.component_type as any) // V2는 저장된 값 유지
          : latestTypeInfo?.componentType || (layout.component_type as any),
        position: {
          x: layout.position_x,
          y: layout.position_y,
          z: properties?.position?.z || 1, // z 값 복원
        },
        size: { width: layout.width, height: layout.height },
        parentId: layout.parent_id,
        ...properties,
        // 🔥 최신 inputType이 있으면 widgetType, componentType 덮어쓰기 (단, V2 컴포넌트는 제외)
        ...(!isV2Component &&
          latestTypeInfo && {
            widgetType: latestTypeInfo.inputType,
            inputType: latestTypeInfo.inputType,
            componentType: latestTypeInfo.componentType,
            componentConfig: {
              ...properties?.componentConfig,
              type: latestTypeInfo.componentType,
              inputType: latestTypeInfo.inputType,
            },
          }),
      };

      console.log(`로드된 컴포넌트:`, {
        id: component.id,
        type: component.type,
        position: component.position,
        size: component.size,
        parentId: component.parentId,
        title: (component as any).title,
        widgetType: (component as any).widgetType,
        componentType: (component as any).componentType,
        latestTypeInfo,
      });

      return component;
    });

    console.log(`=== 레이아웃 로드 완료 ===`);
    console.log(`반환할 컴포넌트 수: ${components.length}`);
    console.log(`최종 격자 설정:`, gridSettings);
    console.log(`최종 해상도 설정:`, screenResolution);
    console.log(`테이블명:`, existingScreen.table_name);

    return {
      components,
      gridSettings,
      screenResolution,
      tableName: existingScreen.table_name, // 🆕 테이블명 추가
    };
  }

  /**
   * V1 레이아웃 조회 (component_url + custom_config 기반)
   * screen_layouts_v1 테이블에서 조회
   *
   * 🔒 확정 사항:
   * - component_url: 컴포넌트 파일 경로 (필수, NOT NULL)
   * - custom_config: 회사별 커스텀 설정 (slot 포함)
   * - company_code: 멀티테넌시 필터 필수
   */
  async getLayoutV1(
    screenId: number,
    companyCode: string,
  ): Promise<LayoutData | null> {
    console.log(`=== V1 레이아웃 로드 시작 ===`);
    console.log(`화면 ID: ${screenId}, 회사: ${companyCode}`);

    // 권한 확인 및 테이블명 조회
    const screens = await query<{
      company_code: string | null;
      table_name: string | null;
    }>(
      `SELECT company_code, table_name FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
      [screenId],
    );

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

    const existingScreen = screens[0];

    if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
      throw new Error("이 화면의 레이아웃을 조회할 권한이 없습니다.");
    }

    // V1 테이블에서 조회 (company_code 필터 포함 - 멀티테넌시 필수)
    const layouts = await query<any>(
      `SELECT * FROM screen_layouts_v1
       WHERE screen_id = $1
         AND (company_code = $2 OR $2 = '*')
       ORDER BY display_order ASC NULLS LAST, layout_id ASC`,
      [screenId, companyCode],
    );

    console.log(`V1 DB에서 조회된 레이아웃 수: ${layouts.length}`);

    if (layouts.length === 0) {
      return {
        components: [],
        gridSettings: {
          columns: 12,
          gap: 16,
          padding: 16,
          snapToGrid: true,
          showGrid: true,
        },
        screenResolution: null,
      };
    }

    const components: ComponentData[] = layouts.map((layout: any) => {
      // component_url에서 컴포넌트 타입 추출
      // "@/lib/registry/components/split-panel-layout" → "split-panel-layout"
      const componentUrl = layout.component_url || "";
      const componentType = componentUrl.split("/").pop() || "unknown";

      // custom_config가 곧 componentConfig
      const componentConfig = layout.custom_config || {};

      const component = {
        id: layout.component_id,
        type: componentType as any,
        componentType: componentType,
        componentUrl: componentUrl, // URL도 전달
        position: {
          x: layout.position_x,
          y: layout.position_y,
          z: 1,
        },
        size: {
          width: layout.width,
          height: layout.height,
        },
        parentId: layout.parent_id,
        componentConfig,
      };

      return component;
    });

    console.log(`=== V1 레이아웃 로드 완료 ===`);
    console.log(`반환할 컴포넌트 수: ${components.length}`);

    return {
      components,
      gridSettings: {
        columns: 12,
        gap: 16,
        padding: 16,
        snapToGrid: true,
        showGrid: true,
      },
      screenResolution: null,
      tableName: existingScreen.table_name,
    };
  }

  /**
   * 입력 타입에 해당하는 컴포넌트 ID 반환
   * (프론트엔드 webTypeMapping.ts와 동일한 매핑)
   */
  private getComponentIdFromInputType(inputType: string): string {
    const mapping: Record<string, string> = {
      // 텍스트 입력
      text: "text-input",
      email: "text-input",
      password: "text-input",
      tel: "text-input",
      // 숫자 입력
      number: "number-input",
      decimal: "number-input",
      // 날짜/시간
      date: "date-input",
      datetime: "date-input",
      time: "date-input",
      // 텍스트 영역
      textarea: "textarea-basic",
      // 선택
      select: "select-basic",
      dropdown: "select-basic",
      // 체크박스/라디오
      checkbox: "checkbox-basic",
      radio: "radio-basic",
      boolean: "toggle-switch",
      // 파일
      file: "file-upload",
      // 이미지
      image: "image-widget",
      img: "image-widget",
      picture: "image-widget",
      photo: "image-widget",
      // 버튼
      button: "button-primary",
      // 기타
      label: "text-display",
      code: "select-basic",
      entity: "entity-search-input", // 엔티티는 entity-search-input 사용
      category: "select-basic",
    };

    return mapping[inputType] || "text-input";
  }

  /**
   * 컴포넌트들의 최신 inputType 정보 조회
   * @param layouts - 레이아웃 목록
   * @param companyCode - 회사 코드
   * @returns Map<"tableName.columnName", { inputType, componentType }>
   */
  private async getLatestInputTypes(
    layouts: any[],
    companyCode: string,
  ): Promise<Map<string, { inputType: string; componentType: string }>> {
    const inputTypeMap = new Map<
      string,
      { inputType: string; componentType: string }
    >();

    // tableName과 columnName이 있는 컴포넌트들의 고유 조합 추출
    const tableColumnPairs = new Set<string>();
    for (const layout of layouts) {
      const properties = layout.properties as any;
      if (properties?.tableName && properties?.columnName) {
        tableColumnPairs.add(
          `${properties.tableName}|${properties.columnName}`,
        );
      }
    }

    if (tableColumnPairs.size === 0) {
      return inputTypeMap;
    }

    // 각 테이블-컬럼 조합에 대해 최신 inputType 조회
    const pairs = Array.from(tableColumnPairs).map((pair) => {
      const [tableName, columnName] = pair.split("|");
      return { tableName, columnName };
    });

    // 배치 쿼리로 한 번에 조회
    const placeholders = pairs
      .map((_, i) => `($${i * 2 + 1}, $${i * 2 + 2})`)
      .join(", ");
    const params = pairs.flatMap((p) => [p.tableName, p.columnName]);

    try {
      const results = await query<{
        table_name: string;
        column_name: string;
        input_type: string;
      }>(
        `SELECT table_name, column_name, input_type 
         FROM table_type_columns 
         WHERE (table_name, column_name) IN (${placeholders})
           AND company_code = $${params.length + 1}`,
        [...params, companyCode],
      );

      for (const row of results) {
        const componentType = this.getComponentIdFromInputType(row.input_type);
        inputTypeMap.set(`${row.table_name}.${row.column_name}`, {
          inputType: row.input_type,
          componentType: componentType,
        });
      }

      console.log(`최신 inputType 조회 완료: ${results.length}개`);
    } catch (error) {
      console.warn(`최신 inputType 조회 실패 (무시됨):`, error);
    }

    return inputTypeMap;
  }

  // ========================================
  // 템플릿 관리
  // ========================================

  /**
   * 템플릿 목록 조회 (회사별) (✅ Raw Query 전환 완료)
   */
  async getTemplatesByCompany(
    companyCode: string,
    type?: string,
    isPublic?: boolean,
  ): Promise<ScreenTemplate[]> {
    const whereConditions: string[] = [];
    const params: any[] = [];

    if (companyCode !== "*") {
      whereConditions.push(`company_code = $${params.length + 1}`);
      params.push(companyCode);
    }

    if (type) {
      whereConditions.push(`template_type = $${params.length + 1}`);
      params.push(type);
    }

    if (isPublic !== undefined) {
      whereConditions.push(`is_public = $${params.length + 1}`);
      params.push(isPublic);
    }

    const whereSQL =
      whereConditions.length > 0
        ? `WHERE ${whereConditions.join(" AND ")}`
        : "";

    const templates = await query<any>(
      `SELECT * FROM screen_templates
       ${whereSQL}
       ORDER BY created_date DESC`,
      params,
    );

    return templates.map(this.mapToScreenTemplate);
  }

  /**
   * 템플릿 생성 (✅ Raw Query 전환 완료)
   */
  async createTemplate(
    templateData: Partial<ScreenTemplate>,
  ): Promise<ScreenTemplate> {
    const [template] = await query<any>(
      `INSERT INTO screen_templates (
        template_name, template_type, company_code, description,
        layout_data, is_public, created_by
      ) VALUES ($1, $2, $3, $4, $5, $6, $7)
      RETURNING *`,
      [
        templateData.templateName!,
        templateData.templateType!,
        templateData.companyCode!,
        templateData.description || null,
        templateData.layoutData
          ? JSON.stringify(JSON.parse(JSON.stringify(templateData.layoutData)))
          : null,
        templateData.isPublic || false,
        templateData.createdBy || null,
      ],
    );

    return this.mapToScreenTemplate(template);
  }

  // ========================================
  // 메뉴 할당 관리
  // ========================================

  /**
   * 화면-메뉴 할당 (✅ Raw Query 전환 완료)
   */
  async assignScreenToMenu(
    screenId: number,
    assignmentData: MenuAssignmentRequest,
  ): Promise<void> {
    // 중복 할당 방지
    const existing = await query<{ assignment_id: number }>(
      `SELECT assignment_id FROM screen_menu_assignments
       WHERE screen_id = $1 AND menu_objid = $2 AND company_code = $3
       LIMIT 1`,
      [screenId, assignmentData.menuObjid, assignmentData.companyCode],
    );

    if (existing.length > 0) {
      throw new Error("이미 할당된 화면입니다.");
    }

    // screen_menu_assignments에 할당 추가
    await query(
      `INSERT INTO screen_menu_assignments (
        screen_id, menu_objid, company_code, display_order, created_by
      ) VALUES ($1, $2, $3, $4, $5)`,
      [
        screenId,
        assignmentData.menuObjid,
        assignmentData.companyCode,
        assignmentData.displayOrder || 0,
        assignmentData.createdBy || null,
      ],
    );

    // 화면 정보 조회 (screen_code 가져오기)
    const screen = await queryOne<{ screen_code: string }>(
      `SELECT screen_code FROM screen_definitions WHERE screen_id = $1`,
      [screenId],
    );

    if (screen) {
      // menu_info 테이블도 함께 업데이트 (menu_url과 screen_code 설정)
      // 관리자 메뉴인지 확인
      const menu = await queryOne<{ menu_type: string }>(
        `SELECT menu_type FROM menu_info WHERE objid = $1`,
        [assignmentData.menuObjid],
      );

      const isAdminMenu =
        menu && (menu.menu_type === "0" || menu.menu_type === "admin");
      const menuUrl = isAdminMenu
        ? `/screens/${screenId}?mode=admin`
        : `/screens/${screenId}`;

      await query(
        `UPDATE menu_info 
         SET menu_url = $1, screen_code = $2
         WHERE objid = $3`,
        [menuUrl, screen.screen_code, assignmentData.menuObjid],
      );

      logger.info("화면 할당 완료 (menu_info 업데이트)", {
        screenId,
        menuObjid: assignmentData.menuObjid,
        menuUrl,
        screenCode: screen.screen_code,
      });
    }
  }

  /**
   * 메뉴별 화면 목록 조회
   * company_code 매칭: 본인 회사 할당 + SUPER_ADMIN 글로벌 할당('*') 모두 조회
   * 본인 회사 할당이 우선, 없으면 글로벌 할당 사용
   */
  async getScreensByMenu(
    menuObjid: number,
    companyCode: string,
  ): Promise<ScreenDefinition[]> {
    const screens = await query<any>(
      `SELECT sd.*
       FROM screen_menu_assignments sma
       INNER JOIN screen_definitions sd ON sma.screen_id = sd.screen_id
       WHERE sma.menu_objid = $1
         AND (sma.company_code = $2 OR sma.company_code = '*')
         AND sma.is_active = 'Y'
       ORDER BY
         CASE WHEN sma.company_code = $2 THEN 0 ELSE 1 END,
         sma.display_order ASC`,
      [menuObjid, companyCode],
    );

    return screens.map((screen) => this.mapToScreenDefinition(screen));
  }

  /**
   * 화면에 할당된 메뉴 조회 (첫 번째 할당만 반환)
   * 화면 편집기에서 menuObjid를 가져오기 위해 사용
   */
  async getMenuByScreen(
    screenId: number,
    companyCode: string,
  ): Promise<{ menuObjid: number; menuName?: string } | null> {
    const result = await queryOne<{
      menu_objid: string;
      menu_name_kor?: string;
    }>(
      `SELECT sma.menu_objid, mi.menu_name_kor
       FROM screen_menu_assignments sma
       LEFT JOIN menu_info mi ON sma.menu_objid = mi.objid
       WHERE sma.screen_id = $1
         AND sma.company_code = $2
         AND sma.is_active = 'Y'
       ORDER BY sma.created_date ASC
       LIMIT 1`,
      [screenId, companyCode],
    );

    if (!result) {
      return null;
    }

    return {
      menuObjid: parseInt(result.menu_objid),
      menuName: result.menu_name_kor,
    };
  }

  /**
   * 화면-메뉴 할당 해제 (✅ Raw Query 전환 완료)
   */
  async unassignScreenFromMenu(
    screenId: number,
    menuObjid: number,
    companyCode: string,
  ): Promise<void> {
    // screen_menu_assignments에서 할당 삭제
    await query(
      `DELETE FROM screen_menu_assignments
       WHERE screen_id = $1 AND menu_objid = $2 AND company_code = $3`,
      [screenId, menuObjid, companyCode],
    );

    // menu_info 테이블도 함께 업데이트 (menu_url과 screen_code 제거)
    await query(
      `UPDATE menu_info 
       SET menu_url = NULL, screen_code = NULL
       WHERE objid = $1`,
      [menuObjid],
    );

    logger.info("화면 할당 해제 완료 (menu_info 업데이트)", {
      screenId,
      menuObjid,
      companyCode,
    });
  }

  // ========================================
  // 테이블 타입 연계
  // ========================================

  /**
   * 컬럼 정보 조회 (웹 타입 포함) (✅ Raw Query 전환 완료)
   */
  async getColumnInfo(tableName: string): Promise<ColumnInfo[]> {
    const columns = await query<any>(
      `SELECT
        c.column_name,
        COALESCE(ttc.column_label, c.column_name) as column_label,
        c.data_type,
        COALESCE(ttc.input_type, 'text') as web_type,
        c.is_nullable,
        c.column_default,
        c.character_maximum_length,
        c.numeric_precision,
        c.numeric_scale,
        ttc.detail_settings,
        ttc.code_category,
        ttc.reference_table,
        ttc.reference_column,
        ttc.display_column,
        ttc.is_visible,
        ttc.display_order,
        ttc.description
       FROM information_schema.columns c
       LEFT JOIN table_type_columns ttc ON c.table_name = ttc.table_name
         AND c.column_name = ttc.column_name AND ttc.company_code = '*'
       WHERE c.table_name = $1
       ORDER BY COALESCE(ttc.display_order, c.ordinal_position)`,
      [tableName],
    );

    return columns as ColumnInfo[];
  }

  /**
   * 입력 타입 설정 (✅ Raw Query 전환 완료)
   */
  async setColumnWebType(
    tableName: string,
    columnName: string,
    webType: WebType,
    additionalSettings?: Partial<ColumnWebTypeSetting>,
  ): Promise<void> {
    // UPSERT를 INSERT ... ON CONFLICT로 변환 (table_type_columns 사용)
    await query(
      `INSERT INTO table_type_columns (
        table_name, column_name, column_label, input_type, detail_settings,
        code_category, reference_table, reference_column, display_column,
        is_visible, display_order, description, is_nullable, company_code, created_date, updated_date
      ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'Y', '*', $13, $14)
      ON CONFLICT (table_name, column_name, company_code)
      DO UPDATE SET
        input_type = EXCLUDED.input_type,
        column_label = COALESCE(EXCLUDED.column_label, table_type_columns.column_label),
        detail_settings = COALESCE(EXCLUDED.detail_settings, table_type_columns.detail_settings),
        code_category = COALESCE(EXCLUDED.code_category, table_type_columns.code_category),
        reference_table = COALESCE(EXCLUDED.reference_table, table_type_columns.reference_table),
        reference_column = COALESCE(EXCLUDED.reference_column, table_type_columns.reference_column),
        display_column = COALESCE(EXCLUDED.display_column, table_type_columns.display_column),
        is_visible = COALESCE(EXCLUDED.is_visible, table_type_columns.is_visible),
        display_order = COALESCE(EXCLUDED.display_order, table_type_columns.display_order),
        description = COALESCE(EXCLUDED.description, table_type_columns.description),
        updated_date = EXCLUDED.updated_date`,
      [
        tableName,
        columnName,
        additionalSettings?.columnLabel || null,
        webType,
        additionalSettings?.detailSettings
          ? JSON.stringify(additionalSettings.detailSettings)
          : null,
        additionalSettings?.codeCategory || null,
        additionalSettings?.referenceTable || null,
        additionalSettings?.referenceColumn || null,
        (additionalSettings as any)?.displayColumn || null,
        additionalSettings?.isVisible ?? true,
        additionalSettings?.displayOrder ?? 0,
        additionalSettings?.description || null,
        new Date(),
        new Date(),
      ],
    );
  }

  /**
   * 웹 타입별 위젯 생성
   */
  generateWidgetFromColumn(column: ColumnInfo): WidgetData {
    const baseWidget = {
      id: generateId(),
      tableName: column.tableName,
      columnName: column.columnName,
      type: column.webType || "text",
      label: column.columnLabel || column.columnName,
      required: column.isNullable === "N",
      readonly: false,
    };

    // detail_settings JSON 파싱
    const detailSettings = column.detailSettings
      ? JSON.parse(column.detailSettings)
      : {};

    switch (column.webType) {
      case "text":
        return {
          ...baseWidget,
          maxLength: detailSettings.maxLength || column.characterMaximumLength,
          placeholder: `Enter ${column.columnLabel || column.columnName}`,
          pattern: detailSettings.pattern,
        };

      case "number":
        return {
          ...baseWidget,
          min: detailSettings.min,
          max:
            detailSettings.max ||
            (column.numericPrecision
              ? Math.pow(10, column.numericPrecision) - 1
              : undefined),
          step:
            detailSettings.step ||
            (column.numericScale && column.numericScale > 0
              ? Math.pow(10, -column.numericScale)
              : 1),
        };

      case "date":
        return {
          ...baseWidget,
          format: detailSettings.format || "YYYY-MM-DD",
          minDate: detailSettings.minDate,
          maxDate: detailSettings.maxDate,
        };

      case "code":
        return {
          ...baseWidget,
          codeCategory: column.codeCategory,
          multiple: detailSettings.multiple || false,
          searchable: detailSettings.searchable || false,
        };

      case "entity":
        return {
          ...baseWidget,
          referenceTable: column.referenceTable,
          referenceColumn: column.referenceColumn,
          searchable: detailSettings.searchable || true,
          multiple: detailSettings.multiple || false,
        };

      case "textarea":
        return {
          ...baseWidget,
          rows: detailSettings.rows || 3,
          maxLength: detailSettings.maxLength || column.characterMaximumLength,
        };

      case "select":
        return {
          ...baseWidget,
          options: detailSettings.options || [],
          multiple: detailSettings.multiple || false,
          searchable: detailSettings.searchable || false,
        };

      case "checkbox":
        return {
          ...baseWidget,
          defaultChecked: detailSettings.defaultChecked || false,
          label: detailSettings.label || column.columnLabel,
        };

      case "radio":
        return {
          ...baseWidget,
          options: detailSettings.options || [],
          inline: detailSettings.inline || false,
        };

      case "file":
        return {
          ...baseWidget,
          accept: detailSettings.accept || "*/*",
          maxSize: detailSettings.maxSize || 10485760, // 10MB
          multiple: detailSettings.multiple || false,
        };

      default:
        return {
          ...baseWidget,
          type: "text",
        };
    }
  }

  // ========================================
  // 유틸리티 메서드
  // ========================================

  private mapToScreenDefinition(
    data: any,
    tableLabelMap?: Map<string, string>,
  ): ScreenDefinition {
    const tableLabel = tableLabelMap?.get(data.table_name) || data.table_name;

    return {
      screenId: data.screen_id,
      screenName: data.screen_name,
      screenCode: data.screen_code,
      tableName: data.table_name,
      tableLabel: tableLabel, // 라벨이 있으면 라벨, 없으면 테이블명
      companyCode: data.company_code,
      description: data.description,
      isActive: data.is_active,
      createdDate: data.created_date,
      createdBy: data.created_by,
      updatedDate: data.updated_date,
      updatedBy: data.updated_by,
      dbSourceType: data.db_source_type || "internal",
      dbConnectionId: data.db_connection_id || undefined,
      // REST API 관련 필드
      dataSourceType: data.data_source_type || "database",
      restApiConnectionId: data.rest_api_connection_id || undefined,
      restApiEndpoint: data.rest_api_endpoint || undefined,
      restApiJsonPath: data.rest_api_json_path || "data",
    };
  }

  private mapToScreenTemplate(data: any): ScreenTemplate {
    return {
      templateId: data.template_id,
      templateName: data.template_name,
      templateType: data.template_type,
      companyCode: data.company_code,
      description: data.description,
      layoutData: data.layout_data,
      isPublic: data.is_public,
      createdBy: data.created_by,
      createdDate: data.created_date,
    };
  }

  /**
   * 화면 코드 자동 생성 (회사코드 + '_' + 순번) (✅ Raw Query 전환 완료)
   * 동시성 문제 방지: Advisory Lock 사용
   */
  async generateScreenCode(companyCode: string): Promise<string> {
    return await transaction(async (client) => {
      // 회사 코드를 숫자로 변환하여 advisory lock ID로 사용
      const lockId = Buffer.from(companyCode).reduce(
        (acc, byte) => acc + byte,
        0,
      );

      // Advisory lock 획득 (다른 트랜잭션이 같은 회사 코드를 생성하는 동안 대기)
      await client.query("SELECT pg_advisory_xact_lock($1)", [lockId]);

      // 해당 회사의 기존 화면 코드들 조회 (모든 화면 - 삭제된 코드도 재사용 방지)
      // LIMIT 제거하고 숫자 추출하여 최대값 찾기
      const existingScreens = await client.query<{ screen_code: string }>(
        `SELECT screen_code FROM screen_definitions
         WHERE screen_code LIKE $1
         ORDER BY screen_code DESC`,
        [`${companyCode}_%`],
      );

      // 회사 코드 뒤의 숫자 부분 추출하여 최대값 찾기
      let maxNumber = 0;
      const pattern = new RegExp(
        `^${companyCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}_(\\d+)$`,
      );

      console.log(
        `🔍 화면 코드 생성 - 조회된 화면 수: ${existingScreens.rows.length}`,
      );
      console.log(`🔍 패턴: ${pattern}`);

      for (const screen of existingScreens.rows) {
        const match = screen.screen_code.match(pattern);
        if (match) {
          const number = parseInt(match[1], 10);
          console.log(`🔍 매칭: ${screen.screen_code} → 숫자: ${number}`);
          if (number > maxNumber) {
            maxNumber = number;
          }
        }
      }

      // 다음 순번으로 화면 코드 생성
      const nextNumber = maxNumber + 1;
      // 숫자가 3자리 이상이면 패딩 없이, 아니면 3자리 패딩
      const newCode = `${companyCode}_${nextNumber}`;
      console.log(
        `🔢 화면 코드 생성: ${companyCode} → ${newCode} (maxNumber: ${maxNumber}, nextNumber: ${nextNumber})`,
      );

      return newCode;
      // Advisory lock은 트랜잭션 종료 시 자동으로 해제됨
    });
  }

  /**
   * 여러 개의 화면 코드를 한 번에 생성 (중복 방지)
   * 한 트랜잭션 내에서 순차적으로 생성하여 중복 방지
   */
  async generateMultipleScreenCodes(
    companyCode: string,
    count: number,
  ): Promise<string[]> {
    return await transaction(async (client) => {
      // Advisory lock 획득
      const lockId = Buffer.from(companyCode).reduce(
        (acc, byte) => acc + byte,
        0,
      );
      await client.query("SELECT pg_advisory_xact_lock($1)", [lockId]);

      // 현재 최대 번호 조회 (숫자 추출 후 정렬)
      // 패턴: COMPANY_CODE_XXX 또는 COMPANY_CODEXXX
      const existingScreens = await client.query<{
        screen_code: string;
        num: number;
      }>(
        `SELECT screen_code, 
                COALESCE(
                  NULLIF(
                    regexp_replace(screen_code, $2, '\\1'),
                    screen_code
                  )::integer,
                  0
                ) as num
         FROM screen_definitions
         WHERE company_code = $1 
           AND screen_code ~ $2
           AND deleted_date IS NULL
         ORDER BY num DESC
         LIMIT 1`,
        [
          companyCode,
          `^${companyCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[_]?(\\d+)$`,
        ],
      );

      let maxNumber = 0;
      if (existingScreens.rows.length > 0 && existingScreens.rows[0].num) {
        maxNumber = existingScreens.rows[0].num;
      }

      console.log(`🔢 현재 최대 화면 코드 번호: ${companyCode} → ${maxNumber}`);

      // count개의 코드를 순차적으로 생성
      const codes: string[] = [];
      for (let i = 0; i < count; i++) {
        const nextNumber = maxNumber + i + 1;
        const paddedNumber = nextNumber.toString().padStart(3, "0");
        codes.push(`${companyCode}_${paddedNumber}`);
      }

      console.log(
        `🔢 화면 코드 일괄 생성 (${count}개): ${companyCode} → [${codes.join(", ")}]`,
      );

      return codes;
    });
  }

  /**
   * 화면명 중복 체크
   * 같은 회사 내에서 동일한 화면명이 있는지 확인
   */
  async checkDuplicateScreenName(
    companyCode: string,
    screenName: string,
  ): Promise<boolean> {
    const result = await query<any>(
      `SELECT COUNT(*) as count
       FROM screen_definitions
       WHERE company_code = $1
         AND screen_name = $2
         AND deleted_date IS NULL`,
      [companyCode, screenName],
    );

    const count = parseInt(result[0]?.count || "0", 10);
    return count > 0;
  }

  /**
   * 화면에 연결된 모달/화면들을 재귀적으로 자동 감지
   * - 버튼 컴포넌트: popup/modal/edit/openModalWithData 액션의 targetScreenId
   * - 조건부 컨테이너: sections[].screenId (조건별 화면 할당)
   * - 중첩된 화면들도 모두 감지 (재귀)
   */
  async detectLinkedModalScreens(
    screenId: number,
  ): Promise<{ screenId: number; screenName: string; screenCode: string }[]> {
    console.log(`\n🔍 [재귀 감지 시작] 화면 ID: ${screenId}`);

    const allLinkedScreenIds = new Set<number>();
    const visited = new Set<number>(); // 무한 루프 방지
    const queue: number[] = [screenId]; // BFS 큐

    // BFS로 연결된 모든 화면 탐색
    while (queue.length > 0) {
      const currentScreenId = queue.shift()!;

      // 이미 방문한 화면은 스킵 (순환 참조 방지)
      if (visited.has(currentScreenId)) {
        console.log(`⏭️  이미 방문한 화면 스킵: ${currentScreenId}`);
        continue;
      }

      visited.add(currentScreenId);
      console.log(
        `\n📋 현재 탐색 중인 화면: ${currentScreenId} (깊이: ${visited.size})`,
      );

      // 현재 화면의 모든 레이아웃 조회
      const layouts = await query<any>(
        `SELECT layout_id, properties
         FROM screen_layouts
         WHERE screen_id = $1
           AND component_type = 'component'
           AND properties IS NOT NULL`,
        [currentScreenId],
      );

      console.log(`  📦 레이아웃 개수: ${layouts.length}`);

      // 각 레이아웃에서 연결된 화면 ID 확인
      for (const layout of layouts) {
        try {
          const properties = layout.properties;

          // 1. 버튼 컴포넌트의 액션 확인
          if (
            properties?.componentType === "button" ||
            properties?.componentType?.startsWith("button-")
          ) {
            const action = properties?.componentConfig?.action;

            const modalActionTypes = [
              "popup",
              "modal",
              "edit",
              "openModalWithData",
            ];
            if (
              modalActionTypes.includes(action?.type) &&
              action?.targetScreenId
            ) {
              const targetScreenId = parseInt(action.targetScreenId);
              if (
                !isNaN(targetScreenId) &&
                targetScreenId !== currentScreenId
              ) {
                // 메인 화면이 아닌 경우에만 추가
                if (targetScreenId !== screenId) {
                  allLinkedScreenIds.add(targetScreenId);
                }
                // 아직 방문하지 않은 화면이면 큐에 추가
                if (!visited.has(targetScreenId)) {
                  queue.push(targetScreenId);
                  console.log(
                    `  🔗 [버튼] 연결된 화면 발견: ${targetScreenId} (action: ${action.type}) → 큐에 추가`,
                  );
                }
              }
            }
          }

          // 2. conditional-container 컴포넌트의 sections 확인
          if (properties?.componentType === "conditional-container") {
            const sections = properties?.componentConfig?.sections || [];

            for (const section of sections) {
              if (section?.screenId) {
                const sectionScreenId = parseInt(section.screenId);
                if (
                  !isNaN(sectionScreenId) &&
                  sectionScreenId !== currentScreenId
                ) {
                  // 메인 화면이 아닌 경우에만 추가
                  if (sectionScreenId !== screenId) {
                    allLinkedScreenIds.add(sectionScreenId);
                  }
                  // 아직 방문하지 않은 화면이면 큐에 추가
                  if (!visited.has(sectionScreenId)) {
                    queue.push(sectionScreenId);
                    console.log(
                      `  🔗 [조건부컨테이너] 연결된 화면 발견: ${sectionScreenId} (condition: ${section.condition}) → 큐에 추가`,
                    );
                  }
                }
              }
            }
          }
        } catch (error) {
          console.warn(`  ⚠️  레이아웃 ${layout.layout_id} 파싱 오류:`, error);
        }
      }
    }

    console.log(
      `\n✅ [재귀 감지 완료] 총 방문한 화면: ${visited.size}개, 연결된 화면: ${allLinkedScreenIds.size}개`,
    );
    console.log(`   방문한 화면 ID: [${Array.from(visited).join(", ")}]`);
    console.log(
      `   연결된 화면 ID: [${Array.from(allLinkedScreenIds).join(", ")}]`,
    );

    // 감지된 화면 ID들의 정보 조회
    if (allLinkedScreenIds.size === 0) {
      console.log(`ℹ️  연결된 화면이 없습니다.`);
      return [];
    }

    const screenIds = Array.from(allLinkedScreenIds);
    const placeholders = screenIds.map((_, i) => `$${i + 1}`).join(", ");

    const linkedScreens = await query<any>(
      `SELECT screen_id, screen_name, screen_code
       FROM screen_definitions
       WHERE screen_id IN (${placeholders})
         AND deleted_date IS NULL
       ORDER BY screen_name`,
      screenIds,
    );

    console.log(`\n📋 최종 감지된 화면 목록:`);
    linkedScreens.forEach((s: any) => {
      console.log(
        `   - ${s.screen_name} (ID: ${s.screen_id}, 코드: ${s.screen_code})`,
      );
    });

    return linkedScreens.map((s) => ({
      screenId: s.screen_id,
      screenName: s.screen_name,
      screenCode: s.screen_code,
    }));
  }

  /**
   * 화면 레이아웃에서 사용하는 numberingRuleId 수집
   * - componentConfig.autoGeneration.options.numberingRuleId (text-input)
   * - componentConfig.sections[].fields[].numberingRule.ruleId (universal-form-modal)
   * - componentConfig.action.excelNumberingRuleId (엑셀 업로드)
   */
  private collectNumberingRuleIdsFromLayouts(layouts: any[]): Set<string> {
    const ruleIds = new Set<string>();

    for (const layout of layouts) {
      const props = layout.properties;
      if (!props) continue;

      // 1. componentConfig.autoGeneration.options.numberingRuleId (text-input 컴포넌트)
      const autoGenRuleId =
        props?.componentConfig?.autoGeneration?.options?.numberingRuleId;
      if (
        autoGenRuleId &&
        typeof autoGenRuleId === "string" &&
        autoGenRuleId.startsWith("rule-")
      ) {
        ruleIds.add(autoGenRuleId);
      }

      // 2. componentConfig.sections[].fields[].numberingRule.ruleId (universal-form-modal)
      const sections = props?.componentConfig?.sections;
      if (Array.isArray(sections)) {
        for (const section of sections) {
          const fields = section?.fields;
          if (Array.isArray(fields)) {
            for (const field of fields) {
              const ruleId = field?.numberingRule?.ruleId;
              if (
                ruleId &&
                typeof ruleId === "string" &&
                ruleId.startsWith("rule-")
              ) {
                ruleIds.add(ruleId);
              }
            }
          }
          // optionalFieldGroups 내부의 필드들도 확인
          const optGroups = section?.optionalFieldGroups;
          if (Array.isArray(optGroups)) {
            for (const optGroup of optGroups) {
              const optFields = optGroup?.fields;
              if (Array.isArray(optFields)) {
                for (const field of optFields) {
                  const ruleId = field?.numberingRule?.ruleId;
                  if (
                    ruleId &&
                    typeof ruleId === "string" &&
                    ruleId.startsWith("rule-")
                  ) {
                    ruleIds.add(ruleId);
                  }
                }
              }
            }
          }
        }
      }

      // 3. componentConfig.action.excelNumberingRuleId (엑셀 업로드)
      const excelRuleId = props?.componentConfig?.action?.excelNumberingRuleId;
      if (
        excelRuleId &&
        typeof excelRuleId === "string" &&
        excelRuleId.startsWith("rule-")
      ) {
        ruleIds.add(excelRuleId);
      }

      // 4. componentConfig.action.numberingRuleId (버튼 액션)
      const actionRuleId = props?.componentConfig?.action?.numberingRuleId;
      if (
        actionRuleId &&
        typeof actionRuleId === "string" &&
        actionRuleId.startsWith("rule-")
      ) {
        ruleIds.add(actionRuleId);
      }
    }

    return ruleIds;
  }

  /**
   * 채번 규칙 복사 및 ID 매핑 반환
   * - 원본 회사의 채번 규칙을 대상 회사로 복사
   * - 이름이 같은 규칙이 있으면 재사용
   * - current_sequence는 0으로 초기화
   */
  /**
   * 채번 규칙 복제 (numbering_rules 테이블 사용)
   * - menu_objid 의존성 제거됨
   * - table_name + column_name + company_code 기반
   */
  private async copyNumberingRulesForScreen(
    ruleIds: Set<string>,
    sourceCompanyCode: string,
    targetCompanyCode: string,
    client: any,
  ): Promise<Map<string, string>> {
    const ruleIdMap = new Map<string, string>();

    if (ruleIds.size === 0) {
      return ruleIdMap;
    }

    console.log(`🔄 채번 규칙 복사 시작: ${ruleIds.size}개 규칙`);

    // 1. 원본 채번 규칙 조회 (numbering_rules 테이블)
    const ruleIdArray = Array.from(ruleIds);
    const sourceRulesResult = await client.query(
      `SELECT * FROM numbering_rules WHERE rule_id = ANY($1)`,
      [ruleIdArray],
    );

    if (sourceRulesResult.rows.length === 0) {
      console.log(`  📭 복사할 채번 규칙 없음 (해당 rule_id 없음)`);
      return ruleIdMap;
    }

    console.log(`  📋 원본 채번 규칙: ${sourceRulesResult.rows.length}개`);

    // 2. 대상 회사의 기존 채번 규칙 조회 (이름 기준)
    const existingRulesResult = await client.query(
      `SELECT rule_id, rule_name FROM numbering_rules WHERE company_code = $1`,
      [targetCompanyCode],
    );
    const existingRulesByName = new Map<string, string>(
      existingRulesResult.rows.map((r: any) => [r.rule_name, r.rule_id]),
    );

    // 3. 각 규칙 복사 또는 재사용
    for (const rule of sourceRulesResult.rows) {
      const existingId = existingRulesByName.get(rule.rule_name);

      if (existingId) {
        // 기존 규칙 재사용
        ruleIdMap.set(rule.rule_id, existingId);
        console.log(
          `    ♻️ 기존 채번 규칙 재사용: ${rule.rule_name} (${rule.rule_id} → ${existingId})`,
        );
      } else {
        // 새로 복사 - 새 rule_id 생성
        const newRuleId = `rule-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

        // numbering_rules 복사 (current_sequence = 0으로 초기화)
        await client.query(
          `INSERT INTO numbering_rules (
            rule_id, rule_name, description, separator, reset_period,
            current_sequence, table_name, column_name, company_code,
            created_at, updated_at, created_by, last_generated_date,
            category_column, category_value_id
          ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)`,
          [
            newRuleId,
            rule.rule_name,
            rule.description,
            rule.separator,
            rule.reset_period,
            0, // current_sequence 초기화
            rule.table_name,
            rule.column_name,
            targetCompanyCode,
            new Date(),
            new Date(),
            rule.created_by,
            null, // last_generated_date 초기화
            rule.category_column,
            rule.category_value_id,
          ],
        );

        // numbering_rule_parts 복사
        const partsResult = await client.query(
          `SELECT * FROM numbering_rule_parts WHERE rule_id = $1 ORDER BY part_order`,
          [rule.rule_id],
        );

        for (const part of partsResult.rows) {
          await client.query(
            `INSERT INTO numbering_rule_parts (
              rule_id, part_order, part_type, generation_method,
              auto_config, manual_config, company_code, created_at
            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
            [
              newRuleId,
              part.part_order,
              part.part_type,
              part.generation_method,
              part.auto_config ? JSON.stringify(part.auto_config) : null,
              part.manual_config ? JSON.stringify(part.manual_config) : null,
              targetCompanyCode,
              new Date(),
            ],
          );
        }

        ruleIdMap.set(rule.rule_id, newRuleId);
        console.log(
          `    ➕ 채번 규칙 복사: ${rule.rule_name} (${rule.rule_id} → ${newRuleId}), 파트 ${partsResult.rows.length}개`,
        );
      }
    }

    console.log(`  ✅ 채번 규칙 복사 완료: 매핑 ${ruleIdMap.size}개`);
    return ruleIdMap;
  }

  /**
   * properties 내의 numberingRuleId 매핑
   * - componentConfig.autoGeneration.options.numberingRuleId (text-input)
   * - componentConfig.sections[].fields[].numberingRule.ruleId (universal-form-modal)
   * - componentConfig.action.excelNumberingRuleId (엑셀 업로드)
   */
  private updateNumberingRuleIdsInProperties(
    properties: any,
    ruleIdMap: Map<string, string>,
  ): any {
    if (!properties || ruleIdMap.size === 0) return properties;

    const updated = JSON.parse(JSON.stringify(properties));

    // 1. componentConfig.autoGeneration.options.numberingRuleId (text-input)
    if (updated?.componentConfig?.autoGeneration?.options?.numberingRuleId) {
      const oldId =
        updated.componentConfig.autoGeneration.options.numberingRuleId;
      const newId = ruleIdMap.get(oldId);
      if (newId) {
        updated.componentConfig.autoGeneration.options.numberingRuleId = newId;
        console.log(
          `      🔗 autoGeneration.numberingRuleId: ${oldId} → ${newId}`,
        );
      }
    }

    // 2. componentConfig.sections[].fields[].numberingRule.ruleId (universal-form-modal)
    if (Array.isArray(updated?.componentConfig?.sections)) {
      for (const section of updated.componentConfig.sections) {
        // 일반 필드
        if (Array.isArray(section?.fields)) {
          for (const field of section.fields) {
            if (field?.numberingRule?.ruleId) {
              const oldId = field.numberingRule.ruleId;
              const newId = ruleIdMap.get(oldId);
              if (newId) {
                field.numberingRule.ruleId = newId;
                console.log(
                  `      🔗 field.numberingRule.ruleId: ${oldId} → ${newId}`,
                );
              }
            }
          }
        }
        // optionalFieldGroups 내부의 필드들
        if (Array.isArray(section?.optionalFieldGroups)) {
          for (const optGroup of section.optionalFieldGroups) {
            if (Array.isArray(optGroup?.fields)) {
              for (const field of optGroup.fields) {
                if (field?.numberingRule?.ruleId) {
                  const oldId = field.numberingRule.ruleId;
                  const newId = ruleIdMap.get(oldId);
                  if (newId) {
                    field.numberingRule.ruleId = newId;
                    console.log(
                      `      🔗 optField.numberingRule.ruleId: ${oldId} → ${newId}`,
                    );
                  }
                }
              }
            }
          }
        }
      }
    }

    // 3. componentConfig.action.excelNumberingRuleId
    if (updated?.componentConfig?.action?.excelNumberingRuleId) {
      const oldId = updated.componentConfig.action.excelNumberingRuleId;
      const newId = ruleIdMap.get(oldId);
      if (newId) {
        updated.componentConfig.action.excelNumberingRuleId = newId;
        console.log(`      🔗 excelNumberingRuleId: ${oldId} → ${newId}`);
      }
    }

    // 4. componentConfig.action.numberingRuleId (버튼 액션)
    if (updated?.componentConfig?.action?.numberingRuleId) {
      const oldId = updated.componentConfig.action.numberingRuleId;
      const newId = ruleIdMap.get(oldId);
      if (newId) {
        updated.componentConfig.action.numberingRuleId = newId;
        console.log(`      🔗 action.numberingRuleId: ${oldId} → ${newId}`);
      }
    }

    return updated;
  }

  /**
   * properties 내의 탭 컴포넌트 screenId 매핑
   * - componentConfig.tabs[].screenId (tabs-widget)
   */
  private updateTabScreenIdsInProperties(
    properties: any,
    screenIdMap: Map<number, number>,
  ): any {
    if (!properties || screenIdMap.size === 0) return properties;

    const updated = JSON.parse(JSON.stringify(properties));

    // componentConfig.tabs[].screenId (tabs-widget)
    if (Array.isArray(updated?.componentConfig?.tabs)) {
      for (const tab of updated.componentConfig.tabs) {
        if (tab?.screenId) {
          const oldId = Number(tab.screenId);
          const newId = screenIdMap.get(oldId);
          if (newId) {
            tab.screenId = newId;
            console.log(`      🔗 tab.screenId: ${oldId} → ${newId}`);
          }
        }
      }
    }

    return updated;
  }

  /**
   * 그룹 복제 완료 후 모든 컴포넌트의 화면 참조 일괄 업데이트
   * - tabs 컴포넌트의 screenId
   * - conditional-container의 screenId
   * - 버튼/액션의 modalScreenId
   * - 버튼/액션의 targetScreenId (화면 이동, 모달 열기 등)
   * @param targetScreenIds 복제된 대상 화면 ID 목록
   * @param screenIdMap 원본 화면 ID -> 새 화면 ID 매핑
   */
  async updateTabScreenReferences(
    targetScreenIds: number[],
    screenIdMap: { [key: number]: number },
  ): Promise<{ updated: number; details: string[] }> {
    const result = { updated: 0, details: [] as string[] };

    if (targetScreenIds.length === 0 || Object.keys(screenIdMap).length === 0) {
      console.log(
        `⚠️ updateTabScreenReferences 스킵: targetScreenIds=${targetScreenIds.length}, screenIdMap keys=${Object.keys(screenIdMap).length}`,
      );
      return result;
    }

    console.log(`🔄 updateTabScreenReferences 시작:`);
    console.log(`   - targetScreenIds: ${targetScreenIds.length}개`);
    console.log(`   - screenIdMap: ${JSON.stringify(screenIdMap)}`);

    const screenMap = new Map<number, number>(
      Object.entries(screenIdMap).map(([k, v]) => [Number(k), v]),
    );

    await transaction(async (client) => {
      // 대상 화면들의 모든 레이아웃 조회 (screenId, modalScreenId, targetScreenId 참조가 있는 것)
      const placeholders = targetScreenIds
        .map((_, i) => `$${i + 1}`)
        .join(", ");
      const layoutsResult = await client.query(
        `SELECT layout_id, screen_id, properties 
         FROM screen_layouts 
         WHERE screen_id IN (${placeholders})
         AND (
           properties::text LIKE '%"screenId"%'
           OR properties::text LIKE '%"modalScreenId"%'
           OR properties::text LIKE '%"targetScreenId"%'
         )`,
        targetScreenIds,
      );

      console.log(
        `🔍 참조 업데이트 대상 레이아웃: ${layoutsResult.rows.length}개`,
      );

      for (const layout of layoutsResult.rows) {
        let properties = layout.properties;
        if (typeof properties === "string") {
          try {
            properties = JSON.parse(properties);
          } catch (e) {
            continue;
          }
        }

        let hasChanges = false;

        // 재귀적으로 모든 screenId/modalScreenId 참조 업데이트
        const updateReferences = async (
          obj: any,
          path: string = "",
        ): Promise<void> => {
          if (!obj || typeof obj !== "object") return;

          for (const key of Object.keys(obj)) {
            const value = obj[key];
            const currentPath = path ? `${path}.${key}` : key;

            // screenId 업데이트
            if (key === "screenId" && typeof value === "number") {
              const newId = screenMap.get(value);
              if (newId) {
                obj[key] = newId;
                hasChanges = true;
                result.details.push(
                  `layout_id=${layout.layout_id}: ${currentPath} ${value} → ${newId}`,
                );
                console.log(
                  `🔗 screenId 매핑: ${value} → ${newId} (${currentPath})`,
                );

                // screenName도 함께 업데이트 (있는 경우)
                if (obj.screenName !== undefined) {
                  const newScreenResult = await client.query(
                    `SELECT screen_name FROM screen_definitions WHERE screen_id = $1`,
                    [newId],
                  );
                  if (newScreenResult.rows.length > 0) {
                    obj.screenName = newScreenResult.rows[0].screen_name;
                  }
                }
              } else {
                console.log(
                  `⚠️ screenId ${value} 매핑 없음 (${currentPath}) - screenMap에 해당 키 없음`,
                );
              }
            }

            // modalScreenId 업데이트
            if (key === "modalScreenId" && typeof value === "number") {
              const newId = screenMap.get(value);
              if (newId) {
                obj[key] = newId;
                hasChanges = true;
                result.details.push(
                  `layout_id=${layout.layout_id}: ${currentPath} ${value} → ${newId}`,
                );
                console.log(
                  `🔗 modalScreenId 매핑: ${value} → ${newId} (${currentPath})`,
                );
              } else {
                console.log(
                  `⚠️ modalScreenId ${value} 매핑 없음 (${currentPath}) - screenMap에 해당 키 없음`,
                );
              }
            }

            // targetScreenId 업데이트 (버튼 액션에서 사용, 문자열 또는 숫자)
            if (key === "targetScreenId") {
              const oldId =
                typeof value === "string" ? parseInt(value, 10) : value;
              if (!isNaN(oldId)) {
                const newId = screenMap.get(oldId);
                if (newId) {
                  // 원래 타입 유지 (문자열이면 문자열, 숫자면 숫자)
                  obj[key] =
                    typeof value === "string" ? newId.toString() : newId;
                  hasChanges = true;
                  result.details.push(
                    `layout_id=${layout.layout_id}: ${currentPath} ${oldId} → ${newId}`,
                  );
                  console.log(
                    `🔗 targetScreenId 매핑: ${oldId} → ${newId} (${currentPath})`,
                  );
                } else {
                  console.log(
                    `⚠️ targetScreenId ${oldId} 매핑 없음 (${currentPath}) - screenMap에 해당 키 없음`,
                  );
                }
              }
            }

            // 배열 처리
            if (Array.isArray(value)) {
              for (let i = 0; i < value.length; i++) {
                await updateReferences(value[i], `${currentPath}[${i}]`);
              }
            }
            // 객체 재귀
            else if (typeof value === "object" && value !== null) {
              await updateReferences(value, currentPath);
            }
          }
        };

        await updateReferences(properties);

        if (hasChanges) {
          await client.query(
            `UPDATE screen_layouts SET properties = $1 WHERE layout_id = $2`,
            [JSON.stringify(properties), layout.layout_id],
          );
          result.updated++;
        }
      }

      console.log(
        `✅ V1 screenId/modalScreenId/targetScreenId 참조 업데이트 완료: ${result.updated}개 레이아웃`,
      );

      // V2 레이아웃(screen_layouts_v2)도 동일하게 처리
      const v2LayoutsResult = await client.query(
        `SELECT screen_id, layer_id, company_code, layout_data
         FROM screen_layouts_v2
         WHERE screen_id IN (${placeholders})
         AND layout_data::text ~ '"(screenId|targetScreenId|modalScreenId|leftScreenId|rightScreenId|addModalScreenId|editModalScreenId)"'`,
        targetScreenIds,
      );

      console.log(
        `🔍 V2 참조 업데이트 대상 레이아웃: ${v2LayoutsResult.rows.length}개`,
      );

      let v2Updated = 0;
      for (const v2Layout of v2LayoutsResult.rows) {
        let layoutData = v2Layout.layout_data;
        if (!layoutData) continue;

        let v2HasChanges = false;

        const updateV2References = (obj: any): void => {
          if (!obj || typeof obj !== "object") return;
          if (Array.isArray(obj)) {
            for (const item of obj) updateV2References(item);
            return;
          }
          for (const key of Object.keys(obj)) {
            const value = obj[key];
            if (
              (key === "screenId" || key === "targetScreenId" || key === "modalScreenId" ||
               key === "leftScreenId" || key === "rightScreenId" ||
               key === "addModalScreenId" || key === "editModalScreenId")
            ) {
              const numVal = typeof value === "number" ? value : parseInt(value);
              if (!isNaN(numVal) && numVal > 0) {
                const newId = screenMap.get(numVal);
                if (newId) {
                  obj[key] = typeof value === "number" ? newId : String(newId);
                  v2HasChanges = true;
                  console.log(`🔗 V2 ${key} 매핑: ${numVal} → ${newId}`);
                }
              }
            }
            if (typeof value === "object" && value !== null) {
              updateV2References(value);
            }
          }
        };

        updateV2References(layoutData);

        if (v2HasChanges) {
          await client.query(
            `UPDATE screen_layouts_v2 SET layout_data = $1, updated_at = NOW()
             WHERE screen_id = $2 AND layer_id = $3 AND company_code = $4`,
            [JSON.stringify(layoutData), v2Layout.screen_id, v2Layout.layer_id, v2Layout.company_code],
          );
          v2Updated++;
        }
      }

      console.log(
        `✅ V2 참조 업데이트 완료: ${v2Updated}개 레이아웃`,
      );
      result.updated += v2Updated;
    });

    return result;
  }

  /**
   * 탭 컴포넌트의 screenId를 대상 회사에서 같은 이름의 화면으로 자동 매핑
   * @param properties 레이아웃 properties
   * @param targetCompanyCode 대상 회사 코드
   * @param client PostgreSQL 클라이언트
   * @returns 업데이트된 properties
   */
  private async autoMapTabScreenIds(
    properties: any,
    targetCompanyCode: string,
    client: any,
  ): Promise<any> {
    if (!Array.isArray(properties?.componentConfig?.tabs)) {
      return properties;
    }

    const tabs = properties.componentConfig.tabs;
    let hasChanges = false;

    for (const tab of tabs) {
      if (!tab?.screenId) continue;

      const oldScreenId = Number(tab.screenId);
      const oldScreenName = tab.screenName;

      // 1. 원본 화면 이름 조회 (screenName이 없는 경우)
      let screenNameToFind = oldScreenName;
      if (!screenNameToFind) {
        const sourceResult = await client.query(
          `SELECT screen_name FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
          [oldScreenId],
        );
        if (sourceResult.rows.length > 0) {
          screenNameToFind = sourceResult.rows[0].screen_name;
        }
      }

      if (!screenNameToFind) continue;

      // 2. 대상 회사에서 유사한 이름의 화면 찾기
      // 원본 화면 이름에서 회사 접두어를 제거하고 핵심 이름으로 검색
      // 예: "탑씰 품목 카테고리설정" → "카테고리설정"으로 검색
      const nameParts = screenNameToFind.split(" ");
      const coreNamePart =
        nameParts.length > 1 ? nameParts.slice(-1)[0] : screenNameToFind;

      const targetResult = await client.query(
        `SELECT screen_id, screen_name 
         FROM screen_definitions 
         WHERE company_code = $1 
         AND deleted_date IS NULL 
         AND is_active = 'Y'
         AND screen_name LIKE $2
         ORDER BY screen_id DESC
         LIMIT 1`,
        [targetCompanyCode, `%${coreNamePart}`],
      );

      if (targetResult.rows.length > 0) {
        const newScreen = targetResult.rows[0];
        tab.screenId = newScreen.screen_id;
        tab.screenName = newScreen.screen_name;
        hasChanges = true;
        console.log(
          `🔗 탭 screenId 자동 매핑: ${oldScreenId} (${oldScreenName}) → ${newScreen.screen_id} (${newScreen.screen_name})`,
        );
      }
    }

    return properties;
  }

  /**
   * 화면 레이아웃에서 사용하는 flowId 수집
   */
  private collectFlowIdsFromLayouts(layouts: any[]): Set<number> {
    const flowIds = new Set<number>();

    for (const layout of layouts) {
      const props = layout.properties;
      if (!props) continue;

      // webTypeConfig.dataflowConfig.flowConfig.flowId
      const flowId = props?.webTypeConfig?.dataflowConfig?.flowConfig?.flowId;
      if (flowId && !isNaN(parseInt(flowId))) {
        flowIds.add(parseInt(flowId));
      }

      // webTypeConfig.dataflowConfig.selectedDiagramId
      const diagramId = props?.webTypeConfig?.dataflowConfig?.selectedDiagramId;
      if (diagramId && !isNaN(parseInt(diagramId))) {
        flowIds.add(parseInt(diagramId));
      }

      // webTypeConfig.dataflowConfig.flowControls[].flowId
      const flowControls = props?.webTypeConfig?.dataflowConfig?.flowControls;
      if (Array.isArray(flowControls)) {
        for (const control of flowControls) {
          if (control?.flowId && !isNaN(parseInt(control.flowId))) {
            flowIds.add(parseInt(control.flowId));
          }
        }
      }

      // componentConfig.action.excelAfterUploadFlows[].flowId
      const excelFlows = props?.componentConfig?.action?.excelAfterUploadFlows;
      if (Array.isArray(excelFlows)) {
        for (const flow of excelFlows) {
          if (flow?.flowId && !isNaN(parseInt(flow.flowId))) {
            flowIds.add(parseInt(flow.flowId));
          }
        }
      }
    }

    return flowIds;
  }

  /**
   * V2 레이아웃에서 flowId 수집 (screen_layouts_v2용)
   * - overrides.flowId (flow-widget)
   * - overrides.webTypeConfig.dataflowConfig.flowConfig.flowId (버튼)
   * - overrides.webTypeConfig.dataflowConfig.flowControls[].flowId
   * - overrides.action.excelAfterUploadFlows[].flowId
   */
  private collectFlowIdsFromLayoutData(layoutData: any): Set<number> {
    const flowIds = new Set<number>();
    if (!layoutData?.components) return flowIds;

    for (const comp of layoutData.components) {
      const overrides = comp.overrides || {};

      // 1. overrides.flowId (flow-widget 등)
      if (overrides.flowId && !isNaN(parseInt(overrides.flowId))) {
        flowIds.add(parseInt(overrides.flowId));
      }

      // 2. webTypeConfig.dataflowConfig.flowConfig.flowId (버튼)
      const flowConfigId = overrides?.webTypeConfig?.dataflowConfig?.flowConfig?.flowId;
      if (flowConfigId && !isNaN(parseInt(flowConfigId))) {
        flowIds.add(parseInt(flowConfigId));
      }

      // 3. webTypeConfig.dataflowConfig.selectedDiagramId
      const diagramId = overrides?.webTypeConfig?.dataflowConfig?.selectedDiagramId;
      if (diagramId && !isNaN(parseInt(diagramId))) {
        flowIds.add(parseInt(diagramId));
      }

      // 4. webTypeConfig.dataflowConfig.flowControls[].flowId
      const flowControls = overrides?.webTypeConfig?.dataflowConfig?.flowControls;
      if (Array.isArray(flowControls)) {
        for (const control of flowControls) {
          if (control?.flowId && !isNaN(parseInt(control.flowId))) {
            flowIds.add(parseInt(control.flowId));
          }
        }
      }

      // 5. action.excelAfterUploadFlows[].flowId
      const excelFlows = overrides?.action?.excelAfterUploadFlows;
      if (Array.isArray(excelFlows)) {
        for (const flow of excelFlows) {
          if (flow?.flowId && !isNaN(parseInt(flow.flowId))) {
            flowIds.add(parseInt(flow.flowId));
          }
        }
      }
    }

    return flowIds;
  }

  /**
   * V2 레이아웃에서 numberingRuleId 수집 (screen_layouts_v2용)
   * - overrides.autoGeneration.options.numberingRuleId
   * - overrides.sections[].fields[].numberingRule.ruleId
   * - overrides.action.excelNumberingRuleId
   * - overrides.action.numberingRuleId
   */
  private collectNumberingRuleIdsFromLayoutData(layoutData: any): Set<string> {
    const ruleIds = new Set<string>();
    if (!layoutData?.components) return ruleIds;

    for (const comp of layoutData.components) {
      const overrides = comp.overrides || {};

      // 1. autoGeneration.options.numberingRuleId
      const autoGenRuleId = overrides?.autoGeneration?.options?.numberingRuleId;
      if (autoGenRuleId && typeof autoGenRuleId === "string" && autoGenRuleId.startsWith("rule-")) {
        ruleIds.add(autoGenRuleId);
      }

      // 2. sections[].fields[].numberingRule.ruleId
      const sections = overrides?.sections;
      if (Array.isArray(sections)) {
        for (const section of sections) {
          const fields = section?.fields;
          if (Array.isArray(fields)) {
            for (const field of fields) {
              const ruleId = field?.numberingRule?.ruleId;
              if (ruleId && typeof ruleId === "string" && ruleId.startsWith("rule-")) {
                ruleIds.add(ruleId);
              }
            }
          }
          // optionalFieldGroups 내부
          const optGroups = section?.optionalFieldGroups;
          if (Array.isArray(optGroups)) {
            for (const optGroup of optGroups) {
              const optFields = optGroup?.fields;
              if (Array.isArray(optFields)) {
                for (const field of optFields) {
                  const ruleId = field?.numberingRule?.ruleId;
                  if (ruleId && typeof ruleId === "string" && ruleId.startsWith("rule-")) {
                    ruleIds.add(ruleId);
                  }
                }
              }
            }
          }
        }
      }

      // 3. action.excelNumberingRuleId
      const excelRuleId = overrides?.action?.excelNumberingRuleId;
      if (excelRuleId && typeof excelRuleId === "string" && excelRuleId.startsWith("rule-")) {
        ruleIds.add(excelRuleId);
      }

      // 4. action.numberingRuleId
      const actionRuleId = overrides?.action?.numberingRuleId;
      if (actionRuleId && typeof actionRuleId === "string" && actionRuleId.startsWith("rule-")) {
        ruleIds.add(actionRuleId);
      }
    }

    return ruleIds;
  }

  /**
   * V2 레이아웃 데이터의 참조 ID들을 업데이트
   * - componentId, flowId, numberingRuleId, screenId 매핑 적용
   */
  private updateReferencesInLayoutData(
    layoutData: any,
    mappings: {
      componentIdMap: Map<string, string>;
      flowIdMap?: Map<number, number>;
      ruleIdMap?: Map<string, string>;
      screenIdMap?: Map<number, number>;
    },
  ): any {
    if (!layoutData?.components) return layoutData;

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

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

      // flowId 매핑
      if (mappings.flowIdMap && mappings.flowIdMap.size > 0) {
        overrides = this.updateFlowIdsInOverrides(overrides, mappings.flowIdMap);
      }

      // numberingRuleId 매핑
      if (mappings.ruleIdMap && mappings.ruleIdMap.size > 0) {
        overrides = this.updateNumberingRuleIdsInOverrides(overrides, mappings.ruleIdMap);
      }

      // screenId 매핑 (탭, 버튼 등)
      if (mappings.screenIdMap && mappings.screenIdMap.size > 0) {
        overrides = this.updateScreenIdsInOverrides(overrides, mappings.screenIdMap);
      }

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

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

  /**
   * V2 overrides 내의 flowId 업데이트
   */
  private updateFlowIdsInOverrides(
    overrides: any,
    flowIdMap: Map<number, number>,
  ): any {
    if (!overrides || flowIdMap.size === 0) return overrides;

    // 1. overrides.flowId (flow-widget)
    if (overrides.flowId) {
      const oldId = parseInt(overrides.flowId);
      const newId = flowIdMap.get(oldId);
      if (newId) {
        overrides.flowId = newId;
        console.log(`      🔗 flowId: ${oldId} → ${newId}`);
      }
    }

    // 2. webTypeConfig.dataflowConfig.flowConfig.flowId
    if (overrides?.webTypeConfig?.dataflowConfig?.flowConfig?.flowId) {
      const oldId = parseInt(overrides.webTypeConfig.dataflowConfig.flowConfig.flowId);
      const newId = flowIdMap.get(oldId);
      if (newId) {
        overrides.webTypeConfig.dataflowConfig.flowConfig.flowId = newId;
        console.log(`      🔗 flowConfig.flowId: ${oldId} → ${newId}`);
      }
    }

    // 3. webTypeConfig.dataflowConfig.selectedDiagramId
    if (overrides?.webTypeConfig?.dataflowConfig?.selectedDiagramId) {
      const oldId = parseInt(overrides.webTypeConfig.dataflowConfig.selectedDiagramId);
      const newId = flowIdMap.get(oldId);
      if (newId) {
        overrides.webTypeConfig.dataflowConfig.selectedDiagramId = newId;
        console.log(`      🔗 selectedDiagramId: ${oldId} → ${newId}`);
      }
    }

    // 4. webTypeConfig.dataflowConfig.flowControls[]
    if (Array.isArray(overrides?.webTypeConfig?.dataflowConfig?.flowControls)) {
      for (const control of overrides.webTypeConfig.dataflowConfig.flowControls) {
        if (control?.flowId) {
          const oldId = parseInt(control.flowId);
          const newId = flowIdMap.get(oldId);
          if (newId) {
            control.flowId = newId;
            console.log(`      🔗 flowControls.flowId: ${oldId} → ${newId}`);
          }
        }
      }
    }

    // 5. action.excelAfterUploadFlows[]
    if (Array.isArray(overrides?.action?.excelAfterUploadFlows)) {
      for (const flow of overrides.action.excelAfterUploadFlows) {
        if (flow?.flowId) {
          const oldId = parseInt(flow.flowId);
          const newId = flowIdMap.get(oldId);
          if (newId) {
            flow.flowId = newId;
            console.log(`      🔗 excelAfterUploadFlows.flowId: ${oldId} → ${newId}`);
          }
        }
      }
    }

    return overrides;
  }

  /**
   * V2 overrides 내의 numberingRuleId 업데이트
   */
  private updateNumberingRuleIdsInOverrides(
    overrides: any,
    ruleIdMap: Map<string, string>,
  ): any {
    if (!overrides || ruleIdMap.size === 0) return overrides;

    // 1. autoGeneration.options.numberingRuleId
    if (overrides?.autoGeneration?.options?.numberingRuleId) {
      const oldId = overrides.autoGeneration.options.numberingRuleId;
      const newId = ruleIdMap.get(oldId);
      if (newId) {
        overrides.autoGeneration.options.numberingRuleId = newId;
        console.log(`      🔗 autoGeneration.numberingRuleId: ${oldId} → ${newId}`);
      }
    }

    // 2. sections[].fields[].numberingRule.ruleId
    if (Array.isArray(overrides?.sections)) {
      for (const section of overrides.sections) {
        if (Array.isArray(section?.fields)) {
          for (const field of section.fields) {
            if (field?.numberingRule?.ruleId) {
              const oldId = field.numberingRule.ruleId;
              const newId = ruleIdMap.get(oldId);
              if (newId) {
                field.numberingRule.ruleId = newId;
                console.log(`      🔗 field.numberingRule.ruleId: ${oldId} → ${newId}`);
              }
            }
          }
        }
        if (Array.isArray(section?.optionalFieldGroups)) {
          for (const optGroup of section.optionalFieldGroups) {
            if (Array.isArray(optGroup?.fields)) {
              for (const field of optGroup.fields) {
                if (field?.numberingRule?.ruleId) {
                  const oldId = field.numberingRule.ruleId;
                  const newId = ruleIdMap.get(oldId);
                  if (newId) {
                    field.numberingRule.ruleId = newId;
                    console.log(`      🔗 optField.numberingRule.ruleId: ${oldId} → ${newId}`);
                  }
                }
              }
            }
          }
        }
      }
    }

    // 3. action.excelNumberingRuleId
    if (overrides?.action?.excelNumberingRuleId) {
      const oldId = overrides.action.excelNumberingRuleId;
      const newId = ruleIdMap.get(oldId);
      if (newId) {
        overrides.action.excelNumberingRuleId = newId;
        console.log(`      🔗 excelNumberingRuleId: ${oldId} → ${newId}`);
      }
    }

    // 4. action.numberingRuleId
    if (overrides?.action?.numberingRuleId) {
      const oldId = overrides.action.numberingRuleId;
      const newId = ruleIdMap.get(oldId);
      if (newId) {
        overrides.action.numberingRuleId = newId;
        console.log(`      🔗 action.numberingRuleId: ${oldId} → ${newId}`);
      }
    }

    return overrides;
  }

  /**
   * V2 overrides 내의 screenId 업데이트 (탭, 버튼 등)
   */
  private updateScreenIdsInOverrides(
    overrides: any,
    screenIdMap: Map<number, number>,
  ): any {
    if (!overrides || screenIdMap.size === 0) return overrides;

    // 1. tabs[].screenId (탭 위젯)
    if (Array.isArray(overrides?.tabs)) {
      for (const tab of overrides.tabs) {
        if (tab?.screenId) {
          const oldId = parseInt(tab.screenId);
          const newId = screenIdMap.get(oldId);
          if (newId) {
            tab.screenId = newId;
            console.log(`      🔗 tab.screenId: ${oldId} → ${newId}`);
          }
        }
      }
    }

    // 2. action.targetScreenId (버튼)
    if (overrides?.action?.targetScreenId) {
      const oldId = parseInt(overrides.action.targetScreenId);
      const newId = screenIdMap.get(oldId);
      if (newId) {
        overrides.action.targetScreenId = newId;
        console.log(`      🔗 action.targetScreenId: ${oldId} → ${newId}`);
      }
    }

    // 3. action.modalScreenId
    if (overrides?.action?.modalScreenId) {
      const oldId = parseInt(overrides.action.modalScreenId);
      const newId = screenIdMap.get(oldId);
      if (newId) {
        overrides.action.modalScreenId = newId;
        console.log(`      🔗 action.modalScreenId: ${oldId} → ${newId}`);
      }
    }

    return overrides;
  }

  /**
   * 노드 플로우 복사 및 ID 매핑 반환
   * - 원본 회사의 플로우를 대상 회사로 복사
   * - 이름이 같은 플로우가 있으면 재사용
   */
  private async copyNodeFlowsForScreen(
    flowIds: Set<number>,
    sourceCompanyCode: string,
    targetCompanyCode: string,
    client: any,
  ): Promise<Map<number, number>> {
    const flowIdMap = new Map<number, number>();

    if (flowIds.size === 0) {
      return flowIdMap;
    }

    console.log(`🔄 노드 플로우 복사 시작: ${flowIds.size}개 flowId`);

    // 1. 원본 플로우 조회 (company_code = "*" 전역 플로우는 복사하지 않음)
    const flowIdArray = Array.from(flowIds);
    const sourceFlowsResult = await client.query(
      `SELECT * FROM node_flows 
       WHERE flow_id = ANY($1) 
       AND company_code = $2`,
      [flowIdArray, sourceCompanyCode],
    );

    if (sourceFlowsResult.rows.length === 0) {
      console.log(`  📭 복사할 노드 플로우 없음 (원본 회사 소속 플로우 없음)`);
      return flowIdMap;
    }

    console.log(`  📋 원본 노드 플로우: ${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: any) => [f.flow_name, f.flow_id]),
    );

    // 3. 각 플로우 복사 또는 재사용
    for (const flow of sourceFlowsResult.rows) {
      const existingId = existingFlowsByName.get(flow.flow_name);

      if (existingId) {
        // 기존 플로우 재사용
        flowIdMap.set(flow.flow_id, existingId);
        console.log(
          `    ♻️ 기존 플로우 재사용: ${flow.flow_name} (${flow.flow_id} → ${existingId})`,
        );
      } else {
        // 새로 복사
        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;
        flowIdMap.set(flow.flow_id, newFlowId);
        console.log(
          `    ➕ 플로우 복사: ${flow.flow_name} (${flow.flow_id} → ${newFlowId})`,
        );
      }
    }

    console.log(`  ✅ 노드 플로우 복사 완료: 매핑 ${flowIdMap.size}개`);
    return flowIdMap;
  }

  /**
   * properties 내의 flowId, selectedDiagramId 등을 매핑
   */
  private updateFlowIdsInProperties(
    properties: any,
    flowIdMap: Map<number, number>,
  ): any {
    if (!properties || flowIdMap.size === 0) return properties;

    const updated = JSON.parse(JSON.stringify(properties));

    // webTypeConfig.dataflowConfig.flowConfig.flowId
    if (updated?.webTypeConfig?.dataflowConfig?.flowConfig?.flowId) {
      const oldId = parseInt(
        updated.webTypeConfig.dataflowConfig.flowConfig.flowId,
      );
      const newId = flowIdMap.get(oldId);
      if (newId) {
        updated.webTypeConfig.dataflowConfig.flowConfig.flowId = newId;
        console.log(`      🔗 flowConfig.flowId: ${oldId} → ${newId}`);
      }
    }

    // webTypeConfig.dataflowConfig.selectedDiagramId
    if (updated?.webTypeConfig?.dataflowConfig?.selectedDiagramId) {
      const oldId = parseInt(
        updated.webTypeConfig.dataflowConfig.selectedDiagramId,
      );
      const newId = flowIdMap.get(oldId);
      if (newId) {
        updated.webTypeConfig.dataflowConfig.selectedDiagramId = newId;
        console.log(`      🔗 selectedDiagramId: ${oldId} → ${newId}`);
      }
    }

    // webTypeConfig.dataflowConfig.flowControls[].flowId
    if (Array.isArray(updated?.webTypeConfig?.dataflowConfig?.flowControls)) {
      for (const control of updated.webTypeConfig.dataflowConfig.flowControls) {
        if (control?.flowId) {
          const oldId = parseInt(control.flowId);
          const newId = flowIdMap.get(oldId);
          if (newId) {
            control.flowId = newId;
            console.log(`      🔗 flowControls.flowId: ${oldId} → ${newId}`);
          }
        }
      }
    }

    // componentConfig.action.excelAfterUploadFlows[].flowId
    if (
      Array.isArray(updated?.componentConfig?.action?.excelAfterUploadFlows)
    ) {
      for (const flow of updated.componentConfig.action.excelAfterUploadFlows) {
        if (flow?.flowId) {
          const oldId = parseInt(flow.flowId);
          const newId = flowIdMap.get(oldId);
          if (newId) {
            flow.flowId = String(newId);
            console.log(
              `      🔗 excelAfterUploadFlows.flowId: ${oldId} → ${newId}`,
            );
          }
        }
      }
    }

    return updated;
  }

  /**
   * 화면 복사 (화면 정보 + 레이아웃 모두 복사) (✅ Raw Query 전환 완료)
   */
  async copyScreen(
    sourceScreenId: number,
    copyData: CopyScreenRequest,
  ): Promise<ScreenDefinition> {
    // 트랜잭션으로 처리
    return await transaction(async (client) => {
      // 1. 원본 화면 정보 조회
      // 최고 관리자(company_code = "*")는 모든 화면을 조회할 수 있음
      let sourceScreenQuery: string;
      let sourceScreenParams: any[];

      if (copyData.companyCode === "*") {
        // 최고 관리자: 모든 회사의 화면 조회 가능
        sourceScreenQuery = `
          SELECT * FROM screen_definitions
          WHERE screen_id = $1
          LIMIT 1
        `;
        sourceScreenParams = [sourceScreenId];
      } else {
        // 일반 회사: 자신의 회사 화면만 조회 가능
        sourceScreenQuery = `
          SELECT * FROM screen_definitions
          WHERE screen_id = $1 AND company_code = $2
          LIMIT 1
        `;
        sourceScreenParams = [sourceScreenId, copyData.companyCode];
      }

      const sourceScreens = await client.query<any>(
        sourceScreenQuery,
        sourceScreenParams,
      );

      if (sourceScreens.rows.length === 0) {
        throw new Error("복사할 화면을 찾을 수 없습니다.");
      }

      const sourceScreen = sourceScreens.rows[0];

      // 2. 대상 회사 코드 결정
      // copyData.targetCompanyCode가 있으면 사용 (회사 간 복사)
      // 없으면 원본과 같은 회사에 복사
      const targetCompanyCode =
        copyData.targetCompanyCode || sourceScreen.company_code;

      // 3. 화면 코드 중복 체크 (대상 회사 기준, 삭제되지 않은 화면만)
      const existingScreens = await client.query<any>(
        `SELECT screen_id FROM screen_definitions
         WHERE screen_code = $1 AND company_code = $2 AND deleted_date IS NULL
         LIMIT 1`,
        [copyData.screenCode, targetCompanyCode],
      );

      if (existingScreens.rows.length > 0) {
        throw new Error("이미 존재하는 화면 코드입니다.");
      }

      // 4. 새 화면 생성 (대상 회사에 생성)
      // 삭제된 화면(is_active = 'D')을 복사할 경우 활성 상태('Y')로 변경
      const newIsActive =
        sourceScreen.is_active === "D" ? "Y" : sourceScreen.is_active;

      const newScreenResult = await client.query<any>(
        `INSERT INTO screen_definitions (
          screen_code, screen_name, description, company_code, table_name,
          is_active, created_by, created_date, updated_by, updated_date
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
        RETURNING *`,
        [
          copyData.screenCode,
          copyData.screenName,
          copyData.description || sourceScreen.description,
          targetCompanyCode, // 대상 회사 코드 사용
          sourceScreen.table_name,
          newIsActive, // 삭제된 화면은 활성 상태로 복사
          copyData.createdBy,
          new Date(),
          copyData.createdBy,
          new Date(),
        ],
      );

      const newScreen = newScreenResult.rows[0];

      // 4. 원본 화면의 V2 레이아웃 전체 조회 (모든 레이어)
      let sourceLayoutV2Result = 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`,
        [sourceScreenId, sourceScreen.company_code],
      );

      // 없으면 공통(*) 레이아웃 조회
      if (sourceLayoutV2Result.rows.length === 0 && sourceScreen.company_code !== "*") {
        sourceLayoutV2Result = 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 = '*'
           ORDER BY layer_id`,
          [sourceScreenId],
        );
      }

      // 모든 레이어에서 컴포넌트를 수집하여 componentIdMap 생성
      const componentIdMap = new Map<string, string>();
      for (const layer of sourceLayoutV2Result.rows) {
        const components = layer.layout_data?.components || [];
        for (const comp of components) {
          if (!componentIdMap.has(comp.id)) {
            componentIdMap.set(comp.id, generateId());
          }
        }
      }

      const hasComponents = componentIdMap.size > 0;
      // 첫 번째 레이어의 layoutData (flowId/ruleId 수집용 - 모든 레이어에서 수집)
      const allLayoutDatas = sourceLayoutV2Result.rows.map((r: any) => r.layout_data).filter(Boolean);

      // 5. 노드 플로우 복사 (회사가 다른 경우)
      let flowIdMap = new Map<number, number>();
      if (
        hasComponents &&
        sourceScreen.company_code !== targetCompanyCode
      ) {
        const flowIds = new Set<number>();
        for (const ld of allLayoutDatas) {
          const ids = this.collectFlowIdsFromLayoutData(ld);
          ids.forEach((id: number) => flowIds.add(id));
        }

        if (flowIds.size > 0) {
          console.log(`🔍 화면 복사 - flowId 수집: ${flowIds.size}개`);
          flowIdMap = await this.copyNodeFlowsForScreen(
            flowIds,
            sourceScreen.company_code,
            targetCompanyCode,
            client,
          );
        }
      }

      // 5.1. 채번 규칙 복사 (회사가 다른 경우)
      let ruleIdMap = new Map<string, string>();
      if (
        hasComponents &&
        sourceScreen.company_code !== targetCompanyCode
      ) {
        const ruleIds = new Set<string>();
        for (const ld of allLayoutDatas) {
          const ids = this.collectNumberingRuleIdsFromLayoutData(ld);
          ids.forEach((id: string) => ruleIds.add(id));
        }

        if (ruleIds.size > 0) {
          console.log(`🔍 화면 복사 - 채번 규칙 ID 수집: ${ruleIds.size}개`);
          ruleIdMap = await this.copyNumberingRulesForScreen(
            ruleIds,
            sourceScreen.company_code,
            targetCompanyCode,
            client,
          );
        }
      }

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

          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`,
              [newScreen.screen_id, 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) {
            console.log(`    ↳ 조건부 영역 복사: ${zonesResult.rows.length}개`);
          }
        } catch (error) {
          console.error("조건부 영역 복사 중 오류:", error);
        }
      }

      // 6. V2 레이아웃 복사 (모든 레이어 순회)
      if (sourceLayoutV2Result.rows.length > 0 && hasComponents) {
        try {
          let totalComponents = 0;

          for (const layer of sourceLayoutV2Result.rows) {
            const layoutData = layer.layout_data;
            const components = layoutData?.components || [];

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

            // V2 레이아웃 데이터 복사 및 참조 업데이트
            const updatedLayoutData = this.updateReferencesInLayoutData(
              layoutData,
              {
                componentIdMap,
                flowIdMap: flowIdMap.size > 0 ? flowIdMap : undefined,
                ruleIdMap: ruleIdMap.size > 0 ? ruleIdMap : undefined,
              },
            );

            // 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()`,
              [
                newScreen.screen_id,
                targetCompanyCode,
                layer.layer_id,
                layer.layer_name,
                JSON.stringify(updatedLayoutData),
                updatedConditionConfig ? JSON.stringify(updatedConditionConfig) : null,
              ],
            );
          }

          console.log(`    ↳ V2 레이아웃 복사: ${sourceLayoutV2Result.rows.length}개 레이어, ${totalComponents}개 컴포넌트`);
        } catch (error) {
          console.error("V2 레이아웃 복사 중 오류:", error);
        }
      }

      // 7. 생성된 화면 정보 반환
      return {
        screenId: newScreen.screen_id,
        screenCode: newScreen.screen_code,
        screenName: newScreen.screen_name,
        description: newScreen.description || "",
        companyCode: newScreen.company_code,
        tableName: newScreen.table_name,
        isActive: newScreen.is_active,
        createdBy: newScreen.created_by || undefined,
        createdDate: newScreen.created_date,
        updatedBy: newScreen.updated_by || undefined,
        updatedDate: newScreen.updated_date,
      };
    });
  }

  /**
   * 메인 화면 + 연결된 모달 화면들 일괄 복사
   */
  async copyScreenWithModals(data: {
    sourceScreenId: number;
    companyCode: string;
    userId: string;
    targetCompanyCode?: string; // 최고 관리자 전용: 다른 회사로 복사
    mainScreen: {
      screenName: string;
      screenCode: string;
      description?: string;
    };
    modalScreens: Array<{
      sourceScreenId: number;
      screenName: string;
      screenCode: string;
    }>;
  }): Promise<{
    mainScreen: ScreenDefinition;
    modalScreens: ScreenDefinition[];
  }> {
    const targetCompany = data.targetCompanyCode || data.companyCode;
    console.log(
      `🔄 일괄 복사 시작: 메인(${data.sourceScreenId}) + 모달(${data.modalScreens.length}개) → ${targetCompany}`,
    );

    // 1. 메인 화면 복사
    const mainScreen = await this.copyScreen(data.sourceScreenId, {
      screenName: data.mainScreen.screenName,
      screenCode: data.mainScreen.screenCode,
      description: data.mainScreen.description || "",
      companyCode: data.companyCode,
      createdBy: data.userId,
      targetCompanyCode: data.targetCompanyCode, // 대상 회사 코드 전달
    });

    console.log(
      `✅ 메인 화면 복사 완료: ${mainScreen.screenId} (${mainScreen.screenCode}) @ ${mainScreen.companyCode}`,
    );

    // 2. 모달 화면들 복사 (원본 screenId → 새 screenId 매핑)
    const modalScreens: ScreenDefinition[] = [];
    const screenIdMapping: Map<number, number> = new Map(); // 원본 ID → 새 ID

    for (const modalData of data.modalScreens) {
      const copiedModal = await this.copyScreen(modalData.sourceScreenId, {
        screenName: modalData.screenName,
        screenCode: modalData.screenCode,
        description: "",
        companyCode: data.companyCode,
        createdBy: data.userId,
        targetCompanyCode: data.targetCompanyCode, // 대상 회사 코드 전달
      });

      modalScreens.push(copiedModal);
      screenIdMapping.set(modalData.sourceScreenId, copiedModal.screenId);

      console.log(
        `✅ 모달 화면 복사 완료: ${modalData.sourceScreenId} → ${copiedModal.screenId} (${copiedModal.screenCode})`,
      );
    }

    // 3. 메인 화면의 버튼 액션에서 targetScreenId 업데이트
    // 모든 복사가 완료되고 커밋된 후에 실행
    console.log(
      `🔧 버튼 업데이트 시작: 메인 화면 ${mainScreen.screenId}, 매핑:`,
      Array.from(screenIdMapping.entries()),
    );

    const updateCount = await this.updateButtonTargetScreenIds(
      mainScreen.screenId,
      screenIdMapping,
    );

    console.log(
      `🎉 일괄 복사 완료: 메인(${mainScreen.screenId}) + 모달(${modalScreens.length}개), 버튼 ${updateCount}개 업데이트`,
    );

    return {
      mainScreen,
      modalScreens,
    };
  }

  /**
   * 화면 레이아웃에서 버튼의 targetScreenId를 새 screenId로 업데이트
   * (독립적인 트랜잭션으로 실행)
   */
  private async updateButtonTargetScreenIds(
    screenId: number,
    screenIdMapping: Map<number, number>,
  ): Promise<number> {
    console.log(
      `🔍 updateButtonTargetScreenIds 호출: screenId=${screenId}, 매핑 개수=${screenIdMapping.size}`,
    );

    // 화면의 모든 레이아웃 조회
    const layouts = await query<any>(
      `SELECT layout_id, properties
       FROM screen_layouts
       WHERE screen_id = $1
         AND component_type = 'component'
         AND properties IS NOT NULL`,
      [screenId],
    );

    console.log(`📦 조회된 레이아웃 개수: ${layouts.length}`);

    let updateCount = 0;

    for (const layout of layouts) {
      try {
        const properties = layout.properties;
        let needsUpdate = false;

        // 1. 버튼 컴포넌트의 targetScreenId 업데이트
        if (
          properties?.componentType === "button" ||
          properties?.componentType?.startsWith("button-")
        ) {
          const action = properties?.componentConfig?.action;

          // targetScreenId가 있는 액션 (popup, modal, edit, openModalWithData)
          const modalActionTypes = [
            "popup",
            "modal",
            "edit",
            "openModalWithData",
          ];
          if (
            modalActionTypes.includes(action?.type) &&
            action?.targetScreenId
          ) {
            const oldScreenId = parseInt(action.targetScreenId);
            console.log(
              `🔍 [버튼] 발견: layout ${layout.layout_id}, action=${action.type}, targetScreenId=${oldScreenId}`,
            );

            // 매핑에 있으면 업데이트
            if (screenIdMapping.has(oldScreenId)) {
              const newScreenId = screenIdMapping.get(oldScreenId)!;
              console.log(`✅ 매핑 발견: ${oldScreenId} → ${newScreenId}`);

              // properties 업데이트
              properties.componentConfig.action.targetScreenId =
                newScreenId.toString();
              needsUpdate = true;

              console.log(
                `🔗 [버튼] targetScreenId 업데이트 준비: ${oldScreenId} → ${newScreenId} (layout ${layout.layout_id})`,
              );
            } else {
              console.log(`⚠️ 매핑 없음: ${oldScreenId} (업데이트 건너뜀)`);
            }
          }
        }

        // 2. conditional-container 컴포넌트의 sections[].screenId 업데이트
        if (properties?.componentType === "conditional-container") {
          const sections = properties?.componentConfig?.sections || [];

          for (const section of sections) {
            if (section?.screenId) {
              const oldScreenId = parseInt(section.screenId);
              console.log(
                `🔍 [조건부컨테이너] section 발견: layout ${layout.layout_id}, condition=${section.condition}, screenId=${oldScreenId}`,
              );

              // 매핑에 있으면 업데이트
              if (screenIdMapping.has(oldScreenId)) {
                const newScreenId = screenIdMapping.get(oldScreenId)!;
                console.log(`✅ 매핑 발견: ${oldScreenId} → ${newScreenId}`);

                // section.screenId 업데이트
                section.screenId = newScreenId;
                needsUpdate = true;

                console.log(
                  `🔗 [조건부컨테이너] screenId 업데이트 준비: ${oldScreenId} → ${newScreenId} (layout ${layout.layout_id}, condition=${section.condition})`,
                );
              } else {
                console.log(`⚠️ 매핑 없음: ${oldScreenId} (업데이트 건너뜀)`);
              }
            }
          }
        }

        // 3. 업데이트가 필요한 경우 DB 저장
        if (needsUpdate) {
          await query(
            `UPDATE screen_layouts
             SET properties = $1
             WHERE layout_id = $2`,
            [JSON.stringify(properties), layout.layout_id],
          );
          updateCount++;
          console.log(`💾 레이아웃 ${layout.layout_id} 업데이트 완료`);
        }
      } catch (error) {
        console.warn(`❌ 레이아웃 ${layout.layout_id} 업데이트 오류:`, error);
        // 개별 레이아웃 오류는 무시하고 계속 진행
      }
    }

    console.log(
      `✅ V1: ${updateCount}개 레이아웃 업데이트 완료`,
    );

    // V2 레이아웃(screen_layouts_v2)에서도 targetScreenId 등 재매핑
    const v2Layouts = await query<any>(
      `SELECT screen_id, layer_id, company_code, layout_data
       FROM screen_layouts_v2
       WHERE screen_id = $1
       AND layout_data IS NOT NULL`,
      [screenId],
    );

    let v2UpdateCount = 0;
    for (const v2Layout of v2Layouts) {
      const layoutData = v2Layout.layout_data;
      if (!layoutData?.components) continue;

      let v2Changed = false;
      const updateV2Refs = (obj: any): void => {
        if (!obj || typeof obj !== "object") return;
        if (Array.isArray(obj)) { for (const item of obj) updateV2Refs(item); return; }
        for (const key of Object.keys(obj)) {
          const value = obj[key];
          if (
            (key === "targetScreenId" || key === "screenId" || key === "modalScreenId" ||
             key === "leftScreenId" || key === "rightScreenId" ||
             key === "addModalScreenId" || key === "editModalScreenId")
          ) {
            const numVal = typeof value === "number" ? value : parseInt(value);
            if (!isNaN(numVal) && screenIdMapping.has(numVal)) {
              obj[key] = typeof value === "number" ? screenIdMapping.get(numVal)! : screenIdMapping.get(numVal)!.toString();
              v2Changed = true;
            }
          }
          if (typeof value === "object" && value !== null) updateV2Refs(value);
        }
      };
      updateV2Refs(layoutData);

      if (v2Changed) {
        await query(
          `UPDATE screen_layouts_v2 SET layout_data = $1, updated_at = NOW()
           WHERE screen_id = $2 AND layer_id = $3 AND company_code = $4`,
          [JSON.stringify(layoutData), v2Layout.screen_id, v2Layout.layer_id, v2Layout.company_code],
        );
        v2UpdateCount++;
      }
    }

    const total = updateCount + v2UpdateCount;
    console.log(
      `✅ 총 ${total}개 레이아웃 업데이트 완료 (V1: ${updateCount}, V2: ${v2UpdateCount})`,
    );
    return total;
  }

  /**
   * 화면-메뉴 할당 복제 (screen_menu_assignments)
   *
   * @param sourceCompanyCode 원본 회사 코드
   * @param targetCompanyCode 대상 회사 코드
   * @param screenIdMap 원본 화면 ID -> 새 화면 ID 매핑
   * @returns 복제 결과
   */
  async copyScreenMenuAssignments(
    sourceCompanyCode: string,
    targetCompanyCode: string,
    screenIdMap: Record<number, number>,
  ): Promise<{ copiedCount: number; skippedCount: number; details: string[] }> {
    const result = {
      copiedCount: 0,
      skippedCount: 0,
      details: [] as string[],
    };

    return await transaction(async (client) => {
      logger.info("🔗 화면-메뉴 할당 복제 시작", {
        sourceCompanyCode,
        targetCompanyCode,
      });

      // 1. 원본 회사의 screen_groups (menu_objid 포함) 조회
      const sourceGroupsResult = await client.query<{
        id: number;
        group_name: string;
        menu_objid: string | null;
      }>(
        `SELECT id, group_name, menu_objid 
         FROM screen_groups 
         WHERE company_code = $1 AND menu_objid IS NOT NULL`,
        [sourceCompanyCode],
      );

      // 2. 대상 회사의 screen_groups (menu_objid 포함) 조회
      const targetGroupsResult = await client.query<{
        id: number;
        group_name: string;
        menu_objid: string | null;
      }>(
        `SELECT id, group_name, menu_objid 
         FROM screen_groups 
         WHERE company_code = $1 AND menu_objid IS NOT NULL`,
        [targetCompanyCode],
      );

      // 3. 그룹 이름 기반으로 menu_objid 매핑 생성
      const menuObjidMap = new Map<string, string>(); // 원본 menu_objid -> 새 menu_objid
      for (const sourceGroup of sourceGroupsResult.rows) {
        if (!sourceGroup.menu_objid) continue;

        const matchingTarget = targetGroupsResult.rows.find(
          (t) => t.group_name === sourceGroup.group_name,
        );

        if (matchingTarget?.menu_objid) {
          menuObjidMap.set(sourceGroup.menu_objid, matchingTarget.menu_objid);
          logger.debug(
            `메뉴 매핑: ${sourceGroup.group_name} | ${sourceGroup.menu_objid} → ${matchingTarget.menu_objid}`,
          );
        }
      }

      logger.info(`📋 메뉴 매핑 생성 완료: ${menuObjidMap.size}개`);

      // 4. 원본 screen_menu_assignments 조회
      const assignmentsResult = await client.query<{
        screen_id: number;
        menu_objid: string;
        display_order: number;
        is_active: string;
      }>(
        `SELECT screen_id, menu_objid::text, display_order, is_active 
         FROM screen_menu_assignments 
         WHERE company_code = $1`,
        [sourceCompanyCode],
      );

      logger.info(`📌 원본 할당: ${assignmentsResult.rowCount}개`);

      // 5. 새 할당 생성
      for (const assignment of assignmentsResult.rows) {
        const newScreenId = screenIdMap[assignment.screen_id];
        const newMenuObjid = menuObjidMap.get(assignment.menu_objid);

        if (!newScreenId) {
          logger.warn(`⚠️ 화면 ID 매핑 없음: ${assignment.screen_id}`);
          result.skippedCount++;
          result.details.push(`화면 ${assignment.screen_id}: 매핑 없음`);
          continue;
        }

        if (!newMenuObjid) {
          logger.warn(`⚠️ 메뉴 objid 매핑 없음: ${assignment.menu_objid}`);
          result.skippedCount++;
          result.details.push(`메뉴 ${assignment.menu_objid}: 매핑 없음`);
          continue;
        }

        try {
          await client.query(
            `INSERT INTO screen_menu_assignments 
             (screen_id, menu_objid, company_code, display_order, is_active, created_by)
             VALUES ($1, $2, $3, $4, $5, 'system')
             ON CONFLICT (screen_id, menu_objid, company_code) DO NOTHING`,
            [
              newScreenId,
              newMenuObjid,
              targetCompanyCode,
              assignment.display_order,
              assignment.is_active,
            ],
          );

          // 🔧 menu_info.menu_url도 새 화면 ID로 업데이트
          const menuInfo = await client.query<{
            menu_type: string;
            screen_code: string | null;
          }>(
            `SELECT mi.menu_type, sd.screen_code
             FROM menu_info mi
             LEFT JOIN screen_definitions sd ON sd.screen_id = $1
             WHERE mi.objid = $2`,
            [newScreenId, newMenuObjid],
          );

          if (menuInfo.rows.length > 0) {
            // menu_type: "0" = 관리자 메뉴, "1" = 사용자 메뉴
            const isAdminMenu = menuInfo.rows[0].menu_type === "0";
            const newMenuUrl = isAdminMenu
              ? `/screens/${newScreenId}?mode=admin`
              : `/screens/${newScreenId}`;
            const screenCode = menuInfo.rows[0].screen_code;

            await client.query(
              `UPDATE menu_info 
               SET menu_url = $1, screen_code = $2
               WHERE objid = $3`,
              [newMenuUrl, screenCode, newMenuObjid],
            );
            logger.debug(
              `✅ menu_info.menu_url 업데이트: ${newMenuObjid} → ${newMenuUrl}`,
            );
          }

          result.copiedCount++;
          logger.debug(
            `✅ 할당 복제: screen ${newScreenId} → menu ${newMenuObjid}`,
          );
        } catch (error: any) {
          logger.error(`❌ 할당 복제 실패: ${error.message}`);
          result.skippedCount++;
          result.details.push(`할당 실패: ${error.message}`);
        }
      }

      logger.info(
        `✅ 화면-메뉴 할당 복제 완료: ${result.copiedCount}개 복제, ${result.skippedCount}개 스킵`,
      );
      return result;
    });
  }

  /**
   * 코드 카테고리 + 코드 복제
   */
  async copyCodeCategoryAndCodes(
    sourceCompanyCode: string,
    targetCompanyCode: string,
    menuObjidMap?: Map<string, string>,
  ): Promise<{
    copiedCategories: number;
    copiedCodes: number;
    details: string[];
  }> {
    const result = {
      copiedCategories: 0,
      copiedCodes: 0,
      details: [] as string[],
    };

    // 같은 회사로 복제하는 경우 스킵 (자기 자신의 데이터 삭제 방지)
    if (sourceCompanyCode === targetCompanyCode) {
      logger.warn(
        `⚠️ 같은 회사로 코드 카테고리/코드 복제 시도 - 스킵: ${sourceCompanyCode}`,
      );
      result.details.push("같은 회사로는 복제할 수 없습니다.");
      return result;
    }

    return transaction(async (client) => {
      logger.info(
        `📦 코드 카테고리/코드 복제: ${sourceCompanyCode} → ${targetCompanyCode}`,
      );

      // 1. 기존 대상 회사 데이터 삭제
      await client.query(`DELETE FROM code_info WHERE company_code = $1`, [
        targetCompanyCode,
      ]);
      await client.query(`DELETE FROM code_category WHERE company_code = $1`, [
        targetCompanyCode,
      ]);

      // 2. menuObjidMap 생성 (없는 경우)
      if (!menuObjidMap || menuObjidMap.size === 0) {
        menuObjidMap = new Map();
        const groupPairs = await client.query<{
          source_objid: string;
          target_objid: string;
        }>(
          `SELECT DISTINCT 
             sg1.menu_objid::text as source_objid,
             sg2.menu_objid::text as target_objid
           FROM screen_groups sg1
           JOIN screen_groups sg2 ON sg1.group_name = sg2.group_name
           WHERE sg1.company_code = $1 AND sg2.company_code = $2
           AND sg1.menu_objid IS NOT NULL AND sg2.menu_objid IS NOT NULL`,
          [sourceCompanyCode, targetCompanyCode],
        );
        groupPairs.rows.forEach((p) =>
          menuObjidMap!.set(p.source_objid, p.target_objid),
        );
      }

      // 3. 코드 카테고리 복제
      const categories = await client.query(
        `SELECT * FROM code_category WHERE company_code = $1`,
        [sourceCompanyCode],
      );

      for (const cat of categories.rows) {
        const newMenuObjid = cat.menu_objid
          ? menuObjidMap.get(cat.menu_objid.toString()) || cat.menu_objid
          : null;

        await client.query(
          `INSERT INTO code_category 
           (category_code, category_name, category_name_eng, description, sort_order, is_active, company_code, menu_objid, created_by)
           VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'system')`,
          [
            cat.category_code,
            cat.category_name,
            cat.category_name_eng,
            cat.description,
            cat.sort_order,
            cat.is_active,
            targetCompanyCode,
            newMenuObjid,
          ],
        );
        result.copiedCategories++;
      }

      // 4. 코드 정보 복제
      const codes = await client.query(
        `SELECT * FROM code_info WHERE company_code = $1`,
        [sourceCompanyCode],
      );

      for (const code of codes.rows) {
        const newMenuObjid = code.menu_objid
          ? menuObjidMap.get(code.menu_objid.toString()) || code.menu_objid
          : null;

        await client.query(
          `INSERT INTO code_info 
           (code_category, code_value, code_name, code_name_eng, description, sort_order, is_active, company_code, menu_objid, parent_code_value, depth, created_by)
           VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'system')`,
          [
            code.code_category,
            code.code_value,
            code.code_name,
            code.code_name_eng,
            code.description,
            code.sort_order,
            code.is_active,
            targetCompanyCode,
            newMenuObjid,
            code.parent_code_value,
            code.depth,
          ],
        );
        result.copiedCodes++;
      }

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

  /**
   * 카테고리 값 복제 (category_values 테이블 사용)
   * - menu_objid 의존성 제거됨
   * - table_name + column_name + company_code 기반
   */
  async copyCategoryMapping(
    sourceCompanyCode: string,
    targetCompanyCode: string,
  ): Promise<{
    copiedMappings: number;
    copiedValues: number;
    details: string[];
  }> {
    const result = {
      copiedMappings: 0,
      copiedValues: 0,
      details: [] as string[],
    };

    // 같은 회사로 복제하는 경우 스킵 (자기 자신의 데이터 삭제 방지)
    if (sourceCompanyCode === targetCompanyCode) {
      logger.warn(
        `⚠️ 같은 회사로 카테고리 값 복제 시도 - 스킵: ${sourceCompanyCode}`,
      );
      result.details.push("같은 회사로는 복제할 수 없습니다.");
      return result;
    }

    return transaction(async (client) => {
      logger.info(
        `📦 카테고리 값 복제: ${sourceCompanyCode} → ${targetCompanyCode}`,
      );

      // 1. 기존 대상 회사 데이터 삭제 (다른 회사로 복제 시에만)
      await client.query(
        `DELETE FROM category_values WHERE company_code = $1`,
        [targetCompanyCode],
      );

      // 2. category_values 복제
      const values = await client.query(
        `SELECT * FROM category_values WHERE company_code = $1`,
        [sourceCompanyCode],
      );

      // value_id 매핑 (parent_value_id 참조 업데이트용)
      const valueIdMap = new Map<number, number>();

      for (const v of values.rows) {
        const insertResult = await client.query(
          `INSERT INTO category_values 
           (table_name, column_name, value_code, value_label, value_order, 
            parent_value_id, depth, path, description, color, icon, 
            is_active, is_default, company_code, created_by)
           VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, 'system')
           RETURNING value_id`,
          [
            v.table_name,
            v.column_name,
            v.value_code,
            v.value_label,
            v.value_order,
            null, // parent_value_id는 나중에 업데이트
            v.depth,
            v.path,
            v.description,
            v.color,
            v.icon,
            v.is_active,
            v.is_default,
            targetCompanyCode,
          ],
        );

        valueIdMap.set(v.value_id, insertResult.rows[0].value_id);
        result.copiedValues++;
      }

      // 3. parent_value_id 업데이트 (새 value_id로 매핑)
      for (const v of values.rows) {
        if (v.parent_value_id) {
          const newParentId = valueIdMap.get(v.parent_value_id);
          const newValueId = valueIdMap.get(v.value_id);
          if (newParentId && newValueId) {
            await client.query(
              `UPDATE category_values SET parent_value_id = $1 WHERE value_id = $2`,
              [newParentId, newValueId],
            );
          }
        }
      }

      logger.info(`✅ 카테고리 값 복제 완료: ${result.copiedValues}개`);
      return result;
    });
  }

  /**
   * 테이블 타입관리 입력타입 설정 복제
   * - column_labels 통합 후 모든 컬럼 포함
   */
  async copyTableTypeColumns(
    sourceCompanyCode: string,
    targetCompanyCode: string,
  ): Promise<{ copiedCount: number; details: string[] }> {
    const result = {
      copiedCount: 0,
      details: [] as string[],
    };

    // 같은 회사로 복제하는 경우 스킵 (자기 자신의 데이터 삭제 방지)
    if (sourceCompanyCode === targetCompanyCode) {
      logger.warn(
        `⚠️ 같은 회사로 테이블 타입 컬럼 복제 시도 - 스킵: ${sourceCompanyCode}`,
      );
      result.details.push("같은 회사로는 복제할 수 없습니다.");
      return result;
    }

    return transaction(async (client) => {
      logger.info(
        `📦 테이블 타입 컬럼 복제: ${sourceCompanyCode} → ${targetCompanyCode}`,
      );

      // 1. 기존 대상 회사 데이터 삭제
      await client.query(
        `DELETE FROM table_type_columns WHERE company_code = $1`,
        [targetCompanyCode],
      );

      // 2. 복제 (column_labels 통합 후 모든 컬럼 포함)
      const columns = await client.query(
        `SELECT * FROM table_type_columns WHERE company_code = $1`,
        [sourceCompanyCode],
      );

      for (const col of columns.rows) {
        await client.query(
          `INSERT INTO table_type_columns 
           (table_name, column_name, input_type, detail_settings, is_nullable, display_order, 
            column_label, description, is_visible, code_category, code_value, 
            reference_table, reference_column, display_column, company_code,
            created_date, updated_date)
           VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW(), NOW())`,
          [
            col.table_name,
            col.column_name,
            col.input_type,
            col.detail_settings,
            col.is_nullable,
            col.display_order,
            col.column_label,
            col.description,
            col.is_visible,
            col.code_category,
            col.code_value,
            col.reference_table,
            col.reference_column,
            col.display_column,
            targetCompanyCode,
          ],
        );
        result.copiedCount++;
      }

      logger.info(`✅ 테이블 타입 컬럼 복제 완료: ${result.copiedCount}개`);
      return result;
    });
  }

  /**
   * 연쇄관계 설정 복제
   */
  async copyCascadingRelation(
    sourceCompanyCode: string,
    targetCompanyCode: string,
  ): Promise<{ copiedCount: number; details: string[] }> {
    const result = {
      copiedCount: 0,
      details: [] as string[],
    };

    // 같은 회사로 복제하는 경우 스킵 (자기 자신의 데이터 삭제 방지)
    if (sourceCompanyCode === targetCompanyCode) {
      logger.warn(
        `⚠️ 같은 회사로 연쇄관계 설정 복제 시도 - 스킵: ${sourceCompanyCode}`,
      );
      result.details.push("같은 회사로는 복제할 수 없습니다.");
      return result;
    }

    return transaction(async (client) => {
      logger.info(
        `📦 연쇄관계 설정 복제: ${sourceCompanyCode} → ${targetCompanyCode}`,
      );

      // 1. 기존 대상 회사 데이터 삭제
      await client.query(
        `DELETE FROM cascading_relation WHERE company_code = $1`,
        [targetCompanyCode],
      );

      // 2. 복제
      const relations = await client.query(
        `SELECT * FROM cascading_relation WHERE company_code = $1`,
        [sourceCompanyCode],
      );

      for (const rel of relations.rows) {
        // 새로운 relation_code 생성
        const newRelationCode = `${rel.relation_code}_${targetCompanyCode}`;

        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)
           VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, 'system')`,
          [
            newRelationCode,
            rel.relation_name,
            rel.description,
            rel.parent_table,
            rel.parent_value_column,
            rel.parent_label_column,
            rel.child_table,
            rel.child_filter_column,
            rel.child_value_column,
            rel.child_label_column,
            rel.child_order_column,
            rel.child_order_direction,
            rel.empty_parent_message,
            rel.no_options_message,
            rel.loading_message,
            rel.clear_on_parent_change,
            targetCompanyCode,
            rel.is_active,
          ],
        );
        result.copiedCount++;
      }

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

  // ========================================
  // V2 레이아웃 관리 (1 레코드 방식)
  // ========================================

  /**
   * V2 레이아웃 조회 (1 레코드 방식)
   * - screen_layouts_v2 테이블에서 화면당 1개 레코드 조회
   * - layout_data JSON에 모든 컴포넌트 포함
   */
  async getLayoutV2(
    screenId: number,
    companyCode: string,
    userType?: string,
  ): Promise<any | null> {
    

    // SUPER_ADMIN 여부 확인
    const isSuperAdmin = userType === "SUPER_ADMIN";

    // 권한 확인
    const screens = await query<{
      company_code: string | null;
      table_name: string | null;
    }>(
      `SELECT company_code, table_name FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
      [screenId],
    );

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

    const existingScreen = screens[0];

    // SUPER_ADMIN이 아니고 회사 코드가 다르면 권한 없음
    if (!isSuperAdmin && companyCode !== "*" && existingScreen.company_code !== companyCode) {
      throw new Error("이 화면의 레이아웃을 조회할 권한이 없습니다.");
    }

    let layout: { layout_data: any } | null = null;

    // 🆕 기본 레이어(layer_id=1)를 우선 로드
    // SUPER_ADMIN이거나 companyCode가 "*"인 경우: 화면의 회사 코드로 레이아웃 조회
    if (isSuperAdmin || companyCode === "*") {
      // 1. 화면 정의의 회사 코드 + 기본 레이어
      layout = await queryOne<{ layout_data: any }>(
        `SELECT layout_data FROM screen_layouts_v2
         WHERE screen_id = $1 AND company_code = $2 AND layer_id = 1`,
        [screenId, existingScreen.company_code],
      );

      // 2. 기본 레이어 없으면 layer_id 조건 없이 조회 (하위 호환)
      if (!layout) {
        layout = await queryOne<{ layout_data: any }>(
          `SELECT layout_data FROM screen_layouts_v2
           WHERE screen_id = $1 AND company_code = $2
           ORDER BY layer_id ASC
           LIMIT 1`,
          [screenId, existingScreen.company_code],
        );
      }

      // 3. 화면 정의의 회사 코드로 없으면, 해당 화면의 모든 레이아웃 중 첫 번째
      if (!layout) {
        layout = await queryOne<{ layout_data: any }>(
          `SELECT layout_data FROM screen_layouts_v2
           WHERE screen_id = $1
           ORDER BY layer_id ASC
           LIMIT 1`,
          [screenId],
        );
      }
    } else {
      // 일반 사용자: 회사별 우선 + 기본 레이어
      layout = await queryOne<{ layout_data: any }>(
        `SELECT layout_data FROM screen_layouts_v2
         WHERE screen_id = $1 AND company_code = $2 AND layer_id = 1`,
        [screenId, companyCode],
      );

      // 회사별 기본 레이어 없으면 layer_id 조건 없이 (하위 호환)
      if (!layout) {
        layout = await queryOne<{ layout_data: any }>(
          `SELECT layout_data FROM screen_layouts_v2
           WHERE screen_id = $1 AND company_code = $2
           ORDER BY layer_id ASC
           LIMIT 1`,
          [screenId, companyCode],
        );
      }

      // 회사별 레이아웃이 없으면 공통(*) 레이아웃 조회
      if (!layout && companyCode !== "*") {
        layout = await queryOne<{ layout_data: any }>(
          `SELECT layout_data FROM screen_layouts_v2
           WHERE screen_id = $1 AND company_code = '*'
           ORDER BY layer_id ASC
           LIMIT 1`,
          [screenId],
        );
      }
    }

    if (!layout) {
      
      return null;
    }

    
    return layout.layout_data;
  }

  /**
   * V2 레이아웃 저장 (레이어별 저장)
   * - screen_layouts_v2 테이블에 화면당 레이어별 1개 레코드 저장
   * - layout_data JSON에 해당 레이어의 컴포넌트 포함
   */
  async saveLayoutV2(
    screenId: number,
    layoutData: any,
    companyCode: string,
  ): Promise<void> {
    const layerId = layoutData.layerId || 1;
    const layerName = layoutData.layerName || (layerId === 1 ? '기본 레이어' : `레이어 ${layerId}`);
    // conditionConfig가 명시적으로 전달되었는지 확인 (undefined = 미전달, null/object = 명시적 전달)
    const hasConditionConfig = 'conditionConfig' in layoutData;
    const conditionConfig = layoutData.conditionConfig || null;

    

    // 권한 확인
    const screens = await query<{ company_code: string | null }>(
      `SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
      [screenId],
    );

    if (screens.length === 0) {
      throw new Error("화면을 찾을 수 없습니다.");
    }

    const existingScreen = screens[0];

    if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
      throw new Error("이 화면의 레이아웃을 저장할 권한이 없습니다.");
    }

    // 화면의 기본 테이블 업데이트 (테이블이 선택된 경우)
    const mainTableName = layoutData.mainTableName;
    if (mainTableName) {
      await query(
        `UPDATE screen_definitions SET table_name = $1, updated_date = NOW() WHERE screen_id = $2`,
        [mainTableName, screenId],
      );
      console.log(`✅ [saveLayoutV2] 화면 기본 테이블 업데이트: ${mainTableName}`);
    }

    // 저장할 layout_data에서 레이어 메타 정보 제거 (순수 레이아웃만 저장)
    const { layerId: _lid, layerName: _ln, conditionConfig: _cc, mainTableName: _mtn, ...pureLayoutData } = layoutData;
    const dataToSave = {
      version: "2.0",
      ...pureLayoutData,
    };

    if (hasConditionConfig) {
      // conditionConfig가 명시적으로 전달된 경우: condition_config도 함께 저장
      await query(
        `INSERT INTO screen_layouts_v2 (screen_id, company_code, layer_id, layer_name, condition_config, layout_data, 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 = $6, layer_name = $4, condition_config = $5, updated_at = NOW()`,
        [screenId, companyCode, layerId, layerName, conditionConfig ? JSON.stringify(conditionConfig) : null, JSON.stringify(dataToSave)],
      );
    } else {
      // conditionConfig가 전달되지 않은 경우: 기존 condition_config 유지, layout_data만 업데이트
      await query(
        `INSERT INTO screen_layouts_v2 (screen_id, company_code, layer_id, layer_name, layout_data, created_at, updated_at)
         VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
         ON CONFLICT (screen_id, company_code, layer_id) 
         DO UPDATE SET layout_data = $5, layer_name = $4, updated_at = NOW()`,
        [screenId, companyCode, layerId, layerName, JSON.stringify(dataToSave)],
      );
    }

    
  }

  /**
   * 화면의 모든 레이어 목록 조회
   * 레이어가 없으면 기본 레이어를 자동 생성
   */
  async getScreenLayers(
    screenId: number,
    companyCode: string,
  ): Promise<any[]> {
    let layers;

    if (companyCode === "*") {
      layers = await query<any>(
        `SELECT layer_id, layer_name, condition_config, 
                jsonb_array_length(COALESCE(layout_data->'components', '[]'::jsonb)) as component_count,
                updated_at
         FROM screen_layouts_v2
         WHERE screen_id = $1
         ORDER BY layer_id`,
        [screenId],
      );
    } else {
      layers = await query<any>(
        `SELECT layer_id, layer_name, condition_config,
                jsonb_array_length(COALESCE(layout_data->'components', '[]'::jsonb)) as component_count,
                updated_at
         FROM screen_layouts_v2
         WHERE screen_id = $1 AND company_code = $2
         ORDER BY layer_id`,
        [screenId, companyCode],
      );

      // 회사별 레이어가 없으면 공통(*) 레이어 조회
      if (layers.length === 0 && companyCode !== "*") {
        layers = await query<any>(
          `SELECT layer_id, layer_name, condition_config,
                  jsonb_array_length(COALESCE(layout_data->'components', '[]'::jsonb)) as component_count,
                  updated_at
           FROM screen_layouts_v2
           WHERE screen_id = $1 AND company_code = '*'
           ORDER BY layer_id`,
          [screenId],
        );
      }
    }

    // 레이어가 없으면 기본 레이어 자동 생성
    if (layers.length === 0) {
      const defaultLayout = JSON.stringify({ version: "2.0", components: [] });
      await query(
        `INSERT INTO screen_layouts_v2 (screen_id, company_code, layer_id, layer_name, layout_data, created_at, updated_at)
         VALUES ($1, $2, 1, '기본 레이어', $3, NOW(), NOW())
         ON CONFLICT (screen_id, company_code, layer_id) DO NOTHING`,
        [screenId, companyCode, defaultLayout],
      );
      console.log(`기본 레이어 자동 생성: screen_id=${screenId}, company_code=${companyCode}`);
      
      // 다시 조회
      layers = await query<any>(
        `SELECT layer_id, layer_name, condition_config,
                jsonb_array_length(COALESCE(layout_data->'components', '[]'::jsonb)) as component_count,
                updated_at
         FROM screen_layouts_v2
         WHERE screen_id = $1 AND company_code = $2
         ORDER BY layer_id`,
        [screenId, companyCode],
      );
    }

    return layers;
  }

  /**
   * 특정 레이어의 레이아웃 조회
   */
  async getLayerLayout(
    screenId: number,
    layerId: number,
    companyCode: string,
  ): Promise<any> {
    let layout = await queryOne<{ layout_data: any; layer_name: string; condition_config: any }>(
      `SELECT layout_data, layer_name, condition_config FROM screen_layouts_v2
       WHERE screen_id = $1 AND company_code = $2 AND layer_id = $3`,
      [screenId, companyCode, layerId],
    );

    // 최고관리자(*): 화면 정의의 company_code로 재조회
    if (!layout && companyCode === "*") {
      const screenDef = await queryOne<{ company_code: string }>(
        `SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
        [screenId],
      );
      if (screenDef && screenDef.company_code && screenDef.company_code !== "*") {
        layout = await queryOne<{ layout_data: any; layer_name: string; condition_config: any }>(
          `SELECT layout_data, layer_name, condition_config FROM screen_layouts_v2
           WHERE screen_id = $1 AND company_code = $2 AND layer_id = $3`,
          [screenId, screenDef.company_code, layerId],
        );
      }
    }

    // 일반 사용자: 회사별 레이어가 없으면 공통(*) 조회
    if (!layout && companyCode !== "*") {
      layout = await queryOne<{ layout_data: any; layer_name: string; condition_config: any }>(
        `SELECT layout_data, layer_name, condition_config FROM screen_layouts_v2
         WHERE screen_id = $1 AND company_code = '*' AND layer_id = $2`,
        [screenId, layerId],
      );
    }

    if (!layout) return null;

    return {
      ...layout.layout_data,
      layerId,
      layerName: layout.layer_name,
      conditionConfig: layout.condition_config,
    };
  }

  /**
   * 레이어 삭제
   */
  async deleteLayer(
    screenId: number,
    layerId: number,
    companyCode: string,
  ): Promise<void> {
    if (layerId === 1) {
      throw new Error("기본 레이어는 삭제할 수 없습니다.");
    }

    await query(
      `DELETE FROM screen_layouts_v2 
       WHERE screen_id = $1 AND company_code = $2 AND layer_id = $3`,
      [screenId, companyCode, layerId],
    );

    console.log(`레이어 삭제 완료: screen_id=${screenId}, layer_id=${layerId}`);
  }

  /**
   * 레이어 조건 설정 업데이트
   */
  async updateLayerCondition(
    screenId: number,
    layerId: number,
    companyCode: string,
    conditionConfig: any,
    layerName?: string,
  ): Promise<void> {
    const setClauses = ['condition_config = $4', 'updated_at = NOW()'];
    const params: any[] = [screenId, companyCode, layerId, conditionConfig ? JSON.stringify(conditionConfig) : null];

    if (layerName) {
      setClauses.push(`layer_name = $${params.length + 1}`);
      params.push(layerName);
    }

    await query(
      `UPDATE screen_layouts_v2 SET ${setClauses.join(', ')}
       WHERE screen_id = $1 AND company_code = $2 AND layer_id = $3`,
      params,
    );
  }

  // ========================================
  // 조건부 영역(Zone) 관리
  // ========================================

  /**
   * 화면의 조건부 영역(Zone) 목록 조회
   */
  async getScreenZones(screenId: number, companyCode: string): Promise<any[]> {
    let zones;
    if (companyCode === "*") {
      // 최고 관리자: 모든 회사 Zone 조회 가능
      zones = await query<any>(
        `SELECT * FROM screen_conditional_zones WHERE screen_id = $1 ORDER BY zone_id`,
        [screenId],
      );
    } else {
      // 일반 회사: 자사 Zone + 공통(*) Zone 조회
      zones = await query<any>(
        `SELECT * FROM screen_conditional_zones 
         WHERE screen_id = $1 AND (company_code = $2 OR company_code = '*')
         ORDER BY zone_id`,
        [screenId, companyCode],
      );
    }
    return zones;
  }

  /**
   * 조건부 영역(Zone) 생성
   */
  async createZone(
    screenId: number,
    companyCode: string,
    zoneData: {
      zone_name?: string;
      x: number;
      y: number;
      width: number;
      height: number;
      trigger_component_id?: string;
      trigger_operator?: string;
    },
  ): Promise<any> {
    const result = await queryOne<any>(
      `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 *`,
      [
        screenId,
        companyCode,
        zoneData.zone_name || '조건부 영역',
        zoneData.x,
        zoneData.y,
        zoneData.width,
        zoneData.height,
        zoneData.trigger_component_id || null,
        zoneData.trigger_operator || 'eq',
      ],
    );
    return result;
  }

  /**
   * 조건부 영역(Zone) 업데이트 (위치/크기/트리거)
   */
  async updateZone(
    zoneId: number,
    companyCode: string,
    updates: {
      zone_name?: string;
      x?: number;
      y?: number;
      width?: number;
      height?: number;
      trigger_component_id?: string;
      trigger_operator?: string;
    },
  ): Promise<void> {
    const setClauses: string[] = ['updated_at = NOW()'];
    const params: any[] = [zoneId, companyCode];
    let paramIdx = 3;

    for (const [key, value] of Object.entries(updates)) {
      if (value !== undefined) {
        setClauses.push(`${key} = $${paramIdx}`);
        params.push(value);
        paramIdx++;
      }
    }

    await query(
      `UPDATE screen_conditional_zones SET ${setClauses.join(', ')}
       WHERE zone_id = $1 AND company_code = $2`,
      params,
    );
  }

  /**
   * 조건부 영역(Zone) 삭제 + 소속 레이어들의 condition_config 정리
   */
  async deleteZone(zoneId: number, companyCode: string): Promise<void> {
    // Zone에 소속된 레이어들의 condition_config에서 zone_id 제거
    await query(
      `UPDATE screen_layouts_v2 SET condition_config = NULL, updated_at = NOW()
       WHERE company_code = $1 AND condition_config->>'zone_id' = $2::text`,
      [companyCode, String(zoneId)],
    );

    await query(
      `DELETE FROM screen_conditional_zones WHERE zone_id = $1 AND company_code = $2`,
      [zoneId, companyCode],
    );
  }

  /**
   * Zone에 레이어 추가 (빈 레이아웃으로 새 레이어 생성 + zone_id 할당)
   */
  async addLayerToZone(
    screenId: number,
    companyCode: string,
    zoneId: number,
    conditionValue: string,
    layerName?: string,
  ): Promise<{ layerId: number }> {
    // 다음 layer_id 계산
    const maxResult = await queryOne<{ max_id: number }>(
      `SELECT COALESCE(MAX(layer_id), 1) as max_id FROM screen_layouts_v2 
       WHERE screen_id = $1 AND company_code = $2`,
      [screenId, companyCode],
    );
    const newLayerId = (maxResult?.max_id || 1) + 1;

    // Zone 정보로 캔버스 크기 결정 (company_code 필터링 필수)
    const zone = await queryOne<any>(
      `SELECT * FROM screen_conditional_zones WHERE zone_id = $1 AND company_code = $2`,
      [zoneId, companyCode],
    );

    const layoutData = {
      version: "2.1",
      components: [],
      screenResolution: zone
        ? { width: zone.width, height: zone.height }
        : { width: 800, height: 200 },
    };

    const conditionConfig = {
      zone_id: zoneId,
      condition_value: conditionValue,
    };

    await query(
      `INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data, layer_id, layer_name, condition_config)
       VALUES ($1, $2, $3, $4, $5, $6)
       ON CONFLICT (screen_id, company_code, layer_id) DO UPDATE
       SET layout_data = EXCLUDED.layout_data,
           layer_name = EXCLUDED.layer_name,
           condition_config = EXCLUDED.condition_config,
           updated_at = NOW()`,
      [screenId, companyCode, JSON.stringify(layoutData), newLayerId, layerName || `레이어 ${newLayerId}`, JSON.stringify(conditionConfig)],
    );

    return { layerId: newLayerId };
  }

  // ========================================
  // POP 레이아웃 관리 (모바일/태블릿)
  // v2.0: 4모드 레이아웃 지원 (태블릿 가로/세로, 모바일 가로/세로)
  // ========================================

  /**
   * POP v1 → v2 마이그레이션 (백엔드)
   * - 단일 sections 배열 → 4모드별 layouts + 공유 sections/components
   */
  private migratePopV1ToV2(v1Data: any): any {
    console.log("POP v1 → v2 마이그레이션 시작");
    
    // 기본 v2 구조
    const v2Data: any = {
      version: "pop-2.0",
      layouts: {
        tablet_landscape: { sectionPositions: {}, componentPositions: {} },
        tablet_portrait: { sectionPositions: {}, componentPositions: {} },
        mobile_landscape: { sectionPositions: {}, componentPositions: {} },
        mobile_portrait: { sectionPositions: {}, componentPositions: {} },
      },
      sections: {},
      components: {},
      dataFlow: {
        sectionConnections: [],
      },
      settings: {
        touchTargetMin: 48,
        mode: "normal",
        canvasGrid: v1Data.canvasGrid || { columns: 24, rowHeight: 20, gap: 4 },
      },
      metadata: v1Data.metadata,
    };

    // v1 섹션 배열 처리
    const sections = v1Data.sections || [];
    const modeKeys = ["tablet_landscape", "tablet_portrait", "mobile_landscape", "mobile_portrait"];

    for (const section of sections) {
      // 섹션 정의 생성
      v2Data.sections[section.id] = {
        id: section.id,
        label: section.label,
        componentIds: (section.components || []).map((c: any) => c.id),
        innerGrid: section.innerGrid || { columns: 3, rows: 3, gap: 4 },
        style: section.style,
      };

      // 섹션 위치 복사 (4모드 모두 동일)
      const sectionPos = section.grid || { col: 1, row: 1, colSpan: 3, rowSpan: 4 };
      for (const mode of modeKeys) {
        v2Data.layouts[mode].sectionPositions[section.id] = { ...sectionPos };
      }

      // 컴포넌트별 처리
      for (const comp of section.components || []) {
        // 컴포넌트 정의 생성
        v2Data.components[comp.id] = {
          id: comp.id,
          type: comp.type,
          label: comp.label,
          dataBinding: comp.dataBinding,
          style: comp.style,
          config: comp.config,
        };

        // 컴포넌트 위치 복사 (4모드 모두 동일)
        const compPos = comp.grid || { col: 1, row: 1, colSpan: 1, rowSpan: 1 };
        for (const mode of modeKeys) {
          v2Data.layouts[mode].componentPositions[comp.id] = { ...compPos };
        }
      }
    }

    const sectionCount = Object.keys(v2Data.sections).length;
    const componentCount = Object.keys(v2Data.components).length;
    console.log(`POP v1 → v2 마이그레이션 완료: ${sectionCount}개 섹션, ${componentCount}개 컴포넌트`);
    
    return v2Data;
  }

  /**
   * POP 레이아웃 조회
   * - screen_layouts_pop 테이블에서 화면당 1개 레코드 조회
   * - v1 데이터는 자동으로 v2로 마이그레이션하여 반환
   */
  async getLayoutPop(
    screenId: number,
    companyCode: string,
    userType?: string,
  ): Promise<any | null> {
    console.log(`=== POP 레이아웃 로드 시작 ===`);
    console.log(`화면 ID: ${screenId}, 회사: ${companyCode}, 사용자 유형: ${userType}`);

    // SUPER_ADMIN 여부 확인
    const isSuperAdmin = userType === "SUPER_ADMIN";

    // 권한 확인
    const screens = await query<{
      company_code: string | null;
      table_name: string | null;
    }>(
      `SELECT company_code, table_name FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
      [screenId],
    );

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

    const existingScreen = screens[0];

    // SUPER_ADMIN이 아니고 회사 코드가 다르면 권한 없음
    if (!isSuperAdmin && companyCode !== "*" && existingScreen.company_code !== companyCode) {
      throw new Error("이 화면의 POP 레이아웃을 조회할 권한이 없습니다.");
    }

    let layout: { layout_data: any } | null = null;

    // SUPER_ADMIN인 경우: 화면의 회사 코드로 레이아웃 조회
    if (isSuperAdmin) {
      // 1. 화면 정의의 회사 코드로 레이아웃 조회
      layout = await queryOne<{ layout_data: any }>(
        `SELECT layout_data FROM screen_layouts_pop
         WHERE screen_id = $1 AND company_code = $2`,
        [screenId, existingScreen.company_code],
      );

      // 2. 화면 정의의 회사 코드로 없으면, 해당 화면의 모든 레이아웃 중 첫 번째 조회
      if (!layout) {
        layout = await queryOne<{ layout_data: any }>(
          `SELECT layout_data FROM screen_layouts_pop
           WHERE screen_id = $1
           ORDER BY updated_at DESC
           LIMIT 1`,
          [screenId],
        );
      }
    } else {
      // 일반 사용자: 회사별 우선, 없으면 공통(*) 조회
      layout = await queryOne<{ layout_data: any }>(
        `SELECT layout_data FROM screen_layouts_pop
         WHERE screen_id = $1 AND company_code = $2`,
        [screenId, companyCode],
      );

      // 회사별 레이아웃이 없으면 공통(*) 레이아웃 조회
      if (!layout && companyCode !== "*") {
        layout = await queryOne<{ layout_data: any }>(
          `SELECT layout_data FROM screen_layouts_pop
           WHERE screen_id = $1 AND company_code = '*'`,
          [screenId],
        );
      }
    }

    if (!layout) {
      console.log(`POP 레이아웃 없음: screen_id=${screenId}`);
      return null;
    }

    const layoutData = layout.layout_data;

    // v1 → v2 자동 마이그레이션
    if (layoutData && layoutData.version === "pop-1.0") {
      console.log("POP v1 레이아웃 감지, v2로 마이그레이션");
      return this.migratePopV1ToV2(layoutData);
    }

    // v2 또는 버전 태그 없는 경우 (버전 태그 없으면 sections 구조 확인)
    if (layoutData && !layoutData.version && layoutData.sections && Array.isArray(layoutData.sections)) {
      console.log("버전 태그 없는 v1 레이아웃 감지, v2로 마이그레이션");
      return this.migratePopV1ToV2({ ...layoutData, version: "pop-1.0" });
    }

    // v2 레이아웃 그대로 반환
    const sectionCount = layoutData?.sections ? Object.keys(layoutData.sections).length : 0;
    const componentCount = layoutData?.components ? Object.keys(layoutData.components).length : 0;
    console.log(`POP v2 레이아웃 로드 완료: ${sectionCount}개 섹션, ${componentCount}개 컴포넌트`);
    
    return layoutData;
  }

  /**
   * POP 레이아웃 저장
   * - screen_layouts_pop 테이블에 화면당 1개 레코드 저장
   * - v3 형식 지원 (version: "pop-3.0", 섹션 제거)
   * - v2/v1 하위 호환
   */
  async saveLayoutPop(
    screenId: number,
    layoutData: any,
    companyCode: string,
    userId?: string,
  ): Promise<void> {
    console.log(`=== POP 레이아웃 저장 (v5 그리드 시스템) ===`);
    console.log(`화면 ID: ${screenId}, 회사: ${companyCode}`);
    
    // v5 그리드 레이아웃만 지원
    const componentCount = Object.keys(layoutData.components || {}).length;
    console.log(`컴포넌트: ${componentCount}개`);
    
    // v5 형식 검증
    if (layoutData.version && layoutData.version !== "pop-5.0") {
      console.warn(`레거시 버전 감지 (${layoutData.version}), v5로 변환 필요`);
    }

    // 권한 확인
    const screens = await query<{ company_code: string | null }>(
      `SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
      [screenId],
    );

    if (screens.length === 0) {
      throw new Error("화면을 찾을 수 없습니다.");
    }

    const existingScreen = screens[0];

    if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
      throw new Error("이 화면의 POP 레이아웃을 저장할 권한이 없습니다.");
    }

    // SUPER_ADMIN인 경우: 화면 정의의 company_code로 저장 (로드와 동일하게)
    const targetCompanyCode = companyCode === "*" 
      ? (existingScreen.company_code || "*") 
      : companyCode;
    
    console.log(`저장 대상 company_code: ${targetCompanyCode} (사용자: ${companyCode}, 화면: ${existingScreen.company_code})`);

    // v5 그리드 레이아웃으로 저장 (단일 버전)
    const dataToSave = {
      ...layoutData,
      version: "pop-5.0",
    };
    console.log(`저장: gridConfig=${JSON.stringify(dataToSave.gridConfig || 'default')}`)

    // UPSERT (있으면 업데이트, 없으면 삽입)
    await query(
      `INSERT INTO screen_layouts_pop (screen_id, company_code, layout_data, created_at, updated_at, created_by, updated_by)
       VALUES ($1, $2, $3, NOW(), NOW(), $4, $4)
       ON CONFLICT (screen_id, company_code) 
       DO UPDATE SET layout_data = $3, updated_at = NOW(), updated_by = $4`,
      [screenId, targetCompanyCode, JSON.stringify(dataToSave), userId || null],
    );

    console.log(`POP 레이아웃 저장 완료 (version: ${dataToSave.version}, company: ${targetCompanyCode})`);
  }

  /**
   * POP 레이아웃이 존재하는 화면 ID 목록 조회
   * - 옵션 B: POP 레이아웃 존재 여부로 화면 구분
   */
  async getScreenIdsWithPopLayout(
    companyCode: string,
  ): Promise<number[]> {
    let result: { screen_id: number }[];

    if (companyCode === "*") {
      result = await query<{ screen_id: number }>(
        `SELECT DISTINCT screen_id FROM screen_layouts_pop`,
        [],
      );
    } else {
      // 일반 회사: 해당 회사 레이아웃만 조회 (company_code='*'는 최고관리자 전용)
      result = await query<{ screen_id: number }>(
        `SELECT DISTINCT screen_id FROM screen_layouts_pop
         WHERE company_code = $1`,
        [companyCode],
      );
    }

    const screenIds = result.map((r) => r.screen_id);
    logger.info("POP 레이아웃 존재 화면 ID 조회", { companyCode, count: screenIds.length });
    return screenIds;
  }

  /**
   * POP 레이아웃 삭제
   */
  async deleteLayoutPop(
    screenId: number,
    companyCode: string,
  ): Promise<boolean> {
    console.log(`=== POP 레이아웃 삭제 시작 ===`);
    console.log(`화면 ID: ${screenId}, 회사: ${companyCode}`);

    // 권한 확인
    const screens = await query<{ company_code: string | null }>(
      `SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
      [screenId],
    );

    if (screens.length === 0) {
      throw new Error("화면을 찾을 수 없습니다.");
    }

    const existingScreen = screens[0];

    if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
      throw new Error("이 화면의 POP 레이아웃을 삭제할 권한이 없습니다.");
    }

    const result = await query(
      `DELETE FROM screen_layouts_pop WHERE screen_id = $1 AND company_code = $2`,
      [screenId, companyCode],
    );

    console.log(`POP 레이아웃 삭제 완료`);
    return true;
  }

  // ============================================================
  // POP 화면 배포 (다른 회사로 복사)
  // ============================================================

  /**
   * POP layout_data 내 다른 화면 참조를 스캔하여 연결 관계 분석
   */
  async analyzePopScreenLinks(
    screenId: number,
    companyCode: string,
  ): Promise<{
    linkedScreenIds: number[];
    references: Array<{
      componentId: string;
      referenceType: string;
      targetScreenId: number;
    }>;
  }> {
    const layoutResult = await queryOne<{ layout_data: any }>(
      `SELECT layout_data FROM screen_layouts_pop
       WHERE screen_id = $1 AND company_code = $2`,
      [screenId, companyCode],
    );

    if (!layoutResult?.layout_data) {
      return { linkedScreenIds: [], references: [] };
    }

    const layoutData = layoutResult.layout_data;
    const references: Array<{
      componentId: string;
      referenceType: string;
      targetScreenId: number;
    }> = [];

    const scanComponents = (components: Record<string, any>) => {
      for (const [compId, comp] of Object.entries(components)) {
        const config = (comp as any).config || {};

        if (config.cart?.cartScreenId) {
          const sid = parseInt(config.cart.cartScreenId);
          if (!isNaN(sid) && sid !== screenId) {
            references.push({
              componentId: compId,
              referenceType: "cartScreenId",
              targetScreenId: sid,
            });
          }
        }

        if (config.cartListMode?.sourceScreenId) {
          const sid =
            typeof config.cartListMode.sourceScreenId === "number"
              ? config.cartListMode.sourceScreenId
              : parseInt(config.cartListMode.sourceScreenId);
          if (!isNaN(sid) && sid !== screenId) {
            references.push({
              componentId: compId,
              referenceType: "sourceScreenId",
              targetScreenId: sid,
            });
          }
        }

        if (Array.isArray(config.followUpActions)) {
          for (const action of config.followUpActions) {
            if (action.targetScreenId) {
              const sid = parseInt(action.targetScreenId);
              if (!isNaN(sid) && sid !== screenId) {
                references.push({
                  componentId: compId,
                  referenceType: "targetScreenId",
                  targetScreenId: sid,
                });
              }
            }
          }
        }

        if (config.action?.modalScreenId) {
          const sid = parseInt(config.action.modalScreenId);
          if (!isNaN(sid) && sid !== screenId) {
            references.push({
              componentId: compId,
              referenceType: "modalScreenId",
              targetScreenId: sid,
            });
          }
        }
      }
    };

    if (layoutData.components) {
      scanComponents(layoutData.components);
    }

    if (Array.isArray(layoutData.modals)) {
      for (const modal of layoutData.modals) {
        if (modal.components) {
          scanComponents(modal.components);
        }
      }
    }

    const linkedScreenIds = [
      ...new Set(references.map((r) => r.targetScreenId)),
    ];

    return { linkedScreenIds, references };
  }

  /**
   * POP 화면 배포 (최고관리자 화면을 특정 회사로 복사)
   * - screen_definitions + screen_layouts_pop 복사
   * - 화면 간 참조(cartScreenId, sourceScreenId 등) 자동 치환
   * - numberingRuleId 초기화
   */
  async deployPopScreens(data: {
    screens: Array<{
      sourceScreenId: number;
      screenName: string;
      screenCode: string;
    }>;
    groupStructure?: {
      sourceGroupId: number;
      groupName: string;
      groupCode: string;
      children?: Array<{
        sourceGroupId: number;
        groupName: string;
        groupCode: string;
        screenIds: number[];
      }>;
      screenIds: number[];
    };
    targetCompanyCode: string;
    companyCode: string;
    userId: string;
  }): Promise<{
    deployedScreens: Array<{
      sourceScreenId: number;
      newScreenId: number;
      screenName: string;
      screenCode: string;
    }>;
    createdGroups?: number;
  }> {
    if (data.companyCode !== "*") {
      throw new Error("최고 관리자만 POP 화면을 배포할 수 있습니다.");
    }

    return await transaction(async (client) => {
      const screenIdMap = new Map<number, number>();
      const deployedScreens: Array<{
        sourceScreenId: number;
        newScreenId: number;
        screenName: string;
        screenCode: string;
      }> = [];

      // 1단계: screen_definitions 복사
      for (const screen of data.screens) {
        const sourceResult = await client.query<any>(
          `SELECT * FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
          [screen.sourceScreenId],
        );

        if (sourceResult.rows.length === 0) {
          throw new Error(
            `원본 화면(ID: ${screen.sourceScreenId})을 찾을 수 없습니다.`,
          );
        }

        const sourceScreen = sourceResult.rows[0];

        const existingResult = await client.query<any>(
          `SELECT screen_id FROM screen_definitions
           WHERE screen_code = $1 AND company_code = $2 AND deleted_date IS NULL
           LIMIT 1`,
          [screen.screenCode, data.targetCompanyCode],
        );

        if (existingResult.rows.length > 0) {
          throw new Error(
            `화면 코드 "${screen.screenCode}"가 대상 회사에 이미 존재합니다.`,
          );
        }

        const newScreenResult = await client.query<any>(
          `INSERT INTO screen_definitions (
            screen_code, screen_name, description, company_code, table_name,
            is_active, created_by, created_date, updated_by, updated_date
          ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $7, NOW())
          RETURNING *`,
          [
            screen.screenCode,
            screen.screenName,
            sourceScreen.description,
            data.targetCompanyCode,
            sourceScreen.table_name,
            "Y",
            data.userId,
          ],
        );

        const newScreen = newScreenResult.rows[0];
        screenIdMap.set(screen.sourceScreenId, newScreen.screen_id);

        deployedScreens.push({
          sourceScreenId: screen.sourceScreenId,
          newScreenId: newScreen.screen_id,
          screenName: screen.screenName,
          screenCode: screen.screenCode,
        });

        logger.info("POP 화면 배포 - screen_definitions 생성", {
          sourceScreenId: screen.sourceScreenId,
          newScreenId: newScreen.screen_id,
          targetCompanyCode: data.targetCompanyCode,
        });
      }

      // 2단계: screen_layouts_pop 복사 + 참조 치환
      for (const screen of data.screens) {
        const newScreenId = screenIdMap.get(screen.sourceScreenId);
        if (!newScreenId) continue;

        // 원본 POP 레이아웃 조회 (company_code = '*' 우선, fallback)
        let layoutResult = await client.query<{ layout_data: any }>(
          `SELECT layout_data FROM screen_layouts_pop
           WHERE screen_id = $1 AND company_code = '*'`,
          [screen.sourceScreenId],
        );

        let layoutData = layoutResult.rows[0]?.layout_data;
        if (!layoutData) {
          const fallbackResult = await client.query<{ layout_data: any }>(
            `SELECT layout_data FROM screen_layouts_pop
             WHERE screen_id = $1 LIMIT 1`,
            [screen.sourceScreenId],
          );
          layoutData = fallbackResult.rows[0]?.layout_data;
        }

        if (!layoutData) {
          logger.warn("POP 레이아웃 없음, 건너뜀", {
            sourceScreenId: screen.sourceScreenId,
          });
          continue;
        }

        const updatedLayoutData = this.updatePopLayoutScreenReferences(
          JSON.parse(JSON.stringify(layoutData)),
          screenIdMap,
        );

        await client.query(
          `INSERT INTO screen_layouts_pop (screen_id, company_code, layout_data, created_at, updated_at, created_by, updated_by)
           VALUES ($1, $2, $3, NOW(), NOW(), $4, $4)
           ON CONFLICT (screen_id, company_code)
           DO UPDATE SET layout_data = $3, updated_at = NOW(), updated_by = $4`,
          [
            newScreenId,
            data.targetCompanyCode,
            JSON.stringify(updatedLayoutData),
            data.userId,
          ],
        );

        logger.info("POP 레이아웃 복사 완료", {
          sourceScreenId: screen.sourceScreenId,
          newScreenId,
          componentCount: Object.keys(updatedLayoutData.components || {})
            .length,
        });
      }

      // 3단계: 그룹 구조 복사 (groupStructure가 있는 경우)
      let createdGroups = 0;
      if (data.groupStructure) {
        const gs = data.groupStructure;

        // 대상 회사의 POP 루트 그룹 찾기/생성
        let popRootResult = await client.query<any>(
          `SELECT id FROM screen_groups
           WHERE hierarchy_path = 'POP' AND company_code = $1 LIMIT 1`,
          [data.targetCompanyCode],
        );

        let popRootId: number;
        if (popRootResult.rows.length > 0) {
          popRootId = popRootResult.rows[0].id;
        } else {
          const createRootResult = await client.query<any>(
            `INSERT INTO screen_groups (group_name, group_code, hierarchy_path, company_code, writer, is_active, display_order)
             VALUES ('POP 화면', 'POP_ROOT', 'POP', $1, $2, 'Y', 0) RETURNING id`,
            [data.targetCompanyCode, data.userId],
          );
          popRootId = createRootResult.rows[0].id;
        }

        // 메인 그룹 생성 (중복 코드 방지: _COPY 접미사 추가)
        const mainGroupCode = gs.groupCode + "_COPY";
        const dupCheck = await client.query<any>(
          `SELECT id FROM screen_groups WHERE group_code = $1 AND company_code = $2`,
          [mainGroupCode, data.targetCompanyCode],
        );

        let mainGroupId: number;
        if (dupCheck.rows.length > 0) {
          mainGroupId = dupCheck.rows[0].id;
        } else {
          const mainGroupResult = await client.query<any>(
            `INSERT INTO screen_groups (group_name, group_code, hierarchy_path, company_code, parent_group_id, writer, is_active, display_order)
             VALUES ($1, $2, $3, $4, $5, $6, 'Y', 0) RETURNING id`,
            [
              gs.groupName,
              mainGroupCode,
              `POP/${mainGroupCode}`,
              data.targetCompanyCode,
              popRootId,
              data.userId,
            ],
          );
          mainGroupId = mainGroupResult.rows[0].id;
          createdGroups++;
        }

        // 메인 그룹에 화면 연결
        for (const oldScreenId of gs.screenIds) {
          const newScreenId = screenIdMap.get(oldScreenId);
          if (!newScreenId) continue;
          await client.query(
            `INSERT INTO screen_group_screens (group_id, screen_id, screen_role, display_order, is_default, company_code)
             VALUES ($1, $2, 'main', 0, 'N', $3)
             ON CONFLICT DO NOTHING`,
            [mainGroupId, newScreenId, data.targetCompanyCode],
          );
        }

        // 하위 그룹 생성 + 화면 연결
        if (gs.children) {
          for (let i = 0; i < gs.children.length; i++) {
            const child = gs.children[i];
            const childGroupCode = child.groupCode + "_COPY";

            const childDupCheck = await client.query<any>(
              `SELECT id FROM screen_groups WHERE group_code = $1 AND company_code = $2`,
              [childGroupCode, data.targetCompanyCode],
            );

            let childGroupId: number;
            if (childDupCheck.rows.length > 0) {
              childGroupId = childDupCheck.rows[0].id;
            } else {
              const childResult = await client.query<any>(
                `INSERT INTO screen_groups (group_name, group_code, hierarchy_path, company_code, parent_group_id, writer, is_active, display_order)
                 VALUES ($1, $2, $3, $4, $5, $6, 'Y', $7) RETURNING id`,
                [
                  child.groupName,
                  childGroupCode,
                  `POP/${mainGroupCode}/${childGroupCode}`,
                  data.targetCompanyCode,
                  mainGroupId,
                  data.userId,
                  i,
                ],
              );
              childGroupId = childResult.rows[0].id;
              createdGroups++;
            }

            for (const oldScreenId of child.screenIds) {
              const newScreenId = screenIdMap.get(oldScreenId);
              if (!newScreenId) continue;
              await client.query(
                `INSERT INTO screen_group_screens (group_id, screen_id, screen_role, display_order, is_default, company_code)
                 VALUES ($1, $2, 'main', 0, 'N', $3)
                 ON CONFLICT DO NOTHING`,
                [childGroupId, newScreenId, data.targetCompanyCode],
              );
            }
          }
        }

        logger.info("POP 그룹 구조 복사 완료", {
          targetCompanyCode: data.targetCompanyCode,
          createdGroups,
          mainGroupName: gs.groupName,
        });
      }

      return { deployedScreens, createdGroups };
    });
  }

  /**
   * POP layout_data 내 screen_id 참조 치환
   * componentId, connectionId는 레이아웃 내부 식별자이므로 변경 불필요
   */
  private updatePopLayoutScreenReferences(
    layoutData: any,
    screenIdMap: Map<number, number>,
  ): any {
    if (!layoutData?.components) return layoutData;

    const updateComponents = (
      components: Record<string, any>,
    ): Record<string, any> => {
      const updated: Record<string, any> = {};

      for (const [compId, comp] of Object.entries(components)) {
        const updatedComp = JSON.parse(JSON.stringify(comp));
        const config = updatedComp.config || {};

        // cart.cartScreenId (string)
        if (config.cart?.cartScreenId) {
          const oldId = parseInt(config.cart.cartScreenId);
          const newId = screenIdMap.get(oldId);
          if (newId) {
            config.cart.cartScreenId = String(newId);
            logger.info(`POP 참조 치환: cartScreenId ${oldId} -> ${newId}`);
          }
        }

        // cartListMode.sourceScreenId (number)
        if (config.cartListMode?.sourceScreenId) {
          const oldId =
            typeof config.cartListMode.sourceScreenId === "number"
              ? config.cartListMode.sourceScreenId
              : parseInt(config.cartListMode.sourceScreenId);
          const newId = screenIdMap.get(oldId);
          if (newId) {
            config.cartListMode.sourceScreenId = newId;
            logger.info(
              `POP 참조 치환: sourceScreenId ${oldId} -> ${newId}`,
            );
          }
        }

        // followUpActions[].targetScreenId (string)
        if (Array.isArray(config.followUpActions)) {
          for (const action of config.followUpActions) {
            if (action.targetScreenId) {
              const oldId = parseInt(action.targetScreenId);
              const newId = screenIdMap.get(oldId);
              if (newId) {
                action.targetScreenId = String(newId);
                logger.info(
                  `POP 참조 치환: targetScreenId ${oldId} -> ${newId}`,
                );
              }
            }
          }
        }

        // action.modalScreenId (숫자형이면 화면 참조로 간주)
        if (config.action?.modalScreenId) {
          const oldId = parseInt(config.action.modalScreenId);
          if (!isNaN(oldId)) {
            const newId = screenIdMap.get(oldId);
            if (newId) {
              config.action.modalScreenId = String(newId);
              logger.info(
                `POP 참조 치환: modalScreenId ${oldId} -> ${newId}`,
              );
            }
          }
        }

        // numberingRuleId 초기화 (배포 후 대상 회사에서 재설정 필요)
        if (config.numberingRuleId) {
          logger.info(`POP 채번규칙 초기화: ${config.numberingRuleId}`);
          config.numberingRuleId = "";
        }
        if (config.autoGenMappings) {
          for (const mapping of Object.values(config.autoGenMappings) as any[]) {
            if (mapping?.numberingRuleId) {
              logger.info(
                `POP 채번규칙 초기화: ${mapping.numberingRuleId}`,
              );
              mapping.numberingRuleId = "";
            }
          }
        }

        updatedComp.config = config;
        updated[compId] = updatedComp;
      }

      return updated;
    };

    layoutData.components = updateComponents(layoutData.components);

    if (Array.isArray(layoutData.modals)) {
      for (const modal of layoutData.modals) {
        if (modal.components) {
          modal.components = updateComponents(modal.components);
        }
      }
    }

    return layoutData;
  }
}

// 서비스 인스턴스 export
export const screenManagementService = new ScreenManagementService();
