"use client";

/**
 * V2Repeater 컴포넌트
 *
 * 렌더링 모드:
 * - inline: 현재 테이블 컬럼 직접 입력
 * - modal: 엔티티 선택 (FK 저장) + 추가 입력 컬럼
 *
 * RepeaterTable 및 ItemSelectionModal 재사용
 */

import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
import { cn } from "@/lib/utils";
import {
  V2RepeaterConfig,
  V2RepeaterProps,
  RepeaterColumnConfig as V2ColumnConfig,
  DEFAULT_REPEATER_CONFIG,
} from "@/types/v2-repeater";
import { apiClient } from "@/lib/api/client";
import { allocateNumberingCode } from "@/lib/api/numberingRule";
import { v2EventBus, V2_EVENTS, V2ErrorBoundary } from "@/lib/v2-core";
import { useScreenContextOptional } from "@/contexts/ScreenContext";
import { DataReceivable } from "@/types/data-transfer";
import { toast } from "sonner";

// modal-repeater-table 컴포넌트 재사용
import { RepeaterTable } from "@/lib/registry/components/modal-repeater-table/RepeaterTable";
import { ItemSelectionModal } from "@/lib/registry/components/modal-repeater-table/ItemSelectionModal";
import { RepeaterColumnConfig } from "@/lib/registry/components/modal-repeater-table/types";

// 전역 V2Repeater 등록 (buttonActions에서 사용)
declare global {
  interface Window {
    __v2RepeaterInstances?: Set<string>;
  }
}

