import { getPool } from "../database/db";
import { logger } from "../utils/logger";
import { getSiblingMenuObjids } from "./menuService";
import {
  TableCategoryValue,
  CategoryColumn,
} from "../types/tableCategoryValue";

class TableCategoryValueService {
  /**
   * 테이블의 카테고리 타입 컬럼 목록 조회
   */
  async getCategoryColumns(
    tableName: string,
    companyCode: string
  ): Promise<CategoryColumn[]> {
    try {
      logger.info("카테고리 컬럼 목록 조회", { tableName, companyCode });

      const pool = getPool();
      
      // 멀티테넌시: 최고 관리자만 company_code="*" 데이터를 볼 수 있음
      let query: string;
      
      if (companyCode === "*") {
        // 최고 관리자: 모든 카테고리 값 조회
        query = `
          SELECT 
            tc.table_name AS "tableName",
            tc.column_name AS "columnName",
            tc.column_name AS "columnLabel",
            COUNT(cv.value_id) AS "valueCount"
          FROM table_type_columns tc
          LEFT JOIN category_values cv 
            ON tc.table_name = cv.table_name 
            AND tc.column_name = cv.column_name
            AND cv.is_active = true
          WHERE tc.table_name = $1
            AND tc.input_type = 'category'
          GROUP BY tc.table_name, tc.column_name, tc.display_order
          ORDER BY tc.display_order, tc.column_name
        `;
        logger.info("최고 관리자 카테고리 컬럼 조회");
      } else {
        // 일반 회사: 자신의 카테고리 값만 조회
        query = `
          SELECT 
            tc.table_name AS "tableName",
            tc.column_name AS "columnName",
            tc.column_name AS "columnLabel",
            COUNT(cv.value_id) AS "valueCount"
          FROM table_type_columns tc
          LEFT JOIN category_values cv 
            ON tc.table_name = cv.table_name 
            AND tc.column_name = cv.column_name
            AND cv.is_active = true
            AND cv.company_code = $2
          WHERE tc.table_name = $1
            AND tc.input_type = 'category'
          GROUP BY tc.table_name, tc.column_name, tc.display_order
          ORDER BY tc.display_order, tc.column_name
        `;
        logger.info("회사별 카테고리 컬럼 조회", { companyCode });
      }

      // 쿼리 파라미터는 company_code에 따라 다름
      const params = companyCode === "*" ? [tableName] : [tableName, companyCode];
      const result = await pool.query(query, params);

      logger.info(`카테고리 컬럼 ${result.rows.length}개 조회 완료`, {
        tableName,
        companyCode,
      });

      return result.rows;
    } catch (error: any) {
      logger.error(`카테고리 컬럼 조회 실패: ${error.message}`);
      throw error;
    }
  }

  /**
   * 모든 테이블의 카테고리 컬럼 목록 조회 (Select 옵션 설정용)
   * 테이블 선택 없이 등록된 모든 카테고리 컬럼을 조회합니다.
   */
  async getAllCategoryColumns(
    companyCode: string
  ): Promise<CategoryColumn[]> {
    try {
      logger.info("전체 카테고리 컬럼 목록 조회", { companyCode });

      const pool = getPool();
      
      let query: string;
      let params: any[];
      
      if (companyCode === "*") {
        // 최고 관리자: 모든 카테고리 컬럼 조회 (중복 제거)
        query = `
          SELECT 
            tc.table_name AS "tableName",
            tc.column_name AS "columnName",
            tc.column_name AS "columnLabel",
            COALESCE(cv_count.cnt, 0) AS "valueCount"
          FROM (
            SELECT DISTINCT table_name, column_name, MIN(display_order) as display_order
            FROM table_type_columns
            WHERE input_type = 'category'
            GROUP BY table_name, column_name
          ) tc
          LEFT JOIN (
            SELECT table_name, column_name, COUNT(*) as cnt
            FROM category_values
            WHERE is_active = true
            GROUP BY table_name, column_name
          ) cv_count ON tc.table_name = cv_count.table_name AND tc.column_name = cv_count.column_name
          ORDER BY tc.table_name, tc.display_order, tc.column_name
        `;
        params = [];
      } else {
        // 일반 회사: 자신의 카테고리 값만 카운트 (중복 제거)
        query = `
          SELECT 
            tc.table_name AS "tableName",
            tc.column_name AS "columnName",
            tc.column_name AS "columnLabel",
            COALESCE(cv_count.cnt, 0) AS "valueCount"
          FROM (
            SELECT DISTINCT table_name, column_name, MIN(display_order) as display_order
            FROM table_type_columns
            WHERE input_type = 'category'
            GROUP BY table_name, column_name
          ) tc
          LEFT JOIN (
            SELECT table_name, column_name, COUNT(*) as cnt
            FROM category_values
            WHERE is_active = true AND company_code = $1
            GROUP BY table_name, column_name
          ) cv_count ON tc.table_name = cv_count.table_name AND tc.column_name = cv_count.column_name
          ORDER BY tc.table_name, tc.display_order, tc.column_name
        `;
        params = [companyCode];
      }

      const result = await pool.query(query, params);

      logger.info(`전체 카테고리 컬럼 ${result.rows.length}개 조회 완료`, {
        companyCode,
      });

      return result.rows;
    } catch (error: any) {
      logger.error(`전체 카테고리 컬럼 조회 실패: ${error.message}`);
      throw error;
    }
  }

