import { query, queryOne, transaction } from "../database/db";
import { logger } from "../utils/logger";
import { cache, CacheKeys } from "../utils/cache";
import {
  TableInfo,
  ColumnTypeInfo,
  ColumnSettings,
  TableLabels,
  ColumnLabels,
  EntityJoinResponse,
  EntityJoinConfig,
} from "../types/tableManagement";
import { WebType } from "../types/v2-web-types";
import { entityJoinService } from "./entityJoinService";
import { referenceCacheService } from "./referenceCacheService";

// 🔧 Prisma 클라이언트 중복 생성 방지 - 기존 인스턴스 재사용

export class TableManagementService {
  constructor() {}

  /**
   * 컬럼이 코드 타입인지 확인하고 코드 카테고리 반환
   */
  private async getCodeTypeInfo(
    tableName: string,
    columnName: string
  ): Promise<{ isCodeType: boolean; codeCategory?: string }> {
    try {
      // table_type_columns 테이블에서 해당 컬럼의 input_type이 'code'인지 확인
      const result = await query(
        `SELECT input_type, code_category 
         FROM table_type_columns 
         WHERE table_name = $1
         AND column_name = $2
         AND input_type = 'code'
         AND company_code = '*'`,
        [tableName, columnName]
      );

      if (Array.isArray(result) && result.length > 0) {
        const row = result[0] as any;
        return {
          isCodeType: true,
          codeCategory: row.code_category,
        };
      }

      return { isCodeType: false };
    } catch (error) {
      logger.warn(
        `코드 타입 컬럼 확인 중 오류: ${tableName}.${columnName}`,
        error
      );
      return { isCodeType: false };
    }
  }