export const V2Repeater: React.FC<V2RepeaterProps> = ({
  config: propConfig,
  componentId,
  parentId,
  data: initialData,
  onDataChange,
  onRowClick,
  className,
  formData: parentFormData,
  groupedData,
  ...restProps
}) => {
  // componentId 결정: 직접 전달 또는 component 객체에서 추출
  const effectiveComponentId = componentId || (restProps as any).component?.id;

  // ScreenContext 연동 (DataReceiver 등록, Provider 없으면 null)
  const screenContext = useScreenContextOptional();
  // 설정 병합
  const config: V2RepeaterConfig = useMemo(
    () => ({
      ...DEFAULT_REPEATER_CONFIG,
      ...propConfig,
      dataSource: { ...DEFAULT_REPEATER_CONFIG.dataSource, ...propConfig.dataSource },
      features: { ...DEFAULT_REPEATER_CONFIG.features, ...propConfig.features },
      modal: { ...DEFAULT_REPEATER_CONFIG.modal, ...propConfig.modal },
    }),
    [propConfig],
  );

  // 상태
  const [data, setData] = useState<any[]>(initialData || []);
  const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
  const [modalOpen, setModalOpen] = useState(false);

  // 저장 이벤트 핸들러에서 항상 최신 data를 참조하기 위한 ref
  const dataRef = useRef<any[]>(data);
  useEffect(() => {
    dataRef.current = data;
  }, [data]);

  // 수정 모드에서 로드된 원본 ID 목록 (삭제 추적용)
  const loadedIdsRef = useRef<Set<string>>(new Set());

  // 🆕 데이터 변경 시 자동으로 컬럼 너비 조정 트리거
  const [autoWidthTrigger, setAutoWidthTrigger] = useState(0);

  // ScreenContext DataReceiver 등록 (데이터 전달 액션 수신)
  const onDataChangeRef = useRef(onDataChange);
  onDataChangeRef.current = onDataChange;

  // Entity 조인 설정을 ref로 보관 (이벤트 핸들러 closure에서 항상 최신값 참조)
  const entityJoinsRef = useRef(config.entityJoins);
  useEffect(() => {
    entityJoinsRef.current = config.entityJoins;
  }, [config.entityJoins]);

  // Entity 조인 해석: FK 값을 기반으로 참조 테이블에서 표시 데이터를 가져와 행에 채움
  const resolveEntityJoins = useCallback(async (rows: any[]): Promise<any[]> => {
    const entityJoins = entityJoinsRef.current;
    console.log("🔍 [V2Repeater] resolveEntityJoins 시작:", {
      entityJoins,
      rowCount: rows.length,
      sampleRow: rows[0],
    });

    if (!entityJoins || entityJoins.length === 0) {
      console.warn("⚠️ [V2Repeater] entityJoins 설정 없음 - 해석 스킵");
      return rows;
    }

    const resolvedRows = rows.map((r) => ({ ...r }));

    for (const join of entityJoins) {
      const fkValues = [...new Set(resolvedRows.map((r) => r[join.sourceColumn]).filter(Boolean))];
      console.log(`🔍 [V2Repeater] FK 값 추출: ${join.sourceColumn} → [${fkValues.join(", ")}]`);
      if (fkValues.length === 0) continue;

      try {
        const response = await apiClient.post(`/table-management/tables/${join.referenceTable}/data`, {
          page: 1,
          size: fkValues.length + 10,
          dataFilter: {
            enabled: true,
            filters: [{ columnName: "id", operator: "in", value: fkValues }],
          },
          autoFilter: true,
        });

        console.log(`🔍 [V2Repeater] API 응답:`, response.data);
        const refData = response.data?.data?.data || response.data?.data?.rows || [];
        const lookupMap = new Map(refData.map((r: any) => [String(r.id), r]));

        resolvedRows.forEach((row) => {
          const fkVal = String(row[join.sourceColumn] || "");
          const refRecord = lookupMap.get(fkVal);
          if (refRecord) {
            join.columns.forEach((col) => {
              row[col.displayField] = refRecord[col.referenceField];
            });
          }
        });

        console.log(`✅ [V2Repeater] Entity 조인 해석 완료: ${join.referenceTable} (${fkValues.length}건, 조회결과: ${refData.length}건)`);
      } catch (error) {
        console.error(`❌ [V2Repeater] Entity 조인 해석 실패: ${join.referenceTable}`, error);
      }
    }

    return resolvedRows;
  }, []);

  const handleReceiveData = useCallback(
    async (incomingData: any[], configOrMode?: any) => {
      console.log("📥 [V2Repeater] 데이터 수신:", { count: incomingData?.length, configOrMode });

      if (!incomingData || incomingData.length === 0) {
        toast.warning("전달할 데이터가 없습니다");
        return;
      }

      // mappingRules 처리: configOrMode에 mappingRules가 있으면 적용
      const mappingRules = configOrMode?.mappingRules;

      // 데이터 정규화: {0: {...}} 형태 처리 + 소스 테이블 메타 필드 제거
      const metaFieldsToStrip = new Set([
        "id",
        "created_date",
        "updated_date",
        "created_by",
        "updated_by",
        "company_code",
      ]);
      let normalizedData = incomingData.map((item: any, index: number) => {
        let raw = item;
        if (item && typeof item === "object" && item[0] && typeof item[0] === "object") {
          const { 0: originalData, ...additionalFields } = item;
          raw = { ...originalData, ...additionalFields };
        }

        // mappingRules가 있으면 규칙에 따라 매핑 (필요한 필드만 추출)
        if (mappingRules && mappingRules.length > 0) {
          const mapped: Record<string, any> = { _id: `receive_${Date.now()}_${index}` };
          for (const rule of mappingRules) {
            mapped[rule.targetField] = raw[rule.sourceField];
          }
          // additionalSources에서 추가된 필드도 유지 (mappingRules에 없는 필드 중 메타가 아닌 것)
          for (const [key, value] of Object.entries(raw)) {
            if (!metaFieldsToStrip.has(key) && !(key in mapped) && !key.startsWith("_")) {
              // 소스 테이블의 컬럼이 아닌 추가 데이터만 유지 (additionalSources 등)
              const isMappingSource = mappingRules.some((r: any) => r.sourceField === key);
              if (!isMappingSource) {
                mapped[key] = value;
              }
            }
          }
          return mapped;
        }

        // mappingRules 없으면 기존 로직: 메타 필드만 제거
        const cleaned: Record<string, any> = {};
        for (const [key, value] of Object.entries(raw)) {
          if (!metaFieldsToStrip.has(key)) {
            cleaned[key] = value;
          }
        }
        return cleaned;
      });

      console.log("📥 [V2Repeater] 매핑 후 데이터:", normalizedData);

      // Entity 조인 해석 (FK → 참조 테이블 데이터)
      normalizedData = await resolveEntityJoins(normalizedData);

      console.log("📥 [V2Repeater] Entity 조인 후 데이터:", normalizedData);

      const mode = configOrMode?.mode || configOrMode || "append";

      // 카테고리 코드 → 라벨 변환
      const codesToResolve = new Set<string>();
      for (const item of normalizedData) {
        for (const [key, val] of Object.entries(item)) {
          if (key.startsWith("_")) continue;
          if (typeof val === "string" && val && !categoryLabelMapRef.current[val]) {
            codesToResolve.add(val as string);
          }
        }
      }

      if (codesToResolve.size > 0) {
        try {
          const resp = await apiClient.post("/table-categories/labels-by-codes", {
            valueCodes: Array.from(codesToResolve),
          });
          if (resp.data?.success && resp.data.data) {
            const labelData = resp.data.data as Record<string, string>;
            setCategoryLabelMap((prev) => ({ ...prev, ...labelData }));
            for (const item of normalizedData) {
              for (const key of Object.keys(item)) {
                if (key.startsWith("_")) continue;
                const val = item[key];
                if (typeof val === "string" && labelData[val]) {
                  item[key] = labelData[val];
                }
              }
            }
          }
        } catch {
          // 변환 실패 시 코드 유지
        }
      }

      setData((prev) => {
        const next = mode === "replace" ? normalizedData : [...prev, ...normalizedData];
        onDataChangeRef.current?.(next);
        return next;
      });

      toast.success(`${normalizedData.length}개 항목이 추가되었습니다.`);
    },
    [resolveEntityJoins],
  );

  useEffect(() => {
    if (screenContext && effectiveComponentId) {
      const receiver: DataReceivable = {
        componentId: effectiveComponentId,
        componentType: "v2-repeater",
        receiveData: handleReceiveData,
      };
      console.log("📋 [V2Repeater] ScreenContext에 데이터 수신자 등록:", effectiveComponentId);
      screenContext.registerDataReceiver(effectiveComponentId, receiver);

      return () => {
        screenContext.unregisterDataReceiver(effectiveComponentId);
      };
    }
  }, [screenContext, effectiveComponentId, handleReceiveData]);

  // 소스 테이블 컬럼 라벨 매핑
  const [sourceColumnLabels, setSourceColumnLabels] = useState<Record<string, string>>({});

  // 🆕 소스 테이블의 카테고리 타입 컬럼 목록
  const [sourceCategoryColumns, setSourceCategoryColumns] = useState<string[]>([]);

  // 🆕 카테고리 코드 → 라벨 매핑 (RepeaterTable 표시용)
  const [categoryLabelMap, setCategoryLabelMap] = useState<Record<string, string>>({});
  const categoryLabelMapRef = useRef<Record<string, string>>({});
  useEffect(() => {
    categoryLabelMapRef.current = categoryLabelMap;
  }, [categoryLabelMap]);

  // 현재 테이블 컬럼 정보 (inputType 매핑용)
  const [currentTableColumnInfo, setCurrentTableColumnInfo] = useState<Record<string, any>>({});

  // 동적 데이터 소스 상태
  const [activeDataSources, setActiveDataSources] = useState<Record<string, string>>({});

  // 🆕 최신 엔티티 참조 정보 (column_labels에서 조회)
  const [resolvedSourceTable, setResolvedSourceTable] = useState<string>("");
  const [resolvedReferenceKey, setResolvedReferenceKey] = useState<string>("id");

  const isModalMode = config.renderMode === "modal";

  // 전역 리피터 등록
  // tableName이 비어있어도 반드시 등록 (repeaterSave 이벤트 발행 가드에 필요)
  useEffect(() => {
    const targetTableName =
      config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName;
    const registrationKey = targetTableName || "__v2_repeater_same_table__";

    if (!window.__v2RepeaterInstances) {
      window.__v2RepeaterInstances = new Set();
    }
    window.__v2RepeaterInstances.add(registrationKey);

    return () => {
      if (window.__v2RepeaterInstances) {
        window.__v2RepeaterInstances.delete(registrationKey);
      }
    };
  }, [config.useCustomTable, config.mainTableName, config.dataSource?.tableName]);

  // 저장 이벤트 리스너 (dataRef/categoryLabelMapRef를 사용하여 항상 최신 상태 참조)
  useEffect(() => {
    const handleSaveEvent = async (event: CustomEvent) => {
      const currentData = dataRef.current;
      const currentCategoryMap = categoryLabelMapRef.current;

      const configTableName =
        config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName;
      const tableName = configTableName || event.detail?.tableName;
      const mainFormData = event.detail?.mainFormData;
      const masterRecordId = event.detail?.masterRecordId || mainFormData?.id;

      console.log("🔵 [V2Repeater] repeaterSave 이벤트 수신:", {
        configTableName,
        tableName,
        masterRecordId,
        dataLength: currentData.length,
        foreignKeyColumn: config.foreignKeyColumn,
        foreignKeySourceColumn: config.foreignKeySourceColumn,
        dataSnapshot: currentData.map((r: any) => ({ id: r.id, item_name: r.item_name })),
      });
      toast.info(`[디버그] V2Repeater 이벤트 수신: ${currentData.length}건, table=${tableName}`);

      if (!tableName || currentData.length === 0) {
        console.warn("🔴 [V2Repeater] 저장 스킵:", { tableName, dataLength: currentData.length });
        toast.warning(`[디버그] V2Repeater 저장 스킵: data=${currentData.length}, table=${tableName}`);
        window.dispatchEvent(new CustomEvent("repeaterSaveComplete"));
        return;
      }

      if (config.foreignKeyColumn) {
        const sourceCol = config.foreignKeySourceColumn;
        const hasFkSource = sourceCol && mainFormData && mainFormData[sourceCol] !== undefined;
        if (!hasFkSource && !masterRecordId) {
          console.warn("🔴 [V2Repeater] FK 소스 값/masterRecordId 모두 없어 저장 스킵");
          window.dispatchEvent(new CustomEvent("repeaterSaveComplete"));
          return;
        }
      }

      console.log("V2Repeater 저장 시작", {
        tableName,
        foreignKeyColumn: config.foreignKeyColumn,
        masterRecordId,
        dataLength: currentData.length,
      });

      try {
        let validColumns: Set<string> = new Set();
        try {
          const columnsResponse = await apiClient.get(`/table-management/tables/${tableName}/columns`);
          const columns =
            columnsResponse.data?.data?.columns || columnsResponse.data?.columns || columnsResponse.data || [];
          validColumns = new Set(columns.map((col: any) => col.columnName || col.column_name || col.name));
        } catch {
          console.warn("테이블 컬럼 정보 조회 실패");
        }

        for (let i = 0; i < currentData.length; i++) {
          const row = currentData[i];
          const cleanRow = Object.fromEntries(Object.entries(row).filter(([key]) => !key.startsWith("_")));

          let mergedData: Record<string, any>;
          if (config.useCustomTable && config.mainTableName) {
            mergedData = { ...cleanRow };

            if (config.foreignKeyColumn) {
              const sourceColumn = config.foreignKeySourceColumn;
              let fkValue: any;

              if (sourceColumn && mainFormData && mainFormData[sourceColumn] !== undefined) {
                fkValue = mainFormData[sourceColumn];
              } else {
                fkValue = masterRecordId;
              }

              if (fkValue !== undefined && fkValue !== null) {
                mergedData[config.foreignKeyColumn] = fkValue;
              }
            }
          } else {
            const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {};
            mergedData = {
              ...mainFormDataWithoutId,
              ...cleanRow,
            };
          }

          const filteredData: Record<string, any> = {};
          for (const [key, value] of Object.entries(mergedData)) {
            if (validColumns.size === 0 || validColumns.has(key)) {
              if (typeof value === "string" && currentCategoryMap[value]) {
                filteredData[key] = currentCategoryMap[value];
              } else {
                filteredData[key] = value;
              }
            }
          }

          const rowId = row.id;
          console.log(`🔧 [V2Repeater] 행 ${i} 저장:`, {
            rowId,
            isUpdate: rowId && typeof rowId === "string" && rowId.includes("-"),
            filteredDataKeys: Object.keys(filteredData),
          });
          if (rowId && typeof rowId === "string" && rowId.includes("-")) {
            const { id: _, created_date: _cd, updated_date: _ud, ...updateFields } = filteredData;
            await apiClient.put(`/table-management/tables/${tableName}/edit`, {
              originalData: { id: rowId },
              updatedData: updateFields,
            });
          } else {
            await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData);
          }
        }
        // 삭제된 행 처리: 원본에는 있었지만 현재 data에 없는 ID를 DELETE
        const currentIds = new Set(currentData.map((r) => r.id).filter(Boolean));
        const deletedIds = Array.from(loadedIdsRef.current).filter((id) => !currentIds.has(id));
        if (deletedIds.length > 0) {
          console.log("🗑️ [V2Repeater] 삭제할 행:", deletedIds);
          try {
            await apiClient.delete(`/table-management/tables/${tableName}/delete`, {
              data: deletedIds.map((id) => ({ id })),
            });
            console.log(`✅ [V2Repeater] ${deletedIds.length}건 삭제 완료`);
          } catch (deleteError) {
            console.error("❌ [V2Repeater] 삭제 실패:", deleteError);
          }
        }

        // 저장 완료 후 loadedIdsRef 갱신
        loadedIdsRef.current = new Set(currentData.map((r) => r.id).filter(Boolean));

        toast.success(`V2Repeater ${currentData.length}건 저장 완료`);
      } catch (error) {
        console.error("❌ V2Repeater 저장 실패:", error);
        toast.error(`V2Repeater 저장 실패: ${error}`);
      } finally {
        window.dispatchEvent(new CustomEvent("repeaterSaveComplete"));
      }
    };

    const unsubscribe = v2EventBus.subscribe(
      V2_EVENTS.REPEATER_SAVE,
      async (payload) => {
        const configTableName =
          config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName;
        if (!configTableName || payload.tableName === configTableName) {
          await handleSaveEvent({ detail: payload } as CustomEvent);
        }
      },
      { componentId: `v2-repeater-${config.dataSource?.tableName || "same-table"}` },
    );

    window.addEventListener("repeaterSave" as any, handleSaveEvent);
    return () => {
      unsubscribe();
      window.removeEventListener("repeaterSave" as any, handleSaveEvent);
    };
  }, [
    config.dataSource?.tableName,
    config.useCustomTable,
    config.mainTableName,
    config.foreignKeyColumn,
    config.foreignKeySourceColumn,
    parentId,
  ]);

  // 수정 모드: useCustomTable + FK 기반으로 기존 디테일 데이터 자동 로드
  const dataLoadedRef = useRef(false);
  useEffect(() => {
    if (dataLoadedRef.current) return;
    if (!config.useCustomTable || !config.mainTableName || !config.foreignKeyColumn) return;
    if (!parentFormData) return;

    const fkSourceColumn = config.foreignKeySourceColumn || config.foreignKeyColumn;
    const fkValue = parentFormData[fkSourceColumn];
    if (!fkValue) return;

    // 이미 데이터가 있으면 로드하지 않음
    if (data.length > 0) return;

    const loadExistingData = async () => {
      try {
        console.log("📥 [V2Repeater] 수정 모드 데이터 로드:", {
          tableName: config.mainTableName,
          fkColumn: config.foreignKeyColumn,
          fkValue,
        });

        let rows: any[] = [];
        const useEntityJoinForLoad = config.sourceDetailConfig?.useEntityJoin;

        if (useEntityJoinForLoad) {
          // 엔티티 조인을 사용하여 데이터 로드 (part_code → item_info 자동 조인)
          const searchParam = JSON.stringify({ [config.foreignKeyColumn!]: fkValue });
          const params: Record<string, any> = {
            page: 1,
            size: 1000,
            search: searchParam,
            enableEntityJoin: true,
            autoFilter: JSON.stringify({ enabled: true }),
          };
          const addJoinCols = config.sourceDetailConfig?.additionalJoinColumns;
          if (addJoinCols && addJoinCols.length > 0) {
            params.additionalJoinColumns = JSON.stringify(addJoinCols);
          }
          const response = await apiClient.get(
            `/table-management/tables/${config.mainTableName}/data-with-joins`,
            { params }
          );
          const resultData = response.data?.data;
          const rawRows = Array.isArray(resultData)
            ? resultData
            : resultData?.data || resultData?.rows || [];
          // 엔티티 조인 시 참조 테이블에 중복 레코드가 있으면 행이 늘어날 수 있으므로 id 기준 중복 제거
          const seenIds = new Set<string>();
          rows = rawRows.filter((row: any) => {
            if (!row.id || seenIds.has(row.id)) return false;
            seenIds.add(row.id);
            return true;
          });
        } else {
          const response = await apiClient.post(
            `/table-management/tables/${config.mainTableName}/data`,
            {
              page: 1,
              size: 1000,
              dataFilter: {
                enabled: true,
                filters: [{ columnName: config.foreignKeyColumn, operator: "equals", value: fkValue }],
              },
              autoFilter: true,
            }
          );
          rows = response.data?.data?.data || response.data?.data?.rows || response.data?.rows || [];
        }

        if (Array.isArray(rows) && rows.length > 0) {
          console.log(`✅ [V2Repeater] 기존 데이터 ${rows.length}건 로드 완료`, useEntityJoinForLoad ? "(엔티티 조인)" : "");

          // 엔티티 조인 사용 시: columnMapping으로 _display_ 필드 보강
          const columnMapping = config.sourceDetailConfig?.columnMapping;
          if (useEntityJoinForLoad && columnMapping) {
            const sourceDisplayColumns = config.columns.filter((col) => col.isSourceDisplay);
            rows.forEach((row: any) => {
              sourceDisplayColumns.forEach((col) => {
                const mappedKey = columnMapping[col.key];
                const value = mappedKey ? row[mappedKey] : row[col.key];
                row[`_display_${col.key}`] = value ?? "";
              });
            });
            console.log("✅ [V2Repeater] 엔티티 조인 표시 데이터 보강 완료");
          }

          // isSourceDisplay 컬럼이 있으면 소스 테이블에서 표시 데이터 보강 (엔티티 조인 미사용 시)
          if (!useEntityJoinForLoad) {
            const sourceDisplayColumns = config.columns.filter((col) => col.isSourceDisplay);
            const sourceTable = config.dataSource?.sourceTable;
            const fkColumn = config.dataSource?.foreignKey;
            const refKey = config.dataSource?.referenceKey || "id";

            if (sourceDisplayColumns.length > 0 && sourceTable && fkColumn) {
              try {
                const fkValues = rows.map((row) => row[fkColumn]).filter(Boolean);
                const uniqueValues = [...new Set(fkValues)];

                if (uniqueValues.length > 0) {
                  const sourcePromises = uniqueValues.map((val) =>
                    apiClient.post(`/table-management/tables/${sourceTable}/data`, {
                      page: 1, size: 1,
                      search: { [refKey]: val },
                      autoFilter: true,
                    }).then((r) => r.data?.data?.data || r.data?.data?.rows || [])
                      .catch(() => [])
                  );
                  const sourceResults = await Promise.all(sourcePromises);
                  const sourceMap = new Map<string, any>();
                  sourceResults.flat().forEach((sr: any) => {
                    if (sr[refKey]) sourceMap.set(String(sr[refKey]), sr);
                  });

                  rows.forEach((row: any) => {
                    const sourceRecord = sourceMap.get(String(row[fkColumn]));
                    if (sourceRecord) {
                      sourceDisplayColumns.forEach((col) => {
                        const displayValue = sourceRecord[col.key] ?? null;
                        row[col.key] = displayValue;
                        row[`_display_${col.key}`] = displayValue;
                      });
                    }
                  });
                  console.log("✅ [V2Repeater] 소스 테이블 표시 데이터 보강 완료");
                }
              } catch (sourceError) {
                console.warn("⚠️ [V2Repeater] 소스 테이블 조회 실패 (표시만 영향):", sourceError);
              }
            }
          }

          // DB에서 로드된 데이터 중 CATEGORY_ 코드가 있으면 라벨로 변환
          const codesToResolve = new Set<string>();
          for (const row of rows) {
            for (const val of Object.values(row)) {
              if (typeof val === "string" && val.startsWith("CATEGORY_")) {
                codesToResolve.add(val);
              }
            }
          }

          if (codesToResolve.size > 0) {
            try {
              const labelResp = await apiClient.post("/table-categories/labels-by-codes", {
                valueCodes: Array.from(codesToResolve),
              });
              if (labelResp.data?.success && labelResp.data.data) {
                const labelData = labelResp.data.data as Record<string, string>;
                setCategoryLabelMap((prev) => ({ ...prev, ...labelData }));
                for (const row of rows) {
                  for (const key of Object.keys(row)) {
                    if (key.startsWith("_")) continue;
                    const val = row[key];
                    if (typeof val === "string" && labelData[val]) {
                      row[key] = labelData[val];
                    }
                  }
                }
              }
            } catch {
              // 라벨 변환 실패 시 코드 유지
            }
          }

          // 원본 ID 목록 기록 (삭제 추적용)
          const ids = rows.map((r: any) => r.id).filter(Boolean);
          loadedIdsRef.current = new Set(ids);
          console.log("📋 [V2Repeater] 원본 ID 기록:", ids);

          setData(rows);
          dataLoadedRef.current = true;
          if (onDataChange) onDataChange(rows);
        }
      } catch (error) {
        console.error("[V2Repeater] 기존 데이터 로드 실패:", error);
      }
    };

    loadExistingData();
  }, [
    config.useCustomTable,
    config.mainTableName,
    config.foreignKeyColumn,
    config.foreignKeySourceColumn,
    parentFormData,
    data.length,
    onDataChange,
  ]);

  // 현재 테이블 컬럼 정보 로드
  useEffect(() => {
    const loadCurrentTableColumnInfo = async () => {
      const tableName = config.dataSource?.tableName;
      if (!tableName) return;

      try {
        const [colResponse, typeResponse] = await Promise.all([
          apiClient.get(`/table-management/tables/${tableName}/columns`),
          apiClient.get(`/table-management/tables/${tableName}/web-types`),
        ]);
        const columns = colResponse.data?.data?.columns || colResponse.data?.columns || colResponse.data || [];
        const inputTypes = typeResponse.data?.data || [];

        // inputType/categoryRef 매핑 생성
        const typeMap: Record<string, any> = {};
        inputTypes.forEach((t: any) => {
          typeMap[t.columnName] = t;
        });

        const columnMap: Record<string, any> = {};
        columns.forEach((col: any) => {
          const name = col.columnName || col.column_name || col.name;
          const typeInfo = typeMap[name];
          columnMap[name] = {
            inputType: typeInfo?.inputType || col.inputType || col.input_type || col.webType || "text",
            displayName: col.displayName || col.display_name || col.label || name,
            detailSettings: col.detailSettings || col.detail_settings,
            categoryRef: typeInfo?.categoryRef || null,
          };
        });
        setCurrentTableColumnInfo(columnMap);
      } catch (error) {
        console.error("컬럼 정보 로드 실패:", error);
      }
    };
    loadCurrentTableColumnInfo();
  }, [config.dataSource?.tableName]);

  // 🆕 FK 컬럼 기반으로 최신 참조 테이블 정보 조회 (column_labels에서)
  useEffect(() => {
    const resolveEntityReference = async () => {
      const tableName = config.dataSource?.tableName;
      const foreignKey = config.dataSource?.foreignKey;

      if (!isModalMode || !tableName || !foreignKey) {
        // config에 저장된 값을 기본값으로 사용
        setResolvedSourceTable(config.dataSource?.sourceTable || "");
        setResolvedReferenceKey(config.dataSource?.referenceKey || "id");
        return;
      }

      try {
        // 현재 테이블의 컬럼 정보에서 FK 컬럼의 참조 테이블 조회
        const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
        const columns = response.data?.data?.columns || response.data?.columns || response.data || [];

        const fkColumn = columns.find((col: any) => (col.columnName || col.column_name || col.name) === foreignKey);

        if (fkColumn) {
          // column_labels의 reference_table 사용 (항상 최신값)
          const refTable =
            fkColumn.detailSettings?.referenceTable ||
            fkColumn.reference_table ||
            fkColumn.referenceTable ||
            config.dataSource?.sourceTable ||
            "";
          const refKey =
            fkColumn.detailSettings?.referenceColumn ||
            fkColumn.reference_column ||
            fkColumn.referenceColumn ||
            config.dataSource?.referenceKey ||
            "id";

          setResolvedSourceTable(refTable);
          setResolvedReferenceKey(refKey);
        } else {
          // FK 컬럼을 찾지 못한 경우 config 값 사용
          setResolvedSourceTable(config.dataSource?.sourceTable || "");
          setResolvedReferenceKey(config.dataSource?.referenceKey || "id");
        }
      } catch (error) {
        console.error("엔티티 참조 정보 조회 실패:", error);
        // 오류 시 config 값 사용
        setResolvedSourceTable(config.dataSource?.sourceTable || "");
        setResolvedReferenceKey(config.dataSource?.referenceKey || "id");
      }
    };

    resolveEntityReference();
  }, [
    config.dataSource?.tableName,
    config.dataSource?.foreignKey,
    config.dataSource?.sourceTable,
    config.dataSource?.referenceKey,
    isModalMode,
  ]);

  // 소스 테이블 컬럼 라벨 로드 (modal 모드) - resolvedSourceTable 사용
  // 🆕 카테고리 타입 컬럼도 함께 감지
  useEffect(() => {
    const loadSourceColumnLabels = async () => {
      if (!isModalMode || !resolvedSourceTable) return;

      try {
        const response = await apiClient.get(`/table-management/tables/${resolvedSourceTable}/columns`);
        const columns = response.data?.data?.columns || response.data?.columns || response.data || [];

        const labels: Record<string, string> = {};
        const categoryCols: string[] = [];

        columns.forEach((col: any) => {
          const name = col.columnName || col.column_name || col.name;
          labels[name] = col.displayName || col.display_name || col.label || name;

          // 🆕 카테고리 타입 컬럼 감지
          const inputType = col.inputType || col.input_type || "";
          if (inputType === "category") {
            categoryCols.push(name);
          }
        });

        setSourceColumnLabels(labels);
        setSourceCategoryColumns(categoryCols);
      } catch (error) {
        console.error("소스 컬럼 라벨 로드 실패:", error);
      }
    };
    loadSourceColumnLabels();
  }, [resolvedSourceTable, isModalMode]);

  // V2ColumnConfig → RepeaterColumnConfig 변환
  // 🆕 모든 컬럼을 columns 배열의 순서대로 처리 (isSourceDisplay 플래그로 구분)
  const repeaterColumns: RepeaterColumnConfig[] = useMemo(() => {
    return config.columns
      .filter((col: V2ColumnConfig) => col.visible !== false)
      .map((col: V2ColumnConfig): RepeaterColumnConfig => {
        const colInfo = currentTableColumnInfo[col.key];
        const inputType = col.inputType || colInfo?.inputType || "text";

        // 소스 표시 컬럼인 경우 (모달 모드에서 읽기 전용)
        if (col.isSourceDisplay) {
          const label = col.title || sourceColumnLabels[col.key] || col.key;
          return {
            field: `_display_${col.key}`,
            label,
            type: "text",
            editable: false,
            calculated: true,
            width: col.width === "auto" ? undefined : col.width,
          };
        }

        // 일반 입력 컬럼
        let type: "text" | "number" | "date" | "select" | "category" = "text";
        if (inputType === "number" || inputType === "decimal") type = "number";
        else if (inputType === "date" || inputType === "datetime") type = "date";
        else if (inputType === "code") type = "select";
        else if (inputType === "category") type = "category"; // 🆕 카테고리 타입

        // 카테고리 참조 ID 결정
        // DB의 category_ref 설정 우선, 없으면 자기 테이블.컬럼명 사용
        let categoryRef: string | undefined;
        if (inputType === "category") {
          const dbCategoryRef = colInfo?.detailSettings?.categoryRef || colInfo?.categoryRef;
          if (dbCategoryRef) {
            categoryRef = dbCategoryRef;
          } else {
            const tableName = col.isSourceDisplay ? resolvedSourceTable : config.dataSource?.tableName;
            if (tableName) {
              categoryRef = `${tableName}.${col.key}`;
            }
          }
        }

        return {
          field: col.key,
          label: col.title || colInfo?.displayName || col.key,
          type,
          editable: col.editable !== false,
          width: col.width === "auto" ? undefined : col.width,
          required: false,
          categoryRef, // 🆕 카테고리 참조 ID 전달
          hidden: col.hidden, // 🆕 히든 처리
          autoFill: col.autoFill, // 🆕 자동 입력 설정
        };
      });
  }, [config.columns, sourceColumnLabels, currentTableColumnInfo, resolvedSourceTable, config.dataSource?.tableName]);

  // 리피터 컬럼 설정에서 카테고리 타입 컬럼 자동 감지
  // repeaterColumns의 resolved type 사용 (config + DB 메타데이터 모두 반영)
  const allCategoryColumns = useMemo(() => {
    const fromRepeater = repeaterColumns
      .filter((col) => col.type === "category")
      .map((col) => col.field.replace(/^_display_/, ""));
    const merged = new Set([...sourceCategoryColumns, ...fromRepeater]);
    return Array.from(merged);
  }, [sourceCategoryColumns, repeaterColumns]);

  // CATEGORY_ 코드 배열을 받아 라벨을 일괄 조회하는 함수
  const fetchCategoryLabels = useCallback(async (codes: string[]) => {
    if (codes.length === 0) return;
    try {
      const response = await apiClient.post("/table-categories/labels-by-codes", {
        valueCodes: codes,
      });
      if (response.data?.success && response.data.data) {
        setCategoryLabelMap((prev) => ({ ...prev, ...response.data.data }));
      }
    } catch (error) {
      console.error("카테고리 라벨 조회 실패:", error);
    }
  }, []);

  // parentFormData(마스터 행)에서 카테고리 코드를 미리 로드
  // fromMainForm autoFill에서 참조할 마스터 필드의 라벨을 사전에 확보
  useEffect(() => {
    if (!parentFormData) return;
    const codes: string[] = [];

    // fromMainForm autoFill의 sourceField 값 중 카테고리 컬럼에 해당하는 것만 수집
    for (const col of config.columns) {
      if (col.autoFill?.type === "fromMainForm" && col.autoFill.sourceField) {
        const val = parentFormData[col.autoFill.sourceField];
        if (typeof val === "string" && val && !categoryLabelMap[val]) {
          codes.push(val);
        }
      }
      // receiveFromParent 패턴
      if ((col as any).receiveFromParent) {
        const parentField = (col as any).parentFieldName || col.key;
        const val = parentFormData[parentField];
        if (typeof val === "string" && val && !categoryLabelMap[val]) {
          codes.push(val);
        }
      }
    }

    if (codes.length > 0) {
      fetchCategoryLabels(codes);
    }
  }, [parentFormData, config.columns, fetchCategoryLabels]);

  // 데이터 변경 시 카테고리 라벨 로드
  useEffect(() => {
    if (data.length === 0) return;

    const allCodes = new Set<string>();

    for (const row of data) {
      for (const col of allCategoryColumns) {
        const val = row[`_display_${col}`] || row[col];
        if (val && typeof val === "string") {
          val.split(",").map((c: string) => c.trim()).filter(Boolean).forEach((code: string) => {
            if (!categoryLabelMap[code]) allCodes.add(code);
          });
        }
      }
    }

    fetchCategoryLabels(Array.from(allCodes));
  }, [data, allCategoryColumns, fetchCategoryLabels]);

  // 계산 규칙 적용 (소스 테이블의 _display_* 필드도 참조 가능)
  const applyCalculationRules = useCallback(
    (row: any): any => {
      const rules = config.calculationRules;
      if (!rules || rules.length === 0) return row;

      const updatedRow = { ...row };
      for (const rule of rules) {
        if (!rule.targetColumn || !rule.formula) continue;
        try {
          let formula = rule.formula;
          const fieldMatches = formula.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || [];
          for (const field of fieldMatches) {
            if (field === rule.targetColumn) continue;
            // 직접 필드 → _display_* 필드 순으로 값 탐색
            const raw = updatedRow[field] ?? updatedRow[`_display_${field}`];
            const value = parseFloat(raw) || 0;
            formula = formula.replace(new RegExp(`\\b${field}\\b`, "g"), value.toString());
          }
          updatedRow[rule.targetColumn] = new Function(`return ${formula}`)();
        } catch {
          updatedRow[rule.targetColumn] = 0;
        }
      }
      return updatedRow;
    },
    [config.calculationRules],
  );

  // _targetTable 메타데이터 포함하여 onDataChange 호출
  const notifyDataChange = useCallback(
    (newData: any[]) => {
      if (!onDataChange) return;
      const targetTable =
        config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName;
      if (targetTable) {
        onDataChange(newData.map((row) => ({ ...row, _targetTable: targetTable })));
      } else {
        onDataChange(newData);
      }
    },
    [onDataChange, config.useCustomTable, config.mainTableName, config.dataSource?.tableName],
  );

  // 데이터 변경 핸들러
  const handleDataChange = useCallback(
    (newData: any[]) => {
      const calculated = newData.map(applyCalculationRules);
      setData(calculated);
      notifyDataChange(calculated);
      setAutoWidthTrigger((prev) => prev + 1);
    },
    [applyCalculationRules, notifyDataChange],
  );

  // 행 변경 핸들러
  const handleRowChange = useCallback(
    (index: number, newRow: any) => {
      const calculated = applyCalculationRules(newRow);
      const newData = [...data];
      newData[index] = calculated;
      setData(newData);
      notifyDataChange(newData);
    },
    [data, applyCalculationRules, notifyDataChange],
  );

  // 행 삭제 핸들러
  const handleRowDelete = useCallback(
    (index: number) => {
      const newData = data.filter((_, i) => i !== index);
      handleDataChange(newData); // 🆕 handleDataChange 사용

      // 선택 상태 업데이트
      const newSelected = new Set<number>();
      selectedRows.forEach((i) => {
        if (i < index) newSelected.add(i);
        else if (i > index) newSelected.add(i - 1);
      });
      setSelectedRows(newSelected);
    },
    [data, selectedRows, handleDataChange],
  );

  // 일괄 삭제 핸들러
  const handleBulkDelete = useCallback(() => {
    const newData = data.filter((_, index) => !selectedRows.has(index));
    handleDataChange(newData); // 🆕 handleDataChange 사용
    setSelectedRows(new Set());
  }, [data, selectedRows, handleDataChange]);

  // 행 추가 (inline 모드)
  // 🆕 자동 입력 값 생성 함수 (동기 - 채번 제외)
  const generateAutoFillValueSync = useCallback(
    (col: any, rowIndex: number, mainFormData?: Record<string, unknown>) => {
      if (!col.autoFill || col.autoFill.type === "none") return undefined;

      const now = new Date();

      const pad = (n: number) => String(n).padStart(2, "0");
      const localDate = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
      const localTime = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
      switch (col.autoFill.type) {
        case "currentDate":
          return localDate;

        case "currentDateTime":
          return `${localDate} ${localTime}`;

        case "sequence":
          return rowIndex + 1; // 1부터 시작하는 순번

        case "numbering":
          // 채번은 별도 비동기 처리 필요
          return null; // null 반환하여 비동기 처리 필요함을 표시

        case "fromMainForm":
          if (col.autoFill.sourceField && mainFormData) {
            const rawValue = mainFormData[col.autoFill.sourceField];
            // categoryLabelMap에 매핑이 있으면 라벨로 변환 (접두사 무관)
            if (typeof rawValue === "string" && categoryLabelMap[rawValue]) {
              return categoryLabelMap[rawValue];
            }
            return rawValue;
          }
          return "";

        case "fixed":
          return col.autoFill.fixedValue ?? "";

        case "parentSequence": {
          const parentField = col.autoFill.parentField;
          const separator = col.autoFill.separator ?? "-";
          const seqLength = col.autoFill.sequenceLength ?? 2;
          const parentValue = parentField && mainFormData ? String(mainFormData[parentField] ?? "") : "";
          const seqNum = String(rowIndex + 1).padStart(seqLength, "0");
          return parentValue ? `${parentValue}${separator}${seqNum}` : seqNum;
        }

        default:
          return undefined;
      }
    },
    [categoryLabelMap],
  );

  // 🆕 채번 API 호출 (비동기)
  // 🆕 수동 입력 부분 지원을 위해 userInputCode 파라미터 추가
  const generateNumberingCode = useCallback(
    async (ruleId: string, userInputCode?: string, formData?: Record<string, any>): Promise<string> => {
      try {
        const result = await allocateNumberingCode(ruleId, userInputCode, formData);
        if (result.success && result.data?.generatedCode) {
          return result.data.generatedCode;
        }
        console.error("채번 실패:", result.error);
        return "";
      } catch (error) {
        console.error("채번 API 호출 실패:", error);
        return "";
      }
    },
    [],
  );

  // sourceDetailConfig가 설정되고 groupedData(모달에서 전달된 마스터 데이터)가 있으면
  // 마스터의 키를 추출하여 디테일 테이블에서 행을 조회 → 리피터에 자동 세팅
  const sourceDetailLoadedRef = useRef(false);
  useEffect(() => {
    if (sourceDetailLoadedRef.current) return;
    if (!groupedData || groupedData.length === 0) return;
    if (!config.sourceDetailConfig) return;

    const { tableName, foreignKey, parentKey } = config.sourceDetailConfig;
    if (!tableName || !foreignKey || !parentKey) return;

    const parentKeys = groupedData
      .map((row) => row[parentKey])
      .filter((v) => v !== undefined && v !== null && v !== "");

    if (parentKeys.length === 0) return;

    sourceDetailLoadedRef.current = true;

    const loadSourceDetails = async () => {
      try {
        const uniqueKeys = [...new Set(parentKeys)] as string[];
        const { useEntityJoin, columnMapping, additionalJoinColumns } = config.sourceDetailConfig!;

        let detailRows: any[] = [];

        if (useEntityJoin) {
          // data-with-joins GET API 사용 (엔티티 조인 자동 적용)
          const searchParam = JSON.stringify({ [foreignKey]: uniqueKeys.join("|") });
          const params: Record<string, any> = {
            page: 1,
            size: 9999,
            search: searchParam,
            enableEntityJoin: true,
            autoFilter: JSON.stringify({ enabled: true }),
          };
          if (additionalJoinColumns && additionalJoinColumns.length > 0) {
            params.additionalJoinColumns = JSON.stringify(additionalJoinColumns);
          }
          const resp = await apiClient.get(`/table-management/tables/${tableName}/data-with-joins`, { params });
          const resultData = resp.data?.data;
          const rawRows = Array.isArray(resultData)
            ? resultData
            : resultData?.data || resultData?.rows || [];
          // 엔티티 조인 시 참조 테이블에 중복 레코드가 있으면 행이 늘어나므로 id 기준 중복 제거
          const seenIds = new Set<string>();
          detailRows = rawRows.filter((row: any) => {
            if (!row.id || seenIds.has(row.id)) return false;
            seenIds.add(row.id);
            return true;
          });
        } else {
          // 기존 POST API 사용
          const resp = await apiClient.post(`/table-management/tables/${tableName}/data`, {
            page: 1,
            size: 9999,
            search: { [foreignKey]: uniqueKeys },
          });
          const resultData = resp.data?.data;
          detailRows = Array.isArray(resultData)
            ? resultData
            : resultData?.data || resultData?.rows || [];
        }

        if (detailRows.length === 0) {
          console.warn("[V2Repeater] sourceDetail 조회 결과 없음:", { tableName, uniqueKeys });
          return;
        }

        console.log("[V2Repeater] sourceDetail 조회 완료:", detailRows.length, "건", useEntityJoin ? "(엔티티 조인)" : "");

        // 디테일 행을 리피터 컬럼에 매핑
        const newRows = detailRows.map((detail, index) => {
          const row: any = { _id: `src_detail_${Date.now()}_${index}` };
          for (const col of config.columns) {
            if (col.isSourceDisplay) {
              // columnMapping이 있으면 조인 alias에서 값 가져오기 (표시용)
              const mappedKey = columnMapping?.[col.key];
              const value = mappedKey ? detail[mappedKey] : detail[col.key];
              row[`_display_${col.key}`] = value ?? "";
              // 원본 값도 저장 (DB persist용 - _display_ 접두사 없이)
              if (detail[col.key] !== undefined) {
                row[col.key] = detail[col.key];
              }
            } else if (col.autoFill) {
              const autoValue = generateAutoFillValueSync(col, index, parentFormData);
              row[col.key] = autoValue ?? "";
            } else if (col.sourceKey && detail[col.sourceKey] !== undefined) {
              row[col.key] = detail[col.sourceKey];
            } else if (detail[col.key] !== undefined) {
              row[col.key] = detail[col.key];
            } else {
              row[col.key] = "";
            }
          }
          return row;
        });

        setData(newRows);
        onDataChange?.(newRows);
      } catch (error) {
        console.error("[V2Repeater] sourceDetail 조회 실패:", error);
      }
    };

    loadSourceDetails();
  }, [groupedData, config.sourceDetailConfig, config.columns, generateAutoFillValueSync, parentFormData, onDataChange]);

  // parentSequence 컬럼의 부모 필드 값이 변경되면 행 데이터 갱신
  useEffect(() => {
    if (data.length === 0) return;

    const parentSeqColumns = config.columns.filter(
      (col) => col.autoFill?.type === "parentSequence" && col.autoFill.parentField,
    );
    if (parentSeqColumns.length === 0) return;

    let needsUpdate = false;
    const updatedData = data.map((row, index) => {
      const updatedRow = { ...row };
      for (const col of parentSeqColumns) {
        const newValue = generateAutoFillValueSync(col, index, parentFormData);
        if (newValue !== undefined && newValue !== row[col.key]) {
          updatedRow[col.key] = newValue;
          needsUpdate = true;
        }
      }
      return updatedRow;
    });

    if (needsUpdate) {
      setData(updatedData);
      onDataChange?.(updatedData);
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [parentFormData, config.columns, generateAutoFillValueSync]);

  // 행 추가 (inline 모드 또는 모달 열기)
  const handleAddRow = useCallback(async () => {
    if (isModalMode) {
      setModalOpen(true);
    } else {
      const newRow: any = { _id: `new_${Date.now()}` };
      const currentRowCount = data.length;

      // 동기적 자동 입력 값 적용
      for (const col of config.columns) {
        const autoValue = generateAutoFillValueSync(col, currentRowCount, parentFormData);
        if (autoValue === null && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId) {
          newRow[col.key] = await generateNumberingCode(col.autoFill.numberingRuleId);
        } else if (autoValue !== undefined) {
          newRow[col.key] = autoValue;
        } else {
          newRow[col.key] = "";
        }
      }

      // fromMainForm 등으로 넘어온 카테고리 코드 → 라벨 변환
      // allCategoryColumns에 해당하는 컬럼이거나 categoryLabelMap에 매핑이 있으면 변환
      const categoryColSet = new Set(allCategoryColumns);
      const unresolvedCodes: string[] = [];
      for (const col of config.columns) {
        const val = newRow[col.key];
        if (typeof val !== "string" || !val) continue;

        // 이 컬럼이 카테고리 타입이거나, fromMainForm으로 가져온 값인 경우
        const isCategoryCol = categoryColSet.has(col.key);
        const isFromMainForm = col.autoFill?.type === "fromMainForm";

        if (isCategoryCol || isFromMainForm) {
          if (categoryLabelMap[val]) {
            newRow[col.key] = categoryLabelMap[val];
          } else {
            unresolvedCodes.push(val);
          }
        }
      }

      if (unresolvedCodes.length > 0) {
        try {
          const resp = await apiClient.post("/table-categories/labels-by-codes", {
            valueCodes: unresolvedCodes,
          });
          if (resp.data?.success && resp.data.data) {
            const labelData = resp.data.data as Record<string, string>;
            setCategoryLabelMap((prev) => ({ ...prev, ...labelData }));
            for (const col of config.columns) {
              const val = newRow[col.key];
              if (typeof val === "string" && labelData[val]) {
                newRow[col.key] = labelData[val];
              }
            }
          }
        } catch {
          // 변환 실패 시 코드 유지
        }
      }

      const newData = [...data, newRow];
      handleDataChange(newData);
    }
  }, [isModalMode, config.columns, data, handleDataChange, generateAutoFillValueSync, generateNumberingCode, parentFormData, categoryLabelMap, allCategoryColumns]);

  // 모달에서 항목 선택 - 비동기로 변경
  const handleSelectItems = useCallback(
    async (items: Record<string, unknown>[]) => {
      const fkColumn = config.dataSource?.foreignKey;
      const currentRowCount = data.length;

      // 채번이 필요한 컬럼 찾기
      const numberingColumns = config.columns.filter(
        (col) => !col.isSourceDisplay && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId,
      );

      const newRows = await Promise.all(
        items.map(async (item, index) => {
          const row: any = { _id: `new_${Date.now()}_${Math.random()}` };

          // FK 값 저장 (resolvedReferenceKey 사용)
          if (fkColumn && item[resolvedReferenceKey]) {
            row[fkColumn] = item[resolvedReferenceKey];
          }

          // 모든 컬럼 처리 (순서대로)
          for (const col of config.columns) {
            if (col.isSourceDisplay) {
              let displayVal = item[col.key] || "";
              // 카테고리 컬럼이면 코드→라벨 변환 (접두사 무관)
              if (typeof displayVal === "string" && categoryLabelMap[displayVal]) {
                displayVal = categoryLabelMap[displayVal];
              }
              row[`_display_${col.key}`] = displayVal;
            } else {
              // 자동 입력 값 적용
              const autoValue = generateAutoFillValueSync(col, currentRowCount + index, parentFormData);
              if (autoValue === null && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId) {
                // 채번 규칙: 즉시 API 호출
                row[col.key] = await generateNumberingCode(col.autoFill.numberingRuleId);
              } else if (autoValue !== undefined) {
                row[col.key] = autoValue;
              } else if (row[col.key] === undefined) {
                // 입력 컬럼: 빈 값으로 초기화
                row[col.key] = "";
              }
            }
          }

          return row;
        }),
      );

      // 카테고리/fromMainForm 컬럼에서 미해결 코드 수집 및 변환
      const categoryColSet = new Set(allCategoryColumns);
      const unresolvedCodes = new Set<string>();
      for (const row of newRows) {
        for (const col of config.columns) {
          const val = row[col.key];
          if (typeof val !== "string" || !val) continue;
          const isCategoryCol = categoryColSet.has(col.key);
          const isFromMainForm = col.autoFill?.type === "fromMainForm";
          if ((isCategoryCol || isFromMainForm) && !categoryLabelMap[val]) {
            unresolvedCodes.add(val);
          }
        }
      }

      if (unresolvedCodes.size > 0) {
        try {
          const resp = await apiClient.post("/table-categories/labels-by-codes", {
            valueCodes: Array.from(unresolvedCodes),
          });
          if (resp.data?.success && resp.data.data) {
            const labelData = resp.data.data as Record<string, string>;
            setCategoryLabelMap((prev) => ({ ...prev, ...labelData }));
            for (const row of newRows) {
              for (const col of config.columns) {
                const val = row[col.key];
                if (typeof val === "string" && labelData[val]) {
                  row[col.key] = labelData[val];
                }
              }
            }
          }
        } catch {
          // 변환 실패 시 코드 유지
        }
      }

      const newData = [...data, ...newRows];
      handleDataChange(newData);
      setModalOpen(false);
    },
    [
      config.dataSource?.foreignKey,
      resolvedReferenceKey,
      config.columns,
      data,
      handleDataChange,
      generateAutoFillValueSync,
      generateNumberingCode,
      parentFormData,
      categoryLabelMap,
      allCategoryColumns,
    ],
  );

  // 소스 컬럼 목록 (모달용) - 🆕 columns 배열에서 isSourceDisplay인 것만 필터링
  const sourceColumns = useMemo(() => {
    return config.columns
      .filter((col) => col.isSourceDisplay && col.visible !== false)
      .map((col) => col.key)
      .filter((key) => key && key !== "none");
  }, [config.columns]);

  // 🆕 beforeFormSave 이벤트에서 채번 placeholder를 실제 값으로 변환
  useEffect(() => {
    const handleBeforeFormSave = async (event: Event) => {
      const customEvent = event as CustomEvent;
      const formData = customEvent.detail?.formData;

      if (!formData || !dataRef.current.length) return;

      // 채번 placeholder가 있는 행들을 찾아서 실제 값으로 변환
      const processedData = await Promise.all(
        dataRef.current.map(async (row) => {
          const newRow = { ...row };

          for (const key of Object.keys(newRow)) {
            const value = newRow[key];
            if (typeof value === "string" && value.startsWith("__NUMBERING_RULE__")) {
              // __NUMBERING_RULE__ruleId__ 형식에서 ruleId 추출
              const match = value.match(/__NUMBERING_RULE__(.+)__/);
              if (match) {
                const ruleId = match[1];
                try {
                  // 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
                  const result = await allocateNumberingCode(ruleId, undefined, newRow);
                  if (result.success && result.data?.generatedCode) {
                    newRow[key] = result.data.generatedCode;
                  } else {
                    console.error("채번 실패:", result.error);
                    newRow[key] = ""; // 채번 실패 시 빈 값
                  }
                } catch (error) {
                  console.error("채번 API 호출 실패:", error);
                  newRow[key] = "";
                }
              }
            }
          }

          return newRow;
        }),
      );

      // 처리된 데이터를 formData에 추가
      const fieldName = config.fieldName || "repeaterData";
      formData[fieldName] = processedData;
    };

    // V2 EventBus 구독
    const unsubscribe = v2EventBus.subscribe(
      V2_EVENTS.FORM_SAVE_COLLECT,
      async (payload) => {
        // formData 객체가 있으면 데이터 수집
        const fakeEvent = {
          detail: { formData: payload.formData },
        } as CustomEvent;
        await handleBeforeFormSave(fakeEvent);
      },
      { componentId: `v2-repeater-${config.dataSource?.tableName}` },
    );

    // 레거시 이벤트도 계속 지원 (점진적 마이그레이션)
    window.addEventListener("beforeFormSave", handleBeforeFormSave);

    return () => {
      unsubscribe();
      window.removeEventListener("beforeFormSave", handleBeforeFormSave);
    };
  }, [config.fieldName]);

  // 🆕 데이터 전달 이벤트 리스너 (transferData 버튼 액션용)
  useEffect(() => {
    // componentDataTransfer: 특정 컴포넌트 ID로 데이터 전달
    const handleComponentDataTransfer = async (event: Event) => {
      const customEvent = event as CustomEvent;
      const { targetComponentId, data: transferData, mappingRules, mode } = customEvent.detail || {};

      // 이 컴포넌트가 대상인지 확인
      if (targetComponentId !== parentId && targetComponentId !== config.fieldName) {
        return;
      }

      if (!transferData || transferData.length === 0) {
        return;
      }

      // 데이터 매핑 처리
      let mappedData = transferData.map((item: any, index: number) => {
        const newRow: any = { _id: `transfer_${Date.now()}_${index}` };

        if (mappingRules && mappingRules.length > 0) {
          mappingRules.forEach((rule: any) => {
            newRow[rule.targetField] = item[rule.sourceField];
          });
        } else {
          Object.assign(newRow, item);
        }

        return newRow;
      });

      // Entity 조인 해석 (FK → 참조 테이블 데이터)
      mappedData = await resolveEntityJoins(mappedData);

      // mode에 따라 데이터 처리
      if (mode === "replace") {
        handleDataChange(mappedData);
      } else if (mode === "merge") {
        const existingIds = new Set(data.map((row) => row.id || row._id));
        const newItems = mappedData.filter((row: any) => !existingIds.has(row.id || row._id));
        handleDataChange([...data, ...newItems]);
      } else {
        handleDataChange([...data, ...mappedData]);
      }
    };

    // splitPanelDataTransfer: 분할 패널에서 전역 이벤트로 전달
    const handleSplitPanelDataTransfer = async (event: Event) => {
      const customEvent = event as CustomEvent;
      const { data: transferData, mappingRules, mode, sourcePosition } = customEvent.detail || {};

      console.log("📨 [V2Repeater] splitPanelDataTransfer 수신:", {
        dataCount: transferData?.length,
        mappingRules,
        mode,
        sourcePosition,
        sampleSourceData: transferData?.[0],
        entityJoinsConfig: entityJoinsRef.current,
      });

      if (!transferData || transferData.length === 0) {
        return;
      }

      // 데이터 매핑 처리
      let mappedData = transferData.map((item: any, index: number) => {
        const newRow: any = { _id: `transfer_${Date.now()}_${index}` };

        if (mappingRules && mappingRules.length > 0) {
          mappingRules.forEach((rule: any) => {
            newRow[rule.targetField] = item[rule.sourceField];
          });
        } else {
          Object.assign(newRow, item);
        }

        return newRow;
      });

      console.log("📨 [V2Repeater] 매핑 후 데이터:", mappedData);

      // Entity 조인 해석 (FK → 참조 테이블 데이터)
      mappedData = await resolveEntityJoins(mappedData);

      console.log("📨 [V2Repeater] Entity 조인 후 데이터:", mappedData);

      // mode에 따라 데이터 처리
      if (mode === "replace") {
        handleDataChange(mappedData);
      } else {
        handleDataChange([...data, ...mappedData]);
      }
    };

    // V2 EventBus 구독
    const unsubscribeComponent = v2EventBus.subscribe(
      V2_EVENTS.COMPONENT_DATA_TRANSFER,
      (payload) => {
        const fakeEvent = {
          detail: {
            targetComponentId: payload.targetComponentId,
            transferData: [payload.data],
            mappingRules: [],
            mode: "append",
          },
        } as CustomEvent;
        handleComponentDataTransfer(fakeEvent);
      },
      { componentId: `v2-repeater-${config.dataSource?.tableName}` },
    );

    const unsubscribeSplitPanel = v2EventBus.subscribe(
      V2_EVENTS.SPLIT_PANEL_DATA_TRANSFER,
      (payload) => {
        const fakeEvent = {
          detail: {
            transferData: [payload.data],
            mappingRules: [],
            mode: "append",
          },
        } as CustomEvent;
        handleSplitPanelDataTransfer(fakeEvent);
      },
      { componentId: `v2-repeater-${config.dataSource?.tableName}` },
    );

    // 레거시 이벤트도 계속 지원 (점진적 마이그레이션)
    window.addEventListener("componentDataTransfer", handleComponentDataTransfer as EventListener);
    window.addEventListener("splitPanelDataTransfer", handleSplitPanelDataTransfer as EventListener);

    return () => {
      unsubscribeComponent();
      unsubscribeSplitPanel();
      window.removeEventListener("componentDataTransfer", handleComponentDataTransfer as EventListener);
      window.removeEventListener("splitPanelDataTransfer", handleSplitPanelDataTransfer as EventListener);
    };
  }, [parentId, config.fieldName, data, handleDataChange]);

  return (
    <div className={cn("flex h-full flex-col overflow-hidden", className)}>
      {/* 헤더 영역 */}
      <div className="flex shrink-0 flex-col gap-2 pb-2 sm:flex-row sm:items-center sm:justify-between">
        <div className="flex items-center gap-2">
          <span className="text-muted-foreground text-sm">
            {data.length > 0 && `${data.length}개 항목`}
            {selectedRows.size > 0 && ` (${selectedRows.size}개 선택됨)`}
          </span>
        </div>
        <div className="flex gap-2">
          {selectedRows.size > 0 && (
            <Button variant="destructive" onClick={handleBulkDelete} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
              선택 삭제 ({selectedRows.size})
            </Button>
          )}
          <Button onClick={handleAddRow} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
            <Plus className="mr-2 h-4 w-4" />
            {isModalMode ? config.modal?.buttonText || "검색" : "추가"}
          </Button>
        </div>
      </div>

      {/* Repeater 테이블 - 남은 공간에서 스크롤, 모바일 가로 스크롤 */}
      <div className="min-h-0 flex-1 overflow-x-auto">
        <RepeaterTable
          columns={repeaterColumns}
          data={data}
          onDataChange={handleDataChange}
          onRowChange={handleRowChange}
          onRowDelete={handleRowDelete}
          activeDataSources={activeDataSources}
          onDataSourceChange={(field, optionId) => {
            setActiveDataSources((prev) => ({ ...prev, [field]: optionId }));
          }}
          selectedRows={selectedRows}
          onSelectionChange={setSelectedRows}
          equalizeWidthsTrigger={autoWidthTrigger}
          categoryColumns={allCategoryColumns}
          categoryLabelMap={categoryLabelMap}
        />
      </div>

      {/* 항목 선택 모달 (modal 모드) - 검색 필드는 표시 컬럼과 동일하게 자동 설정 */}
      {isModalMode && (
        <ItemSelectionModal
          open={modalOpen}
          onOpenChange={setModalOpen}
          sourceTable={resolvedSourceTable}
          sourceColumns={sourceColumns}
          sourceSearchFields={sourceColumns}
          multiSelect={config.features?.multiSelect ?? true}
          modalTitle={config.modal?.title || "항목 검색"}
          alreadySelected={data}
          uniqueField={resolvedReferenceKey}
          onSelect={handleSelectItems}
          columnLabels={sourceColumnLabels}
          categoryColumns={sourceCategoryColumns}
        />
      )}
    </div>
  );
};

V2Repeater.displayName = "V2Repeater";

// V2ErrorBoundary로 래핑된 안전한 버전 export
export const SafeV2Repeater: React.FC<V2RepeaterProps> = (props) => {
  return (
    <V2ErrorBoundary componentId={props.parentId || "v2-repeater"} componentType="V2Repeater" fallbackStyle="compact">
      <V2Repeater {...props} />
    </V2ErrorBoundary>
  );
};

export default V2Repeater;