  /**
   * 특정 컬럼의 카테고리 값 목록 조회 (메뉴 스코프)
   * 
   * 메뉴 스코프 규칙:
   * - menuObjid가 제공되면 해당 메뉴와 형제 메뉴의 카테고리 값을 조회
   * - menuObjid가 없으면 테이블 스코프로 동작 (하위 호환성)
   */
  async getCategoryValues(
    tableName: string,
    columnName: string,
    companyCode: string,
    includeInactive: boolean = false,
    menuObjid?: number
  ): Promise<TableCategoryValue[]> {
    try {
      logger.info("카테고리 값 목록 조회 (메뉴 스코프)", {
        tableName,
        columnName,
        companyCode,
        includeInactive,
        menuObjid,
      });

      const pool = getPool();
      
      // 1. 메뉴 스코프: 형제 메뉴 OBJID 조회
      let siblingObjids: number[] = [];
      if (menuObjid) {
        siblingObjids = await getSiblingMenuObjids(menuObjid);
        logger.info("형제 메뉴 OBJID 목록", { menuObjid, siblingObjids });
      }
      
      // 2. 카테고리 값 조회 (메뉴 스코프 또는 형제 메뉴 포함)
      let query: string;
      let params: any[];
      
      const baseSelect = `
        SELECT 
          value_id AS "valueId",
          table_name AS "tableName",
          column_name AS "columnName",
          value_code AS "valueCode",
          value_label AS "valueLabel",
          value_order AS "valueOrder",
          parent_value_id AS "parentValueId",
          depth,
          description,
          color,
          icon,
          is_active AS "isActive",
          is_default AS "isDefault",
          company_code AS "companyCode",
          menu_objid AS "menuObjid",
          created_at AS "createdAt",
          updated_at AS "updatedAt",
          created_by AS "createdBy",
          updated_by AS "updatedBy"
        FROM category_values
        WHERE table_name = $1
          AND column_name = $2
      `;
      
      // company_code 기반 필터링
      if (companyCode === "*") {
        // 최고 관리자: 공통(*) 카테고리만 조회 (모든 회사 카테고리 혼합 방지)
        query = baseSelect + ` AND company_code = '*'`;
        params = [tableName, columnName];
        logger.info("최고 관리자: 공통 카테고리만 조회 (category_values)");
      } else {
        // 일반 회사: 자신의 회사 또는 공통(*) 카테고리 조회
        query = baseSelect + ` AND (company_code = $3 OR company_code = '*')`;
        params = [tableName, columnName, companyCode];
        logger.info("회사별 카테고리 값 조회 (category_values)", { companyCode });
      }

      if (!includeInactive) {
        query += ` AND is_active = true`;
      }

      query += ` ORDER BY value_order, value_label`;

      const result = await pool.query(query, params);

      // 계층 구조로 변환
      const values = this.buildHierarchy(result.rows);

      logger.info(`카테고리 값 ${result.rows.length}개 조회 완료`, {
        tableName,
        columnName,
        companyCode,
        menuObjid,
        scopeType: menuObjid ? "menu" : "table",
      });

      return values;
    } catch (error: any) {
      logger.error(`카테고리 값 조회 실패: ${error.message}`);
      throw error;
    }
  }

  /**
   * 카테고리 값 추가 (메뉴 스코프)
   * 
   * @param value 카테고리 값 정보
   * @param companyCode 회사 코드
   * @param userId 생성자 ID
   * @param menuObjid 메뉴 OBJID (필수)
   */
  async addCategoryValue(
    value: TableCategoryValue,
    companyCode: string,
    userId: string,
    menuObjid: number
  ): Promise<TableCategoryValue> {
    const pool = getPool();

    try {
      logger.info("카테고리 값 추가 (메뉴 스코프)", {
        tableName: value.tableName,
        columnName: value.columnName,
        valueCode: value.valueCode,
        menuObjid,
        companyCode,
      });

      // 중복 코드 체크 (멀티테넌시 + 메뉴 스코프)
      let duplicateQuery: string;
      let duplicateParams: any[];
      
      if (companyCode === "*") {
        // 최고 관리자: 모든 회사에서 중복 체크
        duplicateQuery = `
          SELECT value_id 
          FROM category_values
          WHERE table_name = $1
            AND column_name = $2
            AND value_code = $3
            AND menu_objid = $4
        `;
        duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid];
      } else {
        // 일반 회사: 자신의 회사에서만 중복 체크
        duplicateQuery = `
          SELECT value_id 
          FROM category_values
          WHERE table_name = $1
            AND column_name = $2
            AND value_code = $3
            AND menu_objid = $4
            AND company_code = $5
        `;
        duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid, companyCode];
      }

      const duplicateResult = await pool.query(duplicateQuery, duplicateParams);

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

      // 라벨 중복 체크 (같은 테이블+컬럼+회사에서 동일한 라벨명 방지)
      let labelDupQuery: string;
      let labelDupParams: any[];

      if (companyCode === "*") {
        labelDupQuery = `
          SELECT value_id
          FROM category_values
          WHERE table_name = $1
            AND column_name = $2
            AND value_label = $3
            AND is_active = true
        `;
        labelDupParams = [value.tableName, value.columnName, value.valueLabel];
      } else {
        labelDupQuery = `
          SELECT value_id
          FROM category_values
          WHERE table_name = $1
            AND column_name = $2
            AND value_label = $3
            AND company_code = $4
            AND is_active = true
        `;
        labelDupParams = [value.tableName, value.columnName, value.valueLabel, companyCode];
      }

      const labelDupResult = await pool.query(labelDupQuery, labelDupParams);

      if (labelDupResult.rows.length > 0) {
        throw new Error(`이미 동일한 이름의 카테고리 값이 존재합니다: "${value.valueLabel}"`);
      }

      const insertQuery = `
        INSERT INTO category_values (
          table_name, column_name, value_code, value_label, value_order,
          parent_value_id, depth, description, color, icon,
          is_active, is_default, company_code, menu_objid, created_by
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
        RETURNING 
          value_id AS "valueId",
          table_name AS "tableName",
          column_name AS "columnName",
          value_code AS "valueCode",
          value_label AS "valueLabel",
          value_order AS "valueOrder",
          parent_value_id AS "parentValueId",
          depth,
          description,
          color,
          icon,
          is_active AS "isActive",
          is_default AS "isDefault",
          company_code AS "companyCode",
          menu_objid AS "menuObjid",
          created_at AS "createdAt",
          created_by AS "createdBy"
      `;

      const result = await pool.query(insertQuery, [
        value.tableName,
        value.columnName,
        value.valueCode,
        value.valueLabel,
        value.valueOrder || 0,
        value.parentValueId || null,
        value.depth || 1,
        value.description || null,
        value.color || null,
        value.icon || null,
        value.isActive !== false,
        value.isDefault || false,
        companyCode,
        menuObjid, // ← 메뉴 OBJID 저장
        userId,
      ]);

      logger.info("카테고리 값 추가 완료", {
        valueId: result.rows[0].valueId,
        tableName: value.tableName,
        columnName: value.columnName,
        menuObjid,
      });

