"use client";

import React, { useState, useEffect, useMemo } from "react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Loader2, Save, X, Layers, Table as TableIcon, Plus, Trash2, RotateCcw, Pencil } from "lucide-react";
import { isColumnRequiredByMeta } from "@/lib/registry/DynamicComponentRenderer";
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
  RepeatScreenModalProps,
  CardData,
  CardColumnConfig,
  GroupedCardData,
  CardRowData,
  AggregationConfig,
  TableColumnConfig,
  CardContentRowConfig,
  AggregationDisplayConfig,
  FooterConfig,
  FooterButtonConfig,
  TableDataSourceConfig,
  TableCrudConfig,
} from "./types";
import { ComponentRendererProps } from "@/types/component";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";

export interface RepeatScreenModalComponentProps extends ComponentRendererProps {
  config?: RepeatScreenModalProps;
  groupedData?: Record<string, any>[]; // EditModal에서 전달하는 그룹 데이터
}

export function RepeatScreenModalComponent({
  component,
  isDesignMode = false,
  formData,
  onFormDataChange,
  config,
  className,
  groupedData: propsGroupedData, // EditModal에서 전달받는 그룹 데이터
  // DynamicComponentRenderer에서 전달되는 props (DOM 전달 방지)
  _initialData,
  _originalData: _propsOriginalData,
  _groupedData,
  ...props
}: RepeatScreenModalComponentProps & { _initialData?: any; _originalData?: any; _groupedData?: any }) {
  // props에서도 groupedData를 추출 (DynamicWebTypeRenderer에서 전달될 수 있음)
  // DynamicComponentRenderer에서는 _groupedData로 전달됨
  const groupedData = propsGroupedData || (props as any).groupedData || _groupedData;
  const componentConfig = {
    ...config,
    ...component?.config,
  };

  // 설정 값 추출
  const dataSource = componentConfig?.dataSource;
  const saveMode = componentConfig?.saveMode || "all";
  const cardSpacing = componentConfig?.cardSpacing || "24px";
  const showCardBorder = componentConfig?.showCardBorder ?? true;
  const showCardTitle = componentConfig?.showCardTitle ?? true;
  const cardTitle = componentConfig?.cardTitle || "카드 {index}";
  const grouping = componentConfig?.grouping;

  // 🆕 v3: 자유 레이아웃
  const contentRows = componentConfig?.contentRows || [];

  // 🆕 v3.1: Footer 설정
  const footerConfig = componentConfig?.footerConfig;

  // (레거시 호환)
  const cardLayout = componentConfig?.cardLayout || [];
  const cardMode = componentConfig?.cardMode || "simple";
  const tableLayout = componentConfig?.tableLayout;

  // 상태
  const [rawData, setRawData] = useState<any[]>([]); // 원본 데이터
  const [cardsData, setCardsData] = useState<CardData[]>([]); // simple 모드용
  const [groupedCardsData, setGroupedCardsData] = useState<GroupedCardData[]>([]); // withTable 모드용
  const [isLoading, setIsLoading] = useState(false);
  const [loadError, setLoadError] = useState<string | null>(null);
  const [isSaving, setIsSaving] = useState(false);

  // 🆕 v3.1: 외부 테이블 데이터 (테이블 행별로 관리)
  const [externalTableData, setExternalTableData] = useState<Record<string, any[]>>({});
  // 🆕 v3.1: 삭제 확인 다이얼로그
  const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
  const [pendingDeleteInfo, setPendingDeleteInfo] = useState<{
    cardId: string;
    rowId: string;
    contentRowId: string;
  } | null>(null);

  // 🆕 v3.13: 외부에서 저장 트리거 가능하도록 이벤트 리스너 추가
  useEffect(() => {
    const handleTriggerSave = async (event: Event) => {
      if (!(event instanceof CustomEvent)) return;

      console.log("[RepeatScreenModal] triggerRepeatScreenModalSave 이벤트 수신");

      try {
        setIsSaving(true);

        // 기존 데이터 저장
        if (cardMode === "withTable") {
          await saveGroupedData();
        } else {
          await saveSimpleData();
        }

        // 외부 테이블 데이터 저장
        await saveExternalTableData();

        // 연동 저장 처리 (syncSaves)
        await processSyncSaves();

        console.log("[RepeatScreenModal] 외부 트리거 저장 완료");

        // 저장 완료 이벤트 발생
        window.dispatchEvent(
          new CustomEvent("repeatScreenModalSaveComplete", {
            detail: { success: true },
          }),
        );

        // 성공 콜백 실행
        if (event.detail?.onSuccess) {
          event.detail.onSuccess();
        }
      } catch (error: any) {
        console.error("[RepeatScreenModal] 외부 트리거 저장 실패:", error);

        // 저장 실패 이벤트 발생
        window.dispatchEvent(
          new CustomEvent("repeatScreenModalSaveComplete", {
            detail: { success: false, error: error.message },
          }),
        );

        // 실패 콜백 실행
        if (event.detail?.onError) {
          event.detail.onError(error);
        }
      } finally {
        setIsSaving(false);
      }
    };

    window.addEventListener("triggerRepeatScreenModalSave", handleTriggerSave as EventListener);
    return () => {
      window.removeEventListener("triggerRepeatScreenModalSave", handleTriggerSave as EventListener);
    };
  }, [cardMode, groupedCardsData, externalTableData, contentRows]);

  // 🆕 v3.9: beforeFormSave 이벤트 핸들러 - ButtonPrimary 저장 시 externalTableData를 formData에 병합
  useEffect(() => {
    const handleBeforeFormSave = (event: Event) => {
      if (!(event instanceof CustomEvent) || !event.detail?.formData) return;

      console.log("[RepeatScreenModal] beforeFormSave 이벤트 수신");
      console.log("[RepeatScreenModal] beforeFormSave - externalTableData:", externalTableData);
      console.log("[RepeatScreenModal] beforeFormSave - groupedCardsData:", groupedCardsData.length, "개 카드");

      // 외부 테이블 데이터에서 dirty 행만 추출하여 저장 데이터 준비
      const saveDataByTable: Record<string, any[]> = {};

      for (const [key, rows] of Object.entries(externalTableData)) {
        // key 형식: cardId-contentRowId
        const keyParts = key.split("-");
        const cardId = keyParts.slice(0, -1).join("-"); // contentRowId를 제외한 나머지가 cardId

        // contentRow 찾기
        const contentRow = contentRows.find((r) => key.includes(r.id));
        if (!contentRow?.tableDataSource?.enabled) continue;

        // 🆕 v3.13: 해당 카드의 대표 데이터 찾기 (joinConditions의 targetKey 값을 가져오기 위해)
        const card = groupedCardsData.find((c) => c._cardId === cardId);
        const representativeData = card?._representativeData || {};

        const targetTable = contentRow.tableCrud?.targetTable || contentRow.tableDataSource.sourceTable;

        // dirty 행 또는 새로운 행 필터링 (삭제된 행 제외)
        // 🆕 v3.13: _isNew 행도 포함 (새로 추가된 행은 _isDirty가 없을 수 있음)
        const dirtyRows = rows.filter((row) => (row._isDirty || row._isNew) && !row._isDeleted);

        console.log(`[RepeatScreenModal] beforeFormSave - ${targetTable} 행 필터링:`, {
          totalRows: rows.length,
          dirtyRows: dirtyRows.length,
          rowDetails: rows.map((r) => ({ _isDirty: r._isDirty, _isNew: r._isNew, _isDeleted: r._isDeleted })),
        });

        if (dirtyRows.length === 0) continue;

        // 저장할 필드만 추출
        const editableFields = (contentRow.tableColumns || []).filter((col) => col.editable).map((col) => col.field);

        // 🆕 v3.13: joinConditions에서 sourceKey (저장 대상 테이블의 FK 컬럼) 추출
        const joinConditions = contentRow.tableDataSource.joinConditions || [];
        const joinKeys = joinConditions.map((cond) => cond.sourceKey);

        const allowedFields = [...new Set([...editableFields, ...joinKeys])];

        if (!saveDataByTable[targetTable]) {
          saveDataByTable[targetTable] = [];
        }

        for (const row of dirtyRows) {
          const saveData: Record<string, any> = {};

          // 허용된 필드만 포함
          for (const field of allowedFields) {
            if (row[field] !== undefined) {
              saveData[field] = row[field];
            }
          }

          // 🆕 v3.13: joinConditions를 사용하여 FK 값 자동 채우기
          // 예: sales_order_id (sourceKey) = card의 id (targetKey)
          for (const joinCond of joinConditions) {
            const { sourceKey, targetKey } = joinCond;
            // sourceKey가 저장 데이터에 없거나 null인 경우, 카드의 대표 데이터에서 targetKey 값을 가져옴
            if (!saveData[sourceKey] && representativeData[targetKey] !== undefined) {
              saveData[sourceKey] = representativeData[targetKey];
              console.log(
                `[RepeatScreenModal] beforeFormSave - FK 자동 채우기: ${sourceKey} = ${representativeData[targetKey]} (from card.${targetKey})`,
              );
            }
          }

          // _isNew 플래그 유지
          saveData._isNew = row._isNew;
          saveData._targetTable = targetTable;

          // 기존 레코드의 경우 id 포함
          if (!row._isNew && row._originalData?.id) {
            saveData.id = row._originalData.id;
          }

          saveDataByTable[targetTable].push(saveData);
        }
      }

      // formData에 테이블별 저장 데이터 추가
      for (const [tableName, rows] of Object.entries(saveDataByTable)) {
        const fieldKey = `_repeatScreenModal_${tableName}`;
        event.detail.formData[fieldKey] = rows;
        console.log(`[RepeatScreenModal] beforeFormSave - ${tableName} 저장 데이터:`, rows);
      }

      // 🆕 v3.9: 집계 저장 설정 정보도 formData에 추가
      if (grouping?.aggregations && groupedCardsData.length > 0) {
        const aggregationSaveConfigs: Array<{
          resultField: string;
          aggregatedValue: number;
          targetTable: string;
          targetColumn: string;
          joinKey: { sourceField: string; targetField: string };
          sourceValue: any; // 조인 키 값
        }> = [];

        for (const card of groupedCardsData) {
          for (const agg of grouping.aggregations) {
            if (agg.saveConfig?.enabled) {
              const { saveConfig, resultField } = agg;
              const { targetTable, targetColumn, joinKey } = saveConfig;

              if (!targetTable || !targetColumn || !joinKey?.sourceField || !joinKey?.targetField) {
                continue;
              }

              const aggregatedValue = card._aggregations?.[resultField] ?? 0;
              const sourceValue = card._representativeData?.[joinKey.sourceField];

              if (sourceValue !== undefined) {
                aggregationSaveConfigs.push({
                  resultField,
                  aggregatedValue,
                  targetTable,
                  targetColumn,
                  joinKey,
                  sourceValue,
                });
              }
            }
          }
        }

        if (aggregationSaveConfigs.length > 0) {
          event.detail.formData._repeatScreenModal_aggregations = aggregationSaveConfigs;
          console.log("[RepeatScreenModal] beforeFormSave - 집계 저장 설정:", aggregationSaveConfigs);
        }
      }
    };

    window.addEventListener("beforeFormSave", handleBeforeFormSave as EventListener);
    return () => {
      window.removeEventListener("beforeFormSave", handleBeforeFormSave as EventListener);
    };
  }, [externalTableData, contentRows, grouping, groupedCardsData]);

  // 초기 데이터 로드
  useEffect(() => {
    const loadInitialData = async () => {
      console.log("[RepeatScreenModal] 데이터 로드 시작");
      console.log("[RepeatScreenModal] groupedData (from EditModal):", groupedData);
      console.log("[RepeatScreenModal] formData:", formData);
      console.log("[RepeatScreenModal] dataSource:", dataSource);

      setIsLoading(true);
      setLoadError(null);

      try {
        let loadedData: any[] = [];

        // 🆕 우선순위 1: EditModal에서 전달받은 groupedData 사용
        if (groupedData && groupedData.length > 0) {
          console.log("[RepeatScreenModal] groupedData 사용:", groupedData.length, "건");
          loadedData = groupedData;
        }
        // 우선순위 2: API 호출
        else if (dataSource && dataSource.sourceTable) {
          // 필터 조건 생성
          const filters: Record<string, any> = {};

          // formData에서 선택된 행 ID 가져오기
          let selectedIds: any[] = [];

          if (formData) {
            // 1. 명시적으로 설정된 filterField 확인
            if (dataSource.filterField) {
              const filterValue = formData[dataSource.filterField];
              if (filterValue) {
                selectedIds = Array.isArray(filterValue) ? filterValue : [filterValue];
              }
            }

            // 2. 일반적인 선택 필드 확인 (fallback)
            if (selectedIds.length === 0) {
              const commonFields = ["selectedRows", "selectedIds", "checkedRows", "checkedIds", "ids"];
              for (const field of commonFields) {
                if (formData[field]) {
                  const value = formData[field];
                  selectedIds = Array.isArray(value) ? value : [value];
                  console.log(`[RepeatScreenModal] ${field}에서 선택된 ID 발견:`, selectedIds);
                  break;
                }
              }
            }

            // 3. formData에 id가 있으면 단일 행
            if (selectedIds.length === 0 && formData.id) {
              selectedIds = [formData.id];
              console.log("[RepeatScreenModal] formData.id 사용:", selectedIds);
            }
          }

          console.log("[RepeatScreenModal] 최종 선택된 ID:", selectedIds);

          // 선택된 ID가 있으면 필터 적용
          if (selectedIds.length > 0) {
            filters.id = selectedIds;
          } else {
            console.warn("[RepeatScreenModal] 선택된 데이터가 없습니다.");
            setRawData([]);
            setCardsData([]);
            setGroupedCardsData([]);
            setIsLoading(false);
            return;
          }

          console.log("[RepeatScreenModal] API 필터:", filters);

          // API 호출
          const response = await apiClient.post(`/table-management/tables/${dataSource.sourceTable}/data`, {
            search: filters,
            page: 1,
            size: 1000,
          });

          if (response.data.success && response.data.data?.data) {
            loadedData = response.data.data.data;
          }
        } else {
          console.log("[RepeatScreenModal] 데이터 소스 없음");
          setRawData([]);
          setCardsData([]);
          setGroupedCardsData([]);
          setIsLoading(false);
          return;
        }

        console.log("[RepeatScreenModal] 로드된 데이터:", loadedData.length, "건");

        if (loadedData.length === 0) {
          setRawData([]);
          setCardsData([]);
          setGroupedCardsData([]);
          setIsLoading(false);
          return;
        }

        setRawData(loadedData);

        // 🆕 v3: contentRows가 있으면 새로운 방식 사용
        const useNewLayout = contentRows && contentRows.length > 0;

        // 그룹핑 모드 확인 (groupByField가 없어도 enabled면 그룹핑 모드로 처리)
        const useGrouping = grouping?.enabled;

        if (useGrouping) {
          // 그룹핑 모드
          const grouped = processGroupedData(loadedData, grouping);
          setGroupedCardsData(grouped);
        } else {
          // 단순 모드: 각 행이 하나의 카드
          const initialCards: CardData[] = await Promise.all(
            loadedData.map(async (row: any, index: number) => ({
              _cardId: `card-${index}-${Date.now()}`,
              _originalData: { ...row },
              _isDirty: false,
              ...(await loadCardData(row)),
            })),
          );
          setCardsData(initialCards);
        }
      } catch (error: any) {
        console.error("데이터 로드 실패:", error);
        setLoadError(error.message || "데이터 로드 중 오류가 발생했습니다.");
      } finally {
        setIsLoading(false);
      }
    };

    loadInitialData();
  }, [dataSource, formData, groupedData, contentRows, grouping?.enabled, grouping?.groupByField]);

  // 🆕 v3.1: 외부 테이블 데이터 로드
  useEffect(() => {
    const loadExternalTableData = async () => {
      // contentRows에서 외부 테이블 데이터 소스가 있는 table 타입 행 찾기
      const tableRowsWithExternalSource = contentRows.filter(
        (row) => row.type === "table" && row.tableDataSource?.enabled,
      );

      if (tableRowsWithExternalSource.length === 0) return;
      if (groupedCardsData.length === 0 && cardsData.length === 0) return;

      const newExternalData: Record<string, any[]> = {};

      for (const contentRow of tableRowsWithExternalSource) {
        const dataSourceConfig = contentRow.tableDataSource!;
        const cards = groupedCardsData.length > 0 ? groupedCardsData : cardsData;

        for (const card of cards) {
          const cardId = card._cardId;
          const representativeData = (card as GroupedCardData)._representativeData || card;

          try {
            // 조인 조건 생성
            const filters: Record<string, any> = {};
            for (const condition of dataSourceConfig.joinConditions) {
              let refValue = representativeData[condition.referenceKey];
              if (refValue !== undefined && refValue !== null) {
                // 숫자형 ID인 경우 숫자로 변환 (문자열 '189' → 숫자 189)
                // 백엔드에서 entity 타입 컬럼 검색 시 문자열이면 ILIKE 검색을 수행하므로
                // 정확한 ID 매칭을 위해 숫자로 변환해야 함
                if (condition.sourceKey.endsWith("_id") || condition.sourceKey === "id") {
                  const numValue = Number(refValue);
                  if (!isNaN(numValue)) {
                    refValue = numValue;
                  }
                }
                filters[condition.sourceKey] = refValue;
              }
            }

            if (Object.keys(filters).length === 0) {
              console.warn(`[RepeatScreenModal] 조인 조건이 없습니다: ${contentRow.id}`);
              continue;
            }

            console.log(`[RepeatScreenModal] 외부 테이블 API 호출:`, {
              sourceTable: dataSourceConfig.sourceTable,
              filters,
              joinConditions: dataSourceConfig.joinConditions,
              representativeDataId: representativeData.id,
              representativeDataIdType: typeof representativeData.id,
            });

            // API 호출 - 메인 테이블 데이터
            const response = await apiClient.post(`/table-management/tables/${dataSourceConfig.sourceTable}/data`, {
              search: filters,
              page: 1,
              size: dataSourceConfig.limit || 100,
              sort: dataSourceConfig.orderBy
                ? {
                    column: dataSourceConfig.orderBy.column,
                    direction: dataSourceConfig.orderBy.direction,
                  }
                : undefined,
            });

            if (response.data.success && response.data.data?.data) {
              let tableData = response.data.data.data;

              console.log(`[RepeatScreenModal] 소스 테이블 데이터 로드 완료:`, {
                sourceTable: dataSourceConfig.sourceTable,
                rowCount: tableData.length,
                sampleRow: tableData[0] ? Object.keys(tableData[0]) : [],
                firstRowData: tableData[0],
                // 디버그: plan_date 필드 확인
                plan_date_value: tableData[0]?.plan_date,
              });

              // 🆕 v3.3: 추가 조인 테이블 데이터 로드 및 병합
              if (dataSourceConfig.additionalJoins && dataSourceConfig.additionalJoins.length > 0) {
                console.log(`[RepeatScreenModal] 조인 설정:`, dataSourceConfig.additionalJoins);
                tableData = await loadAndMergeJoinData(tableData, dataSourceConfig.additionalJoins);
                console.log(`[RepeatScreenModal] 조인 후 데이터:`, {
                  rowCount: tableData.length,
                  sampleRow: tableData[0] ? Object.keys(tableData[0]) : [],
                  firstRowData: tableData[0],
                });
              }

              // 🆕 v3.4: 필터 조건 적용
              if (dataSourceConfig.filterConfig?.enabled) {
                const { filterField, filterType, referenceField, referenceSource } = dataSourceConfig.filterConfig;

                // 비교 값 가져오기
                let referenceValue: any;
                if (referenceSource === "formData") {
                  referenceValue = formData?.[referenceField];
                } else {
                  // representativeData
                  referenceValue = representativeData[referenceField];
                }

                if (referenceValue !== undefined && referenceValue !== null) {
                  tableData = tableData.filter((row: any) => {
                    const rowValue = row[filterField];
                    if (filterType === "equals") {
                      return rowValue === referenceValue;
                    } else {
                      // notEquals
                      return rowValue !== referenceValue;
                    }
                  });

                  console.log(
                    `[RepeatScreenModal] 필터 적용: ${filterField} ${filterType} ${referenceValue}, 결과: ${tableData.length}건`,
                  );
                }
              }

              const key = `${cardId}-${contentRow.id}`;
              newExternalData[key] = tableData.map((row: any, idx: number) => ({
                _rowId: `ext-row-${cardId}-${contentRow.id}-${idx}-${Date.now()}`,
                _originalData: { ...row },
                _isDirty: false,
                _isNew: false,
                _isEditing: false, // 🆕 v3.8: 로드된 데이터는 읽기 전용
                _isDeleted: false,
                ...row,
              }));

              // 디버그: 저장된 외부 테이블 데이터 확인
              console.log(`[RepeatScreenModal] 외부 테이블 데이터 저장:`, {
                key,
                rowCount: newExternalData[key].length,
                firstRow: newExternalData[key][0],
                plan_date_in_firstRow: newExternalData[key][0]?.plan_date,
              });
            }
          } catch (error) {
            console.error(`[RepeatScreenModal] 외부 테이블 데이터 로드 실패:`, error);
          }
        }
      }

      setExternalTableData((prev) => {
        // 이전 데이터와 동일하면 업데이트하지 않음 (무한 루프 방지)
        const prevKeys = Object.keys(prev).sort().join(",");
        const newKeys = Object.keys(newExternalData).sort().join(",");
        if (prevKeys === newKeys) {
          // 키가 같으면 데이터 내용 비교
          const isSame = Object.keys(newExternalData).every(
            (key) => JSON.stringify(prev[key]) === JSON.stringify(newExternalData[key]),
          );
          if (isSame) return prev;
        }

        // 🆕 v3.2: 외부 테이블 데이터 로드 후 집계 재계산
        // 비동기적으로 처리하여 무한 루프 방지
        setTimeout(() => {
          recalculateAggregationsWithExternalData(newExternalData);
        }, 0);

        return newExternalData;
      });
    };

    loadExternalTableData();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [contentRows, groupedCardsData.length, cardsData.length]);

  // 🆕 v3.3: 추가 조인 테이블 데이터 로드 및 병합
  const loadAndMergeJoinData = async (
    mainData: any[],
    additionalJoins: { id: string; joinTable: string; joinType: string; sourceKey: string; targetKey: string }[],
  ): Promise<any[]> => {
    if (mainData.length === 0) return mainData;

    // 각 조인 테이블별로 필요한 키 값들 수집
    for (const joinConfig of additionalJoins) {
      if (!joinConfig.joinTable || !joinConfig.sourceKey || !joinConfig.targetKey) continue;

      // 메인 데이터에서 조인 키 값들 추출
      const joinKeyValues = [...new Set(mainData.map((row) => row[joinConfig.sourceKey]).filter(Boolean))];

      if (joinKeyValues.length === 0) continue;

      try {
        // 조인 테이블 데이터 조회
        const joinResponse = await apiClient.post(`/table-management/tables/${joinConfig.joinTable}/data`, {
          search: { [joinConfig.targetKey]: joinKeyValues },
          page: 1,
          size: 1000, // 충분히 큰 값
        });

        if (joinResponse.data.success && joinResponse.data.data?.data) {
          const joinData = joinResponse.data.data.data;

          // 조인 데이터를 맵으로 변환 (빠른 조회를 위해)
          const joinDataMap = new Map<any, any>();
          for (const joinRow of joinData) {
            joinDataMap.set(joinRow[joinConfig.targetKey], joinRow);
          }

          // 메인 데이터에 조인 데이터 병합
          mainData = mainData.map((row) => {
            const joinKey = row[joinConfig.sourceKey];
            const joinRow = joinDataMap.get(joinKey);

            if (joinRow) {
              // 조인 테이블의 컬럼들을 메인 데이터에 추가 (접두사 없이)
              const mergedRow = { ...row };
              for (const [key, value] of Object.entries(joinRow)) {
                // 이미 존재하는 키가 아닌 경우에만 추가 (메인 테이블 우선)
                if (!(key in mergedRow)) {
                  mergedRow[key] = value;
                } else {
                  // 충돌하는 경우 조인 테이블명을 접두사로 사용
                  mergedRow[`${joinConfig.joinTable}_${key}`] = value;
                }
              }
              return mergedRow;
            }
            return row;
          });
        }
      } catch (error) {
        console.error(`[RepeatScreenModal] 조인 테이블 데이터 로드 실패 (${joinConfig.joinTable}):`, error);
      }
    }

    return mainData;
  };

  // 🆕 v3.2: 외부 테이블 데이터가 로드된 후 집계 재계산
  const recalculateAggregationsWithExternalData = (extData: Record<string, any[]>) => {
    if (!grouping?.aggregations || grouping.aggregations.length === 0) return;
    if (groupedCardsData.length === 0) return;

    // 외부 테이블 집계 또는 formula가 있는지 확인
    const hasExternalAggregation = grouping.aggregations.some((agg) => {
      const sourceType = agg.sourceType || "column";
      if (sourceType === "formula") return true; // formula는 외부 테이블 참조 가능
      if (sourceType === "column") {
        const sourceTable = agg.sourceTable || dataSource?.sourceTable;
        return sourceTable && sourceTable !== dataSource?.sourceTable;
      }
      return false;
    });

    if (!hasExternalAggregation) return;

    // contentRows에서 외부 테이블 데이터 소스가 있는 모든 table 타입 행 찾기
    const tableRowsWithExternalSource = contentRows.filter(
      (row) => row.type === "table" && row.tableDataSource?.enabled,
    );

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

    // 각 카드의 집계 재계산
    const updatedCards = groupedCardsData.map((card) => {
      // 🆕 v3.11: 테이블 행 ID별로 외부 데이터를 구분하여 저장
      const externalRowsByTableId: Record<string, any[]> = {};
      const allExternalRows: any[] = [];

      for (const tableRow of tableRowsWithExternalSource) {
        const key = `${card._cardId}-${tableRow.id}`;
        // 🆕 v3.7: 삭제된 행은 집계에서 제외
        const rows = (extData[key] || []).filter((row) => !row._isDeleted);
        externalRowsByTableId[tableRow.id] = rows;
        allExternalRows.push(...rows);
      }

      // 집계 재계산
      const newAggregations: Record<string, number> = {};

      grouping.aggregations!.forEach((agg) => {
        const sourceType = agg.sourceType || "column";

        if (sourceType === "column") {
          const sourceTable = agg.sourceTable || dataSource?.sourceTable;
          const isExternalTable = sourceTable && sourceTable !== dataSource?.sourceTable;

          if (isExternalTable) {
            // 외부 테이블 집계
            newAggregations[agg.resultField] = calculateColumnAggregation(
              allExternalRows,
              agg.sourceField || "",
              agg.type || "sum",
            );
          } else {
            // 기본 테이블 집계 (기존 값 유지)
            newAggregations[agg.resultField] =
              card._aggregations[agg.resultField] ||
              calculateColumnAggregation(card._rows, agg.sourceField || "", agg.type || "sum");
          }
        } else if (sourceType === "formula" && agg.formula) {
          // 🆕 v3.11: externalTableRefs 기반으로 필터링된 외부 데이터 사용
          let filteredExternalRows: any[];

          if (agg.externalTableRefs && agg.externalTableRefs.length > 0) {
            // 특정 테이블만 참조
            filteredExternalRows = [];
            for (const tableId of agg.externalTableRefs) {
              if (externalRowsByTableId[tableId]) {
                filteredExternalRows.push(...externalRowsByTableId[tableId]);
              }
            }
          } else {
            // 모든 외부 테이블 데이터 사용 (기존 동작)
            filteredExternalRows = allExternalRows;
          }

          // 가상 집계 (연산식) - 외부 테이블 데이터 포함하여 재계산
          newAggregations[agg.resultField] = evaluateFormulaWithContext(
            agg.formula,
            card._representativeData,
            card._rows,
            filteredExternalRows,
            newAggregations, // 이전 집계 결과 참조
          );
        }
      });

      return {
        ...card,
        _aggregations: newAggregations,
      };
    });

    // 변경된 경우에만 업데이트 (무한 루프 방지)
    setGroupedCardsData((prev) => {
      const hasChanges = updatedCards.some((card, idx) => {
        const prevCard = prev[idx];
        if (!prevCard) return true;
        return JSON.stringify(card._aggregations) !== JSON.stringify(prevCard._aggregations);
      });
      return hasChanges ? updatedCards : prev;
    });
  };

  // 🆕 v3.1: 외부 테이블 행 추가 (v3.13: 자동 채번 기능 추가)
  const handleAddExternalRow = async (cardId: string, contentRowId: string, contentRow: CardContentRowConfig) => {
    const key = `${cardId}-${contentRowId}`;
    const card = groupedCardsData.find((c) => c._cardId === cardId) || cardsData.find((c) => c._cardId === cardId);
    const representativeData = (card as GroupedCardData)?._representativeData || card || {};

    // 기본값 생성
    const newRowData: Record<string, any> = {
      _rowId: `new-row-${Date.now()}`,
      _originalData: {},
      _isDirty: true,
      _isNew: true,
    };

    // 🆕 v3.5: 카드 대표 데이터에서 조인 테이블 컬럼 값 자동 채우기
    // tableColumns에서 정의된 필드들 중 representativeData에 있는 값을 자동으로 채움
    if (contentRow.tableColumns) {
      for (const col of contentRow.tableColumns) {
        // representativeData에 해당 필드가 있으면 자동으로 채움
        if (representativeData[col.field] !== undefined && representativeData[col.field] !== null) {
          newRowData[col.field] = representativeData[col.field];
        }
      }
    }

    // 🆕 v3.5: 조인 조건의 키 값도 자동으로 채움 (예: sales_order_id)
    if (contentRow.tableDataSource?.joinConditions) {
      for (const condition of contentRow.tableDataSource.joinConditions) {
        // sourceKey는 소스 테이블(예: shipment_plan)의 컬럼
        // referenceKey는 카드 대표 데이터의 컬럼 (예: id)
        const refValue = representativeData[condition.referenceKey];
        if (refValue !== undefined && refValue !== null) {
          newRowData[condition.sourceKey] = refValue;
        }
      }
    }

    // newRowDefaults 적용 (사용자 정의 기본값이 우선)
    if (contentRow.tableCrud?.newRowDefaults) {
      for (const [field, template] of Object.entries(contentRow.tableCrud.newRowDefaults)) {
        // {fieldName} 형식의 템플릿 치환
        let value = template;
        const matches = template.match(/\{(\w+)\}/g);
        if (matches) {
          for (const match of matches) {
            const fieldName = match.slice(1, -1);
            value = value.replace(match, String(representativeData[fieldName] || ""));
          }
        }
        newRowData[field] = value;
      }
    }

    // 🆕 v3.13: 자동 채번 처리
    const rowNumbering = contentRow.tableCrud?.rowNumbering;
    console.log("[RepeatScreenModal] 채번 설정 확인:", {
      tableCrud: contentRow.tableCrud,
      rowNumbering,
      enabled: rowNumbering?.enabled,
      targetColumn: rowNumbering?.targetColumn,
      numberingRuleId: rowNumbering?.numberingRuleId,
    });
    if (rowNumbering?.enabled && rowNumbering.targetColumn && rowNumbering.numberingRuleId) {
      try {
        console.log("[RepeatScreenModal] 자동 채번 시작:", {
          targetColumn: rowNumbering.targetColumn,
          numberingRuleId: rowNumbering.numberingRuleId,
        });

        // 채번 API 호출 (allocate: 실제 시퀀스 증가)
        // 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
        const { allocateNumberingCode } = await import("@/lib/api/numberingRule");
        const userInputCode = newRowData[rowNumbering.targetColumn] as string;
        const response = await allocateNumberingCode(rowNumbering.numberingRuleId, userInputCode, newRowData);

        if (response.success && response.data) {
          newRowData[rowNumbering.targetColumn] = response.data.generatedCode;

          console.log("[RepeatScreenModal] 자동 채번 완료:", {
            column: rowNumbering.targetColumn,
            generatedCode: response.data.generatedCode,
          });
        } else {
          console.warn("[RepeatScreenModal] 채번 실패:", response);
        }
      } catch (error) {
        console.error("[RepeatScreenModal] 채번 API 호출 실패:", error);
      }
    }

    console.log("[RepeatScreenModal] 새 행 추가:", {
      cardId,
      contentRowId,
      representativeData,
      newRowData,
    });

    setExternalTableData((prev) => {
      const newData = {
        ...prev,
        [key]: [...(prev[key] || []), newRowData],
      };

      // 🆕 v3.5: 새 행 추가 시 집계 재계산
      setTimeout(() => {
        recalculateAggregationsWithExternalData(newData);
      }, 0);

      return newData;
    });
  };

  // 🆕 v3.6: 테이블 영역 저장 기능
  const saveTableAreaData = async (cardId: string, contentRowId: string, contentRow: CardContentRowConfig) => {
    const key = `${cardId}-${contentRowId}`;
    const rows = externalTableData[key] || [];

    console.log("[RepeatScreenModal] saveTableAreaData 시작:", {
      key,
      rowsCount: rows.length,
      contentRowId,
      tableDataSource: contentRow?.tableDataSource,
      tableCrud: contentRow?.tableCrud,
    });

    if (!contentRow?.tableDataSource?.enabled) {
      console.warn("[RepeatScreenModal] 외부 테이블 데이터 소스가 설정되지 않음");
      return { success: false, message: "데이터 소스가 설정되지 않았습니다." };
    }

    const targetTable = contentRow.tableCrud?.targetTable || contentRow.tableDataSource.sourceTable;
    const dirtyRows = rows.filter((row) => row._isDirty);

    console.log("[RepeatScreenModal] 저장 대상:", {
      targetTable,
      dirtyRowsCount: dirtyRows.length,
      dirtyRows: dirtyRows.map((r) => ({ _isNew: r._isNew, _isDirty: r._isDirty, data: r })),
    });

    if (dirtyRows.length === 0) {
      return { success: true, message: "저장할 변경사항이 없습니다.", savedCount: 0 };
    }

    const savePromises: Promise<any>[] = [];
    const savedIds: number[] = [];

    // 🆕 v3.6: editable한 컬럼 + 조인 키만 추출 (읽기 전용 컬럼은 제외)
    const allowedFields = new Set<string>();

    // tableColumns에서 editable: true인 필드만 추가 (읽기 전용 컬럼 제외)
    if (contentRow.tableColumns) {
      contentRow.tableColumns.forEach((col) => {
        // editable이 명시적으로 true이거나, editable이 undefined가 아니고 false가 아닌 경우
        // 또는 inputType이 있는 경우 (입력 가능한 컬럼)
        if (col.field && (col.editable === true || col.inputType)) {
          allowedFields.add(col.field);
        }
      });
    }

    // 조인 조건의 sourceKey 추가 (예: sales_order_id) - 이건 항상 필요
    if (contentRow.tableDataSource?.joinConditions) {
      contentRow.tableDataSource.joinConditions.forEach((cond) => {
        if (cond.sourceKey) allowedFields.add(cond.sourceKey);
      });
    }

    console.log("[RepeatScreenModal] 저장 허용 필드 (editable + 조인키):", Array.from(allowedFields));
    console.log(
      "[RepeatScreenModal] tableColumns 정보:",
      contentRow.tableColumns?.map((c) => ({
        field: c.field,
        editable: c.editable,
        inputType: c.inputType,
      })),
    );

    // 삭제할 행 (기존 데이터 중 _isDeleted가 true인 것)
    const deletedRows = dirtyRows.filter((row) => row._isDeleted && row._originalData?.id);
    // 저장할 행 (삭제되지 않은 것)
    const rowsToSave = dirtyRows.filter((row) => !row._isDeleted);

    console.log("[RepeatScreenModal] 삭제 대상:", deletedRows.length, "건");
    console.log("[RepeatScreenModal] 저장 대상:", rowsToSave.length, "건");

    // 🆕 v3.7: 삭제 처리 (배열 형태로 body에 전달)
    for (const row of deletedRows) {
      const deleteId = row._originalData.id;
      console.log(`[RepeatScreenModal] DELETE 요청: /table-management/tables/${targetTable}/delete`, [
        { id: deleteId },
      ]);
      savePromises.push(
        apiClient
          .request({
            method: "DELETE",
            url: `/table-management/tables/${targetTable}/delete`,
            data: [{ id: deleteId }],
          })
          .then((res) => {
            console.log("[RepeatScreenModal] DELETE 응답:", res.data);
            return { type: "delete", id: deleteId };
          })
          .catch((err) => {
            console.error("[RepeatScreenModal] DELETE 실패:", err.response?.data || err.message);
            throw err;
          }),
      );
    }

    for (const row of rowsToSave) {
      const { _rowId, _originalData, _isDirty, _isNew, _isDeleted, ...allData } = row;

      // 허용된 필드만 필터링
      const dataToSave: Record<string, any> = {};
      for (const field of allowedFields) {
        if (allData[field] !== undefined) {
          dataToSave[field] = allData[field];
        }
      }

      console.log("[RepeatScreenModal] 저장할 데이터:", {
        _isNew,
        _originalData,
        allData,
        dataToSave,
      });

      if (_isNew) {
        // INSERT - /add 엔드포인트 사용
        console.log(`[RepeatScreenModal] INSERT 요청: /table-management/tables/${targetTable}/add`, dataToSave);
        savePromises.push(
          apiClient
            .post(`/table-management/tables/${targetTable}/add`, dataToSave)
            .then((res) => {
              console.log("[RepeatScreenModal] INSERT 응답:", res.data);
              if (res.data?.data?.id) {
                savedIds.push(res.data.data.id);
              }
              return res;
            })
            .catch((err) => {
              console.error("[RepeatScreenModal] INSERT 실패:", err.response?.data || err.message);
              throw err;
            }),
        );
      } else if (_originalData?.id) {
        // UPDATE - /edit 엔드포인트 사용 (originalData와 updatedData 형식)
        const updatePayload = {
          originalData: _originalData,
          updatedData: { ...dataToSave, id: _originalData.id },
        };
        console.log(`[RepeatScreenModal] UPDATE 요청: /table-management/tables/${targetTable}/edit`, updatePayload);
        savePromises.push(
          apiClient
            .put(`/table-management/tables/${targetTable}/edit`, updatePayload)
            .then((res) => {
              console.log("[RepeatScreenModal] UPDATE 응답:", res.data);
              savedIds.push(_originalData.id);
              return res;
            })
            .catch((err) => {
              console.error("[RepeatScreenModal] UPDATE 실패:", err.response?.data || err.message);
              throw err;
            }),
        );
      }
    }

    try {
      await Promise.all(savePromises);

      // 저장 후: 삭제된 행은 제거, 나머지는 dirty/editing 플래그 초기화
      setExternalTableData((prev) => {
        const updated = { ...prev };
        if (updated[key]) {
          // 삭제된 행은 완전히 제거
          updated[key] = updated[key]
            .filter((row) => !row._isDeleted)
            .map((row) => ({
              ...row,
              _isDirty: false,
              _isNew: false,
              _isEditing: false, // 🆕 v3.8: 수정 모드 해제
              _originalData: {
                ...row,
                _rowId: undefined,
                _originalData: undefined,
                _isDirty: undefined,
                _isNew: undefined,
                _isDeleted: undefined,
                _isEditing: undefined,
              },
            }));
        }
        return updated;
      });

      const savedCount = rowsToSave.length;
      const deletedCount = deletedRows.length;
      const message =
        deletedCount > 0 ? `${savedCount}건 저장, ${deletedCount}건 삭제 완료` : `${savedCount}건 저장 완료`;

      return { success: true, message, savedCount, deletedCount, savedIds };
    } catch (error: any) {
      console.error("[RepeatScreenModal] 테이블 영역 저장 실패:", error);
      return { success: false, message: error.message || "저장 중 오류가 발생했습니다." };
    }
  };

  // 🆕 v3.6: 테이블 영역 저장 핸들러
  const handleTableAreaSave = async (cardId: string, contentRowId: string, contentRow: CardContentRowConfig) => {
    setIsSaving(true);
    try {
      const result = await saveTableAreaData(cardId, contentRowId, contentRow);
      if (result.success) {
        console.log("[RepeatScreenModal] 테이블 영역 저장 성공:", result);

        // 🆕 v3.9: 집계 저장 설정이 있는 경우 연관 테이블 동기화
        const card = groupedCardsData.find((c) => c._cardId === cardId);
        if (card && grouping?.aggregations) {
          await saveAggregationsToRelatedTables(card, contentRowId);
        }
      } else {
        console.error("[RepeatScreenModal] 테이블 영역 저장 실패:", result.message);
      }
    } finally {
      setIsSaving(false);
    }
  };

  // 🆕 v3.9: 집계 결과를 연관 테이블에 저장
  const saveAggregationsToRelatedTables = async (card: GroupedCardData, contentRowId: string) => {
    if (!grouping?.aggregations) return;

    const savePromises: Promise<any>[] = [];

    for (const agg of grouping.aggregations) {
      const saveConfig = agg.saveConfig;

      // 저장 설정이 없거나 비활성화된 경우 스킵
      if (!saveConfig?.enabled) continue;

      // 자동 저장이 아닌 경우, 레이아웃에 연결되어 있는지 확인 필요
      // (현재는 자동 저장과 동일하게 처리 - 추후 레이아웃 연결 체크 추가 가능)

      // 집계 결과 값 가져오기
      const aggregatedValue = card._aggregations[agg.resultField];

      if (aggregatedValue === undefined) {
        console.warn(`[RepeatScreenModal] 집계 결과 없음: ${agg.resultField}`);
        continue;
      }

      // 조인 키로 대상 레코드 식별
      const sourceKeyValue = card._representativeData[saveConfig.joinKey.sourceField];

      if (!sourceKeyValue) {
        console.warn(`[RepeatScreenModal] 조인 키 값 없음: ${saveConfig.joinKey.sourceField}`);
        continue;
      }

      console.log(`[RepeatScreenModal] 집계 저장 시작:`, {
        aggregation: agg.resultField,
        value: aggregatedValue,
        targetTable: saveConfig.targetTable,
        targetColumn: saveConfig.targetColumn,
        joinKey: `${saveConfig.joinKey.sourceField}=${sourceKeyValue} -> ${saveConfig.joinKey.targetField}`,
      });

      // UPDATE API 호출
      const updatePayload = {
        originalData: { [saveConfig.joinKey.targetField]: sourceKeyValue },
        updatedData: {
          [saveConfig.targetColumn]: aggregatedValue,
          [saveConfig.joinKey.targetField]: sourceKeyValue,
        },
      };

      savePromises.push(
        apiClient
          .put(`/table-management/tables/${saveConfig.targetTable}/edit`, updatePayload)
          .then((res) => {
            console.log(
              `[RepeatScreenModal] 집계 저장 성공: ${agg.resultField} -> ${saveConfig.targetTable}.${saveConfig.targetColumn}`,
            );
            return res;
          })
          .catch((err) => {
            console.error(`[RepeatScreenModal] 집계 저장 실패: ${agg.resultField}`, err.response?.data || err.message);
            throw err;
          }),
      );
    }

    if (savePromises.length > 0) {
      try {
        await Promise.all(savePromises);
        console.log(`[RepeatScreenModal] 모든 집계 저장 완료: ${savePromises.length}건`);
      } catch (error) {
        console.error("[RepeatScreenModal] 일부 집계 저장 실패:", error);
      }
    }
  };

  // 🆕 v3.1: 외부 테이블 행 삭제 요청
  const handleDeleteExternalRowRequest = (
    cardId: string,
    rowId: string,
    contentRowId: string,
    contentRow: CardContentRowConfig,
  ) => {
    if (contentRow.tableCrud?.deleteConfirm?.enabled !== false) {
      // 삭제 확인 팝업 표시
      setPendingDeleteInfo({ cardId, rowId, contentRowId });
      setDeleteConfirmOpen(true);
    } else {
      // 바로 삭제
      handleDeleteExternalRow(cardId, rowId, contentRowId);
    }
  };

  // 🆕 v3.14: 외부 테이블 행 삭제 실행 (즉시 DELETE API 호출)
  const handleDeleteExternalRow = async (cardId: string, rowId: string, contentRowId: string) => {
    const key = `${cardId}-${contentRowId}`;
    const rows = externalTableData[key] || [];
    const targetRow = rows.find((row) => row._rowId === rowId);

    // 기존 DB 데이터인 경우 (id가 있는 경우) 즉시 삭제
    if (targetRow?._originalData?.id) {
      try {
        const contentRow = contentRows.find((r) => r.id === contentRowId);
        const targetTable = contentRow?.tableCrud?.targetTable || contentRow?.tableDataSource?.sourceTable;

        if (!targetTable) {
          console.error("[RepeatScreenModal] 삭제 대상 테이블을 찾을 수 없습니다.");
          return;
        }

        console.log(`[RepeatScreenModal] DELETE API 호출: ${targetTable}, id=${targetRow._originalData.id}`);

        // 백엔드는 배열 형태의 데이터를 기대함
        await apiClient.request({
          method: "DELETE",
          url: `/table-management/tables/${targetTable}/delete`,
          data: [{ id: targetRow._originalData.id }],
        });

        console.log(`[RepeatScreenModal] DELETE 성공: ${targetTable}, id=${targetRow._originalData.id}`);

        // 성공 시 UI에서 완전히 제거
        setExternalTableData((prev) => {
          const newData = {
            ...prev,
            [key]: prev[key].filter((row) => row._rowId !== rowId),
          };

          // 행 삭제 시 집계 재계산
          setTimeout(() => {
            recalculateAggregationsWithExternalData(newData);
          }, 0);

          return newData;
        });
      } catch (error: any) {
        console.error(`[RepeatScreenModal] DELETE 실패:`, error.response?.data || error.message);
        // 에러 시에도 다이얼로그 닫기
      }
    } else {
      // 새로 추가된 행 (아직 DB에 없음) - UI에서만 제거
      console.log(`[RepeatScreenModal] 새 행 삭제 (DB 없음): rowId=${rowId}`);
      setExternalTableData((prev) => {
        const newData = {
          ...prev,
          [key]: prev[key].filter((row) => row._rowId !== rowId),
        };

        // 행 삭제 시 집계 재계산
        setTimeout(() => {
          recalculateAggregationsWithExternalData(newData);
        }, 0);

        return newData;
      });
    }

    setDeleteConfirmOpen(false);
    setPendingDeleteInfo(null);
  };

  // 🆕 v3.7: 삭제 취소 (소프트 삭제 복원)
  const handleRestoreExternalRow = (cardId: string, rowId: string, contentRowId: string) => {
    const key = `${cardId}-${contentRowId}`;
    setExternalTableData((prev) => {
      const newData = {
        ...prev,
        [key]: (prev[key] || []).map((row) =>
          row._rowId === rowId ? { ...row, _isDeleted: false, _isDirty: true } : row,
        ),
      };

      setTimeout(() => {
        recalculateAggregationsWithExternalData(newData);
      }, 0);

      return newData;
    });
  };

  // 🆕 v3.8: 수정 모드 전환
  const handleEditExternalRow = (cardId: string, rowId: string, contentRowId: string) => {
    const key = `${cardId}-${contentRowId}`;
    setExternalTableData((prev) => ({
      ...prev,
      [key]: (prev[key] || []).map((row) => (row._rowId === rowId ? { ...row, _isEditing: true } : row)),
    }));
  };

  // 🆕 v3.8: 수정 취소
  const handleCancelEditExternalRow = (cardId: string, rowId: string, contentRowId: string) => {
    const key = `${cardId}-${contentRowId}`;
    setExternalTableData((prev) => ({
      ...prev,
      [key]: (prev[key] || []).map((row) =>
        row._rowId === rowId
          ? {
              ...row._originalData,
              _rowId: row._rowId,
              _originalData: row._originalData,
              _isEditing: false,
              _isDirty: false,
              _isNew: false,
              _isDeleted: false,
            }
          : row,
      ),
    }));
  };

  // 🆕 v3.1: 외부 테이블 행 데이터 변경
  const handleExternalRowDataChange = (
    cardId: string,
    contentRowId: string,
    rowId: string,
    field: string,
    value: any,
  ) => {
    const key = `${cardId}-${contentRowId}`;

    // 데이터 업데이트
    setExternalTableData((prev) => {
      const newData = {
        ...prev,
        [key]: (prev[key] || []).map((row) =>
          row._rowId === rowId ? { ...row, [field]: value, _isDirty: true } : row,
        ),
      };

      // 🆕 v3.5: 데이터 변경 시 집계 실시간 재계산
      // setTimeout으로 비동기 처리하여 상태 업데이트 후 재계산
      setTimeout(() => {
        recalculateAggregationsWithExternalData(newData);
      }, 0);

      return newData;
    });
  };

  // 그룹화된 데이터 처리
  const processGroupedData = (data: any[], groupingConfig: typeof grouping): GroupedCardData[] => {
    if (!groupingConfig?.enabled) {
      return [];
    }

    const groupByField = groupingConfig.groupByField;
    const groupMap = new Map<string, any[]>();

    // groupByField가 없으면 각 행을 개별 그룹으로 처리
    if (!groupByField) {
      // 각 행이 하나의 카드 (그룹)
      data.forEach((row, index) => {
        const groupKey = `row-${index}`;
        groupMap.set(groupKey, [row]);
      });
    } else {
      // 그룹별로 데이터 분류
      data.forEach((row) => {
        const groupKey = String(row[groupByField] || "");
        if (!groupMap.has(groupKey)) {
          groupMap.set(groupKey, []);
        }
        groupMap.get(groupKey)!.push(row);
      });
    }

    // GroupedCardData 생성
    const result: GroupedCardData[] = [];
    let cardIndex = 0;

    groupMap.forEach((rows, groupKey) => {
      // 행 데이터 생성
      const cardRows: CardRowData[] = rows.map((row, idx) => ({
        _rowId: `row-${cardIndex}-${idx}-${Date.now()}`,
        _originalData: { ...row },
        _isDirty: false,
        ...row,
      }));

      const representativeData = rows[0] || {};

      // 🆕 v3.2: 집계 계산 (순서대로 - 이전 집계 결과 참조 가능)
      // 1단계: 기본 테이블 컬럼 집계만 (외부 테이블 데이터는 아직 없음)
      const aggregations: Record<string, number> = {};
      if (groupingConfig.aggregations) {
        groupingConfig.aggregations.forEach((agg) => {
          const sourceType = agg.sourceType || "column";

          if (sourceType === "column") {
            // 컬럼 집계 (기본 테이블만 - 외부 테이블은 나중에 처리)
            const sourceTable = agg.sourceTable || dataSource?.sourceTable;
            const isExternalTable = sourceTable && sourceTable !== dataSource?.sourceTable;

            if (!isExternalTable) {
              // 기본 테이블 집계
              aggregations[agg.resultField] = calculateColumnAggregation(
                rows,
                agg.sourceField || "",
                agg.type || "sum",
              );
            } else {
              // 외부 테이블 집계는 나중에 계산 (placeholder)
              aggregations[agg.resultField] = 0;
            }
          } else if (sourceType === "formula") {
            // 가상 집계 (연산식) - 외부 테이블 없이 먼저 계산 시도
            // 외부 테이블 데이터가 필요한 경우 나중에 재계산됨
            if (agg.formula) {
              aggregations[agg.resultField] = evaluateFormulaWithContext(
                agg.formula,
                representativeData,
                rows,
                [], // 외부 테이블 데이터 없음
                aggregations, // 이전 집계 결과 참조
              );
            } else {
              aggregations[agg.resultField] = 0;
            }
          }
        });
      }

      // 안정적인 _cardId 생성 (Date.now() 대신 groupKey 사용)
      // groupKey가 없으면 대표 데이터의 id 사용
      const stableId = groupKey || representativeData.id || cardIndex;
      result.push({
        _cardId: `grouped-card-${cardIndex}-${stableId}`,
        _groupKey: groupKey,
        _groupField: groupByField || "",
        _aggregations: aggregations,
        _rows: cardRows,
        _representativeData: representativeData,
      });

      cardIndex++;
    });

    return result;
  };

  // 집계 계산 (컬럼 집계용)
  const calculateColumnAggregation = (
    rows: any[],
    sourceField: string,
    type: "sum" | "count" | "avg" | "min" | "max",
  ): number => {
    const values = rows.map((row) => Number(row[sourceField]) || 0);

    switch (type) {
      case "sum":
        return values.reduce((a, b) => a + b, 0);
      case "count":
        return values.length;
      case "avg":
        return values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
      case "min":
        return values.length > 0 ? Math.min(...values) : 0;
      case "max":
        return values.length > 0 ? Math.max(...values) : 0;
      default:
        return 0;
    }
  };

  // 🆕 v3.2: 집계 계산 (다중 테이블 및 formula 지원)
  const calculateAggregation = (
    agg: AggregationConfig,
    cardRows: any[], // 기본 테이블 행들
    externalRows: any[], // 외부 테이블 행들
    previousAggregations: Record<string, number>, // 이전 집계 결과들
    representativeData: Record<string, any>, // 카드 대표 데이터
  ): number => {
    const sourceType = agg.sourceType || "column";

    if (sourceType === "column") {
      // 컬럼 집계
      const sourceTable = agg.sourceTable || dataSource?.sourceTable;
      const isExternalTable = sourceTable && sourceTable !== dataSource?.sourceTable;

      // 외부 테이블인 경우 externalRows 사용, 아니면 cardRows 사용
      const targetRows = isExternalTable ? externalRows : cardRows;

      return calculateColumnAggregation(targetRows, agg.sourceField || "", agg.type || "sum");
    } else if (sourceType === "formula") {
      // 가상 집계 (연산식)
      if (!agg.formula) return 0;

      return evaluateFormulaWithContext(agg.formula, representativeData, cardRows, externalRows, previousAggregations);
    }

    return 0;
  };

  // 🆕 v3.1: 집계 표시값 계산 (formula, external 등 지원)
  const calculateAggregationDisplayValue = (
    aggField: AggregationDisplayConfig,
    card: GroupedCardData,
  ): number | string => {
    const sourceType = aggField.sourceType || "aggregation";

    switch (sourceType) {
      case "aggregation":
        // 기존 집계 결과 참조
        return card._aggregations?.[aggField.aggregationResultField || ""] || 0;

      case "formula":
        // 컬럼 간 연산
        if (!aggField.formula) return 0;
        return evaluateFormula(aggField.formula, card._representativeData, card._rows);

      case "external":
        // 외부 테이블 값 (별도 로드 필요 - 현재는 placeholder)
        // TODO: 외부 테이블 값 로드 구현
        return 0;

      case "externalFormula":
        // 외부 테이블 + 연산 (별도 로드 필요 - 현재는 placeholder)
        // TODO: 외부 테이블 값 로드 후 연산 구현
        return 0;

      default:
        return 0;
    }
  };

  // 🆕 v3.2: 연산식 평가 (다중 테이블, 이전 집계 결과 참조 지원)
  const evaluateFormulaWithContext = (
    formula: string,
    representativeData: Record<string, any>,
    cardRows: any[], // 기본 테이블 행들
    externalRows: any[], // 외부 테이블 행들
    previousAggregations: Record<string, number>, // 이전 집계 결과들
  ): number => {
    try {
      let expression = formula;

      // 1. 외부 테이블 집계 함수 처리: SUM_EXT({field}), COUNT_EXT({field}) 등
      const extAggFunctions = ["SUM_EXT", "COUNT_EXT", "AVG_EXT", "MIN_EXT", "MAX_EXT"];
      for (const fn of extAggFunctions) {
        const regex = new RegExp(`${fn}\\(\\{(\\w+)\\}\\)`, "g");
        expression = expression.replace(regex, (match, fieldName) => {
          if (!externalRows || externalRows.length === 0) {
            console.log(`[SUM_EXT] ${fieldName}: 외부 데이터 없음`);
            return "0";
          }
          const values = externalRows.map((row) => Number(row[fieldName]) || 0);
          const sum = values.reduce((a, b) => a + b, 0);
          console.log(`[SUM_EXT] ${fieldName}: ${externalRows.length}개 행, 값들:`, values, `합계: ${sum}`);
          const baseFn = fn.replace("_EXT", "");
          switch (baseFn) {
            case "SUM":
              return String(values.reduce((a, b) => a + b, 0));
            case "COUNT":
              return String(values.length);
            case "AVG":
              return String(values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0);
            case "MIN":
              return String(values.length > 0 ? Math.min(...values) : 0);
            case "MAX":
              return String(values.length > 0 ? Math.max(...values) : 0);
            default:
              return "0";
          }
        });
      }

      // 2. 기본 테이블 집계 함수 처리: SUM({field}), COUNT({field}) 등
      const aggFunctions = ["SUM", "COUNT", "AVG", "MIN", "MAX"];
      for (const fn of aggFunctions) {
        // SUM_EXT는 이미 처리했으므로 제외
        const regex = new RegExp(`(?<!_)${fn}\\(\\{(\\w+)\\}\\)`, "g");
        expression = expression.replace(regex, (match, fieldName) => {
          if (!cardRows || cardRows.length === 0) return "0";
          const values = cardRows.map((row) => Number(row[fieldName]) || 0);
          switch (fn) {
            case "SUM":
              return String(values.reduce((a, b) => a + b, 0));
            case "COUNT":
              return String(values.length);
            case "AVG":
              return String(values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0);
            case "MIN":
              return String(values.length > 0 ? Math.min(...values) : 0);
            case "MAX":
              return String(values.length > 0 ? Math.max(...values) : 0);
            default:
              return "0";
          }
        });
      }

      // 3. 단순 필드 참조 치환 (이전 집계 결과 또는 대표 데이터)
      const fieldRegex = /\{(\w+)\}/g;
      expression = expression.replace(fieldRegex, (match, fieldName) => {
        // 먼저 이전 집계 결과에서 찾기
        if (previousAggregations && fieldName in previousAggregations) {
          return String(previousAggregations[fieldName]);
        }
        // 대표 데이터에서 값 가져오기
        const value = representativeData[fieldName];
        return String(Number(value) || 0);
      });

      // 4. 안전한 수식 평가 (사칙연산만 허용)
      // 허용 문자: 숫자, 소수점, 사칙연산, 괄호, 공백
      if (!/^[\d\s+\-*/().]+$/.test(expression)) {
        console.warn("[RepeatScreenModal] 허용되지 않는 연산식:", expression);
        return 0;
      }

      // eval 대신 Function 사용 (더 안전)
      const result = new Function(`return ${expression}`)();
      return Number(result) || 0;
    } catch (error) {
      console.error("[RepeatScreenModal] 연산식 평가 실패:", formula, error);
      return 0;
    }
  };

  // 레거시 호환: 기존 evaluateFormula 유지
  const evaluateFormula = (formula: string, representativeData: Record<string, any>, rows?: any[]): number => {
    return evaluateFormulaWithContext(formula, representativeData, rows || [], [], {});
  };

  // 카드 데이터 로드 (소스 설정에 따라)
  const loadCardData = async (originalData: any): Promise<Record<string, any>> => {
    const cardData: Record<string, any> = {};

    // 🆕 v3: contentRows 사용
    if (contentRows && contentRows.length > 0) {
      for (const contentRow of contentRows) {
        // 헤더/필드 타입의 컬럼 처리
        if ((contentRow.type === "header" || contentRow.type === "fields") && contentRow.columns) {
          for (const col of contentRow.columns) {
            if (col.sourceConfig) {
              if (col.sourceConfig.type === "direct") {
                cardData[col.field] = originalData[col.sourceConfig.sourceColumn || col.field];
              } else if (col.sourceConfig.type === "join" && col.sourceConfig.joinTable) {
                cardData[col.field] = null; // 조인은 나중에 일괄 처리
              } else if (col.sourceConfig.type === "manual") {
                cardData[col.field] = null;
              }
            } else {
              // sourceConfig가 없으면 원본 데이터에서 직접 가져옴
              cardData[col.field] = originalData[col.field];
            }
          }
        }

        // 테이블 타입의 컬럼 처리
        if (contentRow.type === "table" && contentRow.tableColumns) {
          for (const col of contentRow.tableColumns) {
            cardData[col.field] = originalData[col.field];
          }
        }
      }
    } else {
      // 레거시: cardLayout 사용
      for (const row of cardLayout) {
        for (const col of row.columns) {
          if (col.sourceConfig) {
            if (col.sourceConfig.type === "direct") {
              cardData[col.field] = originalData[col.sourceConfig.sourceColumn || col.field];
            } else if (col.sourceConfig.type === "join" && col.sourceConfig.joinTable) {
              cardData[col.field] = null; // 조인은 나중에 일괄 처리
            } else if (col.sourceConfig.type === "manual") {
              cardData[col.field] = null;
            }
          } else {
            cardData[col.field] = originalData[col.field];
          }
        }
      }
    }

    return cardData;
  };

  // Simple 모드: 카드 데이터 변경
  const handleCardDataChange = (cardId: string, field: string, value: any) => {
    setCardsData((prev) =>
      prev.map((card) => (card._cardId === cardId ? { ...card, [field]: value, _isDirty: true } : card)),
    );
  };

  // WithTable 모드: 행 데이터 변경
  const handleRowDataChange = (cardId: string, rowId: string, field: string, value: any) => {
    setGroupedCardsData((prev) =>
      prev.map((card) => {
        if (card._cardId !== cardId) return card;

        const updatedRows = card._rows.map((row) =>
          row._rowId === rowId ? { ...row, [field]: value, _isDirty: true } : row,
        );

        // 집계값 재계산
        const newAggregations: Record<string, number> = {};
        if (grouping?.aggregations) {
          grouping.aggregations.forEach((agg) => {
            newAggregations[agg.resultField] = calculateAggregation(updatedRows, agg);
          });
        }

        return {
          ...card,
          _rows: updatedRows,
          _aggregations: newAggregations,
        };
      }),
    );
  };

  // 카드 제목 생성
  const getCardTitle = (data: Record<string, any>, index: number): string => {
    let title = cardTitle;
    title = title.replace("{index}", String(index + 1));

    const matches = title.match(/\{(\w+)\}/g);
    if (matches) {
      matches.forEach((match) => {
        const field = match.slice(1, -1);
        const value = data[field] || "";
        title = title.replace(match, String(value));
      });
    }

    return title;
  };

  // 전체 저장
  const handleSaveAll = async () => {
    setIsSaving(true);

    try {
      // 기존 데이터 저장
      if (cardMode === "withTable") {
        await saveGroupedData();
      } else {
        await saveSimpleData();
      }

      // 🆕 v3.1: 외부 테이블 데이터 저장
      await saveExternalTableData();

      // 🆕 v3.12: 연동 저장 처리 (syncSaves)
      await processSyncSaves();

      alert("저장되었습니다.");
    } catch (error: any) {
      console.error("저장 실패:", error);
      alert(`저장 중 오류가 발생했습니다: ${error.message}`);
    } finally {
      setIsSaving(false);
    }
  };

  // 🆕 v3.1: 외부 테이블 데이터 저장
  const saveExternalTableData = async () => {
    const savePromises: Promise<void>[] = [];

    for (const [key, rows] of Object.entries(externalTableData)) {
      // key 형식: cardId-contentRowId
      const [cardId, contentRowId] = key.split("-").slice(0, 2);
      const contentRow = contentRows.find((r) => r.id === contentRowId || key.includes(r.id));

      if (!contentRow?.tableDataSource?.enabled) continue;

      const targetTable = contentRow.tableCrud?.targetTable || contentRow.tableDataSource.sourceTable;
      const dirtyRows = rows.filter((row) => row._isDirty);

      for (const row of dirtyRows) {
        const { _rowId, _originalData, _isDirty, _isNew, ...dataToSave } = row;

        if (_isNew) {
          // INSERT
          savePromises.push(apiClient.post(`/table-management/tables/${targetTable}/data`, dataToSave).then(() => {}));
        } else if (_originalData?.id) {
          // UPDATE
          savePromises.push(
            apiClient
              .put(`/table-management/tables/${targetTable}/data/${_originalData.id}`, dataToSave)
              .then(() => {}),
          );
        }
      }
    }

    await Promise.all(savePromises);

    // 저장 후 dirty 플래그 초기화
    setExternalTableData((prev) => {
      const updated: Record<string, any[]> = {};
      for (const [key, rows] of Object.entries(prev)) {
        updated[key] = rows.map((row) => ({
          ...row,
          _isDirty: false,
          _isNew: false,
          _originalData: {
            ...row,
            _rowId: undefined,
            _originalData: undefined,
            _isDirty: undefined,
            _isNew: undefined,
          },
        }));
      }
      return updated;
    });
  };

  // 🆕 v3.12: 연동 저장 처리 (syncSaves)
  const processSyncSaves = async () => {
    const syncPromises: Promise<void>[] = [];

    // contentRows에서 syncSaves가 설정된 테이블 행 찾기
    for (const contentRow of contentRows) {
      if (contentRow.type !== "table") continue;
      if (!contentRow.tableCrud?.syncSaves?.length) continue;

      const sourceTable = contentRow.tableDataSource?.sourceTable;
      if (!sourceTable) continue;

      // 이 테이블 행의 모든 카드 데이터 수집
      for (const card of groupedCardsData) {
        const key = `${card._cardId}-${contentRow.id}`;
        const rows = (externalTableData[key] || []).filter((row) => !row._isDeleted);

        // 각 syncSave 설정 처리
        for (const syncSave of contentRow.tableCrud.syncSaves) {
          if (!syncSave.enabled) continue;
          if (!syncSave.sourceColumn || !syncSave.targetTable || !syncSave.targetColumn) continue;

          // 조인 키 값 수집 (중복 제거)
          const joinKeyValues = new Set<string | number>();
          for (const row of rows) {
            const keyValue = row[syncSave.joinKey.sourceField];
            if (keyValue !== undefined && keyValue !== null) {
              joinKeyValues.add(keyValue);
            }
          }

          // 각 조인 키별로 집계 계산 및 업데이트
          for (const keyValue of joinKeyValues) {
            // 해당 조인 키에 해당하는 행들만 필터링
            const filteredRows = rows.filter((row) => row[syncSave.joinKey.sourceField] === keyValue);

            // 집계 계산
            let aggregatedValue: number = 0;
            const values = filteredRows.map((row) => Number(row[syncSave.sourceColumn]) || 0);

            switch (syncSave.aggregationType) {
              case "sum":
                aggregatedValue = values.reduce((a, b) => a + b, 0);
                break;
              case "count":
                aggregatedValue = values.length;
                break;
              case "avg":
                aggregatedValue = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
                break;
              case "min":
                aggregatedValue = values.length > 0 ? Math.min(...values) : 0;
                break;
              case "max":
                aggregatedValue = values.length > 0 ? Math.max(...values) : 0;
                break;
              case "latest":
                aggregatedValue = values.length > 0 ? values[values.length - 1] : 0;
                break;
            }

            console.log(
              `[SyncSave] ${sourceTable}.${syncSave.sourceColumn} → ${syncSave.targetTable}.${syncSave.targetColumn}`,
              {
                joinKey: keyValue,
                aggregationType: syncSave.aggregationType,
                values,
                aggregatedValue,
              },
            );

            // 대상 테이블 업데이트
            syncPromises.push(
              apiClient
                .put(`/table-management/tables/${syncSave.targetTable}/data/${keyValue}`, {
                  [syncSave.targetColumn]: aggregatedValue,
                })
                .then(() => {
                  console.log(
                    `[SyncSave] 업데이트 완료: ${syncSave.targetTable}.${syncSave.targetColumn} = ${aggregatedValue} (id=${keyValue})`,
                  );
                })
                .catch((err) => {
                  console.error(`[SyncSave] 업데이트 실패:`, err);
                  throw err;
                }),
            );
          }
        }
      }
    }

    if (syncPromises.length > 0) {
      console.log(`[SyncSave] ${syncPromises.length}개 연동 저장 처리 중...`);
      await Promise.all(syncPromises);
      console.log(`[SyncSave] 연동 저장 완료`);
    }
  };

  // 🆕 v3.1: Footer 버튼 클릭 핸들러
  const handleFooterButtonClick = async (btn: FooterButtonConfig) => {
    switch (btn.action) {
      case "save":
        await handleSaveAll();
        break;
      case "cancel":
      case "close":
        // 모달 닫기 이벤트 발생
        window.dispatchEvent(new CustomEvent("closeScreenModal"));
        break;
      case "reset":
        // 데이터 초기화
        if (confirm("변경 사항을 모두 취소하시겠습니까?")) {
          // 외부 테이블 데이터 초기화
          setExternalTableData({});
          // 기존 데이터 재로드
          setCardsData([]);
          setGroupedCardsData([]);
        }
        break;
      case "custom":
        // 커스텀 액션 이벤트 발생
        if (btn.customAction) {
          window.dispatchEvent(
            new CustomEvent("repeatScreenModalCustomAction", {
              detail: {
                actionType: btn.customAction.type,
                config: btn.customAction.config,
                componentId: component?.id,
              },
            }),
          );
        }
        break;
    }
  };

  // Simple 모드 저장
  const saveSimpleData = async () => {
    const dirtyCards = cardsData.filter((card) => card._isDirty);

    if (dirtyCards.length === 0) {
      alert("변경된 데이터가 없습니다.");
      return;
    }

    const groupedData: Record<string, any[]> = {};

    for (const card of dirtyCards) {
      for (const row of cardLayout) {
        for (const col of row.columns) {
          if (col.targetConfig && col.targetConfig.saveEnabled !== false) {
            const targetTable = col.targetConfig.targetTable;
            const targetColumn = col.targetConfig.targetColumn;
            const value = card[col.field];

            if (!groupedData[targetTable]) {
              groupedData[targetTable] = [];
            }

            let existingRow = groupedData[targetTable].find((r) => r._cardId === card._cardId);

            if (!existingRow) {
              existingRow = {
                _cardId: card._cardId,
                _originalData: card._originalData,
              };
              groupedData[targetTable].push(existingRow);
            }

            existingRow[targetColumn] = value;
          }
        }
      }
    }

    await saveToTables(groupedData);

    setCardsData((prev) => prev.map((card) => ({ ...card, _isDirty: false })));
  };

  // WithTable 모드 저장
  const saveGroupedData = async () => {
    const dirtyCards = groupedCardsData.filter((card) => card._rows.some((row) => row._isDirty));

    if (dirtyCards.length === 0) {
      alert("변경된 데이터가 없습니다.");
      return;
    }

    const groupedData: Record<string, any[]> = {};

    for (const card of dirtyCards) {
      const dirtyRows = card._rows.filter((row) => row._isDirty);

      for (const row of dirtyRows) {
        // 테이블 컬럼에서 저장 대상 추출
        if (tableLayout?.tableColumns) {
          for (const col of tableLayout.tableColumns) {
            if (col.editable && col.targetConfig && col.targetConfig.saveEnabled !== false) {
              const targetTable = col.targetConfig.targetTable;
              const targetColumn = col.targetConfig.targetColumn;
              const value = row[col.field];

              if (!groupedData[targetTable]) {
                groupedData[targetTable] = [];
              }

              let existingRow = groupedData[targetTable].find((r) => r._rowId === row._rowId);

              if (!existingRow) {
                existingRow = {
                  _rowId: row._rowId,
                  _originalData: row._originalData,
                };
                groupedData[targetTable].push(existingRow);
              }

              existingRow[targetColumn] = value;
            }
          }
        }
      }
    }

    await saveToTables(groupedData);

    setGroupedCardsData((prev) =>
      prev.map((card) => ({
        ...card,
        _rows: card._rows.map((row) => ({ ...row, _isDirty: false })),
      })),
    );
  };

  // 테이블별 저장
  const saveToTables = async (groupedData: Record<string, any[]>) => {
    const savePromises = Object.entries(groupedData).map(async ([tableName, rows]) => {
      return Promise.all(
        rows.map(async (row) => {
          const { _cardId, _rowId, _originalData, ...dataToSave } = row;
          const id = _originalData?.id;

          if (id) {
            await apiClient.put(`/table-management/tables/${tableName}/data/${id}`, dataToSave);
          } else {
            await apiClient.post(`/table-management/tables/${tableName}/data`, dataToSave);
          }
        }),
      );
    });

    await Promise.all(savePromises);
  };

  // 수정 여부 확인
  const hasDirtyData = useMemo(() => {
    // 기존 데이터 수정 여부
    let hasBaseDirty = false;
    if (cardMode === "withTable") {
      hasBaseDirty = groupedCardsData.some((card) => card._rows.some((row) => row._isDirty));
    } else {
      hasBaseDirty = cardsData.some((c) => c._isDirty);
    }

    // 🆕 v3.1: 외부 테이블 데이터 수정 여부
    const hasExternalDirty = Object.values(externalTableData).some((rows) => rows.some((row) => row._isDirty));

    return hasBaseDirty || hasExternalDirty;
  }, [cardMode, cardsData, groupedCardsData, externalTableData]);

  // 디자인 모드 렌더링
  if (isDesignMode) {
    // 행 타입별 개수 계산
    const rowTypeCounts = {
      header: contentRows.filter((r) => r.type === "header").length,
      aggregation: contentRows.filter((r) => r.type === "aggregation").length,
      table: contentRows.filter((r) => r.type === "table").length,
      fields: contentRows.filter((r) => r.type === "fields").length,
    };

    return (
      <div
        className={cn(
          "border-primary/50 from-primary/5 to-primary/10 h-full min-h-[400px] w-full rounded-lg border-2 border-dashed bg-gradient-to-br p-8",
          className,
        )}
      >
        <div className="flex h-full flex-col items-center justify-center space-y-6">
          {/* 아이콘 */}
          <div className="bg-primary/10 flex h-20 w-20 items-center justify-center rounded-full">
            <Layers className="text-primary h-10 w-10" />
          </div>

          {/* 제목 */}
          <div className="space-y-2 text-center">
            <div className="text-primary text-xl font-bold">Repeat Screen Modal</div>
            <div className="text-foreground text-base font-semibold">반복 화면 모달</div>
            <Badge variant="secondary">v3 자유 레이아웃</Badge>
          </div>

          {/* 행 구성 정보 */}
          <div className="flex flex-wrap justify-center gap-2">
            {contentRows.length > 0 ? (
              <>
                {rowTypeCounts.header > 0 && (
                  <Badge className="bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300">
                    헤더 {rowTypeCounts.header}개
                  </Badge>
                )}
                {rowTypeCounts.aggregation > 0 && (
                  <Badge className="bg-amber-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300">
                    집계 {rowTypeCounts.aggregation}개
                  </Badge>
                )}
                {rowTypeCounts.table > 0 && (
                  <Badge className="bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary/80">
                    테이블 {rowTypeCounts.table}개
                  </Badge>
                )}
                {rowTypeCounts.fields > 0 && (
                  <Badge className="bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300">
                    필드 {rowTypeCounts.fields}개
                  </Badge>
                )}
              </>
            ) : (
              <Badge variant="outline">행 없음</Badge>
            )}
          </div>

          {/* 통계 정보 */}
          <div className="flex gap-6 text-center">
            <div className="space-y-1">
              <div className="text-primary text-2xl font-bold">{contentRows.length}</div>
              <div className="text-muted-foreground text-xs">행 (Rows)</div>
            </div>
            <div className="bg-border w-px" />
            <div className="space-y-1">
              <div className="text-primary text-2xl font-bold">{grouping?.aggregations?.length || 0}</div>
              <div className="text-muted-foreground text-xs">집계 설정</div>
            </div>
            <div className="bg-border w-px" />
            <div className="space-y-1">
              <div className="text-primary text-2xl font-bold">{dataSource?.sourceTable ? 1 : 0}</div>
              <div className="text-muted-foreground text-xs">데이터 소스</div>
            </div>
          </div>

          {/* 데이터 소스 정보 */}
          {dataSource?.sourceTable && (
            <div className="rounded-md bg-emerald-100 px-3 py-2 text-xs dark:bg-emerald-900">
              소스 테이블: <strong>{dataSource.sourceTable}</strong>
              {dataSource.filterField && <span className="ml-2">(필터: {dataSource.filterField})</span>}
            </div>
          )}

          {/* 그룹핑 정보 */}
          {grouping?.enabled && (
            <div className="rounded-md bg-purple-100 px-3 py-2 text-xs dark:bg-purple-900">
              그룹핑: <strong>{grouping.groupByField}</strong>
            </div>
          )}

          {/* 카드 제목 정보 */}
          {showCardTitle && cardTitle && (
            <div className="bg-muted rounded-md px-3 py-2 text-xs">
              카드 제목: <strong>{cardTitle}</strong>
            </div>
          )}

          {/* 설정 안내 */}
          <div className="text-muted-foreground bg-muted/50 rounded-md border px-4 py-2 text-xs">
            오른쪽 패널에서 행을 추가하고 타입(헤더/집계/테이블/필드)을 선택하세요
          </div>
        </div>
      </div>
    );
  }

  // 로딩 상태
  if (isLoading) {
    return (
      <div className="flex items-center justify-center p-12">
        <Loader2 className="text-primary h-8 w-8 animate-spin" />
        <span className="text-muted-foreground ml-3 text-sm">데이터를 불러오는 중...</span>
      </div>
    );
  }

  // 오류 상태
  if (loadError) {
    return (
      <div className="border-destructive/50 bg-destructive/5 rounded-lg border p-6">
        <div className="text-destructive mb-2 flex items-center gap-2">
          <X className="h-5 w-5" />
          <span className="font-semibold">데이터 로드 실패</span>
        </div>
        <p className="text-muted-foreground text-sm">{loadError}</p>
      </div>
    );
  }

  // 🆕 v3: 자유 레이아웃 렌더링 (contentRows 사용)
  const useNewLayout = contentRows && contentRows.length > 0;
  const useGrouping = grouping?.enabled;

  // 그룹핑 모드 렌더링
  if (useGrouping) {
    return (
      <div className={cn("space-y-6 overflow-x-auto", className)}>
        <div className="min-w-[800px] space-y-4" style={{ gap: cardSpacing }}>
          {groupedCardsData.map((card, cardIndex) => (
            <Card
              key={card._cardId}
              className={cn(
                "transition-shadow",
                showCardBorder && "border-2",
                card._rows.some((r) => r._isDirty) && "border-primary shadow-lg",
              )}
            >
              {/* 카드 제목 (선택사항) */}
              {showCardTitle && (
                <CardHeader className="pb-3">
                  <CardTitle className="flex items-center justify-between text-lg">
                    <span>{getCardTitle(card._representativeData, cardIndex)}</span>
                    {card._rows.some((r) => r._isDirty) && (
                      <Badge variant="outline" className="text-primary text-xs">
                        수정됨
                      </Badge>
                    )}
                  </CardTitle>
                </CardHeader>
              )}
              <CardContent className="space-y-4">
                {/* 🆕 v3: contentRows 기반 렌더링 */}
                {useNewLayout ? (
                  contentRows.map((contentRow, rowIndex) => (
                    <div key={contentRow.id || `crow-${rowIndex}`}>
                      {contentRow.type === "table" && contentRow.tableDataSource?.enabled ? (
                        // 🆕 v3.1: 외부 테이블 데이터 소스 사용
                        <div className="overflow-hidden rounded-lg border">
                          {/* 테이블 헤더 영역: 제목 + 버튼들 */}
                          {(contentRow.tableTitle || contentRow.tableCrud?.allowCreate) && (
                            <div className="bg-muted/30 flex items-center justify-between border-b px-4 py-2 text-sm font-medium">
                              <span>{contentRow.tableTitle || ""}</span>
                              <div className="flex items-center gap-2">
                                {/* 추가 버튼 */}
                                {contentRow.tableCrud?.allowCreate && (
                                  <Button
                                    variant="outline"
                                    size="sm"
                                    onClick={() => handleAddExternalRow(card._cardId, contentRow.id, contentRow)}
                                    className="h-7 gap-1 text-xs"
                                  >
                                    <Plus className="h-3 w-3" />
                                    추가
                                  </Button>
                                )}
                              </div>
                            </div>
                          )}
                          <Table>
                            {contentRow.showTableHeader !== false && (
                              <TableHeader>
                                <TableRow className="bg-muted/50">
                                  {/* 🆕 v3.13: hidden 컬럼 필터링 */}
                                  {(contentRow.tableColumns || [])
                                    .filter((col) => !col.hidden)
                                    .map((col) => (
                                      <TableHead
                                        key={col.id}
                                        style={{ width: col.width }}
                                        className={cn("text-xs", col.align && `text-${col.align}`)}
                                      >
                                        {col.label}
                                      </TableHead>
                                    ))}
                                  {(contentRow.tableCrud?.allowUpdate || contentRow.tableCrud?.allowDelete) && (
                                    <TableHead className="w-[80px] text-center text-xs">작업</TableHead>
                                  )}
                                </TableRow>
                              </TableHeader>
                            )}
                            <TableBody>
                              {(externalTableData[`${card._cardId}-${contentRow.id}`] || []).length === 0 ? (
                                <TableRow>
                                  <TableCell
                                    colSpan={
                                      (contentRow.tableColumns?.filter((col) => !col.hidden)?.length || 0) +
                                      (contentRow.tableCrud?.allowDelete ? 1 : 0)
                                    }
                                    className="text-muted-foreground py-8 text-center"
                                  >
                                    데이터가 없습니다.
                                  </TableCell>
                                </TableRow>
                              ) : (
                                (externalTableData[`${card._cardId}-${contentRow.id}`] || []).map((row) => (
                                  <TableRow
                                    key={row._rowId}
                                    className={cn(
                                      row._isDirty && "bg-primary/5",
                                      row._isNew && "bg-emerald-50 dark:bg-emerald-950",
                                      row._isDeleted && "bg-destructive/10 opacity-60",
                                    )}
                                  >
                                    {/* 🆕 v3.13: hidden 컬럼 필터링 */}
                                    {(contentRow.tableColumns || [])
                                      .filter((col) => !col.hidden)
                                      .map((col) => (
                                        <TableCell
                                          key={`${row._rowId}-${col.id}`}
                                          className={cn(
                                            "text-sm",
                                            col.align && `text-${col.align}`,
                                            row._isDeleted && "text-muted-foreground line-through",
                                          )}
                                        >
                                          {renderTableCell(
                                            col,
                                            row,
                                            (value) =>
                                              handleExternalRowDataChange(
                                                card._cardId,
                                                contentRow.id,
                                                row._rowId,
                                                col.field,
                                                value,
                                              ),
                                            row._isNew || row._isEditing, // 신규 행이거나 수정 모드일 때만 편집 가능
                                          )}
                                        </TableCell>
                                      ))}
                                    {(contentRow.tableCrud?.allowUpdate || contentRow.tableCrud?.allowDelete) && (
                                      <TableCell className="text-center">
                                        <div className="flex items-center justify-center gap-1">
                                          {/* 수정 버튼: 저장된 행(isNew가 아닌)이고 편집 모드가 아닐 때만 표시 */}
                                          {contentRow.tableCrud?.allowUpdate &&
                                            !row._isNew &&
                                            !row._isEditing &&
                                            !row._isDeleted && (
                                              <Button
                                                variant="ghost"
                                                size="sm"
                                                onClick={() =>
                                                  handleEditExternalRow(card._cardId, row._rowId, contentRow.id)
                                                }
                                                className="h-7 w-7 p-0 text-primary hover:bg-primary/10 hover:text-primary"
                                                title="수정"
                                              >
                                                <Pencil className="h-4 w-4" />
                                              </Button>
                                            )}
                                          {/* 수정 취소 버튼: 편집 모드일 때만 표시 */}
                                          {row._isEditing && !row._isNew && (
                                            <Button
                                              variant="ghost"
                                              size="sm"
                                              onClick={() =>
                                                handleCancelEditExternalRow(card._cardId, row._rowId, contentRow.id)
                                              }
                                              className="text-muted-foreground hover:text-foreground hover:bg-muted h-7 w-7 p-0"
                                              title="수정 취소"
                                            >
                                              <X className="h-4 w-4" />
                                            </Button>
                                          )}
                                          {/* 삭제/복원 버튼 */}
                                          {contentRow.tableCrud?.allowDelete &&
                                            (row._isDeleted ? (
                                              <Button
                                                variant="ghost"
                                                size="sm"
                                                onClick={() =>
                                                  handleRestoreExternalRow(card._cardId, row._rowId, contentRow.id)
                                                }
                                                className="text-primary hover:text-primary hover:bg-primary/10 h-7 w-7 p-0"
                                                title="삭제 취소"
                                              >
                                                <RotateCcw className="h-4 w-4" />
                                              </Button>
                                            ) : (
                                              <Button
                                                variant="ghost"
                                                size="sm"
                                                onClick={() =>
                                                  handleDeleteExternalRowRequest(
                                                    card._cardId,
                                                    row._rowId,
                                                    contentRow.id,
                                                    contentRow,
                                                  )
                                                }
                                                className="text-destructive hover:text-destructive hover:bg-destructive/10 h-7 w-7 p-0"
                                                title="삭제"
                                              >
                                                <Trash2 className="h-4 w-4" />
                                              </Button>
                                            ))}
                                        </div>
                                      </TableCell>
                                    )}
                                  </TableRow>
                                ))
                              )}
                            </TableBody>
                          </Table>
                        </div>
                      ) : (
                        // 기존 renderContentRow 사용
                        renderContentRow(contentRow, card, grouping?.aggregations || [], handleRowDataChange)
                      )}
                    </div>
                  ))
                ) : (
                  // 레거시: tableLayout 사용
                  <>
                    {tableLayout?.headerRows && tableLayout.headerRows.length > 0 && (
                      <div className="bg-muted/30 space-y-3 rounded-lg p-4">
                        {tableLayout.headerRows.map((row, rowIndex) => (
                          <div
                            key={row.id || `hrow-${rowIndex}`}
                            className={cn(
                              "grid gap-4",
                              row.layout === "vertical" ? "grid-cols-1" : "auto-cols-fr grid-flow-col",
                              row.backgroundColor && getBackgroundClass(row.backgroundColor),
                              row.rounded && "rounded-lg",
                              row.padding && `p-${row.padding}`,
                            )}
                            style={{ gap: row.gap || "16px" }}
                          >
                            {row.columns.map((col, colIndex) => (
                              <div key={col.id || `hcol-${colIndex}`} style={{ width: col.width }}>
                                {renderHeaderColumn(col, card, grouping?.aggregations || [])}
                              </div>
                            ))}
                          </div>
                        ))}
                      </div>
                    )}

                    {tableLayout?.tableColumns && tableLayout.tableColumns.length > 0 && (
                      <div className="overflow-hidden rounded-lg border">
                        <Table>
                          <TableHeader>
                            <TableRow className="bg-muted/50">
                              {tableLayout.tableColumns.map((col) => (
                                <TableHead
                                  key={col.id}
                                  style={{ width: col.width }}
                                  className={cn("text-xs", col.align && `text-${col.align}`)}
                                >
                                  {col.label}
                                </TableHead>
                              ))}
                            </TableRow>
                          </TableHeader>
                          <TableBody>
                            {card._rows.map((row) => (
                              <TableRow key={row._rowId} className={cn(row._isDirty && "bg-primary/5")}>
                                {tableLayout.tableColumns.map((col) => (
                                  <TableCell
                                    key={`${row._rowId}-${col.id}`}
                                    className={cn("text-sm", col.align && `text-${col.align}`)}
                                  >
                                    {renderTableCell(
                                      col,
                                      row,
                                      (value) => handleRowDataChange(card._cardId, row._rowId, col.field, value),
                                      row._isNew || row._isEditing,
                                    )}
                                  </TableCell>
                                ))}
                              </TableRow>
                            ))}
                          </TableBody>
                        </Table>
                      </div>
                    )}
                  </>
                )}
              </CardContent>
            </Card>
          ))}
        </div>

        {/* 🆕 v3.1: Footer 버튼 영역 */}
        {footerConfig?.enabled && footerConfig.buttons && footerConfig.buttons.length > 0 ? (
          <div
            className={cn(
              "flex gap-2 pt-4",
              footerConfig.position === "sticky" && "bg-background sticky bottom-0 border-t py-4",
              footerConfig.alignment === "left" && "justify-start",
              footerConfig.alignment === "center" && "justify-center",
              footerConfig.alignment === "right" && "justify-end",
              !footerConfig.alignment && "justify-end",
            )}
          >
            {footerConfig.buttons.map((btn) => (
              <Button
                key={btn.id}
                variant={btn.variant || "default"}
                disabled={btn.disabled || (btn.action === "save" && (isSaving || !hasDirtyData))}
                onClick={() => handleFooterButtonClick(btn)}
                className="gap-2"
              >
                {btn.action === "save" && isSaving ? (
                  <Loader2 className="h-4 w-4 animate-spin" />
                ) : btn.icon === "save" ? (
                  <Save className="h-4 w-4" />
                ) : btn.icon === "x" ? (
                  <X className="h-4 w-4" />
                ) : btn.icon === "reset" ? (
                  <RotateCcw className="h-4 w-4" />
                ) : null}
                {btn.label}
              </Button>
            ))}
          </div>
        ) : null}

        {/* 데이터 없음 */}
        {groupedCardsData.length === 0 && !isLoading && (
          <div className="text-muted-foreground py-12 text-center">표시할 데이터가 없습니다.</div>
        )}

        {/* 🆕 v3.1: 삭제 확인 다이얼로그 */}
        <AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
          <AlertDialogContent>
            <AlertDialogHeader>
              <AlertDialogTitle>삭제 확인</AlertDialogTitle>
              <AlertDialogDescription>이 행을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.</AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>취소</AlertDialogCancel>
              <AlertDialogAction
                onClick={() => {
                  if (pendingDeleteInfo) {
                    handleDeleteExternalRow(
                      pendingDeleteInfo.cardId,
                      pendingDeleteInfo.rowId,
                      pendingDeleteInfo.contentRowId,
                    );
                  }
                }}
                className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
              >
                삭제
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>
      </div>
    );
  }

  // 단순 모드 렌더링 (그룹핑 없음)
  return (
    <div className={cn("space-y-6 overflow-x-auto", className)}>
      <div className="min-w-[600px] space-y-4" style={{ gap: cardSpacing }}>
        {cardsData.map((card, cardIndex) => (
          <Card
            key={card._cardId}
            className={cn(
              "transition-shadow",
              showCardBorder && "border-2",
              card._isDirty && "border-primary shadow-lg",
            )}
          >
            {/* 카드 제목 (선택사항) */}
            {showCardTitle && (
              <CardHeader className="pb-3">
                <CardTitle className="flex items-center justify-between text-lg">
                  <span>{getCardTitle(card, cardIndex)}</span>
                  {card._isDirty && <span className="text-primary text-xs font-normal">(수정됨)</span>}
                </CardTitle>
              </CardHeader>
            )}
            <CardContent className="space-y-4">
              {/* 🆕 v3: contentRows 기반 렌더링 */}
              {useNewLayout
                ? contentRows.map((contentRow, rowIndex) => (
                    <div key={contentRow.id || `crow-${rowIndex}`}>
                      {renderSimpleContentRow(contentRow, card, (value, field) =>
                        handleCardDataChange(card._cardId, field, value),
                      )}
                    </div>
                  ))
                : // 레거시: cardLayout 사용
                  cardLayout.map((row, rowIndex) => (
                    <div
                      key={row.id || `row-${rowIndex}`}
                      className={cn(
                        "grid gap-4",
                        row.layout === "vertical" ? "grid-cols-1" : "auto-cols-fr grid-flow-col",
                      )}
                      style={{ gap: row.gap || "16px" }}
                    >
                      {row.columns.map((col, colIndex) => (
                        <div key={col.id || `col-${colIndex}`} style={{ width: col.width }}>
                          {renderColumn(col, card, (value) => handleCardDataChange(card._cardId, col.field, value))}
                        </div>
                      ))}
                    </div>
                  ))}
            </CardContent>
          </Card>
        ))}
      </div>

      {/* 🆕 v3.1: Footer 버튼 영역 */}
      {footerConfig?.enabled && footerConfig.buttons && footerConfig.buttons.length > 0 ? (
        <div
          className={cn(
            "flex gap-2 pt-4",
            footerConfig.position === "sticky" && "bg-background sticky bottom-0 border-t py-4",
            footerConfig.alignment === "left" && "justify-start",
            footerConfig.alignment === "center" && "justify-center",
            footerConfig.alignment === "right" && "justify-end",
            !footerConfig.alignment && "justify-end",
          )}
        >
          {footerConfig.buttons.map((btn) => (
            <Button
              key={btn.id}
              variant={btn.variant || "default"}
              disabled={btn.disabled || (btn.action === "save" && (isSaving || !hasDirtyData))}
              onClick={() => handleFooterButtonClick(btn)}
              className="gap-2"
            >
              {btn.action === "save" && isSaving ? (
                <Loader2 className="h-4 w-4 animate-spin" />
              ) : btn.icon === "save" ? (
                <Save className="h-4 w-4" />
              ) : btn.icon === "x" ? (
                <X className="h-4 w-4" />
              ) : btn.icon === "reset" ? (
                <RotateCcw className="h-4 w-4" />
              ) : null}
              {btn.label}
            </Button>
          ))}
        </div>
      ) : null}

      {/* 데이터 없음 */}
      {cardsData.length === 0 && !isLoading && (
        <div className="text-muted-foreground py-12 text-center">표시할 데이터가 없습니다.</div>
      )}
    </div>
  );
}

// 🆕 v3: contentRow 렌더링 (그룹핑 모드)
function renderContentRow(
  contentRow: CardContentRowConfig,
  card: GroupedCardData,
  aggregations: AggregationConfig[],
  onRowDataChange: (cardId: string, rowId: string, field: string, value: any) => void,
) {
  switch (contentRow.type) {
    case "header":
    case "fields":
      // contentRow에서 직접 columns 가져오기 (v3 구조)
      const headerColumns = contentRow.columns || [];

      if (headerColumns.length === 0) {
        return (
          <div className="bg-muted/30 text-muted-foreground rounded-lg p-4 text-sm">
            헤더 컬럼이 설정되지 않았습니다.
          </div>
        );
      }

      return (
        <div
          className={cn(
            "grid gap-4",
            contentRow.layout === "vertical" ? "grid-cols-1" : "auto-cols-fr grid-flow-col",
            contentRow.backgroundColor && getBackgroundClass(contentRow.backgroundColor),
            contentRow.padding && `p-${contentRow.padding}`,
            "rounded-lg",
          )}
          style={{ gap: contentRow.gap || "16px" }}
        >
          {headerColumns.map((col, colIndex) => (
            <div key={col.id || `col-${colIndex}`} style={{ width: col.width }}>
              {renderHeaderColumn(col, card, aggregations)}
            </div>
          ))}
        </div>
      );

    case "aggregation":
      // contentRow에서 직접 aggregationFields 가져오기 (v3 구조)
      const aggFields = contentRow.aggregationFields || [];

      if (aggFields.length === 0) {
        return (
          <div className="rounded-lg bg-amber-50 p-4 text-sm text-amber-600 dark:bg-orange-950 dark:text-orange-400">
            집계 필드가 설정되지 않았습니다. (레이아웃 탭에서 집계 필드를 추가하세요)
          </div>
        );
      }

      return (
        <div
          className={cn(contentRow.aggregationLayout === "vertical" ? "flex flex-col gap-3" : "flex flex-wrap gap-4")}
        >
          {aggFields.map((aggField, fieldIndex) => {
            // 집계 결과에서 값 가져오기 (aggregationResultField 사용)
            const value = card._aggregations?.[aggField.aggregationResultField] || 0;
            return (
              <div
                key={fieldIndex}
                className={cn(
                  "min-w-[120px] flex-1 rounded-lg p-3",
                  aggField.backgroundColor ? getBackgroundClass(aggField.backgroundColor) : "bg-muted/50",
                )}
              >
                <div className="text-muted-foreground text-xs">{aggField.label || aggField.aggregationResultField}</div>
                <div
                  className={cn(
                    "font-bold",
                    aggField.fontSize ? `text-${aggField.fontSize}` : "text-lg",
                    aggField.textColor && `text-${aggField.textColor}`,
                  )}
                >
                  {typeof value === "number" ? value.toLocaleString() : value || "-"}
                </div>
              </div>
            );
          })}
        </div>
      );

    case "table":
      // contentRow에서 직접 tableColumns 가져오기 (v3 구조)
      const tableColumns = contentRow.tableColumns || [];

      if (tableColumns.length === 0) {
        return (
          <div className="rounded-lg bg-primary/10 p-4 text-sm text-primary dark:bg-primary/15 dark:text-primary/80">
            테이블 컬럼이 설정되지 않았습니다. (레이아웃 탭에서 테이블 컬럼을 추가하세요)
          </div>
        );
      }

      return (
        <div className="overflow-hidden rounded-lg border">
          {contentRow.tableTitle && (
            <div className="bg-muted/30 border-b px-4 py-2 text-sm font-medium">{contentRow.tableTitle}</div>
          )}
          <Table>
            {contentRow.showTableHeader !== false && (
              <TableHeader>
                <TableRow className="bg-muted/50">
                  {tableColumns.map((col) => (
                    <TableHead
                      key={col.id}
                      style={{ width: col.width }}
                      className={cn("text-xs", col.align && `text-${col.align}`)}
                    >
                      {col.label}
                    </TableHead>
                  ))}
                </TableRow>
              </TableHeader>
            )}
            <TableBody>
              {card._rows.map((row) => (
                <TableRow key={row._rowId} className={cn(row._isDirty && "bg-primary/5")}>
                  {tableColumns.map((col) => (
                    <TableCell
                      key={`${row._rowId}-${col.id}`}
                      className={cn("text-sm", col.align && `text-${col.align}`)}
                    >
                      {renderTableCell(
                        col,
                        row,
                        (value) => onRowDataChange(card._cardId, row._rowId, col.field, value),
                        row._isNew || row._isEditing,
                      )}
                    </TableCell>
                  ))}
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </div>
      );

    default:
      return null;
  }
}

// 🆕 v3: contentRow 렌더링 (단순 모드)
function renderSimpleContentRow(
  contentRow: CardContentRowConfig,
  card: CardData,
  onChange: (value: any, field: string) => void,
) {
  switch (contentRow.type) {
    case "header":
    case "fields":
      return (
        <div
          className={cn(
            "grid gap-4",
            contentRow.layout === "vertical" ? "grid-cols-1" : "auto-cols-fr grid-flow-col",
            contentRow.backgroundColor && getBackgroundClass(contentRow.backgroundColor),
            contentRow.padding && `p-${contentRow.padding}`,
            "rounded-lg",
          )}
          style={{ gap: contentRow.gap || "16px" }}
        >
          {(contentRow.columns || []).map((col, colIndex) => (
            <div key={col.id || `col-${colIndex}`} style={{ width: col.width }}>
              {renderColumn(col, card, (value) => onChange(value, col.field))}
            </div>
          ))}
        </div>
      );

    case "aggregation":
      // 단순 모드에서도 집계 표시 (단일 카드 기준)
      // contentRow에서 직접 aggregationFields 가져오기 (v3 구조)
      const aggFields = contentRow.aggregationFields || [];

      if (aggFields.length === 0) {
        return (
          <div className="rounded-lg bg-amber-50 p-4 text-sm text-amber-600 dark:bg-orange-950 dark:text-orange-400">
            집계 필드가 설정되지 않았습니다.
          </div>
        );
      }

      return (
        <div
          className={cn(contentRow.aggregationLayout === "vertical" ? "flex flex-col gap-3" : "flex flex-wrap gap-4")}
        >
          {aggFields.map((aggField, fieldIndex) => {
            // 단순 모드에서는 카드 데이터에서 직접 값을 가져옴 (aggregationResultField 사용)
            const value =
              card[aggField.aggregationResultField] || card._originalData?.[aggField.aggregationResultField];
            return (
              <div
                key={fieldIndex}
                className={cn(
                  "min-w-[120px] flex-1 rounded-lg p-3",
                  aggField.backgroundColor ? getBackgroundClass(aggField.backgroundColor) : "bg-muted/50",
                )}
              >
                <div className="text-muted-foreground text-xs">{aggField.label || aggField.aggregationResultField}</div>
                <div
                  className={cn(
                    "font-bold",
                    aggField.fontSize ? `text-${aggField.fontSize}` : "text-lg",
                    aggField.textColor && `text-${aggField.textColor}`,
                  )}
                >
                  {typeof value === "number" ? value.toLocaleString() : value || "-"}
                </div>
              </div>
            );
          })}
        </div>
      );

    case "table":
      // 단순 모드에서도 테이블 표시 (단일 행)
      // contentRow에서 직접 tableColumns 가져오기 (v3 구조)
      const tableColumns = contentRow.tableColumns || [];

      if (tableColumns.length === 0) {
        return (
          <div className="rounded-lg bg-primary/10 p-4 text-sm text-primary dark:bg-primary/15 dark:text-primary/80">
            테이블 컬럼이 설정되지 않았습니다.
          </div>
        );
      }

      return (
        <div className="overflow-hidden rounded-lg border">
          {contentRow.tableTitle && (
            <div className="bg-muted/30 border-b px-4 py-2 text-sm font-medium">{contentRow.tableTitle}</div>
          )}
          <Table>
            {contentRow.showTableHeader !== false && (
              <TableHeader>
                <TableRow className="bg-muted/50">
                  {tableColumns.map((col) => (
                    <TableHead
                      key={col.id}
                      style={{ width: col.width }}
                      className={cn("text-xs", col.align && `text-${col.align}`)}
                    >
                      {col.label}
                    </TableHead>
                  ))}
                </TableRow>
              </TableHeader>
            )}
            <TableBody>
              {/* 단순 모드: 카드 자체가 하나의 행 */}
              <TableRow className={cn(card._isDirty && "bg-primary/5")}>
                {tableColumns.map((col) => (
                  <TableCell
                    key={`${card._cardId}-${col.id}`}
                    className={cn("text-sm", col.align && `text-${col.align}`)}
                  >
                    {renderSimpleTableCell(col, card, (value) => onChange(value, col.field))}
                  </TableCell>
                ))}
              </TableRow>
            </TableBody>
          </Table>
        </div>
      );

    default:
      return null;
  }
}

// 단순 모드 테이블 셀 렌더링
function renderSimpleTableCell(col: TableColumnConfig, card: CardData, onChange: (value: any) => void) {
  const value = card[col.field] || card._originalData?.[col.field];

  if (!col.editable) {
    // 읽기 전용
    if (col.type === "number") {
      return typeof value === "number" ? value.toLocaleString() : value || "-";
    }
    return value || "-";
  }

  // 편집 가능
  switch (col.type) {
    case "number":
      return (
        <Input
          type="number"
          value={value || ""}
          onChange={(e) => onChange(parseFloat(e.target.value) || 0)}
          className="h-8 text-sm"
        />
      );
    case "date":
      return (
        <Input type="date" value={value || ""} onChange={(e) => onChange(e.target.value)} className="h-8 text-sm" />
      );
    case "select":
      return (
        <Select value={value || ""} onValueChange={onChange}>
          <SelectTrigger className="h-8 text-sm">
            <SelectValue placeholder="선택" />
          </SelectTrigger>
          <SelectContent>
            {(col.selectOptions || []).map((opt) => (
              <SelectItem key={opt.value} value={opt.value}>
                {opt.label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
      );
    default:
      return (
        <Input type="text" value={value || ""} onChange={(e) => onChange(e.target.value)} className="h-8 text-sm" />
      );
  }
}

// 배경색 클래스 변환
function getBackgroundClass(color: string): string {
  const colorMap: Record<string, string> = {
    blue: "bg-primary/10 dark:bg-primary/15",
    green: "bg-emerald-50 dark:bg-emerald-950",
    purple: "bg-purple-50 dark:bg-purple-950",
    orange: "bg-amber-50 dark:bg-orange-950",
  };
  return colorMap[color] || "";
}

// 헤더 컬럼 렌더링 (집계값 포함)
function renderHeaderColumn(col: CardColumnConfig, card: GroupedCardData, aggregations: AggregationConfig[]) {
  let value: any;

  // 집계값 타입이면 집계 결과에서 가져옴
  if (col.type === "aggregation" && col.aggregationField) {
    value = card._aggregations[col.aggregationField];
    const aggConfig = aggregations.find((a) => a.resultField === col.aggregationField);

    return (
      <div className="space-y-1">
        <Label className="text-muted-foreground text-xs font-medium">{col.label}</Label>
        <div
          className={cn(
            "text-lg font-bold",
            col.textColor && `text-${col.textColor}`,
            col.fontSize && `text-${col.fontSize}`,
          )}
        >
          {typeof value === "number" ? value.toLocaleString() : value || "-"}
          {aggConfig && <span className="text-muted-foreground ml-1 text-xs font-normal">({aggConfig.type})</span>}
        </div>
      </div>
    );
  }

  // 일반 필드는 대표 데이터에서 가져옴
  value = card._representativeData[col.field];

  return (
    <div className="space-y-1">
      <Label className="text-muted-foreground text-xs font-medium">{col.label}</Label>
      <div
        className={cn("text-sm", col.fontWeight && `font-${col.fontWeight}`, col.fontSize && `text-${col.fontSize}`)}
      >
        {value || "-"}
      </div>
    </div>
  );
}

// 테이블 셀 렌더링
// 🆕 v3.8: isRowEditable 파라미터 추가 - 행이 편집 가능한 상태인지 (신규 행이거나 수정 모드)
function renderTableCell(
  col: TableColumnConfig,
  row: CardRowData,
  onChange: (value: any) => void,
  isRowEditable?: boolean,
) {
  const value = row[col.field];

  // Badge 타입
  if (col.type === "badge") {
    const badgeColor = col.badgeColorMap?.[value] || col.badgeVariant || "default";
    return <Badge variant={badgeColor as any}>{value || "-"}</Badge>;
  }

  // 🆕 v3.8: 행 수준 편집 가능 여부 체크
  // isRowEditable이 false이면 컬럼 설정과 관계없이 읽기 전용
  const canEdit = col.editable && isRowEditable !== false;

  // 읽기 전용
  if (!canEdit) {
    if (col.type === "number") {
      return <span>{typeof value === "number" ? value.toLocaleString() : value || "-"}</span>;
    }
    if (col.type === "date") {
      // ISO 8601 형식을 표시용으로 변환
      const displayDate = value
        ? typeof value === "string" && value.includes("T")
          ? value.split("T")[0]
          : value
        : "-";
      return <span>{displayDate}</span>;
    }
    return <span>{value || "-"}</span>;
  }

  // 편집 가능
  switch (col.type) {
    case "text":
      return <Input value={value || ""} onChange={(e) => onChange(e.target.value)} className="h-8 text-sm" />;
    case "number":
      return (
        <Input
          type="number"
          value={value || ""}
          onChange={(e) => onChange(Number(e.target.value) || 0)}
          className="h-8 text-right text-sm"
        />
      );
    case "date":
      // ISO 8601 형식('2025-12-02T00:00:00.000Z')을 'YYYY-MM-DD' 형식으로 변환
      const dateValue = value ? (typeof value === "string" && value.includes("T") ? value.split("T")[0] : value) : "";
      return <Input type="date" value={dateValue} onChange={(e) => onChange(e.target.value)} className="h-8 text-sm" />;
    default:
      return <span>{value || "-"}</span>;
  }
}

// 컬럼 렌더링 함수 (Simple 모드)
function renderColumn(col: CardColumnConfig, card: CardData, onChange: (value: any) => void, tableName?: string) {
  const value = card[col.field];
  const isReadOnly = !col.editable;
  const effectiveRequired = col.required || isColumnRequiredByMeta(tableName, col.field);

  return (
    <div className="space-y-1.5">
      <Label className="text-xs font-medium">
        {col.label}{effectiveRequired && <span className="text-orange-500">*</span>}
      </Label>

      {isReadOnly && (
        <div className="text-muted-foreground bg-muted flex min-h-[40px] items-center rounded-md px-3 py-2 text-sm">
          {value || "-"}
        </div>
      )}

      {!isReadOnly && (
        <>
          {col.type === "text" && (
            <Input
              value={value || ""}
              onChange={(e) => onChange(e.target.value)}
              placeholder={col.placeholder}
              className="h-10 text-sm"
            />
          )}

          {col.type === "number" && (
            <Input
              type="number"
              value={value || ""}
              onChange={(e) => onChange(e.target.value)}
              placeholder={col.placeholder}
              className="h-10 text-sm"
            />
          )}

          {col.type === "date" && (
            <Input
              type="date"
              value={value ? (typeof value === "string" && value.includes("T") ? value.split("T")[0] : value) : ""}
              onChange={(e) => onChange(e.target.value)}
              className="h-10 text-sm"
            />
          )}

          {col.type === "select" && (
            <Select value={value || ""} onValueChange={onChange}>
              <SelectTrigger className="h-10 text-sm">
                <SelectValue placeholder={col.placeholder || "선택하세요"} />
              </SelectTrigger>
              <SelectContent>
                {col.selectOptions?.map((option) => (
                  <SelectItem key={option.value} value={option.value}>
                    {option.label}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          )}

          {col.type === "textarea" && (
            <Textarea
              value={value || ""}
              onChange={(e) => onChange(e.target.value)}
              placeholder={col.placeholder}
              className="min-h-[80px] text-sm"
            />
          )}

          {col.type === "component" && col.componentType && (
            <div className="text-muted-foreground rounded border p-2 text-xs">
              컴포넌트: {col.componentType} (개발 중)
            </div>
          )}
        </>
      )}
    </div>
  );
}