  /**
   * 테이블 목록 조회 (PostgreSQL information_schema 활용)
   * 메타데이터 조회는 Prisma로 변경 불가
   */
  async getTableList(): Promise<TableInfo[]> {
    try {
      logger.info("테이블 목록 조회 시작");

      // 캐시에서 먼저 확인
      const cachedTables = cache.get<TableInfo[]>(CacheKeys.TABLE_LIST);
      if (cachedTables) {
        logger.info(`테이블 목록 캐시에서 조회: ${cachedTables.length}개`);
        return cachedTables;
      }

      // information_schema는 여전히 $queryRaw 사용
      const rawTables = await query<any>(
        `SELECT 
          t.table_name as "tableName",
          COALESCE(tl.table_label, t.table_name) as "displayName",
          COALESCE(tl.description, '') as "description",
          (SELECT COUNT(*) FROM information_schema.columns 
           WHERE table_name = t.table_name AND table_schema = 'public') as "columnCount"
        FROM information_schema.tables t
        LEFT JOIN table_labels tl ON t.table_name = tl.table_name
        WHERE t.table_schema = 'public' 
          AND t.table_type = 'BASE TABLE'
          AND t.table_name NOT LIKE 'pg_%'
          AND t.table_name NOT LIKE 'sql_%'
        ORDER BY t.table_name`
      );

      // BigInt를 Number로 변환하여 JSON 직렬화 문제 해결
      const tables: TableInfo[] = rawTables.map((table) => ({
        ...table,
        columnCount: Number(table.columnCount), // BigInt → Number 변환
      }));

      // 캐시에 저장 (10분 TTL)
      cache.set(CacheKeys.TABLE_LIST, tables, 10 * 60 * 1000);

      logger.info(`테이블 목록 조회 완료: ${tables.length}개`);
      return tables;
    } catch (error) {
      logger.error("테이블 목록 조회 중 오류 발생:", error);
      throw new Error(
        `테이블 목록 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    }
  }

  /**
   * 테이블 컬럼 정보 조회 (페이지네이션 지원)
   * 메타데이터 조회는 Prisma로 변경 불가
   */
  async getColumnList(
    tableName: string,
    page: number = 1,
    size: number = 50,
    companyCode?: string, // 🔥 회사 코드 추가
    bustCache: boolean = false // 🔥 캐시 버스팅 옵션
  ): Promise<{
    columns: ColumnTypeInfo[];
    total: number;
    page: number;
    size: number;
    totalPages: number;
  }> {
    try {
      logger.info(
        `컬럼 정보 조회 시작: ${tableName} (page: ${page}, size: ${size}), company: ${companyCode}, bustCache: ${bustCache}`
      );

      // 캐시 키 생성 (companyCode 포함)
      const cacheKey =
        CacheKeys.TABLE_COLUMNS(tableName, page, size) + `_${companyCode}`;
      const countCacheKey = CacheKeys.TABLE_COLUMN_COUNT(tableName);

      // 🔥 캐시 버스팅: bustCache가 true면 캐시 무시
      if (!bustCache) {
        // 캐시에서 먼저 확인
        const cachedResult = cache.get<{
          columns: ColumnTypeInfo[];
          total: number;
          page: number;
          size: number;
          totalPages: number;
        }>(cacheKey);
        if (cachedResult) {
          logger.info(
            `컬럼 정보 캐시에서 조회: ${tableName}, ${cachedResult.columns.length}/${cachedResult.total}개`
          );

          // 디버깅: 캐시된 currency_code 확인
          const cachedCurrency = cachedResult.columns.find(
            (col: any) => col.columnName === "currency_code"
          );
          if (cachedCurrency) {
            console.log(`💾 [캐시] currency_code:`, {
              columnName: cachedCurrency.columnName,
              inputType: cachedCurrency.inputType,
              webType: cachedCurrency.webType,
            });
          }

          return cachedResult;
        }
      } else {
        logger.info(`🔥 캐시 버스팅: ${tableName} 캐시 무시`);
      }

      // 전체 컬럼 수 조회 (캐시 확인)
      let total = cache.get<number>(countCacheKey);
      if (!total) {
        const totalResult = await query<{ count: bigint }>(
          `SELECT COUNT(*) as count
           FROM information_schema.columns c
           WHERE c.table_name = $1`,
          [tableName]
        );
        total = Number(totalResult[0].count);
        // 컬럼 수는 자주 변하지 않으므로 30분 캐시
        cache.set(countCacheKey, total, 30 * 60 * 1000);
      }

      // 페이지네이션 적용한 컬럼 조회
      const offset = (page - 1) * size;

      // 🔥 company_code가 있으면 table_type_columns 조인하여 회사별 inputType 가져오기
      // cl: 공통 설정 (company_code = '*'), ttc: 회사별 설정
      const rawColumns = companyCode
        ? await query<any>(
            `SELECT 
              c.column_name as "columnName",
              COALESCE(NULLIF(ttc.column_label, c.column_name), cl.column_label, c.column_name) as "displayName",
              c.data_type as "dataType",
              c.data_type as "dbType",
              COALESCE(ttc.input_type, cl.input_type, 'text') as "webType",
              COALESCE(ttc.input_type, cl.input_type, 'direct') as "inputType",
              ttc.input_type as "ttc_input_type",
              cl.input_type as "cl_input_type",
              COALESCE(ttc.detail_settings::text, cl.detail_settings::text, '') as "detailSettings",
              COALESCE(ttc.description, cl.description, '') as "description",
              CASE 
                WHEN COALESCE(ttc.is_nullable, cl.is_nullable) IS NOT NULL 
                THEN CASE WHEN COALESCE(ttc.is_nullable, cl.is_nullable) = 'N' THEN 'NO' ELSE 'YES' END
                ELSE c.is_nullable 
              END as "isNullable",
              CASE 
                WHEN COALESCE(ttc.is_unique, cl.is_unique) = 'Y' THEN 'YES'
                ELSE 'NO'
              END as "isUnique",
              CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as "isPrimaryKey",
              c.column_default as "defaultValue",
              c.character_maximum_length as "maxLength",
              c.numeric_precision as "numericPrecision",
              c.numeric_scale as "numericScale",
              COALESCE(ttc.code_category, cl.code_category) as "codeCategory",
              COALESCE(ttc.code_value, cl.code_value) as "codeValue",
              COALESCE(ttc.reference_table, cl.reference_table) as "referenceTable",
              COALESCE(ttc.reference_column, cl.reference_column) as "referenceColumn",
              COALESCE(ttc.display_column, cl.display_column) as "displayColumn",
              COALESCE(ttc.display_order, cl.display_order) as "displayOrder",
              COALESCE(ttc.is_visible, cl.is_visible) as "isVisible",
              dcl.column_label as "displayColumnLabel"
            FROM information_schema.columns c
            LEFT JOIN table_type_columns cl ON c.table_name = cl.table_name AND c.column_name = cl.column_name AND cl.company_code = '*'
            LEFT JOIN table_type_columns ttc ON c.table_name = ttc.table_name AND c.column_name = ttc.column_name AND ttc.company_code = $4
            LEFT JOIN table_type_columns dcl ON COALESCE(ttc.reference_table, cl.reference_table) = dcl.table_name AND COALESCE(ttc.display_column, cl.display_column) = dcl.column_name AND dcl.company_code = '*'
            LEFT JOIN (
              SELECT kcu.column_name, kcu.table_name
              FROM information_schema.table_constraints tc
              JOIN information_schema.key_column_usage kcu 
                ON tc.constraint_name = kcu.constraint_name
                AND tc.table_schema = kcu.table_schema
              WHERE tc.constraint_type = 'PRIMARY KEY'
                AND tc.table_name = $1
            ) pk ON c.column_name = pk.column_name AND c.table_name = pk.table_name
            WHERE c.table_name = $1
            ORDER BY c.ordinal_position
            LIMIT $2 OFFSET $3`,
            [tableName, size, offset, companyCode]
          )
        : await query<any>(
            `SELECT 
              c.column_name as "columnName",
              COALESCE(cl.column_label, c.column_name) as "displayName",
              c.data_type as "dataType",
              c.data_type as "dbType",
              COALESCE(cl.input_type, 'text') as "webType",
              COALESCE(cl.input_type, 'direct') as "inputType",
              COALESCE(cl.detail_settings::text, '') as "detailSettings",
              COALESCE(cl.description, '') as "description",
              CASE 
                WHEN cl.is_nullable IS NOT NULL 
                THEN CASE WHEN cl.is_nullable = 'N' THEN 'NO' ELSE 'YES' END
                ELSE c.is_nullable 
              END as "isNullable",
              CASE 
                WHEN cl.is_unique = 'Y' THEN 'YES'
                ELSE 'NO'
              END as "isUnique",
              CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as "isPrimaryKey",
              c.column_default as "defaultValue",
              c.character_maximum_length as "maxLength",
              c.numeric_precision as "numericPrecision",
              c.numeric_scale as "numericScale",
              cl.code_category as "codeCategory",
              cl.code_value as "codeValue",
              cl.reference_table as "referenceTable",
              cl.reference_column as "referenceColumn",
              cl.display_column as "displayColumn",
              cl.display_order as "displayOrder",
              cl.is_visible as "isVisible",
              dcl.column_label as "displayColumnLabel"
            FROM information_schema.columns c
            LEFT JOIN table_type_columns cl ON c.table_name = cl.table_name AND c.column_name = cl.column_name AND cl.company_code = '*'
            LEFT JOIN table_type_columns dcl ON cl.reference_table = dcl.table_name AND cl.display_column = dcl.column_name AND dcl.company_code = '*'
            LEFT JOIN (
              SELECT kcu.column_name, kcu.table_name
              FROM information_schema.table_constraints tc
              JOIN information_schema.key_column_usage kcu 
                ON tc.constraint_name = kcu.constraint_name
                AND tc.table_schema = kcu.table_schema
              WHERE tc.constraint_type = 'PRIMARY KEY'
                AND tc.table_name = $1
            ) pk ON c.column_name = pk.column_name AND c.table_name = pk.table_name
            WHERE c.table_name = $1
            ORDER BY c.ordinal_position
            LIMIT $2 OFFSET $3`,
            [tableName, size, offset]
          );

      // 🆕 category_column_mapping 조회
      const tableExistsResult = await query<any>(
        `SELECT EXISTS (
          SELECT FROM information_schema.tables 
          WHERE table_name = 'category_column_mapping'
        ) as table_exists`
      );
      const mappingTableExists = tableExistsResult[0]?.table_exists === true;

      let categoryMappings: Map<string, number[]> = new Map();
      if (mappingTableExists && companyCode) {
        logger.info("📥 getColumnList: 카테고리 매핑 조회 시작", {
          tableName,
          companyCode,
        });

        try {
          // menu_objid 컬럼이 있는지 먼저 확인
          const columnCheck = await query<any>(
            `SELECT column_name FROM information_schema.columns 
             WHERE table_name = 'category_column_mapping' AND column_name = 'menu_objid'`
          );

          if (columnCheck.length > 0) {
            // menu_objid 컬럼이 있는 경우
            const mappings = await query<any>(
              `SELECT 
                logical_column_name as "columnName",
                menu_objid as "menuObjid"
              FROM category_column_mapping
              WHERE table_name = $1 
                AND company_code = $2`,
              [tableName, companyCode]
            );

            logger.info("✅ getColumnList: 카테고리 매핑 조회 완료", {
              tableName,
              companyCode,
              mappingCount: mappings.length,
            });

            mappings.forEach((m: any) => {
              if (!categoryMappings.has(m.columnName)) {
                categoryMappings.set(m.columnName, []);
              }
              categoryMappings.get(m.columnName)!.push(Number(m.menuObjid));
            });
          } else {
            // menu_objid 컬럼이 없는 경우 - 매핑 없이 진행
            logger.info(
              "⚠️ getColumnList: menu_objid 컬럼이 없음, 카테고리 매핑 스킵"
            );
          }
        } catch (mappingError: any) {
          logger.warn("⚠️ getColumnList: 카테고리 매핑 조회 실패, 스킵", {
            error: mappingError.message,
          });
        }

        logger.info("✅ getColumnList: categoryMappings Map 생성 완료", {
          size: categoryMappings.size,
          entries: Array.from(categoryMappings.entries()),
        });
      }

      // BigInt를 Number로 변환하여 JSON 직렬화 문제 해결
      const columns: ColumnTypeInfo[] = rawColumns.map((column) => {
        const baseColumn = {
          ...column,
          maxLength: column.maxLength ? Number(column.maxLength) : null,
          numericPrecision: column.numericPrecision
            ? Number(column.numericPrecision)
            : null,
          numericScale: column.numericScale
            ? Number(column.numericScale)
            : null,
          displayOrder: column.displayOrder
            ? Number(column.displayOrder)
            : null,
          // webType은 사용자가 명시적으로 설정한 값을 그대로 사용
          // (자동 추론은 table_type_columns에 없는 경우에만 SQL 쿼리의 COALESCE에서 처리됨)
          webType: column.webType,
        };

        // 카테고리 타입인 경우 categoryMenus 추가
        if (
          column.inputType === "category" &&
          categoryMappings.has(column.columnName)
        ) {
          const menus = categoryMappings.get(column.columnName);
          logger.info(
            `✅ getColumnList: 컬럼 ${column.columnName}에 카테고리 메뉴 추가`,
            { menus }
          );
          return {
            ...baseColumn,
            categoryMenus: menus,
          };
        }

        return baseColumn;
      });

      const totalPages = Math.ceil(total / size);

      const result = {
        columns,
        total,
        page,
        size,
        totalPages,
      };

      // 캐시에 저장 (5분 TTL)
      cache.set(cacheKey, result, 5 * 60 * 1000);

      logger.info(
        `컬럼 정보 조회 완료: ${tableName}, ${columns.length}/${total}개 (${page}/${totalPages} 페이지)`
      );
      return result;
    } catch (error) {
      logger.error(`컬럼 정보 조회 중 오류 발생: ${tableName}`, error);
      throw new Error(
        `컬럼 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    }
  }

  /**
   * 테이블이 table_labels에 없으면 자동 추가
   * Prisma ORM으로 변경
   */
  async insertTableIfNotExists(tableName: string): Promise<void> {
    try {
      logger.info(`테이블 라벨 자동 추가 시작: ${tableName}`);

      await query(
        `INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
         VALUES ($1, $2, $3, NOW(), NOW())
         ON CONFLICT (table_name) DO NOTHING`,
        [tableName, tableName, ""]
      );

      logger.info(`테이블 라벨 자동 추가 완료: ${tableName}`);
    } catch (error) {
      logger.error(`테이블 라벨 자동 추가 중 오류 발생: ${tableName}`, error);
      throw new Error(
        `테이블 라벨 자동 추가 실패: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    }
  }

  /**
   * 테이블 라벨 업데이트
   */
  async updateTableLabel(
    tableName: string,
    displayName: string,
    description?: string
  ): Promise<void> {
    try {
      logger.info(`테이블 라벨 업데이트 시작: ${tableName}`);

      // table_labels 테이블에 UPSERT
      await query(
        `INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
         VALUES ($1, $2, $3, NOW(), NOW())
         ON CONFLICT (table_name) 
         DO UPDATE SET 
           table_label = EXCLUDED.table_label,
           description = EXCLUDED.description,
           updated_date = NOW()`,
        [tableName, displayName, description || ""]
      );

      // 캐시 무효화
      cache.delete(CacheKeys.TABLE_LIST);

      logger.info(`테이블 라벨 업데이트 완료: ${tableName}`);
    } catch (error) {
      logger.error("테이블 라벨 업데이트 중 오류 발생:", error);
      throw new Error(
        `테이블 라벨 업데이트 실패: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    }
  }

  /**
   * 컬럼 설정 업데이트 (UPSERT 방식)
   * Prisma ORM으로 변경
   */
  async updateColumnSettings(
    tableName: string,
    columnName: string,
    settings: ColumnSettings,
    companyCode: string // 🔥 회사 코드 추가
  ): Promise<void> {
    try {
      logger.info(
        `컬럼 설정 업데이트 시작: ${tableName}.${columnName}, company: ${companyCode}`
      );

      // 🔥 "direct" 또는 "auto"는 프론트엔드의 입력 방식 구분값이므로
      // DB의 input_type(웹타입)으로 저장하면 안 됨 - "text"로 변환
      if (settings.inputType === "direct" || settings.inputType === "auto") {
        logger.warn(
          `잘못된 inputType 값 감지: ${settings.inputType} → 'text'로 변환 (${tableName}.${columnName})`
        );
        settings.inputType = "text";
      }

      // 테이블이 table_labels에 없으면 자동 추가
      await this.insertTableIfNotExists(tableName);

      // table_type_columns에 모든 설정 저장 (멀티테넌시 지원)
      // detailSettings가 문자열이면 그대로, 객체면 JSON.stringify
      let detailSettingsStr = settings.detailSettings;
      if (
        typeof settings.detailSettings === "object" &&
        settings.detailSettings !== null
      ) {
        detailSettingsStr = JSON.stringify(settings.detailSettings);
      }

      // 입력타입에 해당하지 않는 설정값은 NULL로 강제 초기화
      const inputType = settings.inputType;
      const referenceTable = inputType === "entity" ? (settings.referenceTable || null) : null;
      const referenceColumn = inputType === "entity" ? (settings.referenceColumn || null) : null;
      const displayColumn = inputType === "entity" ? (settings.displayColumn || null) : null;
      const codeCategory = inputType === "code" ? (settings.codeCategory || null) : null;
      const codeValue = inputType === "code" ? (settings.codeValue || null) : null;
      const categoryRef = inputType === "category" ? (settings.categoryRef || null) : null;

      await query(
        `INSERT INTO table_type_columns (
          table_name, column_name, column_label, input_type, detail_settings,
          code_category, code_value, reference_table, reference_column,
          display_column, display_order, is_visible, is_nullable,
          company_code, category_ref, created_date, updated_date
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'Y', $13, $14, NOW(), NOW())
        ON CONFLICT (table_name, column_name, company_code) 
        DO UPDATE SET
          column_label = COALESCE(EXCLUDED.column_label, table_type_columns.column_label),
          input_type = COALESCE(EXCLUDED.input_type, table_type_columns.input_type),
          detail_settings = COALESCE(EXCLUDED.detail_settings, table_type_columns.detail_settings),
          code_category = EXCLUDED.code_category,
          code_value = EXCLUDED.code_value,
          reference_table = EXCLUDED.reference_table,
          reference_column = EXCLUDED.reference_column,
          display_column = EXCLUDED.display_column,
          display_order = COALESCE(EXCLUDED.display_order, table_type_columns.display_order),
          is_visible = COALESCE(EXCLUDED.is_visible, table_type_columns.is_visible),
          category_ref = EXCLUDED.category_ref,
          updated_date = NOW()`,
        [
          tableName,
          columnName,
          settings.columnLabel,
          inputType,
          detailSettingsStr,
          codeCategory,
          codeValue,
          referenceTable,
          referenceColumn,
          displayColumn,
          settings.displayOrder || 0,
          settings.isVisible !== undefined ? settings.isVisible : true,
          companyCode,
          categoryRef,
        ]
      );

      // 🔥 화면 레이아웃 동기화 (입력 타입 변경 시)
      if (settings.inputType) {
        await this.syncScreenLayoutsInputType(
          tableName,
          columnName,
          settings.inputType as string,
          companyCode
        );
      }

      // 캐시 무효화 - 해당 테이블의 컬럼 캐시 삭제
      cache.deleteByPattern(`table_columns:${tableName}:`);
      cache.delete(CacheKeys.TABLE_COLUMN_COUNT(tableName));

      logger.info(`컬럼 설정 업데이트 완료: ${tableName}.${columnName}`);
    } catch (error) {
      logger.error(
        `컬럼 설정 업데이트 중 오류 발생: ${tableName}.${columnName}`,
        error
      );
      throw new Error(
        `컬럼 설정 업데이트 실패: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    }
  }

  /**
   * 전체 컬럼 설정 일괄 업데이트
   * Prisma 트랜잭션으로 변경
   */
  async updateAllColumnSettings(
    tableName: string,
    columnSettings: ColumnSettings[],
    companyCode: string // 🔥 회사 코드 추가
  ): Promise<void> {
    try {
      logger.info(
        `전체 컬럼 설정 일괄 업데이트 시작: ${tableName}, ${columnSettings.length}개, company: ${companyCode}`
      );

      // Raw Query 트랜잭션 사용
      await transaction(async (client) => {
        // 테이블이 table_labels에 없으면 자동 추가
        await this.insertTableIfNotExists(tableName);

        // 각 컬럼 설정을 순차적으로 업데이트
        for (const columnSetting of columnSettings) {
          // columnName은 실제 DB 컬럼명을 유지해야 함
          const columnName = columnSetting.columnName;
          if (columnName) {
            await this.updateColumnSettings(
              tableName,
              columnName,
              columnSetting,
              companyCode // 🔥 회사 코드 전달
            );
          } else {
            logger.warn(
              `컬럼명이 누락된 설정: ${JSON.stringify(columnSetting)}`
            );
          }
        }
      });

      // 캐시 무효화 - 해당 테이블의 컬럼 캐시 삭제
      cache.deleteByPattern(`table_columns:${tableName}:`);
      cache.delete(CacheKeys.TABLE_COLUMN_COUNT(tableName));

      logger.info(`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}`);
    } catch (error) {
      logger.error(
        `전체 컬럼 설정 일괄 업데이트 중 오류 발생: ${tableName}`,
        error
      );
      throw new Error(
        `전체 컬럼 설정 일괄 업데이트 실패: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    }
  }

  /**
   * 테이블 라벨 정보 조회
   * Prisma ORM으로 변경
   */
  async getTableLabels(tableName: string): Promise<TableLabels | null> {
    try {
      logger.info(`테이블 라벨 정보 조회 시작: ${tableName}`);

      const tableLabel = await queryOne<{
        table_name: string;
        table_label: string | null;
        description: string | null;
        created_date: Date | null;
        updated_date: Date | null;
      }>(
        `SELECT table_name, table_label, description, created_date, updated_date
         FROM table_labels
         WHERE table_name = $1`,
        [tableName]
      );

      if (!tableLabel) {
        return null;
      }

      const result: TableLabels = {
        tableName: tableLabel.table_name,
        tableLabel: tableLabel.table_label || undefined,
        description: tableLabel.description || undefined,
        createdDate: tableLabel.created_date || undefined,
        updatedDate: tableLabel.updated_date || undefined,
      };

      logger.info(`테이블 라벨 정보 조회 완료: ${tableName}`);
      return result;
    } catch (error) {
      logger.error(`테이블 라벨 정보 조회 중 오류 발생: ${tableName}`, error);
      throw new Error(
        `테이블 라벨 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    }
  }

  /**
   * 컬럼 라벨 정보 조회
   * Prisma ORM으로 변경
   */
  async getColumnLabels(
    tableName: string,
    columnName: string
  ): Promise<ColumnLabels | null> {
    try {
      logger.info(`컬럼 라벨 정보 조회 시작: ${tableName}.${columnName}`);

      const columnLabel = await queryOne<{
        id: number;
        table_name: string;
        column_name: string;
        column_label: string | null;
        input_type: string | null;
        detail_settings: any;
        description: string | null;
        display_order: number | null;
        is_visible: boolean | null;
        code_category: string | null;
        code_value: string | null;
        reference_table: string | null;
        reference_column: string | null;
        created_date: Date | null;
        updated_date: Date | null;
      }>(
        `SELECT id, table_name, column_name, column_label, input_type, detail_settings,
                description, display_order, is_visible, code_category, code_value,
                reference_table, reference_column, created_date, updated_date
         FROM table_type_columns
         WHERE table_name = $1 AND column_name = $2 AND company_code = '*'`,
        [tableName, columnName]
      );

      if (!columnLabel) {
        return null;
      }

      const result: ColumnLabels = {
        id: columnLabel.id,
        tableName: columnLabel.table_name || "",
        columnName: columnLabel.column_name || "",
        columnLabel: columnLabel.column_label || undefined,
        webType: columnLabel.input_type || undefined,
        detailSettings: columnLabel.detail_settings || undefined,
        description: columnLabel.description || undefined,
        displayOrder: columnLabel.display_order || undefined,
        isVisible: columnLabel.is_visible || undefined,
        codeCategory: columnLabel.code_category || undefined,
        codeValue: columnLabel.code_value || undefined,
        referenceTable: columnLabel.reference_table || undefined,
        referenceColumn: columnLabel.reference_column || undefined,
        createdDate: columnLabel.created_date || undefined,
        updatedDate: columnLabel.updated_date || undefined,
      };

      logger.info(`컬럼 라벨 정보 조회 완료: ${tableName}.${columnName}`);
      return result;
    } catch (error) {
      logger.error(
        `컬럼 라벨 정보 조회 중 오류 발생: ${tableName}.${columnName}`,
        error
      );
      throw new Error(
        `컬럼 라벨 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    }
  }

  /**
   * 컬럼 입력 타입 설정 (web_type → input_type 통합)
   */
  async updateColumnWebType(
    tableName: string,
    columnName: string,
    webType: string,
    detailSettings?: Record<string, any>,
    inputType?: string
  ): Promise<void> {
    try {
      // 🔥 'direct'나 'auto'는 프론트엔드의 입력 방식 구분값이므로
      // DB의 input_type(웹타입)으로 저장하면 안 됨 - 'text'로 변환
      let finalWebType = webType;
      if (webType === "direct" || webType === "auto") {
        logger.warn(
          `잘못된 webType 값 감지: ${webType} → 'text'로 변환 (${tableName}.${columnName})`
        );
        finalWebType = "text";
      }

      logger.info(
        `컬럼 입력 타입 설정 시작: ${tableName}.${columnName} = ${finalWebType}`
      );

      // 웹 타입별 기본 상세 설정 생성
      const defaultDetailSettings =
        this.generateDefaultDetailSettings(finalWebType);

      // 사용자 정의 설정과 기본 설정 병합
      const finalDetailSettings = {
        ...defaultDetailSettings,
        ...detailSettings,
      };

      // table_type_columns UPSERT로 업데이트 또는 생성 (company_code = '*' 공통 설정)
      await query(
        `INSERT INTO table_type_columns (
          table_name, column_name, input_type, detail_settings, is_nullable, 
          company_code, created_date, updated_date
        ) VALUES ($1, $2, $3, $4, 'Y', '*', NOW(), NOW())
        ON CONFLICT (table_name, column_name, company_code)
        DO UPDATE SET
          input_type = EXCLUDED.input_type,
          detail_settings = EXCLUDED.detail_settings,
          updated_date = NOW()`,
        [
          tableName,
          columnName,
          finalWebType,
          JSON.stringify(finalDetailSettings),
        ]
      );
      logger.info(
        `컬럼 입력 타입 설정 완료: ${tableName}.${columnName} = ${finalWebType}`
      );
    } catch (error) {
      logger.error(
        `컬럼 입력 타입 설정 중 오류 발생: ${tableName}.${columnName}`,
        error
      );
      throw new Error(
        `컬럼 입력 타입 설정 실패: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    }
  }

  /**
   * 컬럼 입력 타입 설정 (새로운 시스템)
   * @param companyCode - 회사 코드 (멀티테넌시)
   */
  async updateColumnInputType(
    tableName: string,
    columnName: string,
    inputType: string,
    companyCode: string,
    detailSettings?: Record<string, any>
  ): Promise<void> {
    try {
      // 🔥 'direct'나 'auto'는 프론트엔드의 입력 방식 구분값이므로
      // DB의 input_type(웹타입)으로 저장하면 안 됨 - 'text'로 변환
      let finalInputType = inputType;
      if (inputType === "direct" || inputType === "auto") {
        logger.warn(
          `잘못된 input_type 값 감지: ${inputType} → 'text'로 변환 (${tableName}.${columnName})`
        );
        finalInputType = "text";
      }

      logger.info(
        `컬럼 입력 타입 설정 시작: ${tableName}.${columnName} = ${finalInputType}, company: ${companyCode}`
      );

      // 입력 타입별 기본 상세 설정 생성
      const defaultDetailSettings =
        this.generateDefaultInputTypeSettings(finalInputType);

      // 사용자 정의 설정과 기본 설정 병합
      const finalDetailSettings = {
        ...defaultDetailSettings,
        ...detailSettings,
      };

      // 입력타입 변경 시 이전 타입의 설정값 초기화
      const clearEntity = finalInputType !== "entity";
      const clearCode = finalInputType !== "code";
      const clearCategory = finalInputType !== "category";

      await query(
        `INSERT INTO table_type_columns (
          table_name, column_name, input_type, detail_settings,
          is_nullable, display_order, company_code, created_date, updated_date
        ) VALUES ($1, $2, $3, $4, 'Y', 0, $5, now(), now())
        ON CONFLICT (table_name, column_name, company_code)
        DO UPDATE SET
          input_type = EXCLUDED.input_type,
          detail_settings = EXCLUDED.detail_settings,
          reference_table = CASE WHEN $6 THEN NULL ELSE table_type_columns.reference_table END,
          reference_column = CASE WHEN $6 THEN NULL ELSE table_type_columns.reference_column END,
          display_column = CASE WHEN $6 THEN NULL ELSE table_type_columns.display_column END,
          code_category = CASE WHEN $7 THEN NULL ELSE table_type_columns.code_category END,
          code_value = CASE WHEN $7 THEN NULL ELSE table_type_columns.code_value END,
          category_ref = CASE WHEN $8 THEN NULL ELSE table_type_columns.category_ref END,
          updated_date = now()`,
        [
          tableName,
          columnName,
          finalInputType,
          JSON.stringify(finalDetailSettings),
          companyCode,
          clearEntity,
          clearCode,
          clearCategory,
        ]
      );

      // 🔥 해당 컬럼을 사용하는 화면 레이아웃의 widgetType도 업데이트
      await this.syncScreenLayoutsInputType(
        tableName,
        columnName,
        finalInputType,
        companyCode
      );

      // 🔥 캐시 무효화: 해당 테이블의 컬럼 캐시 삭제
      const cacheKeyPattern = `${CacheKeys.TABLE_COLUMNS(tableName, 1, 1000)}_${companyCode}`;
      cache.delete(cacheKeyPattern);
      cache.delete(CacheKeys.TABLE_COLUMN_COUNT(tableName));

      logger.info(
        `컬럼 입력 타입 설정 완료: ${tableName}.${columnName} = ${inputType}, company: ${companyCode} (캐시 무효화 완료)`
      );
    } catch (error) {
      logger.error(
        `컬럼 입력 타입 설정 실패: ${tableName}.${columnName}`,
        error
      );
      throw new Error(
        `컬럼 입력 타입 설정 실패: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    }
  }

  /**
   * 입력 타입에 해당하는 컴포넌트 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: "select-basic",
      category: "select-basic",
    };

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

  /**
   * 컬럼 입력 타입 변경 시 해당 컬럼을 사용하는 화면 레이아웃의 widgetType 및 componentType 동기화
   * @param tableName - 테이블명
   * @param columnName - 컬럼명
   * @param inputType - 새로운 입력 타입
   * @param companyCode - 회사 코드
   */
  private async syncScreenLayoutsInputType(
    tableName: string,
    columnName: string,
    inputType: string,
    companyCode: string
  ): Promise<void> {
    try {
      // 해당 컬럼을 사용하는 화면 레이아웃 조회
      const affectedLayouts = await query<{
        layout_id: number;
        screen_id: number;
        component_id: string;
        component_type: string;
        properties: any;
      }>(
        `SELECT sl.layout_id, sl.screen_id, sl.component_id, sl.component_type, sl.properties
         FROM screen_layouts sl
         JOIN screen_definitions sd ON sl.screen_id = sd.screen_id
         WHERE sl.properties->>'tableName' = $1
           AND sl.properties->>'columnName' = $2
           AND (sd.company_code = $3 OR $3 = '*')`,
        [tableName, columnName, companyCode]
      );

      if (affectedLayouts.length === 0) {
        logger.info(
          `화면 레이아웃 동기화: ${tableName}.${columnName}을 사용하는 화면 없음`
        );
        return;
      }

      logger.info(
        `화면 레이아웃 동기화 시작: ${affectedLayouts.length}개 컴포넌트 발견`
      );

      // 새로운 componentType 계산
      const newComponentType = this.getComponentIdFromInputType(inputType);

      // 각 레이아웃의 widgetType, componentType 업데이트
      for (const layout of affectedLayouts) {
        const updatedProperties = {
          ...layout.properties,
          widgetType: inputType,
          inputType: inputType,
          // componentConfig 내부의 type, inputType, webType 모두 업데이트
          componentConfig: {
            ...layout.properties?.componentConfig,
            type: newComponentType,
            inputType: inputType,
            webType: inputType, // 프론트엔드 SelectBasicComponent에서 카테고리 로딩 여부 판단에 사용
          },
        };

        await query(
          `UPDATE screen_layouts 
           SET properties = $1, component_type = $2
           WHERE layout_id = $3`,
          [
            JSON.stringify(updatedProperties),
            newComponentType,
            layout.layout_id,
          ]
        );

        logger.info(
          `화면 레이아웃 업데이트: screen_id=${layout.screen_id}, component_id=${layout.component_id}, widgetType=${inputType}, webType=${inputType}, componentType=${newComponentType}`
        );
      }

      logger.info(
        `화면 레이아웃 동기화 완료: ${affectedLayouts.length}개 컴포넌트 업데이트됨`
      );
    } catch (error) {
      // 화면 레이아웃 동기화 실패는 치명적이지 않으므로 로그만 남기고 계속 진행
      logger.warn(
        `화면 레이아웃 동기화 실패 (무시됨): ${tableName}.${columnName}`,
        error
      );
    }
  }

  /**
   * 입력 타입별 기본 상세 설정 생성
   */
  private generateDefaultInputTypeSettings(
    inputType: string
  ): Record<string, any> {
    switch (inputType) {
      case "text":
        return {
          maxLength: 500,
          placeholder: "텍스트를 입력하세요",
        };

      case "number":
        return {
          min: 0,
          step: 1,
          placeholder: "숫자를 입력하세요",
        };

      case "date":
        return {
          format: "YYYY-MM-DD",
          placeholder: "날짜를 선택하세요",
        };

      case "code":
        return {
          placeholder: "코드를 선택하세요",
          searchable: true,
        };

      case "entity":
        return {
          placeholder: "항목을 선택하세요",
          searchable: true,
        };

      case "select":
        return {
          placeholder: "선택하세요",
          searchable: false,
        };

      case "checkbox":
        return {
          defaultChecked: false,
          trueValue: "Y",
          falseValue: "N",
        };

      case "radio":
        return {
          inline: false,
        };

      default:
        return {};
    }
  }

  /**
   * 웹 타입별 기본 상세 설정 생성 (레거시 지원)
   * @deprecated generateDefaultInputTypeSettings 사용 권장
   */
  private generateDefaultDetailSettings(webType: string): Record<string, any> {
    switch (webType) {
      case "text":
        return {
          maxLength: 255,
          pattern: null,
          placeholder: null,
        };

      case "number":
        return {
          min: null,
          max: null,
          step: 1,
          precision: 2,
        };

      case "date":
        return {
          format: "YYYY-MM-DD",
          minDate: null,
          maxDate: null,
        };

      case "code":
        return {
          codeCategory: null,
          displayFormat: "label",
          searchable: true,
          multiple: false,
        };

      case "entity":
        return {
          referenceTable: null,
          referenceColumn: null,
          searchable: true,
          multiple: false,
        };

      case "textarea":
        return {
          rows: 3,
          maxLength: 1000,
          placeholder: null,
        };

      case "select":
        return {
          options: [],
          multiple: false,
          searchable: false,
        };

      case "checkbox":
        return {
          defaultChecked: false,
          label: null,
        };

      case "radio":
        return {
          options: [],
          inline: false,
        };

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

      default:
        return {};
    }
  }

  /**
   * 파일 데이터 보강 (attach_file_info에서 파일 정보 가져오기)
   */
  private async enrichFileData(
    data: any[],
    fileColumns: string[],
    tableName: string
  ): Promise<any[]> {
    try {
      logger.info(
        `파일 데이터 보강 시작: ${tableName}, ${fileColumns.join(", ")}`
      );

      // 각 행의 파일 정보를 보강
      const enrichedData = await Promise.all(
        data.map(async (row) => {
          const enrichedRow = { ...row };

          // 각 파일 컬럼에 대해 처리
          for (const fileColumn of fileColumns) {
            const filePath = row[fileColumn];
            if (filePath && typeof filePath === "string") {
              // 🎯 컴포넌트별 파일 정보 조회
              // 파일 경로에서 컴포넌트 ID 추출하거나 컬럼명 사용
              const componentId =
                this.extractComponentIdFromPath(filePath) || fileColumn;
              const fileInfos = await this.getFileInfoByColumnAndTarget(
                componentId,
                row.id || row.objid || row.seq, // 기본키 값
                tableName
              );

              if (fileInfos && fileInfos.length > 0) {
                // 파일 정보를 JSON 형태로 저장
                const totalSize = fileInfos.reduce(
                  (sum, file) => sum + (file.size || 0),
                  0
                );
                enrichedRow[fileColumn] = JSON.stringify({
                  files: fileInfos,
                  totalCount: fileInfos.length,
                  totalSize: totalSize,
                });
              } else {
                // 파일이 없으면 빈 상태로 설정
                enrichedRow[fileColumn] = JSON.stringify({
                  files: [],
                  totalCount: 0,
                  totalSize: 0,
                });
              }
            }
          }

          return enrichedRow;
        })
      );

      logger.info(`파일 데이터 보강 완료: ${enrichedData.length}개 행 처리`);
      return enrichedData;
    } catch (error) {
      logger.error("파일 데이터 보강 실패:", error);
      return data; // 실패 시 원본 데이터 반환
    }
  }

  /**
   * 파일 경로에서 컴포넌트 ID 추출 (현재는 사용하지 않음)
   */
  private extractComponentIdFromPath(filePath: string): string | null {
    // 현재는 파일 경로에서 컴포넌트 ID를 추출할 수 없으므로 null 반환
    // 추후 필요시 구현
    return null;
  }

  /**
   * 컬럼별 파일 정보 조회 (컬럼명과 target_objid로 구분)
   */
  private async getFileInfoByColumnAndTarget(
    columnName: string,
    targetObjid: any,
    tableName: string
  ): Promise<any[]> {
    try {
      logger.info(
        `컬럼별 파일 정보 조회: ${tableName}.${columnName}, target: ${targetObjid}`
      );

      // 🎯 컬럼명을 doc_type으로 사용하여 파일 구분
      const fileInfos = await query<{
        objid: string;
        real_file_name: string;
        file_size: number;
        file_ext: string;
        file_path: string;
        doc_type: string;
        doc_type_name: string;
        regdate: Date;
        writer: string;
      }>(
        `SELECT objid, real_file_name, file_size, file_ext, file_path,
                doc_type, doc_type_name, regdate, writer
         FROM attach_file_info
         WHERE target_objid = $1 AND doc_type = $2 AND status = 'ACTIVE'
         ORDER BY regdate DESC`,
        [String(targetObjid), columnName]
      );

      // 파일 정보 포맷팅
      return fileInfos.map((fileInfo) => ({
        name: fileInfo.real_file_name,
        size: Number(fileInfo.file_size) || 0,
        path: fileInfo.file_path,
        ext: fileInfo.file_ext,
        objid: String(fileInfo.objid),
        docType: fileInfo.doc_type,
        docTypeName: fileInfo.doc_type_name,
        regdate: fileInfo.regdate?.toISOString(),
        writer: fileInfo.writer,
      }));
    } catch (error) {
      logger.warn(`컬럼별 파일 정보 조회 실패: ${columnName}`, error);
      return [];
    }
  }

  /**
   * 파일 경로로 파일 정보 조회 (기존 메서드 - 호환성 유지)
   */
  private async getFileInfoByPath(filePath: string): Promise<any | null> {
    try {
      const fileInfo = await queryOne<{
        objid: string;
        real_file_name: string;
        file_size: number;
        file_ext: string;
        file_path: string;
        doc_type: string;
        doc_type_name: string;
        regdate: Date;
        writer: string;
      }>(
        `SELECT objid, real_file_name, file_size, file_ext, file_path,
                doc_type, doc_type_name, regdate, writer
         FROM attach_file_info
         WHERE file_path = $1 AND status = 'ACTIVE'
         LIMIT 1`,
        [filePath]
      );

      if (!fileInfo) {
        return null;
      }

      return {
        name: fileInfo.real_file_name,
        path: fileInfo.file_path,
        size: Number(fileInfo.file_size) || 0,
        type: fileInfo.file_ext,
        objid: fileInfo.objid.toString(),
        docType: fileInfo.doc_type,
        docTypeName: fileInfo.doc_type_name,
        regdate: fileInfo.regdate?.toISOString(),
        writer: fileInfo.writer,
      };
    } catch (error) {
      logger.warn(`파일 정보 조회 실패: ${filePath}`, error);
      return null;
    }
  }

  /**
   * 파일 타입 컬럼 조회
   */
  private async getFileTypeColumns(tableName: string): Promise<string[]> {
    try {
      const fileColumns = await query<{ column_name: string }>(
        `SELECT column_name
         FROM table_type_columns
         WHERE table_name = $1 AND input_type = 'file' AND company_code = '*'`,
        [tableName]
      );

      const columnNames = fileColumns.map((col) => col.column_name);
      logger.info(`파일 타입 컬럼 감지: ${tableName}`, columnNames);
      return columnNames;
    } catch (error) {
      logger.warn(`파일 타입 컬럼 조회 실패: ${tableName}`, error);
      return [];
    }
  }

  /**
   * 고급 검색 조건 구성
   */
  private async buildAdvancedSearchCondition(
    tableName: string,
    columnName: string,
    value: any,
    paramIndex: number
  ): Promise<{
    whereClause: string;
    values: any[];
    paramCount: number;
  } | null> {
    try {
      // 🆕 배열 값 처리 (다중 값 검색 - 분할패널 엔티티 타입에서 "2,3" 형태 지원)
      // 좌측에서 "2"를 선택해도, 우측에서 "2,3"을 가진 행이 표시되도록 함
      if (Array.isArray(value) && value.length > 0) {
        // 배열의 각 값에 대해 OR 조건으로 검색
        // 우측 컬럼에 "2,3" 같은 다중 값이 있을 수 있으므로
        // 각 값을 LIKE 또는 = 조건으로 처리
        const conditions: string[] = [];
        const values: any[] = [];

        value.forEach((v: any, idx: number) => {
          const safeValue = String(v).trim();
          // 정확히 일치하거나, 콤마로 구분된 값 중 하나로 포함
          // 예: "2,3" 컬럼에서 "2"를 찾으려면:
          // - 정확히 "2"
          // - "2," 로 시작
          // - ",2" 로 끝남
          // - ",2," 중간에 포함
          const paramBase = paramIndex + idx * 4;
          conditions.push(`(
            ${columnName}::text = $${paramBase} OR
            ${columnName}::text LIKE $${paramBase + 1} OR
            ${columnName}::text LIKE $${paramBase + 2} OR
            ${columnName}::text LIKE $${paramBase + 3}
          )`);
          values.push(
            safeValue,
            `${safeValue},%`,
            `%,${safeValue}`,
            `%,${safeValue},%`
          );
        });

        logger.info(
          `🔍 다중 값 배열 검색: ${columnName} IN [${value.join(", ")}]`
        );
        return {
          whereClause: `(${conditions.join(" OR ")})`,
          values,
          paramCount: values.length,
        };
      }

      // 🔧 파이프로 구분된 문자열 처리 (다중선택 또는 날짜 범위)
      if (typeof value === "string" && value.includes("|")) {
        const columnInfo = await this.getColumnWebTypeInfo(
          tableName,
          columnName
        );

        // 날짜 타입이면 날짜 범위로 처리
        if (
          columnInfo &&
          (columnInfo.webType === "date" || columnInfo.webType === "datetime")
        ) {
          return this.buildDateRangeCondition(columnName, value, paramIndex);
        }

        // 그 외 타입이면 다중선택(IN 조건)으로 처리
        const multiValues = value
          .split("|")
          .filter((v: string) => v.trim() !== "");
        if (multiValues.length > 0) {
          const placeholders = multiValues
            .map((_: string, idx: number) => `$${paramIndex + idx}`)
            .join(", ");
          logger.info(
            `🔍 다중선택 필터 적용: ${columnName} IN (${multiValues.join(", ")})`
          );
          return {
            whereClause: `${columnName}::text IN (${placeholders})`,
            values: multiValues,
            paramCount: multiValues.length,
          };
        }
      }

      // 🔧 날짜 범위 객체 {from, to} 체크
      if (
        typeof value === "object" &&
        value !== null &&
        ("from" in value || "to" in value)
      ) {
        // 날짜 범위 객체는 그대로 전달
        const columnInfo = await this.getColumnWebTypeInfo(
          tableName,
          columnName
        );
        if (
          columnInfo &&
          (columnInfo.webType === "date" || columnInfo.webType === "datetime")
        ) {
          return this.buildDateRangeCondition(columnName, value, paramIndex);
        }
      }

      // 🔧 {value, operator} 형태의 필터 객체 처리
      let actualValue = value;
      let operator = "contains"; // 기본값

      if (typeof value === "object" && value !== null && "value" in value) {
        actualValue = value.value;
        operator = value.operator || "contains";

        logger.info("🔍 필터 객체 처리:", {
          columnName,
          originalValue: value,
          actualValue,
          operator,
        });
      }

      // 🔧 파이프로 구분된 문자열 처리 (객체에서 추출한 actualValue도 처리)
      if (typeof actualValue === "string" && actualValue.includes("|")) {
        const columnInfo = await this.getColumnWebTypeInfo(
          tableName,
          columnName
        );

        // 날짜 타입이면 날짜 범위로 처리
        if (
          columnInfo &&
          (columnInfo.webType === "date" || columnInfo.webType === "datetime")
        ) {
          return this.buildDateRangeCondition(
            columnName,
            actualValue,
            paramIndex
          );
        }

        // 그 외 타입이면 다중선택(IN 조건)으로 처리
        const multiValues = actualValue
          .split("|")
          .filter((v: string) => v.trim() !== "");
        if (multiValues.length > 0) {
          const placeholders = multiValues
            .map((_: string, idx: number) => `$${paramIndex + idx}`)
            .join(", ");
          logger.info(
            `🔍 다중선택 필터 적용 (객체에서 추출): ${columnName} IN (${multiValues.join(", ")})`
          );
          return {
            whereClause: `${columnName}::text IN (${placeholders})`,
            values: multiValues,
            paramCount: multiValues.length,
          };
        }
      }

      // "__ALL__" 값이거나 빈 값이면 필터 조건을 적용하지 않음
      if (
        actualValue === "__ALL__" ||
        actualValue === "" ||
        actualValue === null ||
        actualValue === undefined
      ) {
        return null;
      }

      // 컬럼 타입 정보 조회
      const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
      logger.info(
        `🔍 [buildAdvancedSearchCondition] ${tableName}.${columnName}`,
        `webType=${columnInfo?.webType || "NULL"}`,
        `inputType=${columnInfo?.inputType || "NULL"}`,
        `actualValue=${JSON.stringify(actualValue)}`,
        `operator=${operator}`
      );

      if (!columnInfo) {
        // 컬럼 정보가 없으면 operator에 따른 기본 검색
        switch (operator) {
          case "equals":
            return {
              whereClause: `${columnName}::text = $${paramIndex}`,
              values: [actualValue],
              paramCount: 1,
            };
          case "contains":
          default:
            return {
              whereClause: `${columnName}::text ILIKE $${paramIndex}`,
              values: [`%${actualValue}%`],
              paramCount: 1,
            };
        }
      }

      const webType = columnInfo.webType;

      // 🔧 다중선택 처리: actualValue가 파이프(|)를 포함하고 날짜 타입이 아닌 경우
      if (
        typeof actualValue === "string" &&
        actualValue.includes("|") &&
        webType !== "date" &&
        webType !== "datetime"
      ) {
        const multiValues = actualValue
          .split("|")
          .filter((v: string) => v.trim() !== "");
        if (multiValues.length > 0) {
          const placeholders = multiValues
            .map((_: string, idx: number) => `$${paramIndex + idx}`)
            .join(", ");
          logger.info(
            `🔍 다중선택 필터 적용 (객체): ${columnName} IN (${multiValues.join(", ")})`
          );
          return {
            whereClause: `${columnName}::text IN (${placeholders})`,
            values: multiValues,
            paramCount: multiValues.length,
          };
        }
      }

      // 웹타입별 검색 조건 구성
      switch (webType) {
        case "date":
        case "datetime":
          return this.buildDateRangeCondition(
            columnName,
            actualValue,
            paramIndex
          );

        case "number":
        case "decimal":
          return this.buildNumberRangeCondition(
            columnName,
            actualValue,
            paramIndex
          );

        case "code":
          return await this.buildCodeSearchCondition(
            tableName,
            columnName,
            actualValue,
            paramIndex,
            operator
          );

        case "entity":
          return await this.buildEntitySearchCondition(
            tableName,
            columnName,
            actualValue,
            paramIndex,
            operator // operator 전달 (equals면 직접 매칭)
          );

        default:
          // operator에 따라 정확 일치 또는 부분 일치 검색
          if (operator === "equals") {
            return {
              whereClause: `${columnName}::text = $${paramIndex}`,
              values: [String(actualValue)],
              paramCount: 1,
            };
          }
          return {
            whereClause: `${columnName}::text ILIKE $${paramIndex}`,
            values: [`%${actualValue}%`],
            paramCount: 1,
          };
      }
    } catch (error) {
      logger.error(
        `고급 검색 조건 구성 실패: ${tableName}.${columnName}`,
        error
      );
      // 오류 시 기본 검색으로 폴백
      let fallbackValue = value;
      let fallbackOperator = "contains";
      if (typeof value === "object" && value !== null && "value" in value) {
        fallbackValue = value.value;
        fallbackOperator = value.operator || "contains";
      }

      if (fallbackOperator === "equals") {
        return {
          whereClause: `${columnName}::text = $${paramIndex}`,
          values: [String(fallbackValue)],
          paramCount: 1,
        };
      }
      return {
        whereClause: `${columnName}::text ILIKE $${paramIndex}`,
        values: [`%${fallbackValue}%`],
        paramCount: 1,
      };
    }
  }

  /**
   * 날짜 범위 검색 조건 구성
   */
  private buildDateRangeCondition(
    columnName: string,
    value: any,
    paramIndex: number
  ): {
    whereClause: string;
    values: any[];
    paramCount: number;
  } {
    const conditions: string[] = [];
    const values: any[] = [];
    let paramCount = 0;

    // 문자열 형식의 날짜 범위 파싱 ("YYYY-MM-DD|YYYY-MM-DD")
    if (typeof value === "string" && value.includes("|")) {
      const [fromStr, toStr] = value.split("|");

      if (fromStr && fromStr.trim() !== "") {
        // VARCHAR 컬럼을 DATE로 캐스팅하여 비교
        conditions.push(
          `${columnName}::date >= $${paramIndex + paramCount}::date`
        );
        values.push(fromStr.trim());
        paramCount++;
      }
      if (toStr && toStr.trim() !== "") {
        // 종료일은 해당 날짜의 23:59:59까지 포함
        conditions.push(
          `${columnName}::date <= $${paramIndex + paramCount}::date`
        );
        values.push(toStr.trim());
        paramCount++;
      }
    }
    // 객체 형식의 날짜 범위 ({from, to})
    else if (typeof value === "object" && value !== null) {
      if (value.from) {
        // VARCHAR 컬럼을 DATE로 캐스팅하여 비교
        conditions.push(
          `${columnName}::date >= $${paramIndex + paramCount}::date`
        );
        values.push(value.from);
        paramCount++;
      }
      if (value.to) {
        // 종료일은 해당 날짜의 23:59:59까지 포함
        conditions.push(
          `${columnName}::date <= $${paramIndex + paramCount}::date`
        );
        values.push(value.to);
        paramCount++;
      }
    }
    // 단일 날짜 검색
    else if (typeof value === "string" && value.trim() !== "") {
      conditions.push(`${columnName}::date = $${paramIndex}::date`);
      values.push(value);
      paramCount = 1;
    }

    if (conditions.length === 0) {
      return {
        whereClause: `${columnName}::text ILIKE $${paramIndex}`,
        values: [`%${value}%`],
        paramCount: 1,
      };
    }

    return {
      whereClause: `(${conditions.join(" AND ")})`,
      values,
      paramCount,
    };
  }

  /**
   * 숫자 범위 검색 조건 구성
   */
  private buildNumberRangeCondition(
    columnName: string,
    value: any,
    paramIndex: number
  ): {
    whereClause: string;
    values: any[];
    paramCount: number;
  } {
    const conditions: string[] = [];
    const values: any[] = [];
    let paramCount = 0;

    if (typeof value === "object" && value !== null) {
      if (value.min !== undefined && value.min !== null && value.min !== "") {
        conditions.push(
          `${columnName}::numeric >= $${paramIndex + paramCount}`
        );
        values.push(parseFloat(value.min));
        paramCount++;
      }
      if (value.max !== undefined && value.max !== null && value.max !== "") {
        conditions.push(
          `${columnName}::numeric <= $${paramIndex + paramCount}`
        );
        values.push(parseFloat(value.max));
        paramCount++;
      }
    } else if (typeof value === "string" || typeof value === "number") {
      // 정확한 값 검색
      conditions.push(`${columnName}::numeric = $${paramIndex}`);
      values.push(parseFloat(value.toString()));
      paramCount = 1;
    }

    if (conditions.length === 0) {
      return {
        whereClause: `${columnName}::text ILIKE $${paramIndex}`,
        values: [`%${value}%`],
        paramCount: 1,
      };
    }

    return {
      whereClause: `(${conditions.join(" AND ")})`,
      values,
      paramCount,
    };
  }

  /**
   * 코드 검색 조건 구성
   */
  private async buildCodeSearchCondition(
    tableName: string,
    columnName: string,
    value: any,
    paramIndex: number,
    operator: string = "contains"
  ): Promise<{
    whereClause: string;
    values: any[];
    paramCount: number;
  }> {
    try {
      const codeTypeInfo = await this.getCodeTypeInfo(tableName, columnName);

      if (!codeTypeInfo.isCodeType || !codeTypeInfo.codeCategory) {
        // 코드 타입이 아니면 operator에 따라 검색
        if (operator === "equals") {
          return {
            whereClause: `${columnName}::text = $${paramIndex}`,
            values: [String(value)],
            paramCount: 1,
          };
        }
        return {
          whereClause: `${columnName}::text ILIKE $${paramIndex}`,
          values: [`%${value}%`],
          paramCount: 1,
        };
      }

      // select 필터(equals)인 경우 정확한 코드값 매칭만 수행
      if (operator === "equals") {
        return {
          whereClause: `${columnName}::text = $${paramIndex}`,
          values: [String(value)],
          paramCount: 1,
        };
      }

      if (typeof value === "string" && value.trim() !== "") {
        // 코드값 또는 코드명으로 검색
        return {
          whereClause: `(
            ${columnName}::text = $${paramIndex} OR 
            EXISTS (
              SELECT 1 FROM code_info ci 
              WHERE ci.code_category = $${paramIndex + 1}
              AND ci.code_value = ${columnName} 
              AND ci.code_name ILIKE $${paramIndex + 2}
            )
          )`,
          values: [value, codeTypeInfo.codeCategory, `%${value}%`],
          paramCount: 3,
        };
      } else {
        // 정확한 코드값 매칭
        return {
          whereClause: `${columnName} = $${paramIndex}`,
          values: [value],
          paramCount: 1,
        };
      }
    } catch (error) {
      logger.error(
        `코드 검색 조건 구성 실패: ${tableName}.${columnName}`,
        error
      );
      return {
        whereClause: `${columnName}::text ILIKE $${paramIndex}`,
        values: [`%${value}%`],
        paramCount: 1,
      };
    }
  }

  /**
   * 엔티티 검색 조건 구성
   */
  private async buildEntitySearchCondition(
    tableName: string,
    columnName: string,
    value: any,
    paramIndex: number,
    operator: string = "contains" // 연결 필터에서 "equals"로 전달되면 직접 매칭
  ): Promise<{
    whereClause: string;
    values: any[];
    paramCount: number;
  }> {
    try {
      const entityTypeInfo = await this.getEntityTypeInfo(
        tableName,
        columnName
      );

      // 배열 처리: IN 절 사용
      if (Array.isArray(value)) {
        if (value.length === 0) {
          // 빈 배열이면 항상 false 조건
          return {
            whereClause: `1 = 0`,
            values: [],
            paramCount: 0,
          };
        }

        // IN 절로 여러 값 검색
        const placeholders = value
          .map((_, idx) => `$${paramIndex + idx}`)
          .join(", ");
        return {
          whereClause: `${columnName} IN (${placeholders})`,
          values: value,
          paramCount: value.length,
        };
      }

      if (!entityTypeInfo.isEntityType || !entityTypeInfo.referenceTable) {
        // 엔티티 타입이 아니면 기본 검색
        return {
          whereClause: `${columnName}::text ILIKE $${paramIndex}`,
          values: [`%${value}%`],
          paramCount: 1,
        };
      }

      if (typeof value === "string" && value.trim() !== "") {
        // equals 연산자인 경우: 직접 값 매칭 (연결 필터에서 코드 값으로 필터링 시 사용)
        if (operator === "equals") {
          logger.info(
            `🔍 [buildEntitySearchCondition] equals 연산자 - 직접 매칭: ${columnName} = ${value}`
          );
          return {
            whereClause: `${columnName} = $${paramIndex}`,
            values: [value],
            paramCount: 1,
          };
        }

        // contains 연산자 (기본): 참조 테이블의 표시 컬럼으로 검색
        const referenceColumn = entityTypeInfo.referenceColumn || "id";
        const referenceTable = entityTypeInfo.referenceTable;

        // displayColumn이 비어있거나 "none"이면 참조 테이블에서 자동 감지 (entityJoinService와 동일한 로직)
        let displayColumn = entityTypeInfo.displayColumn;
        if (
          !displayColumn ||
          displayColumn === "none" ||
          displayColumn === ""
        ) {
          displayColumn = await this.findDisplayColumnForTable(
            referenceTable,
            referenceColumn
          );
          logger.info(
            `🔍 [buildEntitySearchCondition] displayColumn 자동 감지: ${referenceTable} -> ${displayColumn}`
          );
        }

        // 참조 테이블의 표시 컬럼으로 검색
        // 🔧 main. 접두사 추가: EXISTS 서브쿼리에서 외부 테이블 참조 시 명시적으로 지정
        return {
          whereClause: `EXISTS (
            SELECT 1 FROM ${referenceTable} ref 
            WHERE ref.${referenceColumn} = main.${columnName} 
            AND ref.${displayColumn} ILIKE $${paramIndex}
          )`,
          values: [`%${value}%`],
          paramCount: 1,
        };
      } else {
        // 정확한 참조값 매칭
        return {
          whereClause: `${columnName} = $${paramIndex}`,
          values: [value],
          paramCount: 1,
        };
      }
    } catch (error) {
      logger.error(
        `엔티티 검색 조건 구성 실패: ${tableName}.${columnName}`,
        error
      );
      return {
        whereClause: `${columnName}::text ILIKE $${paramIndex}`,
        values: [`%${value}%`],
        paramCount: 1,
      };
    }
  }

  /**
   * 참조 테이블에서 표시 컬럼 자동 감지 (entityJoinService와 동일한 우선순위)
   * 우선순위: *_name > name > label/*_label > title > referenceColumn
   */
  private async findDisplayColumnForTable(
    tableName: string,
    referenceColumn?: string
  ): Promise<string> {
    try {
      const result = await query<{ column_name: string }>(
        `SELECT column_name 
         FROM information_schema.columns 
         WHERE table_name = $1 
         AND table_schema = 'public'
         ORDER BY ordinal_position`,
        [tableName]
      );

      const allColumns = result.map((r) => r.column_name);

      // entityJoinService와 동일한 우선순위
      // 1. *_name 컬럼 (item_name, customer_name, process_name 등) - company_name 제외
      const nameColumn = allColumns.find(
        (col) => col.endsWith("_name") && col !== "company_name"
      );
      if (nameColumn) {
        return nameColumn;
      }

      // 2. name 컬럼
      if (allColumns.includes("name")) {
        return "name";
      }

      // 3. label 또는 *_label 컬럼
      const labelColumn = allColumns.find(
        (col) => col === "label" || col.endsWith("_label")
      );
      if (labelColumn) {
        return labelColumn;
      }

      // 4. title 컬럼
      if (allColumns.includes("title")) {
        return "title";
      }

      // 5. 참조 컬럼 (referenceColumn)
      if (referenceColumn && allColumns.includes(referenceColumn)) {
        return referenceColumn;
      }

      // 6. 기본값: 첫 번째 비-id 컬럼 또는 id
      return allColumns.find((col) => col !== "id") || "id";
    } catch (error) {
      logger.error(`표시 컬럼 감지 실패: ${tableName}`, error);
      return referenceColumn || "id"; // 오류 시 기본값
    }
  }

  /**
   * 불린 검색 조건 구성
   */
  private buildBooleanCondition(
    columnName: string,
    value: any,
    paramIndex: number
  ): {
    whereClause: string;
    values: any[];
    paramCount: number;
  } {
    if (value === "true" || value === true) {
      return {
        whereClause: `${columnName} = true`,
        values: [],
        paramCount: 0,
      };
    } else if (value === "false" || value === false) {
      return {
        whereClause: `${columnName} = false`,
        values: [],
        paramCount: 0,
      };
    } else {
      // 기본 검색
      return {
        whereClause: `${columnName}::text ILIKE $${paramIndex}`,
        values: [`%${value}%`],
        paramCount: 1,
      };
    }
  }

  /**
   * 컬럼 웹타입 정보 조회
   */
  private async getColumnWebTypeInfo(
    tableName: string,
    columnName: string
  ): Promise<{
    webType: string;
    inputType?: string;
    codeCategory?: string;
    referenceTable?: string;
    referenceColumn?: string;
    displayColumn?: string;
  } | null> {
    try {
      const result = await queryOne<{
        input_type: string | null;
        code_category: string | null;
        reference_table: string | null;
        reference_column: string | null;
        display_column: string | null;
      }>(
        `SELECT input_type, code_category, reference_table, reference_column, display_column
         FROM table_type_columns
         WHERE table_name = $1 AND column_name = $2 AND company_code = '*'
         LIMIT 1`,
        [tableName, columnName]
      );

      logger.info(
        `🔍 [getColumnWebTypeInfo] ${tableName}.${columnName} 조회 결과:`,
        {
          found: !!result,
          input_type: result?.input_type,
        }
      );

      if (!result) {
        logger.warn(
          `⚠️ [getColumnWebTypeInfo] 컬럼 정보 없음: ${tableName}.${columnName}`
        );
        return null;
      }

      const columnInfo = {
        webType: result.input_type || "",
        inputType: result.input_type || "",
        codeCategory: result.code_category || undefined,
        referenceTable: result.reference_table || undefined,
        referenceColumn: result.reference_column || undefined,
        displayColumn: result.display_column || undefined,
      };

      logger.info(
        `✅ [getColumnWebTypeInfo] 반환값: webType=${columnInfo.webType}, inputType=${columnInfo.inputType}`
      );
      return columnInfo;
    } catch (error) {
      logger.error(
        `컬럼 웹타입 정보 조회 실패: ${tableName}.${columnName}`,
        error
      );
      return null;
    }
  }

  /**
   * 엔티티 타입 정보 조회
   */
  private async getEntityTypeInfo(
    tableName: string,
    columnName: string
  ): Promise<{
    isEntityType: boolean;
    referenceTable?: string;
    referenceColumn?: string;
    displayColumn?: string;
  }> {
    try {
      const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);

      if (!columnInfo || columnInfo.webType !== "entity") {
        return { isEntityType: false };
      }

      return {
        isEntityType: true,
        referenceTable: columnInfo.referenceTable,
        referenceColumn: columnInfo.referenceColumn,
        displayColumn: columnInfo.displayColumn,
      };
    } catch (error) {
      logger.error(
        `엔티티 타입 정보 조회 실패: ${tableName}.${columnName}`,
        error
      );
      return { isEntityType: false };
    }
  }

  /**
   * 테이블 데이터 조회 (페이징 + 검색)
   */
  async getTableData(
    tableName: string,
    options: {
      page: number;
      size: number;
      search?: Record<string, any>;
      sortBy?: string;
      sortOrder?: string;
      companyCode?: string;
      dataFilter?: any; // 🆕 DataFilterConfig
    }
  ): Promise<{
    data: any[];
    total: number;
    page: number;
    size: number;
    totalPages: number;
  }> {
    try {
      const {
        page,
        size,
        search = {},
        sortBy,
        sortOrder = "asc",
        companyCode,
        dataFilter,
      } = options;
      const offset = (page - 1) * size;

      logger.info(`테이블 데이터 조회: ${tableName}`, options);

      // 🎯 파일 타입 컬럼 감지 (비활성화됨 - 자동 파일 컬럼 생성 방지)
      // const fileColumns = await this.getFileTypeColumns(tableName);
      const fileColumns: string[] = []; // 자동 파일 컬럼 생성 비활성화

      // WHERE 조건 구성
      let whereConditions: string[] = [];
      let searchValues: any[] = [];
      let paramIndex = 1;

      // 멀티테넌시 필터 추가 (company_code)
      if (companyCode) {
        whereConditions.push(`company_code = $${paramIndex}`);
        searchValues.push(companyCode);
        paramIndex++;
        logger.info(
          `🔒 멀티테넌시 필터 추가 (기본 조회): company_code = ${companyCode}`
        );
      }

      if (search && Object.keys(search).length > 0) {
        for (const [column, value] of Object.entries(search)) {
          if (value !== null && value !== undefined && value !== "") {
            // 🎯 추가 조인 컬럼들은 실제 테이블 컬럼이 아니므로 제외
            const additionalJoinColumns = [
              "company_code_status",
              "writer_dept_code",
            ];
            if (additionalJoinColumns.includes(column)) {
              logger.info(
                `🔍 추가 조인 컬럼 ${column} 검색 조건에서 제외 (실제 테이블 컬럼 아님)`
              );
              continue;
            }

            // 🆕 조인 테이블 컬럼 (테이블명.컬럼명)은 기본 데이터 조회에서 제외
            // Entity 조인 조회에서만 처리됨
            if (column.includes(".")) {
              logger.info(
                `🔍 조인 테이블 컬럼 ${column} 기본 조회에서 제외 (Entity 조인에서 처리)`
              );
              continue;
            }

            // 안전한 컬럼명 검증 (SQL 인젝션 방지)
            const safeColumn = column.replace(/[^a-zA-Z0-9_]/g, "");

            // 🎯 고급 필터 처리
            const condition = await this.buildAdvancedSearchCondition(
              tableName,
              safeColumn,
              value,
              paramIndex
            );

            if (condition) {
              whereConditions.push(condition.whereClause);
              searchValues.push(...condition.values);
              paramIndex += condition.paramCount;
            }
          }
        }
      }

      // 🆕 데이터 필터 적용
      if (
        dataFilter &&
        dataFilter.enabled &&
        dataFilter.filters &&
        dataFilter.filters.length > 0
      ) {
        const {
          buildDataFilterWhereClause,
        } = require("../utils/dataFilterUtil");
        const { whereClause: filterWhere, params: filterParams } =
          buildDataFilterWhereClause(dataFilter, paramIndex);

        if (filterWhere) {
          whereConditions.push(filterWhere);
          searchValues.push(...filterParams);
          paramIndex += filterParams.length;

          logger.info(`🔍 데이터 필터 적용: ${filterWhere}`);
          logger.info(`🔍 필터 파라미터:`, filterParams);
        }
      }

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

      // 안전한 테이블명 검증
      const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, "");

      // ORDER BY 조건 구성
      let orderClause = "";
      if (sortBy) {
        const safeSortBy = sortBy.replace(/[^a-zA-Z0-9_]/g, "");
        const safeSortOrder =
          sortOrder.toLowerCase() === "desc" ? "DESC" : "ASC";
        orderClause = `ORDER BY ${safeSortBy} ${safeSortOrder}`;
      } else {
        // sortBy가 없으면 created_date 컬럼이 있는 경우에만 기본 정렬 적용
        const hasCreatedDate = await query<any>(
          `SELECT 1 FROM information_schema.columns WHERE table_name = $1 AND column_name = 'created_date' LIMIT 1`,
          [safeTableName]
        );
        if (hasCreatedDate.length > 0) {
          orderClause = `ORDER BY main.created_date DESC`;
        }
      }

      // 전체 개수 조회 (main 별칭 추가 - buildWhereClause가 main. 접두사를 사용하므로 필요)
      const countQuery = `SELECT COUNT(*) as count FROM ${safeTableName} main ${whereClause}`;
      const countResult = await query<any>(countQuery, searchValues);
      const total = parseInt(countResult[0].count);

      // 데이터 조회 (main 별칭 추가)
      const dataQuery = `
        SELECT main.* FROM ${safeTableName} main 
        ${whereClause} 
        ${orderClause}
        LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
      `;

      logger.info(`🔍 실행할 SQL: ${dataQuery}`);
      logger.info(
        `🔍 파라미터: ${JSON.stringify([...searchValues, size, offset])}`
      );

      let data = await query<any>(dataQuery, [...searchValues, size, offset]);

      // 🎯 파일 컬럼이 있으면 파일 정보 보강
      if (fileColumns.length > 0) {
        data = await this.enrichFileData(data, fileColumns, safeTableName);
      }

      const totalPages = Math.ceil(total / size);

      logger.info(
        `테이블 데이터 조회 완료: ${tableName}, 총 ${total}건, ${data.length}개 반환`
      );

      return {
        data,
        total,
        page,
        size,
        totalPages,
      };
    } catch (error) {
      logger.error(`테이블 데이터 조회 오류: ${tableName}`, error);
      throw error;
    }
  }

  /**
   * 현재 사용자 정보 조회 (JWT 토큰에서)
   */
  private getCurrentUserFromRequest(req?: any): {
    userId: string;
    userName: string;
  } {
    // 실제 프로젝트에서는 req 객체에서 JWT 토큰을 파싱하여 사용자 정보를 가져올 수 있습니다
    // 현재는 기본값을 반환
    return {
      userId: "system",
      userName: "시스템 사용자",
    };
  }

  /**
   * 값을 PostgreSQL 타입에 맞게 변환
   */
  private convertValueForPostgreSQL(value: any, dataType: string): any {
    if (value === null || value === undefined || value === "") {
      return null;
    }

    const lowerDataType = dataType.toLowerCase();

    // 날짜/시간 타입 처리
    if (
      lowerDataType.includes("timestamp") ||
      lowerDataType.includes("datetime")
    ) {
      // YYYY-MM-DDTHH:mm:ss 형식을 PostgreSQL timestamp로 변환
      if (typeof value === "string") {
        try {
          const date = new Date(value);
          return date.toISOString();
        } catch {
          return null;
        }
      }
      return value;
    }

    // 날짜 타입 처리
    if (lowerDataType.includes("date")) {
      if (typeof value === "string") {
        try {
          // YYYY-MM-DD 형식 유지
          if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
            return value;
          }
          const date = new Date(value);
          return date.toISOString().split("T")[0];
        } catch {
          return null;
        }
      }
      return value;
    }

    // 시간 타입 처리
    if (lowerDataType.includes("time")) {
      if (typeof value === "string") {
        // HH:mm:ss 형식 유지
        if (/^\d{2}:\d{2}:\d{2}$/.test(value)) {
          return value;
        }
      }
      return value;
    }

    // 숫자 타입 처리
    if (
      lowerDataType.includes("integer") ||
      lowerDataType.includes("bigint") ||
      lowerDataType.includes("serial")
    ) {
      return parseInt(value) || null;
    }

    if (
      lowerDataType.includes("numeric") ||
      lowerDataType.includes("decimal") ||
      lowerDataType.includes("real") ||
      lowerDataType.includes("double")
    ) {
      return parseFloat(value) || null;
    }

    // 불린 타입 처리
    if (lowerDataType.includes("boolean")) {
      if (typeof value === "string") {
        return value.toLowerCase() === "true" || value === "1";
      }
      return Boolean(value);
    }

    // 기본적으로 문자열로 처리
    return value;
  }

  /**
   * 회사별 NOT NULL 소프트 제약조건 검증
   * table_type_columns.is_nullable = 'N'인 컬럼에 NULL/빈값이 들어오면 위반 목록을 반환한다.
   */
  async validateNotNullConstraints(
    tableName: string,
    data: Record<string, any>,
    companyCode: string
  ): Promise<string[]> {
    try {
      // 회사별 설정 우선, 없으면 공통(*) 설정 사용
      const notNullColumns = await query<{ column_name: string; column_label: string }>(
        `SELECT 
          ttc.column_name,
          COALESCE(ttc.column_label, ttc.column_name) as column_label
         FROM table_type_columns ttc
         WHERE ttc.table_name = $1
           AND ttc.is_nullable = 'N'
           AND ttc.company_code = $2`,
        [tableName, companyCode]
      );

      // 회사별 설정이 없으면 공통 설정 확인
      if (notNullColumns.length === 0 && companyCode !== "*") {
        const globalNotNull = await query<{ column_name: string; column_label: string }>(
          `SELECT 
            ttc.column_name,
            COALESCE(ttc.column_label, ttc.column_name) as column_label
           FROM table_type_columns ttc
           WHERE ttc.table_name = $1
             AND ttc.is_nullable = 'N'
             AND ttc.company_code = '*'
             AND NOT EXISTS (
               SELECT 1 FROM table_type_columns ttc2
               WHERE ttc2.table_name = ttc.table_name
                 AND ttc2.column_name = ttc.column_name
                 AND ttc2.company_code = $2
             )`,
          [tableName, companyCode]
        );
        notNullColumns.push(...globalNotNull);
      }

      if (notNullColumns.length === 0) return [];

      const violations: string[] = [];
      for (const col of notNullColumns) {
        const value = data[col.column_name];
        // NULL, undefined, 빈 문자열을 NOT NULL 위반으로 처리
        if (value === null || value === undefined || value === "") {
          violations.push(col.column_label);
        }
      }

      return violations;
    } catch (error) {
      logger.error(`NOT NULL 검증 오류: ${tableName}`, error);
      return [];
    }
  }

  /**
   * 회사별 UNIQUE 소프트 제약조건 검증
   * table_type_columns.is_unique = 'Y'인 컬럼에 중복 값이 들어오면 위반 목록을 반환한다.
   * @param excludeId 수정 시 자기 자신은 제외
   */
  async validateUniqueConstraints(
    tableName: string,
    data: Record<string, any>,
    companyCode: string,
    excludeId?: string
  ): Promise<string[]> {
    try {
      // 회사별 설정 우선, 없으면 공통(*) 설정 사용
      let uniqueColumns = await query<{ column_name: string; column_label: string }>(
        `SELECT 
          ttc.column_name,
          COALESCE(ttc.column_label, ttc.column_name) as column_label
         FROM table_type_columns ttc
         WHERE ttc.table_name = $1
           AND ttc.is_unique = 'Y'
           AND ttc.company_code = $2`,
        [tableName, companyCode]
      );

      // 회사별 설정이 없으면 공통 설정 확인
      if (uniqueColumns.length === 0 && companyCode !== "*") {
        const globalUnique = await query<{ column_name: string; column_label: string }>(
          `SELECT 
            ttc.column_name,
            COALESCE(ttc.column_label, ttc.column_name) as column_label
           FROM table_type_columns ttc
           WHERE ttc.table_name = $1
             AND ttc.is_unique = 'Y'
             AND ttc.company_code = '*'
             AND NOT EXISTS (
               SELECT 1 FROM table_type_columns ttc2
               WHERE ttc2.table_name = ttc.table_name
                 AND ttc2.column_name = ttc.column_name
                 AND ttc2.company_code = $2
             )`,
          [tableName, companyCode]
        );
        uniqueColumns = globalUnique;
      }

      if (uniqueColumns.length === 0) return [];

      const violations: string[] = [];
      for (const col of uniqueColumns) {
        const value = data[col.column_name];
        if (value === null || value === undefined || value === "") continue;

        // 해당 회사 내에서 같은 값이 이미 존재하는지 확인
        const hasCompanyCode = await query(
          `SELECT column_name FROM information_schema.columns 
           WHERE table_name = $1 AND column_name = 'company_code'`,
          [tableName]
        );

        let dupQuery: string;
        let dupParams: any[];

        if (hasCompanyCode.length > 0 && companyCode !== "*") {
          dupQuery = excludeId
            ? `SELECT 1 FROM "${tableName}" WHERE "${col.column_name}" = $1 AND company_code = $2 AND id != $3 LIMIT 1`
            : `SELECT 1 FROM "${tableName}" WHERE "${col.column_name}" = $1 AND company_code = $2 LIMIT 1`;
          dupParams = excludeId ? [value, companyCode, excludeId] : [value, companyCode];
        } else {
          dupQuery = excludeId
            ? `SELECT 1 FROM "${tableName}" WHERE "${col.column_name}" = $1 AND id != $2 LIMIT 1`
            : `SELECT 1 FROM "${tableName}" WHERE "${col.column_name}" = $1 LIMIT 1`;
          dupParams = excludeId ? [value, excludeId] : [value];
        }

        const dupResult = await query(dupQuery, dupParams);
        if (dupResult.length > 0) {
          violations.push(`${col.column_label} (${value})`);
        }
      }

      return violations;
    } catch (error) {
      logger.error(`UNIQUE 검증 오류: ${tableName}`, error);
      return [];
    }
  }

  /**
   * 테이블에 데이터 추가
   * @returns 무시된 컬럼 정보 (디버깅용)
   */
  async addTableData(
    tableName: string,
    data: Record<string, any>
  ): Promise<{ skippedColumns: string[]; savedColumns: string[]; insertedId: string | null }> {
    try {
      logger.info(`=== 테이블 데이터 추가 시작: ${tableName} ===`);
      logger.info(`추가할 데이터:`, data);

      // 테이블의 컬럼 정보 조회
      const columnInfoQuery = `
        SELECT column_name, data_type, is_nullable
        FROM information_schema.columns 
        WHERE table_name = $1
        ORDER BY ordinal_position
      `;

      const columnInfoResult = (await query(columnInfoQuery, [
        tableName,
      ])) as any[];
      const columnTypeMap = new Map<string, string>();

      columnInfoResult.forEach((col: any) => {
        columnTypeMap.set(col.column_name, col.data_type);
      });

      logger.info(`컬럼 타입 정보:`, Object.fromEntries(columnTypeMap));

      // created_date 컬럼이 있고 값이 없으면 자동으로 현재 시간 추가
      const hasCreatedDate = columnTypeMap.has("created_date");
      if (hasCreatedDate && !data.created_date) {
        data.created_date = new Date().toISOString();
        logger.info(`created_date 자동 추가: ${data.created_date}`);
      }

      // 채번 자동 적용: input_type = 'numbering'인 컬럼에 값이 비어있으면 자동 채번
      try {
        const companyCode = data.company_code || "*";
        const numberingColsResult = await query<any>(
          `SELECT DISTINCT column_name FROM table_type_columns
           WHERE table_name = $1 AND input_type = 'numbering'
           AND company_code IN ($2, '*')`,
          [tableName, companyCode]
        );

        for (const row of numberingColsResult) {
          const col = row.column_name;
          if (!data[col] || data[col] === "" || data[col] === "자동 생성됩니다") {
            const { numberingRuleService } = await import("./numberingRuleService");
            const rule = await numberingRuleService.getNumberingRuleByColumn(companyCode, tableName, col);
            if (rule) {
              const generatedCode = await numberingRuleService.allocateCode(rule.ruleId, companyCode, data);
              data[col] = generatedCode;
              logger.info(`채번 자동 적용: ${tableName}.${col} = ${generatedCode}`);
            }
          }
        }
      } catch (numErr: any) {
        logger.warn(`채번 자동 적용 중 오류 (무시됨): ${numErr.message}`);
      }

      // 🆕 테이블에 존재하는 컬럼만 필터링 (존재하지 않는 컬럼은 무시)
      const skippedColumns: string[] = [];
      const existingColumns = Object.keys(data).filter((col) => {
        const exists = columnTypeMap.has(col);
        if (!exists) {
          skippedColumns.push(col);
        }
        return exists;
      });

      // 무시된 컬럼이 있으면 경고 로그 출력
      if (skippedColumns.length > 0) {
        logger.warn(
          `⚠️ [${tableName}] 테이블에 존재하지 않는 컬럼 ${skippedColumns.length}개 무시됨: ${skippedColumns.join(", ")}`
        );
        logger.warn(
          `⚠️ [${tableName}] 무시된 컬럼 상세:`,
          skippedColumns.map((col) => ({ column: col, value: data[col] }))
        );
      }

      if (existingColumns.length === 0) {
        throw new Error(
          `저장할 유효한 컬럼이 없습니다. 테이블: ${tableName}, 전달된 컬럼: ${Object.keys(data).join(", ")}`
        );
      }

      logger.info(
        `✅ [${tableName}] 저장될 컬럼 ${existingColumns.length}개: ${existingColumns.join(", ")}`
      );

      // 컬럼명과 값을 분리하고 타입에 맞게 변환 (존재하는 컬럼만)
      const columns = existingColumns;
      const values = columns.map((columnName) => {
        const value = data[columnName];
        const dataType = columnTypeMap.get(columnName) || "text";
        const convertedValue = this.convertValueForPostgreSQL(value, dataType);
        logger.info(
          `컬럼 "${columnName}" (${dataType}): "${value}" → "${convertedValue}"`
        );
        return convertedValue;
      });

      // 동적 INSERT 쿼리 생성 (타입 캐스팅 포함)
      const placeholders = columns
        .map((col, index) => {
          const dataType = columnTypeMap.get(col) || "text";
          const lowerDataType = dataType.toLowerCase();

          // PostgreSQL에서 직접 타입 캐스팅
          if (
            lowerDataType.includes("timestamp") ||
            lowerDataType.includes("datetime")
          ) {
            return `$${index + 1}::timestamp`;
          } else if (lowerDataType.includes("date")) {
            return `$${index + 1}::date`;
          } else if (lowerDataType.includes("time")) {
            return `$${index + 1}::time`;
          } else if (
            lowerDataType.includes("integer") ||
            lowerDataType.includes("bigint") ||
            lowerDataType.includes("serial")
          ) {
            return `$${index + 1}::integer`;
          } else if (
            lowerDataType.includes("numeric") ||
            lowerDataType.includes("decimal")
          ) {
            return `$${index + 1}::numeric`;
          } else if (lowerDataType.includes("boolean")) {
            return `$${index + 1}::boolean`;
          }

          return `$${index + 1}`;
        })
        .join(", ");
      const columnNames = columns.map((col) => `"${col}"`).join(", ");

      const insertQuery = `
        INSERT INTO "${tableName}" (${columnNames})
        VALUES (${placeholders})
        RETURNING id
      `;

      logger.info(`실행할 쿼리: ${insertQuery}`);
      logger.info(`쿼리 파라미터:`, values);

      const insertResult = await query(insertQuery, values) as any[];
      const insertedId = insertResult?.[0]?.id ?? null;

      logger.info(`테이블 데이터 추가 완료: ${tableName}, id: ${insertedId}`);

      return {
        skippedColumns,
        savedColumns: existingColumns,
        insertedId,
      };
    } catch (error) {
      logger.error(`테이블 데이터 추가 오류: ${tableName}`, error);
      throw error;
    }
  }

  /**
   * 테이블 데이터 수정
   */
  async editTableData(
    tableName: string,
    originalData: Record<string, any>,
    updatedData: Record<string, any>
  ): Promise<void> {
    try {
      logger.info(`=== 테이블 데이터 수정 시작: ${tableName} ===`);
      logger.info(`원본 데이터:`, originalData);
      logger.info(`수정할 데이터:`, updatedData);

      // 테이블의 컬럼 정보 조회 (PRIMARY KEY 찾기용)
      const columnInfoQuery = `
        SELECT c.column_name, c.data_type, c.is_nullable, 
               CASE WHEN tc.constraint_type = 'PRIMARY KEY' THEN 'YES' ELSE 'NO' END as is_primary_key
        FROM information_schema.columns c
        LEFT JOIN information_schema.key_column_usage kcu ON c.column_name = kcu.column_name AND c.table_name = kcu.table_name
        LEFT JOIN information_schema.table_constraints tc ON kcu.constraint_name = tc.constraint_name AND tc.table_name = c.table_name
        WHERE c.table_name = $1
        ORDER BY c.ordinal_position
      `;

      const columnInfoResult = (await query(columnInfoQuery, [
        tableName,
      ])) as any[];
      const columnTypeMap = new Map<string, string>();
      const primaryKeys: string[] = [];

      columnInfoResult.forEach((col: any) => {
        columnTypeMap.set(col.column_name, col.data_type);
        if (col.is_primary_key === "YES") {
          primaryKeys.push(col.column_name);
        }
      });

      logger.info(`컬럼 타입 정보:`, Object.fromEntries(columnTypeMap));
      logger.info(`PRIMARY KEY 컬럼들:`, primaryKeys);

      // updated_date 컬럼이 있으면 자동으로 현재 시간 추가
      const hasUpdatedDate = columnTypeMap.has("updated_date");
      if (hasUpdatedDate && !updatedData.updated_date) {
        updatedData.updated_date = new Date().toISOString();
        logger.info(`updated_date 자동 추가: ${updatedData.updated_date}`);
      }

      // SET 절 생성 (수정할 데이터) - 먼저 생성
      // 🔧 테이블에 존재하는 컬럼만 UPDATE (가상 컬럼 제외)
      const setConditions: string[] = [];
      const setValues: any[] = [];
      let paramIndex = 1;
      const skippedColumns: string[] = [];

      Object.keys(updatedData).forEach((column) => {
        // 테이블에 존재하지 않는 컬럼은 스킵
        if (!columnTypeMap.has(column)) {
          skippedColumns.push(column);
          return;
        }

        const dataType = columnTypeMap.get(column) || "text";
        setConditions.push(
          `"${column}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}`
        );
        setValues.push(
          this.convertValueForPostgreSQL(updatedData[column], dataType)
        );
        paramIndex++;
      });

      if (skippedColumns.length > 0) {
        logger.info(
          `⚠️ 테이블에 존재하지 않는 컬럼 스킵: ${skippedColumns.join(", ")}`
        );
      }

      // WHERE 조건 생성 (PRIMARY KEY 우선, 없으면 모든 원본 데이터 사용)
      let whereConditions: string[] = [];
      let whereValues: any[] = [];

      if (primaryKeys.length > 0) {
        // PRIMARY KEY로 WHERE 조건 생성
        primaryKeys.forEach((pkColumn) => {
          if (originalData[pkColumn] !== undefined) {
            const dataType = columnTypeMap.get(pkColumn) || "text";
            whereConditions.push(
              `"${pkColumn}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}`
            );
            whereValues.push(
              this.convertValueForPostgreSQL(originalData[pkColumn], dataType)
            );
            paramIndex++;
          }
        });
      } else {
        // PRIMARY KEY가 없으면 모든 원본 데이터로 WHERE 조건 생성
        Object.keys(originalData).forEach((column) => {
          const dataType = columnTypeMap.get(column) || "text";
          whereConditions.push(
            `"${column}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}`
          );
          whereValues.push(
            this.convertValueForPostgreSQL(originalData[column], dataType)
          );
          paramIndex++;
        });
      }

      // UPDATE 쿼리 생성
      const updateQuery = `
        UPDATE "${tableName}"
        SET ${setConditions.join(", ")}
        WHERE ${whereConditions.join(" AND ")}
      `;

      const allValues = [...setValues, ...whereValues];

      logger.info(`실행할 UPDATE 쿼리: ${updateQuery}`);
      logger.info(`쿼리 파라미터:`, allValues);

      const result = await query(updateQuery, allValues);

      logger.info(`테이블 데이터 수정 완료: ${tableName}`, result);
    } catch (error) {
      logger.error(`테이블 데이터 수정 오류: ${tableName}`, error);
      throw error;
    }
  }

  /**
   * PostgreSQL 타입명 반환
   */
  private getPostgreSQLType(dataType: string): string {
    const lowerDataType = dataType.toLowerCase();

    if (
      lowerDataType.includes("timestamp") ||
      lowerDataType.includes("datetime")
    ) {
      return "timestamp";
    } else if (lowerDataType.includes("date")) {
      return "date";
    } else if (lowerDataType.includes("time")) {
      return "time";
    } else if (
      lowerDataType.includes("integer") ||
      lowerDataType.includes("bigint") ||
      lowerDataType.includes("serial")
    ) {
      return "integer";
    } else if (
      lowerDataType.includes("numeric") ||
      lowerDataType.includes("decimal")
    ) {
      return "numeric";
    } else if (lowerDataType.includes("boolean")) {
      return "boolean";
    }

    return "text"; // 기본값
  }

  /**
   * 테이블에서 데이터 삭제
   */
  async deleteTableData(
    tableName: string,
    dataToDelete: Record<string, any>[]
  ): Promise<number> {
    try {
      logger.info(`테이블 데이터 삭제: ${tableName}`, dataToDelete);

      if (!Array.isArray(dataToDelete) || dataToDelete.length === 0) {
        throw new Error("삭제할 데이터가 없습니다.");
      }

      let deletedCount = 0;

      // 테이블의 기본 키 컬럼 찾기 (정확한 식별을 위해)
      const primaryKeyQuery = `
        SELECT column_name
        FROM information_schema.table_constraints tc
        JOIN information_schema.key_column_usage kcu 
          ON tc.constraint_name = kcu.constraint_name
        WHERE tc.table_name = $1 
          AND tc.constraint_type = 'PRIMARY KEY'
        ORDER BY kcu.ordinal_position
      `;

      const primaryKeys = await query<{ column_name: string }>(
        primaryKeyQuery,
        [tableName]
      );

      if (primaryKeys.length === 0) {
        // 기본 키가 없는 경우, 모든 컬럼으로 삭제 조건 생성
        logger.warn(
          `테이블 ${tableName}에 기본 키가 없습니다. 모든 컬럼으로 삭제 조건을 생성합니다.`
        );

        for (const rowData of dataToDelete) {
          const conditions = Object.keys(rowData)
            .map((key, index) => `"${key}" = $${index + 1}`)
            .join(" AND ");

          const values = Object.values(rowData);

          const deleteQuery = `DELETE FROM "${tableName}" WHERE ${conditions}`;

          const result = await query(deleteQuery, values);
          deletedCount += Number(result);
        }
      } else {
        // 기본 키를 사용한 삭제
        const primaryKeyNames = primaryKeys.map((pk) => pk.column_name);

        for (const rowData of dataToDelete) {
          const conditions = primaryKeyNames
            .map((key, index) => `"${key}" = $${index + 1}`)
            .join(" AND ");

          const values = primaryKeyNames.map((key) => rowData[key]);

          // null 값이 있는 경우 스킵
          if (values.some((val) => val === null || val === undefined)) {
            logger.warn(`기본 키 값이 null인 행을 스킵합니다:`, rowData);
            continue;
          }

          const deleteQuery = `DELETE FROM "${tableName}" WHERE ${conditions}`;

          const result = await query(deleteQuery, values);
          deletedCount += Number(result);
        }
      }

      logger.info(
        `테이블 데이터 삭제 완료: ${tableName}, ${deletedCount}건 삭제`
      );
      return deletedCount;
    } catch (error) {
      logger.error(`테이블 데이터 삭제 오류: ${tableName}`, error);
      throw error;
    }
  }

  // ========================================
  // 🎯 Entity 조인 기능
  // ========================================

  /**
   * Entity 조인이 포함된 데이터 조회
   */
  async getTableDataWithEntityJoins(
    tableName: string,
    options: {
      page: number;
      size: number;
      search?: Record<string, any>;
      sortBy?: string;
      sortOrder?: string;
      enableEntityJoin?: boolean;
      companyCode?: string; // 멀티테넌시 필터용
      additionalJoinColumns?: Array<{
        sourceTable: string;
        sourceColumn: string;
        joinAlias: string;
      }>;
      screenEntityConfigs?: Record<string, any>; // 화면별 엔티티 설정
      dataFilter?: any; // 🆕 데이터 필터
      excludeFilter?: {
        enabled: boolean;
        referenceTable: string;
        referenceColumn: string;
        sourceColumn: string;
        filterColumn?: string;
        filterValue?: any;
      }; // 🆕 제외 필터 (다른 테이블에 이미 존재하는 데이터 제외)
      deduplication?: {
        enabled: boolean;
        groupByColumn: string;
        keepStrategy: "latest" | "earliest" | "base_price" | "current_date";
        sortColumn?: string;
      }; // 🆕 중복 제거 설정
    }
  ): Promise<EntityJoinResponse> {
    const startTime = Date.now();

    try {
      logger.info(`Entity 조인 데이터 조회 시작: ${tableName}`);

      // Entity 조인이 비활성화된 경우 기본 데이터 조회
      if (!options.enableEntityJoin) {
        const basicResult = await this.getTableData(tableName, options);
        return {
          data: basicResult.data,
          total: basicResult.total,
          page: options.page,
          size: options.size,
          totalPages: Math.ceil(basicResult.total / options.size),
        };
      }

      // Entity 조인 설정 감지 (화면별 엔티티 설정 + 회사코드 전달)
      let joinConfigs = await entityJoinService.detectEntityJoins(
        tableName,
        options.screenEntityConfigs,
        options.companyCode
      );

      logger.info(
        `🔍 detectEntityJoins 결과: ${joinConfigs.length}개 조인 설정`
      );
      if (joinConfigs.length > 0) {
        joinConfigs.forEach((config, index) => {
          logger.info(
            `  조인 ${index + 1}: ${config.sourceColumn} -> ${config.referenceTable} AS ${config.aliasColumn}`
          );
        });
      }

      // 추가 조인 컬럼 정보가 있으면 조인 설정에 추가
      if (
        options.additionalJoinColumns &&
        options.additionalJoinColumns.length > 0
      ) {
        logger.info(
          `추가 조인 컬럼 처리: ${options.additionalJoinColumns.length}개`
        );
        logger.info(
          "📋 전달받은 additionalJoinColumns:",
          options.additionalJoinColumns
        );

        for (const additionalColumn of options.additionalJoinColumns) {
          // 🔍 1차: sourceColumn을 기준으로 기존 조인 설정 찾기
          let baseJoinConfig = joinConfigs.find(
            (config) => config.sourceColumn === additionalColumn.sourceColumn
          );

          // 🔍 2차: referenceTable을 기준으로 찾기 (프론트엔드가 customer_mng.customer_name 같은 형식을 요청할 때)
          // 실제 소스 컬럼이 partner_id인데 프론트엔드가 customer_id로 추론하는 경우 대응
          if (!baseJoinConfig && (additionalColumn as any).referenceTable) {
            baseJoinConfig = joinConfigs.find(
              (config) =>
                config.referenceTable ===
                (additionalColumn as any).referenceTable
            );
            if (baseJoinConfig) {
              logger.info(
                `🔄 referenceTable로 조인 설정 찾음: ${(additionalColumn as any).referenceTable} → ${baseJoinConfig.sourceColumn}`
              );
            }
          }

          if (baseJoinConfig) {
            // joinAlias에서 실제 컬럼명 추출
            const sourceColumn = baseJoinConfig.sourceColumn; // 실제 소스 컬럼 (예: partner_id)
            const originalJoinAlias = additionalColumn.joinAlias; // 프론트엔드가 보낸 별칭 (예: customer_id_customer_name)

            // 🔄 프론트엔드가 잘못된 소스 컬럼으로 추론한 경우 처리
            // customer_id_customer_name → customer_name 추출 (customer_id_ 부분 제거)
            // 또는 partner_id_customer_name → customer_name 추출 (partner_id_ 부분 제거)
            let actualColumnName: string;

            // 프론트엔드가 보낸 joinAlias에서 실제 컬럼명 추출
            const frontendSourceColumn = additionalColumn.sourceColumn; // 프론트엔드가 추론한 소스 컬럼 (customer_id)
            if (originalJoinAlias.startsWith(`${frontendSourceColumn}_`)) {
              // 프론트엔드가 추론한 소스 컬럼으로 시작하면 그 부분 제거
              actualColumnName = originalJoinAlias.replace(
                `${frontendSourceColumn}_`,
                ""
              );
            } else if (originalJoinAlias.startsWith(`${sourceColumn}_`)) {
              // 실제 소스 컬럼으로 시작하면 그 부분 제거
              actualColumnName = originalJoinAlias.replace(
                `${sourceColumn}_`,
                ""
              );
            } else {
              // 어느 것도 아니면 원본 사용
              actualColumnName = originalJoinAlias;
            }

            // 🆕 올바른 joinAlias 재생성 (실제 소스 컬럼 기반)
            const correctedJoinAlias = `${sourceColumn}_${actualColumnName}`;

            logger.info(`🔍 조인 컬럼 상세 분석:`, {
              sourceColumn,
              frontendSourceColumn,
              originalJoinAlias,
              correctedJoinAlias,
              actualColumnName,
              referenceTable: (additionalColumn as any).referenceTable,
            });

            // 🚨 기본 Entity 조인과 중복되지 않도록 체크
            const isBasicEntityJoin =
              correctedJoinAlias === `${sourceColumn}_name`;

            if (isBasicEntityJoin) {
              logger.info(
                `⚠️ 기본 Entity 조인과 중복: ${correctedJoinAlias} - 건너뜀`
              );
              continue; // 기본 Entity 조인과 중복되면 추가하지 않음
            }

            // 🆕 같은 sourceColumn + referenceTable 조합의 기존 config가 있으면 displayColumns에 병합
            const existingConfig = joinConfigs.find(
              (config) =>
                config.sourceColumn === sourceColumn &&
                config.referenceTable === ((additionalColumn as any).referenceTable || baseJoinConfig.referenceTable)
            );

            if (existingConfig) {
              // 기존 config에 display column 추가 (중복 방지)
              if (!existingConfig.displayColumns?.includes(actualColumnName)) {
                existingConfig.displayColumns = existingConfig.displayColumns || [];
                existingConfig.displayColumns.push(actualColumnName);
                logger.info(
                  `🔄 기존 조인 설정에 컬럼 병합: ${existingConfig.aliasColumn} ← ${actualColumnName} (총 ${existingConfig.displayColumns.length}개)`
                );
              }
            } else {
              // 새 조인 설정 생성
              const additionalJoinConfig: EntityJoinConfig = {
                sourceTable: tableName,
                sourceColumn: sourceColumn, // 실제 소스 컬럼 (partner_id)
                referenceTable:
                  (additionalColumn as any).referenceTable ||
                  baseJoinConfig.referenceTable, // 참조 테이블 (customer_mng)
                referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (customer_code)
                displayColumns: [actualColumnName], // 표시할 컬럼들 (customer_name)
                displayColumn: actualColumnName, // 하위 호환성
                aliasColumn: correctedJoinAlias, // 수정된 별칭 (partner_id_customer_name)
                separator: " - ", // 기본 구분자
              };

              joinConfigs.push(additionalJoinConfig);
              logger.info(
                `✅ 추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn} -> ${actualColumnName}`
              );
              logger.info(`🔍 추가된 조인 설정 상세:`, {
                sourceTable: additionalJoinConfig.sourceTable,
                sourceColumn: additionalJoinConfig.sourceColumn,
                referenceTable: additionalJoinConfig.referenceTable,
                displayColumns: additionalJoinConfig.displayColumns,
                aliasColumn: additionalJoinConfig.aliasColumn,
              });
            }
          }
        }
      }

      // 최종 조인 설정 배열 로깅
      logger.info(`🎯 최종 joinConfigs 배열 (${joinConfigs.length}개):`);
      joinConfigs.forEach((config, index) => {
        logger.info(
          `  ${index + 1}. ${config.sourceColumn} -> ${config.referenceTable} AS ${config.aliasColumn}`,
          {
            displayColumns: config.displayColumns,
            displayColumn: config.displayColumn,
          }
        );
      });

      if (joinConfigs.length === 0) {
        logger.info(`Entity 조인 설정이 없음: ${tableName}`);
        const basicResult = await this.getTableData(tableName, options);
        return {
          data: basicResult.data,
          total: basicResult.total,
          page: options.page,
          size: options.size,
          totalPages: Math.ceil(basicResult.total / options.size),
        };
      }

      // 조인 전략 결정 (테이블 크기 기반)
      // 🚨 additionalJoinColumns가 있는 경우 강제로 full_join 사용 (캐시 안정성 보장)
      let strategy: "full_join" | "cache_lookup" | "hybrid";

      if (
        options.additionalJoinColumns &&
        options.additionalJoinColumns.length > 0
      ) {
        strategy = "full_join";
        console.log(
          `🔧 additionalJoinColumns 감지: 강제로 full_join 전략 사용 (${options.additionalJoinColumns.length}개 추가 조인)`
        );
      } else {
        strategy = await entityJoinService.determineJoinStrategy(joinConfigs);
      }

      console.log(
        `🎯 선택된 조인 전략: ${strategy} (${joinConfigs.length}개 Entity 조인)`
      );

      // 테이블 컬럼 정보 조회
      const columns = await this.getTableColumns(tableName);
      const selectColumns = columns.data.map((col: any) => col.column_name);

      // WHERE 절 구성
      let whereClause = await this.buildWhereClause(tableName, options.search);

      // 멀티테넌시 필터 추가 (company_code)
      if (options.companyCode) {
        const companyFilter = `main.company_code = '${options.companyCode.replace(/'/g, "''")}'`;
        whereClause = whereClause
          ? `${whereClause} AND ${companyFilter}`
          : companyFilter;
        logger.info(
          `🔒 멀티테넌시 필터 추가 (Entity 조인): company_code = ${options.companyCode}`
        );
      }

      // 🆕 데이터 필터 적용 (Entity 조인) - 파라미터 바인딩 없이 직접 값 삽입
      if (
        options.dataFilter &&
        options.dataFilter.enabled &&
        options.dataFilter.filters &&
        options.dataFilter.filters.length > 0
      ) {
        const filterConditions: string[] = [];

        for (const filter of options.dataFilter.filters) {
          const { columnName, operator, value } = filter;

          if (!columnName || value === undefined || value === null) {
            continue;
          }

          const safeColumn = `main."${columnName}"`;

          switch (operator) {
            case "equals":
              filterConditions.push(
                `${safeColumn} = '${String(value).replace(/'/g, "''")}'`
              );
              break;
            case "not_equals":
              filterConditions.push(
                `${safeColumn} != '${String(value).replace(/'/g, "''")}'`
              );
              break;
            case "in": {
              const inArr = Array.isArray(value) ? value : value != null && value !== "" ? [String(value)] : [];
              if (inArr.length > 0) {
                const values = inArr
                  .map((v) => `'${String(v).replace(/'/g, "''")}'`)
                  .join(", ");
                filterConditions.push(`${safeColumn} IN (${values})`);
              }
              break;
            }
            case "not_in": {
              const notInArr = Array.isArray(value) ? value : value != null && value !== "" ? [String(value)] : [];
              if (notInArr.length > 0) {
                const values = notInArr
                  .map((v) => `'${String(v).replace(/'/g, "''")}'`)
                  .join(", ");
                filterConditions.push(`${safeColumn} NOT IN (${values})`);
              }
              break;
            }
            case "contains":
              filterConditions.push(
                `${safeColumn} LIKE '%${String(value).replace(/'/g, "''")}%'`
              );
              break;
            case "starts_with":
              filterConditions.push(
                `${safeColumn} LIKE '${String(value).replace(/'/g, "''")}%'`
              );
              break;
            case "ends_with":
              filterConditions.push(
                `${safeColumn} LIKE '%${String(value).replace(/'/g, "''")}'`
              );
              break;
            case "is_null":
              filterConditions.push(`${safeColumn} IS NULL`);
              break;
            case "is_not_null":
              filterConditions.push(`${safeColumn} IS NOT NULL`);
              break;
          }
        }

        if (filterConditions.length > 0) {
          const logicalOperator =
            options.dataFilter.matchType === "any" ? " OR " : " AND ";
          const filterWhere = `(${filterConditions.join(logicalOperator)})`;

          whereClause = whereClause
            ? `${whereClause} AND ${filterWhere}`
            : filterWhere;

          logger.info(`🔍 데이터 필터 적용 (Entity 조인): ${filterWhere}`);
        }
      }

      // 🆕 제외 필터 적용 (다른 테이블에 이미 존재하는 데이터 제외)
      if (options.excludeFilter && options.excludeFilter.enabled) {
        const {
          referenceTable,
          referenceColumn,
          sourceColumn,
          filterColumn,
          filterValue,
        } = options.excludeFilter;

        if (referenceTable && referenceColumn && sourceColumn) {
          // 서브쿼리로 이미 존재하는 데이터 제외
          let excludeSubquery = `main."${sourceColumn}" NOT IN (
            SELECT "${referenceColumn}" FROM "${referenceTable}"
            WHERE "${referenceColumn}" IS NOT NULL`;

          // 추가 필터 조건이 있으면 적용 (예: 특정 거래처의 품목만 제외)
          if (
            filterColumn &&
            filterValue !== undefined &&
            filterValue !== null
          ) {
            excludeSubquery += ` AND "${filterColumn}" = '${String(filterValue).replace(/'/g, "''")}'`;
          }

          excludeSubquery += ")";

          whereClause = whereClause
            ? `${whereClause} AND ${excludeSubquery}`
            : excludeSubquery;

          logger.info(`🚫 제외 필터 적용 (Entity 조인):`, {
            referenceTable,
            referenceColumn,
            sourceColumn,
            filterColumn,
            filterValue,
            excludeSubquery,
          });
        }
      }

      // ORDER BY 절 구성
      // sortBy가 메인 테이블 컬럼이면 main. 접두사, 조인 별칭이면 접두사 없이 사용
      const hasCreatedDateColumn = selectColumns.includes("created_date");
      const orderBy = options.sortBy
        ? selectColumns.includes(options.sortBy)
          ? `main."${options.sortBy}" ${options.sortOrder === "desc" ? "DESC" : "ASC"}`
          : `"${options.sortBy}" ${options.sortOrder === "desc" ? "DESC" : "ASC"}`
        : hasCreatedDateColumn
          ? `main."created_date" DESC`
          : "";

      // 페이징 계산
      const offset = (options.page - 1) * options.size;

      if (strategy === "full_join") {
        // SQL JOIN 방식
        return await this.executeJoinQuery(
          tableName,
          joinConfigs,
          selectColumns,
          whereClause,
          orderBy,
          options.size,
          offset,
          startTime
        );
      } else if (strategy === "cache_lookup") {
        // 캐시 룩업 방식
        return await this.executeCachedLookup(
          tableName,
          joinConfigs,
          options,
          startTime
        );
      } else {
        // 하이브리드 방식: 일부는 조인, 일부는 캐시
        return await this.executeHybridJoin(
          tableName,
          joinConfigs,
          selectColumns,
          whereClause,
          orderBy,
          options.size,
          offset,
          startTime
        );
      }
    } catch (error) {
      logger.error(`Entity 조인 데이터 조회 실패: ${tableName}`, error);
      throw error;
    }
  }

  /**
   * SQL JOIN 방식으로 데이터 조회
   */
  private async executeJoinQuery(
    tableName: string,
    joinConfigs: EntityJoinConfig[],
    selectColumns: string[],
    whereClause: string,
    orderBy: string,
    limit: number,
    offset: number,
    startTime: number
  ): Promise<EntityJoinResponse> {
    try {
      // 🆕 참조 테이블별 전체 컬럼 목록 미리 조회
      const referenceTableColumns = new Map<string, string[]>();
      const uniqueRefTables = new Set(
        joinConfigs
          .filter((c) => c.referenceTable !== "category_values") // 카테고리는 제외
          .map((c) => `${c.referenceTable}:${c.sourceColumn}`)
      );

      for (const key of uniqueRefTables) {
        const refTable = key.split(":")[0];
        if (!referenceTableColumns.has(key)) {
          const cols = await query<{ column_name: string }>(
            `SELECT column_name FROM information_schema.columns 
             WHERE table_name = $1 AND table_schema = 'public' 
             ORDER BY ordinal_position`,
            [refTable]
          );
          referenceTableColumns.set(key, cols.map((c) => c.column_name));
          logger.info(`🔍 참조 테이블 컬럼 조회: ${refTable} → ${cols.length}개`);
        }
      }

      // 데이터 조회 쿼리
      const dataQuery = entityJoinService.buildJoinQuery(
        tableName,
        joinConfigs,
        selectColumns,
        whereClause,
        orderBy,
        limit,
        offset,
        undefined,
        referenceTableColumns // 🆕 참조 테이블 전체 컬럼 전달
      ).query;

      // 카운트 쿼리
      const countQuery = entityJoinService.buildCountQuery(
        tableName,
        joinConfigs,
        whereClause
      );

      // ⚠️ SQL 쿼리 로깅 (디버깅용)
      logger.info(`🔍 [executeJoinQuery] 실행할 SQL:\n${dataQuery}`);

      // 병렬 실행
      const [dataResult, countResult] = await Promise.all([
        query(dataQuery),
        query(countQuery),
      ]);

      logger.info(
        `✅ [executeJoinQuery] 조회 완료: ${dataResult?.length}개 행`
      );

      let data = Array.isArray(dataResult) ? dataResult : [];
      const total =
        Array.isArray(countResult) && countResult.length > 0
          ? Number((countResult[0] as any).total)
          : 0;

      // 콤마 구분 다중값 후처리 (겸직 부서 등)
      data = await entityJoinService.resolveCommaValues(data, joinConfigs);

      const queryTime = Date.now() - startTime;

      return {
        data,
        total,
        page: Math.floor(offset / limit) + 1,
        size: limit,
        totalPages: Math.ceil(total / limit),
        entityJoinInfo: {
          joinConfigs,
          strategy: "full_join",
          performance: {
            queryTime,
          },
        },
      };
    } catch (error) {
      logger.error("SQL JOIN 쿼리 실행 실패", error);
      throw error;
    }
  }

  /**
   * 캐시 룩업 방식으로 데이터 조회
   */
  private async executeCachedLookup(
    tableName: string,
    joinConfigs: EntityJoinConfig[],
    options: {
      page: number;
      size: number;
      search?: Record<string, any>;
      sortBy?: string;
      sortOrder?: string;
      companyCode?: string;
    },
    startTime: number
  ): Promise<EntityJoinResponse> {
    try {
      // 캐시 데이터 미리 로드
      for (const config of joinConfigs) {
        const displayCol =
          config.displayColumn ||
          config.displayColumns?.[0] ||
          config.referenceColumn;
        logger.info(
          `🔍 캐시 로드 - ${config.referenceTable}: keyCol=${config.referenceColumn}, displayCol=${displayCol}`
        );

        await referenceCacheService.getCachedReference(
          config.referenceTable,
          config.referenceColumn,
          displayCol
        );
      }

      // Entity 조인 컬럼 검색이 있는지 확인 (기본 조인 + 추가 조인 컬럼 모두 포함)
      // 🔧 sourceColumn도 포함: search={"order_no":"..."} 형태도 Entity 검색으로 인식
      const allEntityColumns = [
        ...joinConfigs.map((config) => config.aliasColumn),
        ...joinConfigs.map((config) => config.sourceColumn), // 🔧 소스 컬럼도 포함
        // 추가 조인 컬럼들도 포함 (writer_dept_code, company_code_status 등)
        ...joinConfigs.flatMap((config) => {
          const additionalColumns = [];
          // writer -> writer_dept_code 패턴
          if (config.sourceColumn === "writer") {
            additionalColumns.push("writer_dept_code");
          }
          // company_code -> company_code_status 패턴
          if (config.sourceColumn === "company_code") {
            additionalColumns.push("company_code_status");
          }
          return additionalColumns;
        }),
      ];

      // 🆕 테이블명.컬럼명 형식도 Entity 검색으로 인식
      const hasJoinTableSearch =
        options.search &&
        Object.keys(options.search).some((key) => key.includes("."));

      const hasEntitySearch =
        options.search &&
        (Object.keys(options.search).some((key) =>
          allEntityColumns.includes(key)
        ) ||
          hasJoinTableSearch);

      if (hasEntitySearch) {
        const entitySearchKeys = options.search
          ? Object.keys(options.search).filter(
              (key) => allEntityColumns.includes(key) || key.includes(".")
            )
          : [];
        logger.info(
          `🔍 Entity 조인 컬럼 검색 감지: ${entitySearchKeys.join(", ")}`
        );
      }

      let basicResult;

      if (hasEntitySearch) {
        // Entity 조인 컬럼으로 검색하는 경우 SQL JOIN 방식 사용
        logger.info("🔍 Entity 조인 컬럼 검색 감지, SQL JOIN 방식으로 전환");

        try {
          // 테이블 컬럼 정보 조회
          const columns = await this.getTableColumns(tableName);
          const selectColumns = columns.data.map((col: any) => col.column_name);

          // Entity 조인 컬럼 검색을 위한 WHERE 절 구성
          const whereConditions: string[] = [];
          const entitySearchColumns: string[] = [];

          // Entity 조인 쿼리 생성하여 별칭 매핑 얻기
          const hasCreatedDateForSearch = selectColumns.includes("created_date");
          const joinQueryResult = entityJoinService.buildJoinQuery(
            tableName,
            joinConfigs,
            selectColumns,
            "", // WHERE 절은 나중에 추가
            options.sortBy
              ? selectColumns.includes(options.sortBy)
                ? `main."${options.sortBy}" ${options.sortOrder || "ASC"}`
                : `"${options.sortBy}" ${options.sortOrder || "ASC"}`
              : hasCreatedDateForSearch
                ? `main."created_date" DESC`
                : undefined,
            options.size,
            (options.page - 1) * options.size
          );

          const aliasMap = joinQueryResult.aliasMap;
          logger.info(
            `🔧 [검색] 별칭 매핑 사용: ${Array.from(aliasMap.entries())
              .map(([table, alias]) => `${table}→${alias}`)
              .join(", ")}`
          );

          if (options.search) {
            for (const [key, value] of Object.entries(options.search)) {
              // 검색값 및 operator 추출 (객체 형태일 수 있음)
              let searchValue = value;
              let operator = "contains"; // 기본값: 부분 일치
              if (
                typeof value === "object" &&
                value !== null &&
                "value" in value
              ) {
                searchValue = value.value;
                operator = (value as any).operator || "contains";
              }

              // 빈 값이면 스킵
              if (
                searchValue === "__ALL__" ||
                searchValue === "" ||
                searchValue === null ||
                searchValue === undefined
              ) {
                continue;
              }

              const safeValue = String(searchValue).replace(/'/g, "''");

              // 🆕 테이블명.컬럼명 형식 처리 (예: item_info.item_name)
              if (key.includes(".")) {
                const [refTable, refColumn] = key.split(".");

                // aliasMap에서 별칭 찾기 (테이블명:소스컬럼 형식)
                let foundAlias: string | undefined;
                for (const [aliasKey, alias] of aliasMap.entries()) {
                  if (aliasKey.startsWith(`${refTable}:`)) {
                    foundAlias = alias;
                    break;
                  }
                }

                if (foundAlias) {
                  whereConditions.push(
                    `${foundAlias}.${refColumn}::text ILIKE '%${safeValue}%'`
                  );
                  entitySearchColumns.push(`${key} (${refTable}.${refColumn})`);
                  logger.info(
                    `🎯 조인 테이블 검색: ${key} → ${refTable}.${refColumn} LIKE '%${safeValue}%' (별칭: ${foundAlias})`
                  );
                } else {
                  logger.warn(
                    `⚠️ 조인 테이블 검색 실패: ${key} - 별칭을 찾을 수 없음`
                  );
                }
                continue;
              }

              const joinConfig = joinConfigs.find(
                (config) => config.aliasColumn === key
              );

              if (joinConfig) {
                // 기본 Entity 조인 컬럼인 경우: 조인된 테이블의 표시 컬럼에서 검색
                const aliasKey = `${joinConfig.referenceTable}:${joinConfig.sourceColumn}`;
                const alias = aliasMap.get(aliasKey);

                // 🔧 파이프로 구분된 다중 선택값 처리
                if (safeValue.includes("|")) {
                  const multiValues = safeValue
                    .split("|")
                    .filter((v: string) => v.trim() !== "");
                  if (multiValues.length > 0) {
                    const inClause = multiValues
                      .map((v: string) => `'${v}'`)
                      .join(", ");
                    whereConditions.push(
                      `${alias}.${joinConfig.displayColumn}::text IN (${inClause})`
                    );
                    entitySearchColumns.push(
                      `${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})`
                    );
                    logger.info(
                      `🎯 Entity 조인 다중선택 검색: ${key} → ${joinConfig.referenceTable}.${joinConfig.displayColumn} IN (${multiValues.join(", ")}) (별칭: ${alias})`
                    );
                  }
                } else if (operator === "equals") {
                  // 🔧 equals 연산자: 메인 테이블의 FK 컬럼에서 직접 매칭 (연결 필터용)
                  whereConditions.push(
                    `main.${joinConfig.sourceColumn}::text = '${safeValue}'`
                  );
                  entitySearchColumns.push(
                    `${key} (main.${joinConfig.sourceColumn})`
                  );
                  logger.info(
                    `🎯 Entity 조인 직접 FK 매칭: ${key} → main.${joinConfig.sourceColumn} = '${safeValue}'`
                  );
                } else {
                  // 기본: 부분 일치 (ILIKE)
                  whereConditions.push(
                    `${alias}.${joinConfig.displayColumn} ILIKE '%${safeValue}%'`
                  );
                  entitySearchColumns.push(
                    `${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})`
                  );
                  logger.info(
                    `🎯 Entity 조인 검색: ${key} → ${joinConfig.referenceTable}.${joinConfig.displayColumn} LIKE '%${safeValue}%' (별칭: ${alias})`
                  );
                }
              } else if (key === "writer_dept_code") {
                // writer_dept_code: user_info.dept_code에서 검색
                const userAliasKey = Array.from(aliasMap.keys()).find((k) =>
                  k.startsWith("user_info:")
                );
                const userAlias = userAliasKey
                  ? aliasMap.get(userAliasKey)
                  : undefined;
                if (userAlias) {
                  whereConditions.push(
                    `${userAlias}.dept_code ILIKE '%${safeValue}%'`
                  );
                  entitySearchColumns.push(`${key} (user_info.dept_code)`);
                  logger.info(
                    `🎯 추가 Entity 조인 검색: ${key} → user_info.dept_code LIKE '%${safeValue}%' (별칭: ${userAlias})`
                  );
                }
              } else if (key === "company_code_status") {
                // company_code_status: company_info.status에서 검색
                const companyAliasKey = Array.from(aliasMap.keys()).find((k) =>
                  k.startsWith("company_info:")
                );
                const companyAlias = companyAliasKey
                  ? aliasMap.get(companyAliasKey)
                  : undefined;
                if (companyAlias) {
                  whereConditions.push(
                    `${companyAlias}.status ILIKE '%${safeValue}%'`
                  );
                  entitySearchColumns.push(`${key} (company_info.status)`);
                  logger.info(
                    `🎯 추가 Entity 조인 검색: ${key} → company_info.status LIKE '%${safeValue}%' (별칭: ${companyAlias})`
                  );
                }
              } else {
                // 일반 컬럼인 경우: 메인 테이블에서 검색
                // 🔧 파이프로 구분된 다중 선택값 처리
                if (safeValue.includes("|")) {
                  const multiValues = safeValue
                    .split("|")
                    .filter((v: string) => v.trim() !== "");
                  if (multiValues.length > 0) {
                    const inClause = multiValues
                      .map((v: string) => `'${v}'`)
                      .join(", ");
                    whereConditions.push(`main.${key}::text IN (${inClause})`);
                    logger.info(
                      `🔍 다중선택 컬럼 검색: ${key} → main.${key} IN (${multiValues.join(", ")})`
                    );
                  }
                } else if (operator === "equals") {
                  // 🔧 equals 연산자: 정확히 일치
                  whereConditions.push(`main.${key}::text = '${safeValue}'`);
                  logger.info(
                    `🔍 정확히 일치 검색: ${key} → main.${key} = '${safeValue}'`
                  );
                } else {
                  // 기본: 부분 일치 (ILIKE)
                  whereConditions.push(`main.${key} ILIKE '%${safeValue}%'`);
                  logger.info(
                    `🔍 일반 컬럼 검색: ${key} → main.${key} LIKE '%${safeValue}%'`
                  );
                }
              }
            }
          }

          const whereClause = whereConditions.join(" AND ");
          const hasCreatedDateForOrder = selectColumns.includes("created_date");
          const orderBy = options.sortBy
            ? selectColumns.includes(options.sortBy)
              ? `main."${options.sortBy}" ${options.sortOrder === "desc" ? "DESC" : "ASC"}`
              : `"${options.sortBy}" ${options.sortOrder === "desc" ? "DESC" : "ASC"}`
            : hasCreatedDateForOrder
              ? `main."created_date" DESC`
              : "";

          // 페이징 계산
          const offset = (options.page - 1) * options.size;

          // SQL JOIN 쿼리 실행
          const joinResult = await this.executeJoinQuery(
            tableName,
            joinConfigs,
            selectColumns,
            whereClause,
            orderBy,
            options.size,
            offset,
            startTime
          );

          return joinResult;
        } catch (joinError) {
          logger.error(
            `Entity 조인 검색 실패, 캐시 방식으로 폴백: ${tableName}`,
            joinError
          );

          // Entity 조인 검색 실패 시 Entity 조인 컬럼을 제외한 검색 조건으로 캐시 방식 사용
          const fallbackOptions = { ...options };
          if (options.search) {
            const filteredSearch: Record<string, any> = {};

            // Entity 조인 컬럼을 제외한 검색 조건만 유지
            for (const [key, value] of Object.entries(options.search)) {
              const isEntityColumn = joinConfigs.some(
                (config) => config.aliasColumn === key
              );
              if (!isEntityColumn) {
                filteredSearch[key] = value;
              }
            }

            fallbackOptions.search = filteredSearch;
            logger.info(
              `🔄 Entity 조인 에러 시 검색 조건 필터링: ${Object.keys(filteredSearch).join(", ")}`
            );
          }

          basicResult = await this.getTableData(tableName, {
            ...fallbackOptions,
            companyCode: options.companyCode,
          });
        }
      } else {
        // Entity 조인 컬럼 검색이 없는 경우 기존 캐시 방식 사용
        basicResult = await this.getTableData(tableName, {
          ...options,
          companyCode: options.companyCode,
        });
      }

      // Entity 값들을 캐시에서 룩업하여 변환
      const enhancedData = basicResult.data.map((row: any) => {
        const enhancedRow = { ...row };

        for (const config of joinConfigs) {
          const sourceValue = row[config.sourceColumn];
          if (sourceValue) {
            const lookupValue = referenceCacheService.getLookupValue(
              config.referenceTable,
              config.referenceColumn,
              config.displayColumn || config.displayColumns[0],
              String(sourceValue)
            );

            // null이나 undefined인 경우 빈 문자열로 설정
            enhancedRow[config.aliasColumn] = lookupValue || "";
          } else {
            // sourceValue가 없는 경우도 빈 문자열로 설정
            enhancedRow[config.aliasColumn] = "";
          }
        }

        return enhancedRow;
      });

      const queryTime = Date.now() - startTime;
      const cacheHitRate = referenceCacheService.getOverallCacheHitRate();

      return {
        data: enhancedData,
        total: basicResult.total,
        page: options.page,
        size: options.size,
        totalPages: Math.ceil(basicResult.total / options.size),
        entityJoinInfo: {
          joinConfigs,
          strategy: "cache_lookup",
          performance: {
            queryTime,
            cacheHitRate,
          },
        },
      };
    } catch (error) {
      logger.error("캐시 룩업 실행 실패", error);
      throw error;
    }
  }

  /**
   * WHERE 절 구성 (고급 검색 지원)
   */
  private async buildWhereClause(
    tableName: string,
    search?: Record<string, any>
  ): Promise<string> {
    if (!search || Object.keys(search).length === 0) {
      return "";
    }

    const conditions: string[] = [];

    for (const [columnName, value] of Object.entries(search)) {
      if (
        value === undefined ||
        value === null ||
        value === "" ||
        value === "__ALL__"
      ) {
        continue;
      }

      try {
        // 🆕 조인 테이블 컬럼 검색 처리 (예: item_info.item_name)
        if (columnName.includes(".")) {
          const [refTable, refColumn] = columnName.split(".");

          // 검색값 추출
          let searchValue = value;
          if (typeof value === "object" && value !== null && "value" in value) {
            searchValue = value.value;
          }

          if (
            searchValue === "__ALL__" ||
            searchValue === "" ||
            searchValue === null
          ) {
            continue;
          }

          // 🔍 table_type_columns에서 해당 엔티티 설정 찾기
          // 예: item_info 테이블을 참조하는 컬럼 찾기 (item_code → item_info)
          const entityColumnResult = await query<{
            column_name: string;
            reference_table: string;
            reference_column: string;
          }>(
            `SELECT DISTINCT ON (column_name) column_name, reference_table, reference_column
             FROM table_type_columns
             WHERE table_name = $1 
             AND input_type = 'entity'
             AND reference_table = $2
             ORDER BY column_name, CASE WHEN company_code = '*' THEN 1 ELSE 0 END
             LIMIT 1`,
            [tableName, refTable]
          );

          if (entityColumnResult.length > 0) {
            // 조인 별칭 생성 (entityJoinService.ts와 동일한 패턴: 테이블명 앞 3글자)
            const joinAlias = refTable.substring(0, 3);

            // 조인 테이블 컬럼으로 검색 조건 생성
            const safeValue = String(searchValue).replace(/'/g, "''");
            const condition = `${joinAlias}.${refColumn}::text ILIKE '%${safeValue}%'`;

            logger.info(`🔍 조인 테이블 검색 조건: ${condition}`);
            conditions.push(condition);
          } else {
            logger.warn(
              `⚠️ 조인 테이블 검색 실패: ${columnName} - 엔티티 설정을 찾을 수 없음`
            );
          }

          continue;
        }

        // 고급 검색 조건 구성
        const searchCondition = await this.buildAdvancedSearchCondition(
          tableName,
          columnName,
          value,
          1 // paramIndex는 실제로는 사용되지 않음 (직접 값 삽입)
        );

        if (searchCondition) {
          // SQL 인젝션 방지를 위해 값을 직접 삽입하는 대신 안전한 방식 사용
          let condition = searchCondition.whereClause;

          // 파라미터를 실제 값으로 치환 (안전한 방식)
          searchCondition.values.forEach((val, index) => {
            const paramPlaceholder = `$${index + 1}`;
            if (typeof val === "string") {
              condition = condition.replace(
                paramPlaceholder,
                `'${val.replace(/'/g, "''")}'`
              );
            } else if (typeof val === "number") {
              condition = condition.replace(paramPlaceholder, val.toString());
            } else {
              condition = condition.replace(
                paramPlaceholder,
                `'${String(val).replace(/'/g, "''")}'`
              );
            }
          });

          // main. 접두사 추가 (조인 쿼리용)
          // 🔧 이미 접두사(. 앞)가 있는 경우는 교체하지 않음 (ref.column, main.column 등)
          // Negative lookbehind (?<!\.) 사용: 앞에 .이 없는 경우만 매칭
          condition = condition.replace(
            new RegExp(`(?<!\\.)\\b${columnName}\\b`, "g"),
            `main.${columnName}`
          );
          conditions.push(condition);
        }
      } catch (error) {
        logger.warn(`검색 조건 구성 실패: ${columnName}`, error);
        // 폴백: 기본 문자열 검색
        if (typeof value === "string") {
          conditions.push(
            `main.${columnName}::text ILIKE '%${value.replace(/'/g, "''")}%'`
          );
        } else {
          conditions.push(
            `main.${columnName} = '${String(value).replace(/'/g, "''")}'`
          );
        }
      }
    }

    return conditions.length > 0 ? conditions.join(" AND ") : "";
  }

  /**
   * 테이블의 컬럼 정보 조회
   */
  async getTableColumns(tableName: string): Promise<{
    data: Array<{ column_name: string; data_type: string }>;
  }> {
    try {
      const columns = await query<{
        column_name: string;
        data_type: string;
      }>(
        `SELECT column_name, data_type
         FROM information_schema.columns
         WHERE table_name = $1
         ORDER BY ordinal_position`,
        [tableName]
      );

      return { data: columns };
    } catch (error) {
      logger.error(`테이블 컬럼 조회 실패: ${tableName}`, error);
      throw new Error(
        `테이블 컬럼 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    }
  }

  /**
   * 참조 테이블의 표시 컬럼 목록 조회
   */
  async getReferenceTableColumns(tableName: string, companyCode?: string): Promise<
    Array<{
      columnName: string;
      displayName: string;
      dataType: string;
      inputType?: string;
    }>
  > {
    return await entityJoinService.getReferenceTableColumns(tableName, companyCode);
  }

  /**
   * 컬럼 라벨 정보 업데이트 (display_column 추가)
   */
  async updateColumnLabel(
    tableName: string,
    columnName: string,
    updates: Partial<ColumnLabels>
  ): Promise<void> {
    try {
      logger.info(`컬럼 라벨 업데이트: ${tableName}.${columnName}`);

      await query(
        `INSERT INTO table_type_columns (
          table_name, column_name, column_label, input_type, detail_settings,
          description, display_order, is_visible, code_category, code_value,
          reference_table, reference_column, is_nullable, company_code, created_date, updated_date
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'Y', '*', NOW(), NOW())
        ON CONFLICT (table_name, column_name, company_code)
        DO UPDATE SET
          column_label = COALESCE(EXCLUDED.column_label, table_type_columns.column_label),
          input_type = COALESCE(EXCLUDED.input_type, table_type_columns.input_type),
          detail_settings = COALESCE(EXCLUDED.detail_settings, table_type_columns.detail_settings),
          description = COALESCE(EXCLUDED.description, table_type_columns.description),
          display_order = COALESCE(EXCLUDED.display_order, table_type_columns.display_order),
          is_visible = COALESCE(EXCLUDED.is_visible, table_type_columns.is_visible),
          code_category = COALESCE(EXCLUDED.code_category, table_type_columns.code_category),
          code_value = COALESCE(EXCLUDED.code_value, table_type_columns.code_value),
          reference_table = COALESCE(EXCLUDED.reference_table, table_type_columns.reference_table),
          reference_column = COALESCE(EXCLUDED.reference_column, table_type_columns.reference_column),
          updated_date = NOW()`,
        [
          tableName,
          columnName,
          updates.columnLabel || columnName,
          updates.webType || "text",
          updates.detailSettings,
          updates.description,
          updates.displayOrder || 0,
          updates.isVisible !== false,
          updates.codeCategory,
          updates.codeValue,
          updates.referenceTable,
          updates.referenceColumn,
        ]
      );

      logger.info(`컬럼 라벨 업데이트 완료: ${tableName}.${columnName}`);
    } catch (error) {
      logger.error(
        `컬럼 라벨 업데이트 실패: ${tableName}.${columnName}`,
        error
      );
      throw new Error(
        `컬럼 라벨 업데이트 실패: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    }
  }

  // ========================================
  // 🎯 하이브리드 조인 전략 구현
  // ========================================

  /**
   * 하이브리드 조인 실행: 일부는 조인, 일부는 캐시 룩업
   */
  private async executeHybridJoin(
    tableName: string,
    joinConfigs: EntityJoinConfig[],
    selectColumns: string[],
    whereClause: string,
    orderBy: string,
    limit: number,
    offset: number,
    startTime: number
  ): Promise<EntityJoinResponse> {
    try {
      logger.info(`🔀 하이브리드 조인 실행: ${tableName}`);

      // 각 조인 설정을 캐시 가능 여부에 따라 분류
      const { cacheableJoins, dbJoins } =
        await this.categorizeJoins(joinConfigs);

      console.log(
        `📋 캐시 조인: ${cacheableJoins.length}개, DB 조인: ${dbJoins.length}개`
      );

      // DB 조인이 있는 경우: 조인 쿼리 실행 후 캐시 룩업 적용
      if (dbJoins.length > 0) {
        return await this.executeJoinThenCache(
          tableName,
          dbJoins,
          cacheableJoins,
          selectColumns,
          whereClause,
          orderBy,
          limit,
          offset,
          startTime
        );
      }
      // 모든 조인이 캐시 가능한 경우: 기본 쿼리 + 캐시 룩업
      else {
        // whereClause에서 company_code 추출 (멀티테넌시 필터)
        const companyCodeMatch = whereClause.match(
          /main\.company_code\s*=\s*'([^']+)'/
        );
        const companyCode = companyCodeMatch ? companyCodeMatch[1] : undefined;

        return await this.executeCachedLookup(
          tableName,
          cacheableJoins,
          {
            page: Math.floor(offset / limit) + 1,
            size: limit,
            search: {},
            companyCode,
          },
          startTime
        );
      }
    } catch (error) {
      logger.error("하이브리드 조인 실행 실패", error);
      throw error;
    }
  }

  /**
   * 조인 설정을 캐시 가능 여부에 따라 분류
   */
  private async categorizeJoins(joinConfigs: EntityJoinConfig[]): Promise<{
    cacheableJoins: EntityJoinConfig[];
    dbJoins: EntityJoinConfig[];
  }> {
    const cacheableJoins: EntityJoinConfig[] = [];
    const dbJoins: EntityJoinConfig[] = [];

    // 🔒 멀티테넌시: 회사별 데이터 테이블은 캐시 사용 불가 (company_code 필터링 필요)
    const companySpecificTables = [
      "supplier_mng",
      "customer_mng",
      "item_info",
      "dept_info",
      "sales_order_mng", // 🔧 수주관리 테이블 추가
      "sales_order_detail", // 🔧 수주상세 테이블 추가
      "partner_info", // 🔧 거래처 테이블 추가
      // 필요시 추가
    ];

    for (const config of joinConfigs) {
      // category_values는 특수 조인 조건이 필요하므로 항상 DB 조인
      if (config.referenceTable === "category_values") {
        dbJoins.push(config);
        console.log(`🔗 DB 조인 (특수 조건): ${config.referenceTable}`);
        continue;
      }

      // 🔒 회사별 데이터 테이블은 캐시 사용 불가 (멀티테넌시)
      if (companySpecificTables.includes(config.referenceTable)) {
        dbJoins.push(config);
        console.log(`🔗 DB 조인 (멀티테넌시): ${config.referenceTable}`);
        continue;
      }

      // 캐시 가능성 확인
      const cachedData = await referenceCacheService.getCachedReference(
        config.referenceTable,
        config.referenceColumn,
        config.displayColumn || config.displayColumns[0]
      );

      if (cachedData && cachedData.size > 0) {
        cacheableJoins.push(config);
        console.log(
          `📋 캐시 사용: ${config.referenceTable} (${cachedData.size}건)`
        );
      } else {
        dbJoins.push(config);
        console.log(`🔗 DB 조인: ${config.referenceTable}`);
      }
    }

    return { cacheableJoins, dbJoins };
  }

  /**
   * DB 조인 실행 후 캐시 룩업 적용
   */
  private async executeJoinThenCache(
    tableName: string,
    dbJoins: EntityJoinConfig[],
    cacheableJoins: EntityJoinConfig[],
    selectColumns: string[],
    whereClause: string,
    orderBy: string,
    limit: number,
    offset: number,
    startTime: number
  ): Promise<EntityJoinResponse> {
    // 1. DB 조인 먼저 실행
    const joinResult = await this.executeJoinQuery(
      tableName,
      dbJoins,
      selectColumns,
      whereClause,
      orderBy,
      limit,
      offset,
      startTime
    );

    // 2. 캐시 가능한 조인들을 결과에 추가 적용
    if (cacheableJoins.length > 0) {
      const enhancedData = await this.applyCacheLookupToData(
        joinResult.data,
        cacheableJoins
      );

      return {
        ...joinResult,
        data: enhancedData,
        entityJoinInfo: {
          ...joinResult.entityJoinInfo!,
          strategy: "hybrid",
          performance: {
            ...joinResult.entityJoinInfo!.performance,
            cacheHitRate: await this.calculateCacheHitRate(cacheableJoins),
            hybridBreakdown: {
              dbJoins: dbJoins.length,
              cacheJoins: cacheableJoins.length,
            },
          },
        },
      };
    }

    return joinResult;
  }

  /**
   * 데이터에 캐시 룩업 적용
   */
  private async applyCacheLookupToData(
    data: any[],
    cacheableJoins: EntityJoinConfig[]
  ): Promise<any[]> {
    const enhancedData = [...data];

    for (const config of cacheableJoins) {
      const cachedData = await referenceCacheService.getCachedReference(
        config.referenceTable,
        config.referenceColumn,
        config.displayColumn || config.displayColumns[0]
      );

      if (cachedData) {
        enhancedData.forEach((row) => {
          const keyValue = row[config.sourceColumn];
          if (keyValue) {
            const lookupValue = cachedData.get(String(keyValue));
            // null이나 undefined인 경우 빈 문자열로 설정
            row[config.aliasColumn] = lookupValue || "";
          } else {
            // sourceValue가 없는 경우도 빈 문자열로 설정
            row[config.aliasColumn] = "";
          }
        });
      } else {
        // 캐시가 없는 경우 모든 행에 빈 문자열 설정
        enhancedData.forEach((row) => {
          row[config.aliasColumn] = "";
        });
      }
    }

    return enhancedData;
  }

  /**
   * 캐시 적중률 계산
   */
  private async calculateCacheHitRate(
    cacheableJoins: EntityJoinConfig[]
  ): Promise<number> {
    if (cacheableJoins.length === 0) return 0;

    let totalHitRate = 0;
    for (const config of cacheableJoins) {
      const hitRate = referenceCacheService.getCacheHitRate(
        config.referenceTable,
        config.referenceColumn,
        config.displayColumn || config.displayColumns[0]
      );
      totalHitRate += hitRate;
    }

    return totalHitRate / cacheableJoins.length;
  }

  /**
   * 테이블 스키마 정보 조회 (컬럼 존재 여부 검증용)
   */
  async getTableSchema(tableName: string): Promise<ColumnTypeInfo[]> {
    try {
      logger.info(`테이블 스키마 정보 조회: ${tableName}`);

      const rawColumns = await query<any>(
        `SELECT 
          c.column_name as "columnName",
          c.column_name as "displayName",
          c.data_type as "dataType",
          c.udt_name as "dbType",
          c.is_nullable as "isNullable",
          c.column_default as "defaultValue",
          c.character_maximum_length as "maxLength",
          c.numeric_precision as "numericPrecision",
          c.numeric_scale as "numericScale",
          CASE 
            WHEN c.column_name IN (
              SELECT kcu.column_name FROM information_schema.key_column_usage kcu
              WHERE kcu.table_name = $1 AND kcu.constraint_name LIKE '%_pkey'
            ) THEN true 
            ELSE false 
          END as "isPrimaryKey",
          col_description(
            (SELECT oid FROM pg_class WHERE relname = $1 AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')),
            c.ordinal_position
          ) as "columnComment"
        FROM information_schema.columns c
        WHERE c.table_name = $1
          AND c.table_schema = 'public'
        ORDER BY c.ordinal_position`,
        [tableName]
      );

      const columns: ColumnTypeInfo[] = rawColumns.map((col) => ({
        tableName: tableName,
        columnName: col.columnName,
        displayName: col.displayName,
        dataType: col.dataType,
        dbType: col.dbType,
        webType: "text",
        inputType: "direct",
        detailSettings: "{}",
        description: col.columnComment || "",
        isNullable: col.isNullable,
        isPrimaryKey: col.isPrimaryKey,
        defaultValue: col.defaultValue,
        maxLength: col.maxLength ? Number(col.maxLength) : undefined,
        numericPrecision: col.numericPrecision
          ? Number(col.numericPrecision)
          : undefined,
        numericScale: col.numericScale ? Number(col.numericScale) : undefined,
        displayOrder: 0,
        isVisible: true,
        columnComment: col.columnComment || "",
      }));

      logger.info(
        `테이블 스키마 조회 완료: ${tableName}, ${columns.length}개 컬럼`
      );
      return columns;
    } catch (error) {
      logger.error(`테이블 스키마 조회 실패: ${tableName}`, error);
      throw error;
    }
  }

  /**
   * 테이블 존재 여부 확인
   */
  async checkTableExists(tableName: string): Promise<boolean> {
    try {
      logger.info(`테이블 존재 여부 확인: ${tableName}`);

      const result = await query<any>(
        `SELECT EXISTS (
          SELECT 1 FROM information_schema.tables 
          WHERE table_name = $1
            AND table_schema = 'public'
            AND table_type = 'BASE TABLE'
        ) as "exists"`,
        [tableName]
      );

      const exists = result[0]?.exists || false;
      logger.info(`테이블 존재 여부: ${tableName} = ${exists}`);
      return exists;
    } catch (error) {
      logger.error(`테이블 존재 여부 확인 실패: ${tableName}`, error);
      throw error;
    }
  }

  /**
   * 컬럼 입력타입 정보 조회 (화면관리 연동용)
   * @param companyCode - 회사 코드 (멀티테넌시)
   */
  async getColumnInputTypes(
    tableName: string,
    companyCode: string
  ): Promise<ColumnTypeInfo[]> {
    try {
      logger.info(
        `컬럼 입력타입 정보 조회: ${tableName}, company: ${companyCode}`
      );

      // table_type_columns에서 입력타입 정보 조회
      // 회사별 설정 우선, 없으면 기본 설정(*) fallback
      // detail_settings 컬럼에 유효하지 않은 JSON이 있을 수 있으므로 안전하게 처리
      const rawInputTypes = await query<any>(
        `SELECT DISTINCT ON (ttc.column_name)
          ttc.column_name as "columnName",
          COALESCE(ttc.column_label, ttc.column_name) as "displayName",
          ttc.input_type as "inputType",
          CASE 
            WHEN ttc.detail_settings IS NULL OR ttc.detail_settings = '' THEN '{}'::jsonb
            WHEN ttc.detail_settings ~ '^\\s*\\{.*\\}\\s*$' THEN ttc.detail_settings::jsonb
            ELSE '{}'::jsonb
          END as "detailSettings",
          ttc.is_nullable as "isNullable",
          ic.data_type as "dataType",
          ttc.company_code as "companyCode",
          ttc.category_ref as "categoryRef"
        FROM table_type_columns ttc
        LEFT JOIN information_schema.columns ic 
          ON ttc.table_name = ic.table_name AND ttc.column_name = ic.column_name
        WHERE ttc.table_name = $1 
          AND ttc.company_code IN ($2, '*')
        ORDER BY ttc.column_name, 
          CASE WHEN ttc.company_code = $2 THEN 0 ELSE 1 END,
          ttc.display_order`,
        [tableName, companyCode]
      );

      // category_column_mapping 테이블 존재 여부 확인
      const tableExistsResult = await query<any>(
        `SELECT EXISTS (
          SELECT FROM information_schema.tables 
          WHERE table_name = 'category_column_mapping'
        ) as table_exists`
      );
      const mappingTableExists = tableExistsResult[0]?.table_exists === true;

      // 카테고리 컬럼의 경우, 매핑된 메뉴 목록 조회
      // 회사별 설정 우선, 없으면 기본 설정(*) fallback
      let categoryMappings: Map<string, number[]> = new Map();
      if (mappingTableExists) {
        logger.info("카테고리 매핑 조회 시작", { tableName, companyCode });

        try {
          // menu_objid 컬럼이 있는지 먼저 확인
          const columnCheck = await query<any>(
            `SELECT column_name FROM information_schema.columns 
             WHERE table_name = 'category_column_mapping' AND column_name = 'menu_objid'`
          );

          if (columnCheck.length > 0) {
            const mappings = await query<any>(
              `SELECT DISTINCT ON (logical_column_name, menu_objid)
                logical_column_name as "columnName",
                menu_objid as "menuObjid"
              FROM category_column_mapping
              WHERE table_name = $1 
                AND company_code IN ($2, '*')
              ORDER BY logical_column_name, menu_objid,
                CASE WHEN company_code = $2 THEN 0 ELSE 1 END`,
              [tableName, companyCode]
            );

            logger.info("카테고리 매핑 조회 완료", {
              tableName,
              companyCode,
              mappingCount: mappings.length,
            });

            mappings.forEach((m: any) => {
              if (!categoryMappings.has(m.columnName)) {
                categoryMappings.set(m.columnName, []);
              }
              categoryMappings.get(m.columnName)!.push(Number(m.menuObjid));
            });
          } else {
            logger.info("⚠️ menu_objid 컬럼이 없음, 카테고리 매핑 스킵");
          }
        } catch (mappingError: any) {
          logger.warn("⚠️ 카테고리 매핑 조회 실패, 스킵", {
            error: mappingError.message,
          });
        }

        logger.info("categoryMappings Map 생성 완료", {
          size: categoryMappings.size,
          entries: Array.from(categoryMappings.entries()),
        });
      } else {
        logger.warn("category_column_mapping 테이블이 존재하지 않음");
      }

      const inputTypes: ColumnTypeInfo[] = rawInputTypes.map((col) => {
        const baseInfo: any = {
          tableName: tableName,
          columnName: col.columnName,
          displayName: col.displayName,
          dataType: col.dataType || "varchar",
          inputType: col.inputType,
          detailSettings: col.detailSettings,
          description: "",
          isNullable: col.isNullable === "Y" ? "Y" : "N",
          isPrimaryKey: false,
          displayOrder: 0,
          isVisible: true,
        };

        if (col.categoryRef) {
          baseInfo.categoryRef = col.categoryRef;
        }

        // 카테고리 타입인 경우 categoryMenus 추가
        if (
          col.inputType === "category" &&
          categoryMappings.has(col.columnName)
        ) {
          const menus = categoryMappings.get(col.columnName);
          logger.info(`✅ 컬럼 ${col.columnName}에 카테고리 메뉴 추가`, {
            menus,
          });
          return {
            ...baseInfo,
            categoryMenus: menus,
          };
        }

        if (col.inputType === "category") {
          logger.warn(`⚠️ 카테고리 컬럼 ${col.columnName}에 매핑 없음`);
        }

        return baseInfo;
      });

      logger.info(
        `컬럼 입력타입 정보 조회 완료: ${tableName}, company: ${companyCode}, ${inputTypes.length}개 컬럼`
      );
      return inputTypes;
    } catch (error) {
      logger.error(
        `컬럼 입력타입 정보 조회 실패: ${tableName}, company: ${companyCode}`,
        error
      );
      throw error;
    }
  }

  /**
   * 레거시 지원: 컬럼 웹타입 정보 조회
   * @deprecated getColumnInputTypes 사용 권장
   */
  async getColumnWebTypes(
    tableName: string,
    companyCode: string
  ): Promise<ColumnTypeInfo[]> {
    logger.warn(
      `레거시 메서드 사용: getColumnWebTypes → getColumnInputTypes 사용 권장`
    );
    return this.getColumnInputTypes(tableName, companyCode); // 🔥 FIX: companyCode 파라미터 추가
  }

  /**
   * 데이터베이스 연결 상태 확인
   */
  async checkDatabaseConnection(): Promise<{
    connected: boolean;
    message: string;
  }> {
    try {
      logger.info("데이터베이스 연결 상태 확인");

      // 간단한 쿼리로 연결 테스트
      const result = await query<any>(`SELECT 1 as "test"`);

      if (result && result.length > 0) {
        logger.info("데이터베이스 연결 성공");
        return {
          connected: true,
          message: "데이터베이스에 성공적으로 연결되었습니다.",
        };
      } else {
        logger.warn("데이터베이스 연결 응답 없음");
        return {
          connected: false,
          message: "데이터베이스 연결 응답이 없습니다.",
        };
      }
    } catch (error) {
      logger.error("데이터베이스 연결 확인 실패:", error);
      return {
        connected: false,
        message: `데이터베이스 연결 실패: ${error instanceof Error ? error.message : "알 수 없는 오류"}`,
      };
    }
  }

  /**
   * 데이터 타입으로부터 웹타입 추론
   */
  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";
  }

  // ========================================
  // 🎯 테이블 로그 시스템
  // ========================================

  /**
   * 로그 테이블 생성
   */
  async createLogTable(
    tableName: string,
    pkColumn: { columnName: string; dataType: string },
    userId?: string
  ): Promise<void> {
    try {
      const logTableName = `${tableName}_log`;
      const triggerFuncName = `${tableName}_log_trigger_func`;
      const triggerName = `${tableName}_audit_trigger`;

      logger.info(`로그 테이블 생성 시작: ${logTableName}`);

      // 로그 테이블 DDL 생성
      const logTableDDL = this.generateLogTableDDL(
        logTableName,
        tableName,
        pkColumn.columnName,
        pkColumn.dataType
      );

      // 트리거 함수 DDL 생성
      const triggerFuncDDL = this.generateTriggerFunctionDDL(
        triggerFuncName,
        logTableName,
        tableName,
        pkColumn.columnName
      );

      // 트리거 DDL 생성
      const triggerDDL = this.generateTriggerDDL(
        triggerName,
        tableName,
        triggerFuncName
      );

      // 트랜잭션으로 실행
      await transaction(async (client) => {
        // 1. 로그 테이블 생성
        await client.query(logTableDDL);
        logger.info(`로그 테이블 생성 완료: ${logTableName}`);

        // 2. 트리거 함수 생성
        await client.query(triggerFuncDDL);
        logger.info(`트리거 함수 생성 완료: ${triggerFuncName}`);

        // 3. 트리거 생성
        await client.query(triggerDDL);
        logger.info(`트리거 생성 완료: ${triggerName}`);

        // 4. 로그 설정 저장
        await client.query(
          `INSERT INTO table_log_config (
            original_table_name, log_table_name, trigger_name,
            trigger_function_name, created_by
          ) VALUES ($1, $2, $3, $4, $5)`,
          [tableName, logTableName, triggerName, triggerFuncName, userId]
        );
        logger.info(`로그 설정 저장 완료: ${tableName}`);
      });

      logger.info(`로그 테이블 생성 완료: ${logTableName}`);
    } catch (error) {
      logger.error(`로그 테이블 생성 실패: ${tableName}`, error);
      throw new Error(
        `로그 테이블 생성 실패: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    }
  }

  /**
   * 로그 테이블 DDL 생성
   */
  private generateLogTableDDL(
    logTableName: string,
    originalTableName: string,
    pkColumnName: string,
    pkDataType: string
  ): string {
    return `
      CREATE TABLE ${logTableName} (
        log_id SERIAL PRIMARY KEY,
        operation_type VARCHAR(10) NOT NULL,
        original_id VARCHAR(100),
        changed_column VARCHAR(100),
        old_value TEXT,
        new_value TEXT,
        changed_by VARCHAR(50),
        changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        ip_address VARCHAR(50),
        user_agent TEXT,
        full_row_before JSONB,
        full_row_after JSONB
      );

      CREATE INDEX idx_${logTableName}_original_id ON ${logTableName}(original_id);
      CREATE INDEX idx_${logTableName}_changed_at ON ${logTableName}(changed_at);
      CREATE INDEX idx_${logTableName}_operation ON ${logTableName}(operation_type);

      COMMENT ON TABLE ${logTableName} IS '${originalTableName} 테이블 변경 이력';
      COMMENT ON COLUMN ${logTableName}.operation_type IS '작업 유형 (INSERT/UPDATE/DELETE)';
      COMMENT ON COLUMN ${logTableName}.original_id IS '원본 테이블 PK 값';
      COMMENT ON COLUMN ${logTableName}.changed_column IS '변경된 컬럼명';
      COMMENT ON COLUMN ${logTableName}.old_value IS '변경 전 값';
      COMMENT ON COLUMN ${logTableName}.new_value IS '변경 후 값';
      COMMENT ON COLUMN ${logTableName}.changed_by IS '변경자 ID';
      COMMENT ON COLUMN ${logTableName}.changed_at IS '변경 시각';
      COMMENT ON COLUMN ${logTableName}.ip_address IS '변경 요청 IP';
      COMMENT ON COLUMN ${logTableName}.full_row_before IS '변경 전 전체 행 (JSON)';
      COMMENT ON COLUMN ${logTableName}.full_row_after IS '변경 후 전체 행 (JSON)';
    `;
  }

  /**
   * 트리거 함수 DDL 생성
   */
  private generateTriggerFunctionDDL(
    funcName: string,
    logTableName: string,
    originalTableName: string,
    pkColumnName: string
  ): string {
    return `
      CREATE OR REPLACE FUNCTION ${funcName}()
      RETURNS TRIGGER AS $$
      DECLARE
        v_column_name TEXT;
        v_old_value TEXT;
        v_new_value TEXT;
        v_user_id VARCHAR(50);
        v_ip_address VARCHAR(50);
      BEGIN
        v_user_id := current_setting('app.user_id', TRUE);
        v_ip_address := current_setting('app.ip_address', TRUE);

        IF (TG_OP = 'INSERT') THEN
          EXECUTE format(
            'INSERT INTO ${logTableName} (operation_type, original_id, changed_by, ip_address, full_row_after) 
             VALUES ($1, ($2).%I, $3, $4, $5)', 
            '${pkColumnName}'
          )
          USING 'INSERT', NEW, v_user_id, v_ip_address, row_to_json(NEW)::jsonb;
          RETURN NEW;

        ELSIF (TG_OP = 'UPDATE') THEN
          FOR v_column_name IN
            SELECT column_name
            FROM information_schema.columns
            WHERE table_name = '${originalTableName}'
              AND table_schema = 'public'
          LOOP
            EXECUTE format('SELECT ($1).%I::TEXT, ($2).%I::TEXT', v_column_name, v_column_name)
            INTO v_old_value, v_new_value
            USING OLD, NEW;

            IF v_old_value IS DISTINCT FROM v_new_value THEN
              EXECUTE format(
                'INSERT INTO ${logTableName} (operation_type, original_id, changed_column, old_value, new_value, changed_by, ip_address, full_row_before, full_row_after) 
                 VALUES ($1, ($2).%I, $3, $4, $5, $6, $7, $8, $9)', 
                '${pkColumnName}'
              )
              USING 'UPDATE', NEW, v_column_name, v_old_value, v_new_value, v_user_id, v_ip_address, row_to_json(OLD)::jsonb, row_to_json(NEW)::jsonb;
            END IF;
          END LOOP;
          RETURN NEW;

        ELSIF (TG_OP = 'DELETE') THEN
          EXECUTE format(
            'INSERT INTO ${logTableName} (operation_type, original_id, changed_by, ip_address, full_row_before) 
             VALUES ($1, ($2).%I, $3, $4, $5)', 
            '${pkColumnName}'
          )
          USING 'DELETE', OLD, v_user_id, v_ip_address, row_to_json(OLD)::jsonb;
          RETURN OLD;
        END IF;

        RETURN NULL;
      END;
      $$ LANGUAGE plpgsql;
    `;
  }

  /**
   * 트리거 DDL 생성
   */
  private generateTriggerDDL(
    triggerName: string,
    tableName: string,
    funcName: string
  ): string {
    return `
      CREATE TRIGGER ${triggerName}
      AFTER INSERT OR UPDATE OR DELETE ON ${tableName}
      FOR EACH ROW EXECUTE FUNCTION ${funcName}();
    `;
  }

  /**
   * 로그 설정 조회
   */
  async getLogConfig(tableName: string): Promise<{
    originalTableName: string;
    logTableName: string;
    triggerName: string;
    triggerFunctionName: string;
    isActive: string;
    createdAt: Date;
    createdBy: string;
  } | null> {
    try {
      logger.info(`로그 설정 조회: ${tableName}`);

      const result = await queryOne<{
        original_table_name: string;
        log_table_name: string;
        trigger_name: string;
        trigger_function_name: string;
        is_active: string;
        created_at: Date;
        created_by: string;
      }>(
        `SELECT 
          original_table_name, log_table_name, trigger_name,
          trigger_function_name, is_active, created_at, created_by
         FROM table_log_config
         WHERE original_table_name = $1`,
        [tableName]
      );

      if (!result) {
        return null;
      }

      return {
        originalTableName: result.original_table_name,
        logTableName: result.log_table_name,
        triggerName: result.trigger_name,
        triggerFunctionName: result.trigger_function_name,
        isActive: result.is_active,
        createdAt: result.created_at,
        createdBy: result.created_by,
      };
    } catch (error) {
      logger.error(`로그 설정 조회 실패: ${tableName}`, error);
      throw error;
    }
  }

  /**
   * 로그 데이터 조회
   */
  async getLogData(
    tableName: string,
    options: {
      page: number;
      size: number;
      operationType?: string;
      startDate?: string;
      endDate?: string;
      changedBy?: string;
      originalId?: string;
    }
  ): Promise<{
    data: any[];
    total: number;
    page: number;
    size: number;
    totalPages: number;
  }> {
    try {
      const logTableName = `${tableName}_log`;
      const offset = (options.page - 1) * options.size;

      logger.info(`로그 데이터 조회: ${logTableName}`, options);

      // WHERE 조건 구성
      const whereConditions: string[] = [];
      const values: any[] = [];
      let paramIndex = 1;

      if (options.operationType) {
        whereConditions.push(`operation_type = $${paramIndex}`);
        values.push(options.operationType);
        paramIndex++;
      }

      if (options.startDate) {
        whereConditions.push(`changed_at >= $${paramIndex}::timestamp`);
        values.push(options.startDate);
        paramIndex++;
      }

      if (options.endDate) {
        whereConditions.push(`changed_at <= $${paramIndex}::timestamp`);
        values.push(options.endDate);
        paramIndex++;
      }

      if (options.changedBy) {
        whereConditions.push(`changed_by = $${paramIndex}`);
        values.push(options.changedBy);
        paramIndex++;
      }

      if (options.originalId) {
        whereConditions.push(`original_id::text = $${paramIndex}`);
        values.push(options.originalId);
        paramIndex++;
      }

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

      // 전체 개수 조회
      const countQuery = `SELECT COUNT(*) as count FROM ${logTableName} ${whereClause}`;
      const countResult = await query<any>(countQuery, values);
      const total = parseInt(countResult[0].count);

      // 데이터 조회
      const dataQuery = `
        SELECT * FROM ${logTableName}
        ${whereClause}
        ORDER BY changed_at DESC
        LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
      `;

      const data = await query<any>(dataQuery, [
        ...values,
        options.size,
        offset,
      ]);

      const totalPages = Math.ceil(total / options.size);

      logger.info(
        `로그 데이터 조회 완료: ${logTableName}, 총 ${total}건, ${data.length}개 반환`
      );

      return {
        data,
        total,
        page: options.page,
        size: options.size,
        totalPages,
      };
    } catch (error) {
      logger.error(`로그 데이터 조회 실패: ${tableName}`, error);
      throw error;
    }
  }

  /**
   * 로그 테이블 활성화/비활성화
   */
  async toggleLogTable(tableName: string, isActive: boolean): Promise<void> {
    try {
      const logConfig = await this.getLogConfig(tableName);
      if (!logConfig) {
        throw new Error(`로그 설정을 찾을 수 없습니다: ${tableName}`);
      }

      logger.info(
        `로그 테이블 ${isActive ? "활성화" : "비활성화"}: ${tableName}`
      );

      await transaction(async (client) => {
        // 트리거 활성화/비활성화
        if (isActive) {
          await client.query(
            `ALTER TABLE ${tableName} ENABLE TRIGGER ${logConfig.triggerName}`
          );
        } else {
          await client.query(
            `ALTER TABLE ${tableName} DISABLE TRIGGER ${logConfig.triggerName}`
          );
        }

        // 설정 업데이트
        await client.query(
          `UPDATE table_log_config
           SET is_active = $1, updated_at = NOW()
           WHERE original_table_name = $2`,
          [isActive ? "Y" : "N", tableName]
        );
      });

      logger.info(
        `로그 테이블 ${isActive ? "활성화" : "비활성화"} 완료: ${tableName}`
      );
    } catch (error) {
      logger.error(
        `로그 테이블 ${isActive ? "활성화" : "비활성화"} 실패: ${tableName}`,
        error
      );
      throw error;
    }
  }

  /**
   * 테이블에 특정 컬럼이 존재하는지 확인
   */
  async hasColumn(tableName: string, columnName: string): Promise<boolean> {
    try {
      const result = await query<any>(
        `SELECT column_name 
         FROM information_schema.columns 
         WHERE table_name = $1 AND column_name = $2`,
        [tableName, columnName]
      );
      return result.length > 0;
    } catch (error) {
      logger.error(
        `컬럼 존재 여부 확인 실패: ${tableName}.${columnName}`,
        error
      );
      return false;
    }
  }

  /**
   * 두 테이블 간의 엔티티 관계 자동 감지
   * table_type_columns에서 엔티티 타입 설정을 기반으로 테이블 간 관계를 찾습니다.
   *
   * @param leftTable 좌측 테이블명
   * @param rightTable 우측 테이블명
   * @returns 감지된 엔티티 관계 배열
   */
  async detectTableEntityRelations(
    leftTable: string,
    rightTable: string
  ): Promise<
    Array<{
      leftColumn: string;
      rightColumn: string;
      direction: "left_to_right" | "right_to_left";
      inputType: string;
      displayColumn?: string;
    }>
  > {
    try {
      logger.info(
        `두 테이블 간 엔티티 관계 감지 시작: ${leftTable} <-> ${rightTable}`
      );

      const relations: Array<{
        leftColumn: string;
        rightColumn: string;
        direction: "left_to_right" | "right_to_left";
        inputType: string;
        displayColumn?: string;
      }> = [];

      // 1. 우측 테이블에서 좌측 테이블을 참조하는 엔티티 컬럼 찾기
      // 예: right_table의 customer_id -> left_table(customer_mng)의 customer_code
      const rightToLeftRels = await query<{
        column_name: string;
        reference_column: string;
        input_type: string;
        display_column: string | null;
      }>(
        `SELECT DISTINCT ON (column_name) column_name, reference_column, input_type, display_column
         FROM table_type_columns
         WHERE table_name = $1 
         AND input_type IN ('entity', 'category')
         AND reference_table = $2
         AND reference_column IS NOT NULL 
         AND reference_column != ''
         ORDER BY column_name, CASE WHEN company_code = '*' THEN 1 ELSE 0 END`,
        [rightTable, leftTable]
      );

      for (const rel of rightToLeftRels) {
        relations.push({
          leftColumn: rel.reference_column,
          rightColumn: rel.column_name,
          direction: "right_to_left",
          inputType: rel.input_type,
          displayColumn: rel.display_column || undefined,
        });
      }

      // 2. 좌측 테이블에서 우측 테이블을 참조하는 엔티티 컬럼 찾기
      // 예: left_table의 item_id -> right_table(item_info)의 item_number
      const leftToRightRels = await query<{
        column_name: string;
        reference_column: string;
        input_type: string;
        display_column: string | null;
      }>(
        `SELECT DISTINCT ON (column_name) column_name, reference_column, input_type, display_column
         FROM table_type_columns
         WHERE table_name = $1 
         AND input_type IN ('entity', 'category')
         AND reference_table = $2
         AND reference_column IS NOT NULL 
         AND reference_column != ''
         ORDER BY column_name, CASE WHEN company_code = '*' THEN 1 ELSE 0 END`,
        [leftTable, rightTable]
      );

      for (const rel of leftToRightRels) {
        relations.push({
          leftColumn: rel.column_name,
          rightColumn: rel.reference_column,
          direction: "left_to_right",
          inputType: rel.input_type,
          displayColumn: rel.display_column || undefined,
        });
      }

      logger.info(`엔티티 관계 감지 완료: ${relations.length}개 발견`);
      relations.forEach((rel, idx) => {
        logger.info(
          `  ${idx + 1}. ${leftTable}.${rel.leftColumn} <-> ${rightTable}.${rel.rightColumn} (${rel.direction})`
        );
      });

      return relations;
    } catch (error) {
      logger.error(
        `엔티티 관계 감지 실패: ${leftTable} <-> ${rightTable}`,
        error
      );
      return [];
    }
  }
}