      return result.rows[0];
    } catch (error: any) {
      logger.error(`카테고리 값 추가 실패: ${error.message}`);
      throw error;
    }
  }

  /**
   * 카테고리 값 수정
   */
  async updateCategoryValue(
    valueId: number,
    updates: Partial<TableCategoryValue>,
    companyCode: string,
    userId: string
  ): Promise<TableCategoryValue> {
    const pool = getPool();

    try {
      const setClauses: string[] = [];
      const values: any[] = [];
      let paramIndex = 1;

      if (updates.valueLabel !== undefined) {
        setClauses.push(`value_label = $${paramIndex++}`);
        values.push(updates.valueLabel);
      }

      if (updates.valueOrder !== undefined) {
        setClauses.push(`value_order = $${paramIndex++}`);
        values.push(updates.valueOrder);
      }

      if (updates.description !== undefined) {
        setClauses.push(`description = $${paramIndex++}`);
        values.push(updates.description);
      }

      if (updates.color !== undefined) {
        setClauses.push(`color = $${paramIndex++}`);
        values.push(updates.color);
      }

      if (updates.icon !== undefined) {
        setClauses.push(`icon = $${paramIndex++}`);
        values.push(updates.icon);
      }

      if (updates.isActive !== undefined) {
        setClauses.push(`is_active = $${paramIndex++}`);
        values.push(updates.isActive);
      }

      if (updates.isDefault !== undefined) {
        setClauses.push(`is_default = $${paramIndex++}`);
        values.push(updates.isDefault);
      }

      // 라벨 수정 시 중복 체크 (자기 자신 제외)
      if (updates.valueLabel !== undefined) {
        const currentRow = await pool.query(
          `SELECT table_name, column_name, company_code FROM category_values WHERE value_id = $1`,
          [valueId]
        );

        if (currentRow.rows.length > 0) {
          const { table_name, column_name, company_code } = currentRow.rows[0];
          const labelDupResult = await pool.query(
            `SELECT value_id FROM category_values
             WHERE table_name = $1
               AND column_name = $2
               AND value_label = $3
               AND company_code = $4
               AND is_active = true
               AND value_id != $5`,
            [table_name, column_name, updates.valueLabel, company_code, valueId]
          );

          if (labelDupResult.rows.length > 0) {
            throw new Error(`이미 동일한 이름의 카테고리 값이 존재합니다: "${updates.valueLabel}"`);
          }
        }
      }

      setClauses.push(`updated_at = NOW()`);
      setClauses.push(`updated_by = $${paramIndex++}`);
      values.push(userId);

      // 멀티테넌시: 최고 관리자는 company_code 조건 제외
      let updateQuery: string;
      
      if (companyCode === "*") {
        // 최고 관리자: 모든 카테고리 값 수정 가능
        values.push(valueId);
        updateQuery = `
          UPDATE category_values
          SET ${setClauses.join(", ")}
          WHERE value_id = $${paramIndex++}
          RETURNING 
            value_id AS "valueId",
            table_name AS "tableName",
            column_name AS "columnName",
            value_code AS "valueCode",
            value_label AS "valueLabel",
            value_order AS "valueOrder",
            description,
            color,
            icon,
            is_active AS "isActive",
            is_default AS "isDefault",
            company_code AS "companyCode",
            updated_at AS "updatedAt",
            updated_by AS "updatedBy"
        `;
      } else {
        // 일반 회사: 자신의 카테고리 값만 수정 가능
        values.push(valueId, companyCode);
        updateQuery = `
          UPDATE category_values
          SET ${setClauses.join(", ")}
          WHERE value_id = $${paramIndex++}
            AND company_code = $${paramIndex++}
          RETURNING 
            value_id AS "valueId",
            table_name AS "tableName",
            column_name AS "columnName",
            value_code AS "valueCode",
            value_label AS "valueLabel",
            value_order AS "valueOrder",
            description,
            color,
            icon,
            is_active AS "isActive",
            is_default AS "isDefault",
            company_code AS "companyCode",
            updated_at AS "updatedAt",
            updated_by AS "updatedBy"
        `;
      }

      const result = await pool.query(updateQuery, values);

      if (result.rowCount === 0) {
        throw new Error("카테고리 값을 찾을 수 없습니다");
      }

      logger.info("카테고리 값 수정 완료", { valueId, companyCode });

      return result.rows[0];
    } catch (error: any) {
      logger.error(`카테고리 값 수정 실패: ${error.message}`);
      throw error;
    }
  }

  /**
   * 카테고리 값 사용 여부 확인
   * 실제 데이터 테이블에서 해당 카테고리 값이 사용되고 있는지 확인
   */
  async checkCategoryValueUsage(
    valueId: number,
    companyCode: string
  ): Promise<{ isUsed: boolean; usedInTables: any[]; totalCount: number }> {
    const pool = getPool();

    try {
      logger.info("카테고리 값 사용 여부 확인", { valueId, companyCode });

      // 1. 카테고리 값 정보 조회
      let valueQuery: string;
      let valueParams: any[];

      if (companyCode === "*") {
        valueQuery = `
          SELECT table_name, column_name, value_code
          FROM category_values
          WHERE value_id = $1
        `;
        valueParams = [valueId];
      } else {
        valueQuery = `
          SELECT table_name, column_name, value_code
          FROM category_values
          WHERE value_id = $1
            AND company_code = $2
        `;
        valueParams = [valueId, companyCode];
      }

      const valueResult = await pool.query(valueQuery, valueParams);

      if (valueResult.rowCount === 0) {
        throw new Error("카테고리 값을 찾을 수 없습니다");
      }

      const { table_name, column_name, value_code } = valueResult.rows[0];

      // 2. 실제 데이터 테이블에서 사용 여부 확인
      // 테이블이 존재하는지 먼저 확인
      const tableExistsQuery = `
        SELECT EXISTS (
          SELECT FROM information_schema.tables 
          WHERE table_schema = 'public'
            AND table_name = $1
        ) as exists
      `;

      const tableExistsResult = await pool.query(tableExistsQuery, [table_name]);

      if (!tableExistsResult.rows[0].exists) {
        logger.info("테이블이 존재하지 않음", { table_name });
        return { isUsed: false, usedInTables: [], totalCount: 0 };
      }

      // 3. 해당 테이블에서 value_code를 사용하는 데이터 개수 확인
      let dataCountQuery: string;
      let dataCountParams: any[];

      if (companyCode === "*") {
        dataCountQuery = `
          SELECT COUNT(*) as count
          FROM ${table_name}
          WHERE ${column_name} = $1
        `;
        dataCountParams = [value_code];
      } else {
        dataCountQuery = `
          SELECT COUNT(*) as count
          FROM ${table_name}
          WHERE ${column_name} = $1
            AND company_code = $2
        `;
        dataCountParams = [value_code, companyCode];
      }

      const dataCountResult = await pool.query(dataCountQuery, dataCountParams);
      const totalCount = parseInt(dataCountResult.rows[0].count);
      const isUsed = totalCount > 0;

      // 4. 사용 중인 메뉴 목록 조회 (해당 테이블을 사용하는 화면/메뉴)
      const menuQuery = `
        SELECT DISTINCT
          mi.objid as menu_objid,
          mi.menu_name_kor as menu_name,
          mi.menu_url
        FROM menu_info mi
        INNER JOIN screen_menu_assignments sma ON sma.menu_objid = mi.objid
        INNER JOIN screen_definitions sd ON sd.screen_id = sma.screen_id
        WHERE sd.table_name = $1
          AND mi.company_code = $2
        ORDER BY mi.menu_name_kor
      `;

      const menuResult = await pool.query(menuQuery, [table_name, companyCode]);

      const usedInTables = menuResult.rows.map((row) => ({
        menuObjid: row.menu_objid,
        menuName: row.menu_name,
        menuUrl: row.menu_url,
        tableName: table_name,
        columnName: column_name,
      }));

      logger.info("카테고리 값 사용 여부 확인 완료", {
        valueId,
        isUsed,
        totalCount,
        usedInMenusCount: usedInTables.length,
      });

      return { isUsed, usedInTables, totalCount };
    } catch (error: any) {
      logger.error(`카테고리 값 사용 여부 확인 실패: ${error.message}`);
      throw error;
    }
  }

  /**
   * 모든 하위 카테고리 값 ID 재귀 수집
   */
  private async collectAllChildValueIds(
    valueId: number,
    companyCode: string
  ): Promise<number[]> {
    const pool = getPool();
    const allChildIds: number[] = [];
    
    // 재귀 CTE를 사용하여 모든 하위 카테고리 수집
    let query: string;
    let params: any[];
    
    if (companyCode === "*") {
      query = `
        WITH RECURSIVE category_tree AS (
          SELECT value_id FROM category_values WHERE parent_value_id = $1
          UNION ALL
          SELECT cv.value_id
          FROM category_values cv
          INNER JOIN category_tree ct ON cv.parent_value_id = ct.value_id
        )
        SELECT value_id FROM category_tree
      `;
      params = [valueId];
    } else {
      query = `
        WITH RECURSIVE category_tree AS (
          SELECT value_id FROM category_values 
          WHERE parent_value_id = $1 AND company_code = $2
          UNION ALL
          SELECT cv.value_id
          FROM category_values cv
          INNER JOIN category_tree ct ON cv.parent_value_id = ct.value_id
          WHERE cv.company_code = $2
        )
        SELECT value_id FROM category_tree
      `;
      params = [valueId, companyCode];
    }
    
    const result = await pool.query(query, params);
    result.rows.forEach(row => allChildIds.push(row.value_id));
    
    return allChildIds;
  }

  /**
   * 카테고리 값 삭제 (하위 카테고리 포함 물리적 삭제)
   */
  async deleteCategoryValue(
    valueId: number,
    companyCode: string,
    userId: string
  ): Promise<void> {
    const pool = getPool();

    try {
      // 1. 자기 자신 + 모든 하위 카테고리 ID 수집
      const childValueIds = await this.collectAllChildValueIds(valueId, companyCode);
      const allValueIds = [valueId, ...childValueIds];
      
      logger.info("삭제 대상 카테고리 값 수집 완료", {
        valueId,
        childCount: childValueIds.length,
        totalCount: allValueIds.length,
      });

      // 2. 모든 대상 항목의 사용 여부 확인
      for (const id of allValueIds) {
        const usage = await this.checkCategoryValueUsage(id, companyCode);

        if (usage.isUsed) {
          // 사용 중인 항목 정보 조회
          let labelQuery: string;
          let labelParams: any[];
          
          if (companyCode === "*") {
            labelQuery = `SELECT value_label FROM category_values WHERE value_id = $1`;
            labelParams = [id];
          } else {
            labelQuery = `SELECT value_label FROM category_values WHERE value_id = $1 AND company_code = $2`;
            labelParams = [id, companyCode];
          }
          
          const labelResult = await pool.query(labelQuery, labelParams);
          const valueLabel = labelResult.rows[0]?.value_label || `ID:${id}`;
          
          let errorMessage = `카테고리 "${valueLabel}"을(를) 삭제할 수 없습니다.\n`;
          errorMessage += `\n현재 ${usage.totalCount}개의 데이터에서 사용 중입니다.`;

          if (usage.usedInTables.length > 0) {
            const menuNames = usage.usedInTables.map((t) => t.menuName).join(", ");
            errorMessage += `\n\n다음 메뉴에서 사용 중입니다:\n${menuNames}`;
          }

          errorMessage += "\n\n메뉴에서 사용하는 카테고리 항목을 수정한 후 다시 삭제해주세요.";

          throw new Error(errorMessage);
        }
      }

      // 3. 하위 카테고리부터 역순으로 삭제 (외래키 제약 회피)
      // 가장 깊은 하위부터 삭제해야 하므로 역순으로
      const reversedIds = [...allValueIds].reverse();
      
      for (const id of reversedIds) {
        let deleteQuery: string;
        let deleteParams: any[];
        
        if (companyCode === "*") {
          deleteQuery = `DELETE FROM category_values WHERE value_id = $1`;
          deleteParams = [id];
        } else {
          deleteQuery = `DELETE FROM category_values WHERE value_id = $1 AND company_code = $2`;
          deleteParams = [id, companyCode];
        }
        
        await pool.query(deleteQuery, deleteParams);
      }

      logger.info("카테고리 값 삭제 완료", {
        valueId,
        companyCode,
        deletedCount: allValueIds.length,
        deletedChildCount: childValueIds.length,
      });
    } catch (error: any) {
      logger.error(`카테고리 값 삭제 실패: ${error.message}`);
      throw error;
    }
  }

  /**
   * 카테고리 값 일괄 삭제
   */
  async bulkDeleteCategoryValues(
    valueIds: number[],
    companyCode: string,
    userId: string
  ): Promise<void> {
    const pool = getPool();

    try {
      // 멀티테넌시 적용
      let deleteQuery: string;
      let deleteParams: any[];
      
      if (companyCode === "*") {
        // 최고 관리자: 모든 카테고리 값 일괄 삭제 가능
        deleteQuery = `
          UPDATE category_values
          SET is_active = false, updated_at = NOW(), updated_by = $2
          WHERE value_id = ANY($1::int[])
        `;
        deleteParams = [valueIds, userId];
      } else {
        // 일반 회사: 자신의 카테고리 값만 일괄 삭제 가능
        deleteQuery = `
          UPDATE category_values
          SET is_active = false, updated_at = NOW(), updated_by = $3
          WHERE value_id = ANY($1::int[])
            AND company_code = $2
        `;
        deleteParams = [valueIds, companyCode, userId];
      }

      await pool.query(deleteQuery, deleteParams);

      logger.info("카테고리 값 일괄 삭제 완료", {
        count: valueIds.length,
        companyCode,
      });
    } catch (error: any) {
      logger.error(`카테고리 값 일괄 삭제 실패: ${error.message}`);
      throw error;
    }
  }

  /**
   * 카테고리 값 순서 변경
   */
  async reorderCategoryValues(
    orderedValueIds: number[],
    companyCode: string
  ): Promise<void> {
    const pool = getPool();
    const client = await pool.connect();

    try {
      await client.query("BEGIN");

      for (let i = 0; i < orderedValueIds.length; i++) {
        // 멀티테넌시 적용
        let updateQuery: string;
        let updateParams: any[];
        
        if (companyCode === "*") {
          // 최고 관리자: 모든 카테고리 값 순서 변경 가능
          updateQuery = `
            UPDATE category_values
            SET value_order = $1, updated_at = NOW()
            WHERE value_id = $2
          `;
          updateParams = [i + 1, orderedValueIds[i]];
        } else {
          // 일반 회사: 자신의 카테고리 값만 순서 변경 가능
          updateQuery = `
            UPDATE category_values
            SET value_order = $1, updated_at = NOW()
            WHERE value_id = $2
              AND company_code = $3
          `;
          updateParams = [i + 1, orderedValueIds[i], companyCode];
        }

        await client.query(updateQuery, updateParams);
      }

      await client.query("COMMIT");

      logger.info("카테고리 값 순서 변경 완료", {
        count: orderedValueIds.length,
        companyCode,
      });
    } catch (error: any) {
      await client.query("ROLLBACK");
      logger.error(`카테고리 값 순서 변경 실패: ${error.message}`);
      throw error;
    } finally {
      client.release();
    }
  }

  /**
   * 계층 구조 변환 헬퍼
   */
  private buildHierarchy(
    values: TableCategoryValue[],
    parentId: number | null = null
  ): TableCategoryValue[] {
    return values
      .filter((v) => v.parentValueId === parentId)
      .map((v) => ({
        ...v,
        children: this.buildHierarchy(values, v.valueId!),
      }));
  }

  // ================================================
  // 컬럼 매핑 관련 메서드 (논리명 ↔ 물리명)
  // ================================================

  /**
   * 컬럼 매핑 조회
   * 
   * @param tableName - 테이블명
   * @param menuObjid - 메뉴 OBJID
   * @param companyCode - 회사 코드
   * @returns { logical_column: physical_column } 형태의 매핑 객체
   */
  async getColumnMapping(
    tableName: string,
    menuObjid: number,
    companyCode: string
  ): Promise<Record<string, string>> {
    const pool = getPool();

    try {
      logger.info("컬럼 매핑 조회", { tableName, menuObjid, companyCode });

      // 멀티테넌시 적용
      let query: string;
      let params: any[];

      if (companyCode === "*") {
        // 최고 관리자: 모든 매핑 조회 가능
        query = `
          SELECT 
            logical_column_name AS "logicalColumnName",
            physical_column_name AS "physicalColumnName"
          FROM category_column_mapping
          WHERE table_name = $1
            AND menu_objid = $2
        `;
        params = [tableName, menuObjid];
      } else {
        // 일반 회사: 자신의 매핑만 조회
        query = `
          SELECT 
            logical_column_name AS "logicalColumnName",
            physical_column_name AS "physicalColumnName"
          FROM category_column_mapping
          WHERE table_name = $1
            AND menu_objid = $2
            AND company_code = $3
        `;
        params = [tableName, menuObjid, companyCode];
      }

      const result = await pool.query(query, params);

      // { logical_column: physical_column } 형태로 변환
      const mapping: Record<string, string> = {};
      result.rows.forEach((row: any) => {
        mapping[row.logicalColumnName] = row.physicalColumnName;
      });

      logger.info(`컬럼 매핑 ${Object.keys(mapping).length}개 조회 완료`, {
        tableName,
        menuObjid,
        companyCode,
      });

      return mapping;
    } catch (error: any) {
      logger.error(`컬럼 매핑 조회 실패: ${error.message}`);
      throw error;
    }
  }

  /**
   * 컬럼 매핑 생성/수정
   * 
   * @param tableName - 테이블명
   * @param logicalColumnName - 논리적 컬럼명
   * @param physicalColumnName - 물리적 컬럼명
   * @param menuObjid - 메뉴 OBJID
   * @param companyCode - 회사 코드
   * @param userId - 사용자 ID
   * @param description - 설명 (선택사항)
   */
  async createColumnMapping(
    tableName: string,
    logicalColumnName: string,
    physicalColumnName: string,
    menuObjid: number,
    companyCode: string,
    userId: string,
    description?: string
  ): Promise<any> {
    const pool = getPool();

    try {
      logger.info("컬럼 매핑 생성", {
        tableName,
        logicalColumnName,
        physicalColumnName,
        menuObjid,
        companyCode,
      });

      // 1. 물리적 컬럼이 실제로 존재하는지 확인
      const columnCheckQuery = `
        SELECT column_name
        FROM information_schema.columns
        WHERE table_schema = 'public'
          AND table_name = $1
          AND column_name = $2
      `;

      const columnCheck = await pool.query(columnCheckQuery, [
        tableName,
        physicalColumnName,
      ]);

      if (columnCheck.rowCount === 0) {
        throw new Error(
          `테이블 ${tableName}에 컬럼 ${physicalColumnName}이(가) 존재하지 않습니다`
        );
      }

      // 2. 매핑 저장 (UPSERT)
      const insertQuery = `
        INSERT INTO category_column_mapping (
          table_name,
          logical_column_name,
          physical_column_name,
          menu_objid,
          company_code,
          description,
          created_by,
          updated_by
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
        ON CONFLICT (table_name, logical_column_name, menu_objid, company_code)
        DO UPDATE SET
          physical_column_name = EXCLUDED.physical_column_name,
          description = EXCLUDED.description,
          updated_at = NOW(),
          updated_by = EXCLUDED.updated_by
        RETURNING *
      `;

      const result = await pool.query(insertQuery, [
        tableName,
        logicalColumnName,
        physicalColumnName,
        menuObjid,
        companyCode,
        description || null,
        userId,
        userId,
      ]);

      logger.info("컬럼 매핑 생성 완료", {
        mappingId: result.rows[0].mapping_id,
        tableName,
        logicalColumnName,
        physicalColumnName,
      });

      return result.rows[0];
    } catch (error: any) {
      logger.error(`컬럼 매핑 생성 실패: ${error.message}`);
      throw error;
    }
  }

  /**
   * 논리적 컬럼 목록 조회
   * 
   * @param tableName - 테이블명
   * @param menuObjid - 메뉴 OBJID
   * @param companyCode - 회사 코드
   * @returns 논리적 컬럼 목록
   */
  async getLogicalColumns(
    tableName: string,
    menuObjid: number,
    companyCode: string
  ): Promise<any[]> {
    const pool = getPool();

    try {
      logger.info("논리적 컬럼 목록 조회", {
        tableName,
        menuObjid,
        companyCode,
      });

      // 멀티테넌시 적용
      let query: string;
      let params: any[];

      if (companyCode === "*") {
        // 최고 관리자: 모든 논리적 컬럼 조회
        query = `
          SELECT 
            mapping_id AS "mappingId",
            logical_column_name AS "logicalColumnName",
            physical_column_name AS "physicalColumnName",
            description
          FROM category_column_mapping
          WHERE table_name = $1
            AND menu_objid = $2
          ORDER BY logical_column_name
        `;
        params = [tableName, menuObjid];
      } else {
        // 일반 회사: 자신의 논리적 컬럼만 조회
        query = `
          SELECT 
            mapping_id AS "mappingId",
            logical_column_name AS "logicalColumnName",
            physical_column_name AS "physicalColumnName",
            description
          FROM category_column_mapping
          WHERE table_name = $1
            AND menu_objid = $2
            AND company_code = $3
          ORDER BY logical_column_name
        `;
        params = [tableName, menuObjid, companyCode];
      }

      const result = await pool.query(query, params);

      logger.info(`논리적 컬럼 ${result.rows.length}개 조회 완료`, {
        tableName,
        menuObjid,
        companyCode,
      });

      return result.rows;
    } catch (error: any) {
      logger.error(`논리적 컬럼 목록 조회 실패: ${error.message}`);
      throw error;
    }
  }

  /**
   * 컬럼 매핑 삭제
   * 
   * @param mappingId - 매핑 ID
   * @param companyCode - 회사 코드
   */
  async deleteColumnMapping(
    mappingId: number,
    companyCode: string
  ): Promise<void> {
    const pool = getPool();

    try {
      logger.info("컬럼 매핑 삭제", { mappingId, companyCode });

      // 멀티테넌시 적용
      let deleteQuery: string;
      let deleteParams: any[];

      if (companyCode === "*") {
        // 최고 관리자: 모든 매핑 삭제 가능
        deleteQuery = `
          DELETE FROM category_column_mapping
          WHERE mapping_id = $1
        `;
        deleteParams = [mappingId];
      } else {
        // 일반 회사: 자신의 매핑만 삭제 가능
        deleteQuery = `
          DELETE FROM category_column_mapping
          WHERE mapping_id = $1
            AND company_code = $2
        `;
        deleteParams = [mappingId, companyCode];
      }

      const result = await pool.query(deleteQuery, deleteParams);

      if (result.rowCount === 0) {
        throw new Error("컬럼 매핑을 찾을 수 없거나 권한이 없습니다");
      }

      logger.info("컬럼 매핑 삭제 완료", { mappingId, companyCode });
    } catch (error: any) {
      logger.error(`컬럼 매핑 삭제 실패: ${error.message}`);
      throw error;
    }
  }

  /**
   * 테이블+컬럼 기준으로 모든 매핑 삭제
   * 
   * 메뉴 선택 변경 시 기존 매핑을 모두 삭제하고 새로운 매핑만 추가하기 위해 사용
   * 
   * @param tableName - 테이블명
   * @param columnName - 컬럼명
   * @param companyCode - 회사 코드
   * @returns 삭제된 매핑 수
   */
  async deleteColumnMappingsByColumn(
    tableName: string,
    columnName: string,
    companyCode: string
  ): Promise<number> {
    const pool = getPool();

    try {
      logger.info("테이블+컬럼 기준 매핑 삭제", { tableName, columnName, companyCode });

      // 멀티테넌시 적용
      let deleteQuery: string;
      let deleteParams: any[];

      if (companyCode === "*") {
        // 최고 관리자: 해당 테이블+컬럼의 모든 매핑 삭제
        deleteQuery = `
          DELETE FROM category_column_mapping
          WHERE table_name = $1
            AND logical_column_name = $2
        `;
        deleteParams = [tableName, columnName];
      } else {
        // 일반 회사: 자신의 매핑만 삭제
        deleteQuery = `
          DELETE FROM category_column_mapping
          WHERE table_name = $1
            AND logical_column_name = $2
            AND company_code = $3
        `;
        deleteParams = [tableName, columnName, companyCode];
      }

      const result = await pool.query(deleteQuery, deleteParams);
      const deletedCount = result.rowCount || 0;

      logger.info("테이블+컬럼 기준 매핑 삭제 완료", { 
        tableName, 
        columnName, 
        companyCode,
        deletedCount 
      });

      return deletedCount;
    } catch (error: any) {
      logger.error(`테이블+컬럼 기준 매핑 삭제 실패: ${error.message}`);
      throw error;
    }
  }

  /**
   * 논리적 컬럼명을 물리적 컬럼명으로 변환
   * 
   * 데이터 저장 시 사용
   * 
   * @param tableName - 테이블명
   * @param menuObjid - 메뉴 OBJID
   * @param companyCode - 회사 코드
   * @param data - 논리적 컬럼명으로 된 데이터
   * @returns 물리적 컬럼명으로 변환된 데이터
   */
  async convertToPhysicalColumns(
    tableName: string,
    menuObjid: number,
    companyCode: string,
    data: Record<string, any>
  ): Promise<Record<string, any>> {
    try {
      // 컬럼 매핑 조회
      const mapping = await this.getColumnMapping(tableName, menuObjid, companyCode);

      // 논리적 컬럼명 → 물리적 컬럼명 변환
      const physicalData: Record<string, any> = {};
      for (const [key, value] of Object.entries(data)) {
        const physicalColumn = mapping[key] || key; // 매핑 없으면 원래 이름 사용
        physicalData[physicalColumn] = value;
      }

      logger.info("컬럼명 변환 완료", {
        tableName,
        menuObjid,
        logicalColumns: Object.keys(data),
        physicalColumns: Object.keys(physicalData),
      });

      return physicalData;
    } catch (error: any) {
      logger.error(`컬럼명 변환 실패: ${error.message}`);
      // 매핑이 없으면 원본 데이터 그대로 반환
      return data;
    }
  }

  /**
   * 2레벨 메뉴 목록 조회
   * 
   * 카테고리 컬럼 매핑 생성 시 메뉴 선택용
   * 
   * @param companyCode - 회사 코드
   * @returns 2레벨 메뉴 목록
   */
  async getSecondLevelMenus(companyCode: string): Promise<any[]> {
    const pool = getPool();

    try {
      logger.info("2레벨 메뉴 목록 조회", { companyCode });

      // menu_info 테이블에 company_code 컬럼이 있는지 확인
      const columnCheckQuery = `
        SELECT column_name 
        FROM information_schema.columns 
        WHERE table_name = 'menu_info' AND column_name = 'company_code'
      `;
      const columnCheck = await pool.query(columnCheckQuery);
      const hasCompanyCode = columnCheck.rows.length > 0;

      logger.info("menu_info 테이블 company_code 컬럼 존재 여부", { hasCompanyCode });

      let query: string;
      let params: any[];

      if (!hasCompanyCode) {
        // company_code 컬럼이 없는 경우: 모든 2레벨 사용자 메뉴 조회
        query = `
          SELECT 
            m1.objid as "menuObjid",
            m1.menu_name_kor as "menuName",
            m0.menu_name_kor as "parentMenuName",
            m1.screen_code as "screenCode"
          FROM menu_info m1
          INNER JOIN menu_info m0 ON m1.parent_obj_id = m0.objid
          WHERE m1.menu_type = 1
            AND m1.status = 'active'
            AND m0.parent_obj_id = 0
          ORDER BY m0.seq, m1.seq
        `;
        params = [];
      } else if (companyCode === "*") {
        // 최고 관리자: 모든 회사의 2레벨 사용자 메뉴 조회
        query = `
          SELECT 
            m1.objid as "menuObjid",
            m1.menu_name_kor as "menuName",
            m0.menu_name_kor as "parentMenuName",
            m1.screen_code as "screenCode"
          FROM menu_info m1
          INNER JOIN menu_info m0 ON m1.parent_obj_id = m0.objid
          WHERE m1.menu_type = 1
            AND m1.status = 'active'
            AND m0.parent_obj_id = 0
          ORDER BY m0.seq, m1.seq
        `;
        params = [];
      } else {
        // 일반 회사: 자신의 회사 메뉴만 조회 (공통 메뉴 제외)
        query = `
          SELECT 
            m1.objid as "menuObjid",
            m1.menu_name_kor as "menuName",
            m0.menu_name_kor as "parentMenuName",
            m1.screen_code as "screenCode"
          FROM menu_info m1
          INNER JOIN menu_info m0 ON m1.parent_obj_id = m0.objid
          WHERE m1.menu_type = 1
            AND m1.status = 'active'
            AND m0.parent_obj_id = 0
            AND m1.company_code = $1
          ORDER BY m0.seq, m1.seq
        `;
        params = [companyCode];
      }

      const result = await pool.query(query, params);

      logger.info(`2레벨 메뉴 ${result.rows.length}개 조회 완료`, { companyCode });

      return result.rows;
    } catch (error: any) {
      logger.error(`2레벨 메뉴 목록 조회 실패: ${error.message}`, { error });
      throw error;
    }
  }

  /**
   * 카테고리 코드로 라벨 조회
   * 
   * @param valueCodes - 카테고리 코드 배열
   * @param companyCode - 회사 코드
   * @returns { [code]: label } 형태의 매핑 객체
   */
  async getCategoryLabelsByCodes(
    valueCodes: string[],
    companyCode: string
  ): Promise<Record<string, string>> {
    try {
      if (!valueCodes || valueCodes.length === 0) {
        return {};
      }

      logger.info("카테고리 코드로 라벨 조회", { valueCodes, companyCode });

      const pool = getPool();
      
      const n = valueCodes.length;
      
      // 첫 번째 쿼리용 플레이스홀더: $1 ~ $n
      const placeholders1 = valueCodes.map((_, i) => `$${i + 1}`).join(", ");
      
      let query: string;
      let params: any[];
      
      // is_active 필터 제거: 비활성화된 카테고리도 라벨로 표시되어야 함
      if (companyCode === "*") {
        query = `
          SELECT DISTINCT value_code, value_label
          FROM category_values
          WHERE value_code IN (${placeholders1})
        `;
        params = [...valueCodes];
      } else {
        const companyIdx = n + 1;
        query = `
          SELECT DISTINCT value_code, value_label
          FROM category_values
          WHERE value_code IN (${placeholders1})
            AND (company_code = $${companyIdx} OR company_code = '*')
        `;
        params = [...valueCodes, companyCode];
      }

      const result = await pool.query(query, params);

      // { [code]: label } 형태로 변환 (중복 시 첫 번째 결과 우선)
      const labels: Record<string, string> = {};
      for (const row of result.rows) {
        if (!labels[row.value_code]) {
          labels[row.value_code] = row.value_label;
        }
      }

      logger.info(`카테고리 라벨 ${Object.keys(labels).length}개 조회 완료`, { companyCode });

      return labels;
    } catch (error: any) {
      logger.error(`카테고리 코드로 라벨 조회 실패: ${error.message}`, { error });
      throw error;
    }
  }

  /**
   * 테이블의 카테고리 타입 컬럼과 해당 값 매핑 조회 (라벨 → 코드 변환용)
   * 
   * 엑셀 업로드 등에서 라벨 값을 코드 값으로 변환할 때 사용
   * 
   * @param tableName - 테이블명
   * @param companyCode - 회사 코드
   * @returns { [columnName]: { [label]: code } } 형태의 매핑 객체
   */
  async getCategoryLabelToCodeMapping(
    tableName: string,
    companyCode: string
  ): Promise<Record<string, Record<string, string>>> {
    try {
      logger.info("카테고리 라벨→코드 매핑 조회", { tableName, companyCode });

      const pool = getPool();

      // 1. 해당 테이블의 카테고리 타입 컬럼 조회
      const categoryColumnsQuery = `
        SELECT column_name
        FROM table_type_columns
        WHERE table_name = $1
          AND input_type = 'category'
      `;
      const categoryColumnsResult = await pool.query(categoryColumnsQuery, [tableName]);
      
      if (categoryColumnsResult.rows.length === 0) {
        logger.info("카테고리 타입 컬럼 없음", { tableName });
        return {};
      }

      const categoryColumns = categoryColumnsResult.rows.map(row => row.column_name);
      logger.info(`카테고리 컬럼 ${categoryColumns.length}개 발견`, { categoryColumns });

      // 2. 각 카테고리 컬럼의 라벨→코드 매핑 조회
      const result: Record<string, Record<string, string>> = {};

      for (const columnName of categoryColumns) {
        let query: string;
        let params: any[];

        if (companyCode === "*") {
          // 최고 관리자: 모든 카테고리 값 조회
          query = `
            SELECT value_code, value_label
            FROM category_values
            WHERE table_name = $1
              AND column_name = $2
              AND is_active = true
          `;
          params = [tableName, columnName];
        } else {
          // 일반 회사: 자신의 카테고리 값 + 공통 카테고리 값 조회
          query = `
            SELECT value_code, value_label
            FROM category_values
            WHERE table_name = $1
              AND column_name = $2
              AND is_active = true
              AND (company_code = $3 OR company_code = '*')
          `;
          params = [tableName, columnName, companyCode];
        }

        const valuesResult = await pool.query(query, params);

        // { [label]: code } 형태로 변환
        const labelToCodeMap: Record<string, string> = {};
        for (const row of valuesResult.rows) {
          // 라벨을 소문자로 변환하여 대소문자 구분 없이 매핑
          labelToCodeMap[row.value_label] = row.value_code;
          // 소문자 키도 추가 (대소문자 무시 검색용)
          labelToCodeMap[row.value_label.toLowerCase()] = row.value_code;
        }

        if (Object.keys(labelToCodeMap).length > 0) {
          result[columnName] = labelToCodeMap;
          logger.info(`컬럼 ${columnName}의 라벨→코드 매핑 ${valuesResult.rows.length}개 조회`);
        }
      }

      logger.info(`카테고리 라벨→코드 매핑 조회 완료`, { 
        tableName, 
        columnCount: Object.keys(result).length 
      });

      return result;
    } catch (error: any) {
      logger.error(`카테고리 라벨→코드 매핑 조회 실패: ${error.message}`, { error });
      throw error;
    }
  }

  /**
   * 데이터의 카테고리 라벨 값을 코드 값으로 변환
   * 
   * 엑셀 업로드 등에서 사용자가 입력한 라벨 값을 DB 저장용 코드 값으로 변환
   * 
   * @param tableName - 테이블명
   * @param companyCode - 회사 코드
   * @param data - 변환할 데이터 객체
   * @returns 라벨이 코드로 변환된 데이터 객체
   */
  async convertCategoryLabelsToCodesForData(
    tableName: string,
    companyCode: string,
    data: Record<string, any>
  ): Promise<{ convertedData: Record<string, any>; conversions: Array<{ column: string; label: string; code: string }> }> {
    try {
      // 라벨→코드 매핑 조회
      const labelToCodeMapping = await this.getCategoryLabelToCodeMapping(tableName, companyCode);

      if (Object.keys(labelToCodeMapping).length === 0) {
        // 카테고리 컬럼 없음
        return { convertedData: data, conversions: [] };
      }

      const convertedData = { ...data };
      const conversions: Array<{ column: string; label: string; code: string }> = [];

      for (const [columnName, labelCodeMap] of Object.entries(labelToCodeMapping)) {
        const value = data[columnName];

        if (value !== undefined && value !== null && value !== "") {
          const stringValue = String(value).trim();
          
          // 다중 값 확인 (쉼표로 구분된 경우)
          if (stringValue.includes(",")) {
            // 다중 카테고리 값 처리
            const labels = stringValue.split(",").map(s => s.trim()).filter(s => s !== "");
            const convertedCodes: string[] = [];
            let allConverted = true;

            for (const label of labels) {
              // 정확한 라벨 매칭 시도
              let matchedCode = labelCodeMap[label];
              
              // 대소문자 무시 매칭
              if (!matchedCode) {
                matchedCode = labelCodeMap[label.toLowerCase()];
              }

              if (matchedCode) {
                convertedCodes.push(matchedCode);
                conversions.push({
                  column: columnName,
                  label: label,
                  code: matchedCode,
                });
                logger.info(`카테고리 라벨→코드 변환 (다중): ${columnName} "${label}" → "${matchedCode}"`);
              } else {
                // 이미 코드값인지 확인
                const isAlreadyCode = Object.values(labelCodeMap).includes(label);
                if (isAlreadyCode) {
                  // 이미 코드값이면 그대로 사용
                  convertedCodes.push(label);
                } else {
                  // 라벨도 코드도 아니면 원래 값 유지
                  convertedCodes.push(label);
                  allConverted = false;
                  logger.warn(`카테고리 값 매핑 없음 (다중): ${columnName} = "${label}" (라벨도 코드도 아님)`);
                }
              }
            }

            // 변환된 코드들을 쉼표로 합쳐서 저장
            convertedData[columnName] = convertedCodes.join(",");
            logger.info(`다중 카테고리 변환 완료: ${columnName} "${stringValue}" → "${convertedData[columnName]}"`);
          } else {
            // 단일 값 처리
            // 정확한 라벨 매칭 시도
            let matchedCode = labelCodeMap[stringValue];
            
            // 대소문자 무시 매칭
            if (!matchedCode) {
              matchedCode = labelCodeMap[stringValue.toLowerCase()];
            }

            if (matchedCode) {
              // 라벨 값을 코드 값으로 변환
              convertedData[columnName] = matchedCode;
              conversions.push({
                column: columnName,
                label: stringValue,
                code: matchedCode,
              });
              logger.info(`카테고리 라벨→코드 변환: ${columnName} "${stringValue}" → "${matchedCode}"`);
            } else {
              // 이미 코드값인지 확인 (역방향 확인)
              const isAlreadyCode = Object.values(labelCodeMap).includes(stringValue);
              if (!isAlreadyCode) {
                logger.warn(`카테고리 값 매핑 없음: ${columnName} = "${stringValue}" (라벨도 코드도 아님)`);
              }
              // 변환 없이 원래 값 유지
            }
          }
        }
      }

      logger.info(`카테고리 라벨→코드 변환 완료`, {
        tableName,
        conversionCount: conversions.length,
        conversions,
      });

      return { convertedData, conversions };
    } catch (error: any) {
      logger.error(`카테고리 라벨→코드 변환 실패: ${error.message}`, { error });
      // 실패 시 원본 데이터 반환
      return { convertedData: data, conversions: [] };
    }
  }
}

export default new TableCategoryValueService();
