"use client";

import React, { useState, useEffect, useMemo, useCallback, useRef } from "react";
import { TableListConfig, ColumnConfig } from "./types";
import { WebType } from "@/types/common";
import { tableTypeApi } from "@/lib/api/screen";
import { entityJoinApi } from "@/lib/api/entityJoin";
import { codeCache } from "@/lib/caching/codeCache";
import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization";
import { getFullImageUrl } from "@/lib/api/client";
import { getFilePreviewUrl } from "@/lib/api/file";
import { Button } from "@/components/ui/button";
import { v2EventBus, V2_EVENTS, V2ErrorBoundary } from "@/lib/v2-core";
import { getAdaptiveLabelColor } from "@/lib/utils/darkModeColor";
import { useTabId } from "@/contexts/TabIdContext";
import { formatNumber as centralFormatNumber, formatCurrency as centralFormatCurrency } from "@/lib/formatting";

// 🖼️ 테이블 셀 이미지 썸네일 컴포넌트
// objid인 경우 인증된 API로 blob URL 생성, 경로인 경우 직접 URL 사용
// 다중 이미지(콤마 구분)인 경우 대표 이미지를 우선 표시
const TableCellImage: React.FC<{ value: string }> = React.memo(({ value }) => {
  const [imgSrc, setImgSrc] = React.useState<string | null>(null);
  const [displayObjid, setDisplayObjid] = React.useState<string>("");
  const [error, setError] = React.useState(false);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    let mounted = true;
    const rawValue = String(value);
    const parts = rawValue.split(",").map(s => s.trim()).filter(Boolean);

    // 단일 값 또는 경로인 경우
    if (parts.length <= 1) {
      const strValue = parts[0] || rawValue;
      setDisplayObjid(strValue);
      const isObjid = /^\d+$/.test(strValue);

      if (isObjid) {
        loadImageBlob(strValue, mounted, setImgSrc, setError, setLoading);
      } else {
        setImgSrc(getFullImageUrl(strValue));
        setLoading(false);
      }
      return () => { mounted = false; };
    }

    // 다중 objid: 대표 이미지를 찾아서 표시
    const objids = parts.filter(s => /^\d+$/.test(s));
    if (objids.length === 0) {
      setLoading(false);
      setError(true);
      return () => { mounted = false; };
    }

    (async () => {
      try {
        const { getFileInfoByObjid } = await import("@/lib/api/file");
        let representativeId: string | null = null;

        // 각 objid의 대표 여부를 확인
        for (const objid of objids) {
          const info = await getFileInfoByObjid(objid);
          if (info.success && info.data?.isRepresentative) {
            representativeId = objid;
            break;
          }
        }

        // 대표 이미지가 없으면 첫 번째 사용
        const targetObjid = representativeId || objids[0];
        if (mounted) {
          setDisplayObjid(targetObjid);
          loadImageBlob(targetObjid, mounted, setImgSrc, setError, setLoading);
        }
      } catch {
        if (mounted) {
          // 대표 조회 실패 시 첫 번째 사용
          setDisplayObjid(objids[0]);
          loadImageBlob(objids[0], mounted, setImgSrc, setError, setLoading);
        }
      }
    })();

    return () => { mounted = false; };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [value]);

  if (loading) {
    return (
      <div className="flex items-center justify-center" style={{ minHeight: "32px" }}>
        <div className="h-8 w-8 animate-pulse rounded bg-muted sm:h-10 sm:w-10" />
      </div>
    );
  }

  if (error || !imgSrc) {
    return (
      <div className="flex items-center justify-center" style={{ minHeight: "32px" }}>
        <div className="bg-muted text-muted-foreground flex h-8 w-8 items-center justify-center rounded sm:h-10 sm:w-10" title="이미지를 불러올 수 없습니다">
          <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="2" y1="2" x2="22" y2="22"/><path d="M10.41 10.41a2 2 0 1 1-2.83-2.83"/><line x1="13.5" y1="13.5" x2="6" y2="21"/><line x1="18" y1="12" x2="21" y2="15"/><path d="M3.59 3.59A1.99 1.99 0 0 0 3 5v14a2 2 0 0 0 2 2h14c.55 0 1.052-.22 1.41-.59"/><path d="M21 15V5a2 2 0 0 0-2-2H9"/></svg>
        </div>
      </div>
    );
  }

  return (
    <div className="flex items-center justify-center" style={{ minHeight: "32px" }}>
      <img
        src={imgSrc}
        alt="이미지"
        className="h-8 w-8 cursor-pointer rounded object-cover transition-opacity hover:opacity-80 sm:h-10 sm:w-10"
        style={{ maxWidth: "40px", maxHeight: "40px" }}
        onClick={(e) => {
          e.stopPropagation();
          const isObjid = /^\d+$/.test(displayObjid);
          const openUrl = isObjid ? getFilePreviewUrl(displayObjid) : getFullImageUrl(displayObjid);
          window.open(openUrl, "_blank");
        }}
        onError={() => setError(true)}
      />
    </div>
  );
});
TableCellImage.displayName = "TableCellImage";

// 📎 테이블 셀 파일 컴포넌트
// objid(콤마 구분 포함) 또는 JSON 배열 값을 받아 파일명 표시 + 클릭 시 읽기 전용 모달
const TableCellFile: React.FC<{ value: string }> = React.memo(({ value }) => {
  const [fileInfos, setFileInfos] = React.useState<Array<{ objid: string; name: string; ext: string; size?: number }>>([]);
  const [loading, setLoading] = React.useState(true);
  const [modalOpen, setModalOpen] = React.useState(false);

  React.useEffect(() => {
    let mounted = true;
    const rawValue = String(value).trim();
    if (!rawValue || rawValue === "-") {
      setLoading(false);
      return;
    }

    // JSON 배열 형태인지 확인
    try {
      const parsed = JSON.parse(rawValue);
      if (Array.isArray(parsed)) {
        const infos = parsed.map((f: any) => ({
          objid: String(f.objid || f.id || ""),
          name: f.realFileName || f.real_file_name || f.name || "파일",
          ext: f.fileExt || f.file_ext || "",
          size: f.fileSize || f.file_size || 0,
        }));
        if (mounted) {
          setFileInfos(infos);
          setLoading(false);
        }
        return;
      }
    } catch {
      // JSON 파싱 실패 → objid 문자열로 처리
    }

    // 콤마 구분 objid 또는 단일 objid
    const objids = rawValue.split(",").map(s => s.trim()).filter(Boolean);
    if (objids.length === 0) {
      if (mounted) setLoading(false);
      return;
    }

    Promise.all(
      objids.map(async (oid) => {
        try {
          const { getFileInfoByObjid } = await import("@/lib/api/file");
          const res = await getFileInfoByObjid(oid);
          if (res.success && res.data) {
            return {
              objid: oid,
              name: res.data.realFileName || "파일",
              ext: res.data.fileExt || "",
              size: res.data.fileSize || 0,
            };
          }
        } catch {}
        return { objid: oid, name: `파일(${oid})`, ext: "" };
      })
    ).then((results) => {
      if (mounted) {
        setFileInfos(results);
        setLoading(false);
      }
    });

    return () => { mounted = false; };
  }, [value]);

  if (loading) {
    return <span className="text-muted-foreground text-xs animate-pulse">...</span>;
  }

  if (fileInfos.length === 0) {
    return <span className="text-muted-foreground text-xs">-</span>;
  }

  const { Paperclip, Download: DownloadIcon, FileText: FileTextIcon } = require("lucide-react");
  const fileNames = fileInfos.map(f => f.name).join(", ");

  const getFileIconClass = (ext: string) => {
    const e = (ext || "").toLowerCase().replace(".", "");
    if (["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(e)) return "text-primary";
    if (["pdf"].includes(e)) return "text-destructive";
    if (["doc", "docx", "hwp", "hwpx"].includes(e)) return "text-blue-500";
    if (["xls", "xlsx"].includes(e)) return "text-emerald-500";
    return "text-muted-foreground";
  };

  const handleDownload = async (file: { objid: string; name: string }) => {
    if (!file.objid) return;
    try {
      const { apiClient } = await import("@/lib/api/client");
      const response = await apiClient.get(`/files/download/${file.objid}`, {
        responseType: "blob",
      });
      const blob = new Blob([response.data]);
      const url = window.URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = url;
      link.download = file.name || "download";
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      window.URL.revokeObjectURL(url);
    } catch (err) {
      console.error("파일 다운로드 오류:", err);
    }
  };


  return (
    <>
      <div
        className="flex max-w-full cursor-pointer items-center gap-1.5 text-sm hover:underline"
        title={`클릭하여 첨부파일 보기`}
        onClick={(e) => {
          e.stopPropagation();
          setModalOpen(true);
        }}
      >
        <Paperclip className="h-4 w-4 shrink-0 text-amber-500" />
        <span className="truncate text-blue-600">
          {fileInfos.length === 1 ? fileNames : `첨부파일 ${fileInfos.length}건`}
        </span>
      </div>

      {modalOpen && (
        <div
          className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
          onClick={(e) => {
            e.stopPropagation();
            setModalOpen(false);
          }}
        >
          <div
            className="w-full max-w-md rounded-lg border bg-card p-0 shadow-lg"
            onClick={(e) => e.stopPropagation()}
          >
            <div className="flex items-center justify-between border-b px-4 py-3">
              <div className="flex items-center gap-2">
                <Paperclip className="h-4 w-4 text-amber-500" />
                <span className="text-sm font-semibold">첨부파일 ({fileInfos.length})</span>
              </div>
              <button
                type="button"
                onClick={() => setModalOpen(false)}
                className="rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
              >
                <span className="sr-only">닫기</span>
                <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
                </svg>
              </button>
            </div>
            <div className="max-h-60 overflow-y-auto p-2">
              {fileInfos.map((file, idx) => (
                <div
                  key={file.objid || idx}
                  className="flex items-center gap-3 rounded-md px-3 py-2 hover:bg-muted/50"
                >
                  <FileTextIcon className={`h-5 w-5 shrink-0 ${getFileIconClass(file.ext)}`} />
                  <div className="min-w-0 flex-1">
                    <p className="truncate text-sm font-medium">{file.name}</p>
                    {file.size ? (
                      <p className="text-xs text-muted-foreground">
                        {file.size > 1048576
                          ? `${(file.size / 1048576).toFixed(1)} MB`
                          : `${(file.size / 1024).toFixed(0)} KB`}
                      </p>
                    ) : null}
                  </div>
                  <button
                    type="button"
                    title="다운로드"
                    onClick={(e) => { e.stopPropagation(); handleDownload(file); }}
                    className="shrink-0 rounded p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground"
                  >
                    <DownloadIcon className="h-4 w-4" />
                  </button>
                </div>
              ))}
            </div>
          </div>
        </div>
      )}
    </>
  );
});
TableCellFile.displayName = "TableCellFile";

// 이미지 blob 로딩 헬퍼
function loadImageBlob(
  objid: string,
  mounted: boolean,
  setImgSrc: (url: string) => void,
  setError: (err: boolean) => void,
  setLoading: (loading: boolean) => void,
) {
  import("@/lib/api/client").then(({ apiClient }) => {
    apiClient.get(`/files/preview/${objid}`, { responseType: "blob" })
      .then((response) => {
        if (mounted) {
          const blob = new Blob([response.data]);
          setImgSrc(window.URL.createObjectURL(blob));
          setLoading(false);
        }
      })
      .catch(() => {
        if (mounted) {
          setError(true);
          setLoading(false);
        }
      });
  });
}

// 🆕 RelatedDataButtons 전역 레지스트리 타입 선언
declare global {
  interface Window {
    __relatedButtonsTargetTables?: Set<string>;
  }
}
import {
  ChevronLeft,
  ChevronRight,
  ChevronsLeft,
  ChevronsRight,
  RefreshCw,
  ArrowUp,
  ArrowDown,
  TableIcon,
  Settings,
  X,
  Layers,
  ChevronDown,
  Filter,
  Check,
  Download,
  FileSpreadsheet,
  Copy,
  ClipboardCopy,
  Edit,
  CheckSquare,
  Trash2,
  Lock,
  GripVertical,
} from "lucide-react";
import * as XLSX from "xlsx";
import { FileText, ChevronRightIcon } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { showErrorToast } from "@/lib/utils/toastUtils";
import { tableDisplayStore } from "@/stores/tableDisplayStore";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Label } from "@/components/ui/label";
import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters";
import { SingleTableWithSticky } from "./SingleTableWithSticky";
import { CardModeRenderer } from "./CardModeRenderer";
import { TableOptionsModal } from "@/components/common/TableOptionsModal";
import { useTableOptions } from "@/contexts/TableOptionsContext";
import { TableFilter, ColumnVisibility, GroupSumConfig } from "@/types/table-options";
import { useAuth } from "@/hooks/useAuth";
import { useScreenContextOptional } from "@/contexts/ScreenContext";
import { useSplitPanelContext, SplitPanelPosition } from "@/contexts/SplitPanelContext";
import type { DataProvidable, DataReceivable, DataReceiverConfig } from "@/types/data-transfer";

// ========================================
// 인터페이스
// ========================================

// 그룹화된 데이터 인터페이스
interface GroupedData {
  groupKey: string;
  groupValues: Record<string, any>;
  items: any[];
  count: number;
  summary?: Record<string, { sum: number; avg: number; count: number }>; // 🆕 그룹별 소계
}

// ========================================
// 캐시 및 유틸리티
// ========================================

const tableColumnCache = new Map<string, { columns: any[]; inputTypes?: any[]; timestamp: number }>();
const tableInfoCache = new Map<string, { tables: any[]; timestamp: number }>();
const TABLE_CACHE_TTL = 5 * 60 * 1000; // 5분

const cleanupTableCache = () => {
  const now = Date.now();
  for (const [key, entry] of tableColumnCache.entries()) {
    if (now - entry.timestamp > TABLE_CACHE_TTL) {
      tableColumnCache.delete(key);
    }
  }
  for (const [key, entry] of tableInfoCache.entries()) {
    if (now - entry.timestamp > TABLE_CACHE_TTL) {
      tableInfoCache.delete(key);
    }
  }
};

if (typeof window !== "undefined") {
  setInterval(cleanupTableCache, 10 * 60 * 1000);
}

const debounceTimers = new Map<string, NodeJS.Timeout>();
const activeRequests = new Map<string, Promise<any>>();

const debouncedApiCall = <T extends any[], R>(key: string, fn: (...args: T) => Promise<R>, delay: number = 300) => {
  return (...args: T): Promise<R> => {
    const activeRequest = activeRequests.get(key);
    if (activeRequest) {
      return activeRequest as Promise<R>;
    }

    return new Promise((resolve, reject) => {
      const existingTimer = debounceTimers.get(key);
      if (existingTimer) {
        clearTimeout(existingTimer);
      }

      const timer = setTimeout(async () => {
        try {
          const requestPromise = fn(...args);
          activeRequests.set(key, requestPromise);
          const result = await requestPromise;
          resolve(result);
        } catch (error) {
          reject(error);
        } finally {
          debounceTimers.delete(key);
          activeRequests.delete(key);
        }
      }, delay);

      debounceTimers.set(key, timer);
    });
  };
};

// ========================================
// Filter Builder 인터페이스
// ========================================

interface FilterCondition {
  id: string;
  column: string;
  operator:
    | "equals"
    | "notEquals"
    | "contains"
    | "notContains"
    | "startsWith"
    | "endsWith"
    | "greaterThan"
    | "lessThan"
    | "greaterOrEqual"
    | "lessOrEqual"
    | "isEmpty"
    | "isNotEmpty";
  value: string;
}

interface FilterGroup {
  id: string;
  logic: "AND" | "OR";
  conditions: FilterCondition[];
}

// ========================================
// Props 인터페이스
// ========================================

export interface TableListComponentProps {
  component: any;
  isDesignMode?: boolean;
  isSelected?: boolean;
  isInteractive?: boolean;
  onClick?: () => void;
  onDragStart?: (e: React.DragEvent) => void;
  onDragEnd?: (e: React.DragEvent) => void;
  className?: string;
  style?: React.CSSProperties;
  formData?: Record<string, any>;
  onFormDataChange?: (data: any) => void;
  config?: TableListConfig;
  size?: { width: number; height: number };
  position?: { x: number; y: number; z?: number };
  componentConfig?: any;
  selectedScreen?: any;
  onZoneComponentDrop?: any;
  onZoneClick?: any;
  tableName?: string;
  onRefresh?: () => void;
  onClose?: () => void;
  screenId?: number | string; // 화면 ID (필터 설정 저장용)
  userId?: string; // 사용자 ID (컬럼 순서 저장용)
  onSelectedRowsChange?: (
    selectedRows: any[],
    selectedRowsData: any[],
    sortBy?: string,
    sortOrder?: "asc" | "desc",
    columnOrder?: string[],
    tableDisplayData?: any[],
  ) => void;
  onConfigChange?: (config: any) => void;
  refreshKey?: number;
  // 탭 관련 정보 (탭 내부의 테이블에서 사용)
  parentTabId?: string; // 부모 탭 ID
  parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID
  // 🆕 프리뷰용 회사 코드 (DynamicComponentRenderer에서 전달, 최고 관리자만 오버라이드 가능)
  companyCode?: string;
}

// ========================================
// 메인 컴포넌트
// ========================================

export const TableListComponent: React.FC<TableListComponentProps> = ({
  component,
  isDesignMode = false,
  isSelected = false,
  onClick,
  onDragStart,
  onDragEnd,
  config,
  className,
  style,
  formData: propFormData,
  onFormDataChange,
  componentConfig,
  onSelectedRowsChange,
  onConfigChange,
  refreshKey,
  tableName,
  userId,
  screenId,
  parentTabId,
  parentTabsComponentId,
  companyCode,
}) => {
  // ========================================
  // 설정 및 스타일
  // ========================================

  const tableConfig = {
    ...config,
    ...component.config,
    ...componentConfig,
  } as TableListConfig;

  // selectedTable 안전하게 추출 (문자열인지 확인)
  let finalSelectedTable =
    componentConfig?.selectedTable || component.config?.selectedTable || config?.selectedTable || tableName;

  // 디버그 로그 제거 (성능 최적화)

  // 객체인 경우 tableName 속성 추출 시도
  if (typeof finalSelectedTable === "object" && finalSelectedTable !== null) {
    finalSelectedTable = (finalSelectedTable as any).tableName || (finalSelectedTable as any).name || tableName;
  }

  tableConfig.selectedTable = finalSelectedTable;

  // 디버그 로그 제거 (성능 최적화)

  const currentTabId = useTabId();

  const buttonColor = getAdaptiveLabelColor(component.style?.labelColor);
  const buttonTextColor = component.config?.buttonTextColor || "#ffffff";

  const gridColumns = component.gridColumns || 1;
  let calculatedWidth: string;

  if (isDesignMode) {
    if (gridColumns === 1) {
      calculatedWidth = "400px";
    } else if (gridColumns === 2) {
      calculatedWidth = "800px";
    } else {
      calculatedWidth = "100%";
    }
  } else {
    calculatedWidth = "100%";
  }

  const componentStyle: React.CSSProperties = {
    position: "relative",
    display: "flex",
    flexDirection: "column",
    backgroundColor: "hsl(var(--background))",
    overflow: "hidden",
    boxSizing: "border-box",
    width: "100%",
    height: "100%",
    minHeight: isDesignMode ? "300px" : "100%",
    ...style, // style prop이 위의 기본값들을 덮어씀
  };

  // ========================================
  // 상태 관리
  // ========================================

  // 사용자 정보 (props에서 받거나 useAuth에서 가져오기)
  const { userId: authUserId } = useAuth();
  const currentUserId = userId || authUserId;

  // 화면 컨텍스트 (데이터 제공자로 등록)
  const screenContext = useScreenContextOptional();

  // 분할 패널 컨텍스트 (분할 패널 내부에서 데이터 수신자로 등록)
  const splitPanelContext = useSplitPanelContext();
  // 🆕 ScreenContext에서 splitPanelPosition 가져오기 (중첩 화면에서도 작동)
  const splitPanelPosition = screenContext?.splitPanelPosition;

  // 🆕 연결된 필터 상태 (다른 컴포넌트 값으로 필터링)
  const [linkedFilterValues, setLinkedFilterValues] = useState<Record<string, any>>({});

  // 🆕 RelatedDataButtons 컴포넌트에서 발생하는 필터 상태
  const [relatedButtonFilter, setRelatedButtonFilter] = useState<{
    filterColumn: string;
    filterValue: any;
  } | null>(null);

  // 🆕 RelatedDataButtons가 이 테이블을 대상으로 등록되어 있는지 여부
  const [isRelatedButtonTarget, setIsRelatedButtonTarget] = useState(() => {
    // 초기값: 전역 레지스트리에서 확인
    if (typeof window !== "undefined" && window.__relatedButtonsTargetTables) {
      return window.__relatedButtonsTargetTables.has(tableConfig.selectedTable || "");
    }
    return false;
  });

  // TableOptions Context
  const { registerTable, unregisterTable, updateTableDataCount } = useTableOptions();
  const [filters, setFilters] = useState<TableFilter[]>([]);
  const [grouping, setGrouping] = useState<string[]>([]);
  const [columnVisibility, setColumnVisibility] = useState<ColumnVisibility[]>([]);

  // filters가 변경되면 searchValues 업데이트 (실시간 검색)
  useEffect(() => {
    const newSearchValues: Record<string, any> = {};
    filters.forEach((filter) => {
      if (filter.value) {
        // operator 정보도 함께 전달 (백엔드에서 equals/contains 구분)
        newSearchValues[filter.columnName] = {
          value: filter.value,
          operator: filter.operator || "contains",
        };
      }
    });

    // filters → searchValues 변환 완료

    setSearchValues(newSearchValues);
    setCurrentPage(1); // 필터 변경 시 첫 페이지로
  }, [filters]);

  // grouping이 변경되면 groupByColumns 업데이트
  useEffect(() => {
    setGroupByColumns(grouping);
  }, [grouping]);

  // 초기 로드 시 localStorage에서 저장된 설정 불러오기
  useEffect(() => {
    if (tableConfig.selectedTable && currentUserId) {
      const storageKey = `table_column_visibility_${tableConfig.selectedTable}_${currentUserId}`;
      const savedSettings = localStorage.getItem(storageKey);

      if (savedSettings) {
        try {
          const parsed = JSON.parse(savedSettings) as ColumnVisibility[];
          setColumnVisibility(parsed);
        } catch (error) {
          console.error("저장된 컬럼 설정 불러오기 실패:", error);
        }
      }
    }
  }, [tableConfig.selectedTable, currentUserId]);

  // columnVisibility 변경 시 컬럼 순서, 가시성, 너비 적용
  useEffect(() => {
    if (columnVisibility.length > 0) {
      const newOrder = columnVisibility.map((cv) => cv.columnName).filter((name) => name !== "__checkbox__"); // 체크박스 제외
      setColumnOrder(newOrder);

      // 너비 적용
      const newWidths: Record<string, number> = {};
      columnVisibility.forEach((cv) => {
        if (cv.width) {
          newWidths[cv.columnName] = cv.width;
        }
      });
      if (Object.keys(newWidths).length > 0) {
        setColumnWidths((prev) => ({ ...prev, ...newWidths }));

        // table_column_widths_* localStorage도 동기화 (초기 너비 로드 시 올바른 값 사용)
        if (tableConfig.selectedTable && userId) {
          const widthsKey = `table_column_widths_${tableConfig.selectedTable}_${userId}`;
          try {
            const existing = localStorage.getItem(widthsKey);
            const merged = existing ? { ...JSON.parse(existing), ...newWidths } : newWidths;
            localStorage.setItem(widthsKey, JSON.stringify(merged));
          } catch { /* ignore */ }
        }
      }

      // localStorage에 저장 (사용자별)
      if (tableConfig.selectedTable && currentUserId) {
        const storageKey = `table_column_visibility_${tableConfig.selectedTable}_${currentUserId}`;
        localStorage.setItem(storageKey, JSON.stringify(columnVisibility));
      }
    }
  }, [columnVisibility, tableConfig.selectedTable, currentUserId]);

  // 🆕 columnOrder를 visibleColumns 이전에 정의 (visibleColumns에서 사용)
  const [columnOrder, setColumnOrder] = useState<string[]>([]);

  // 🆕 visibleColumns를 상단에서 정의 (다른 useCallback/useMemo에서 사용하기 위해)
  const visibleColumns = useMemo(() => {
    let cols = (tableConfig.columns || []).filter((col) => col.visible !== false);

    // columnVisibility가 있으면 가시성 적용
    if (columnVisibility.length > 0) {
      cols = cols.filter((col) => {
        const visibilityConfig = columnVisibility.find((cv) => cv.columnName === col.columnName);
        return visibilityConfig ? visibilityConfig.visible : true;
      });
    }

    // 체크박스 컬럼 (나중에 위치 결정)
    // 기본값: enabled가 undefined면 true로 처리
    let checkboxCol: ColumnConfig | null = null;
    if (tableConfig.checkbox?.enabled ?? true) {
      checkboxCol = {
        columnName: "__checkbox__",
        displayName: "",
        visible: true,
        sortable: false,
        searchable: false,
        width: 40,
        align: "center" as const,
        order: -1,
        editable: false, // 체크박스는 편집 불가
      };
    }

    // columnOrder가 있으면 해당 순서로 정렬
    if (columnOrder.length > 0) {
      const orderMap = new Map(columnOrder.map((name, idx) => [name, idx]));
      cols = [...cols].sort((a, b) => {
        const aIdx = orderMap.get(a.columnName) ?? 9999;
        const bIdx = orderMap.get(b.columnName) ?? 9999;
        return aIdx - bIdx;
      });
    }

    // 체크박스 위치 결정
    if (checkboxCol) {
      const checkboxPosition = tableConfig.checkbox?.position || "left";
      if (checkboxPosition === "left") {
        return [checkboxCol, ...cols];
      } else {
        return [...cols, checkboxCol];
      }
    }

    return cols;
  }, [tableConfig.columns, tableConfig.checkbox, columnVisibility, columnOrder]);

  const [data, setData] = useState<Record<string, any>[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // 🆕 컬럼 헤더 필터 상태 (상단에서 선언)
  const [headerFilters, setHeaderFilters] = useState<Record<string, Set<string>>>({});
  const [openFilterColumn, setOpenFilterColumn] = useState<string | null>(null);

  // 🆕 Filter Builder (고급 필터) 관련 상태 - filteredData보다 먼저 정의해야 함
  const [filterGroups, setFilterGroups] = useState<FilterGroup[]>([]);
  
  // 🆕 joinColumnMapping - filteredData에서 사용하므로 먼저 정의해야 함
  const [joinColumnMapping, setJoinColumnMapping] = useState<Record<string, string>>({});

  // 🆕 분할 패널에서 우측에 이미 추가된 항목 필터링 (좌측 테이블에만 적용) + 헤더 필터
  const filteredData = useMemo(() => {
    let result = data;

    // 1. 분할 패널 좌측에 있고, 우측에 추가된 항목이 있는 경우 필터링
    if (splitPanelPosition === "left" && splitPanelContext?.addedItemIds && splitPanelContext.addedItemIds.size > 0) {
      const addedIds = splitPanelContext.addedItemIds;
      result = result.filter((row) => {
        const rowId = String(row.id || row.po_item_id || row.item_id || "");
        return !addedIds.has(rowId);
      });
    }

    // 2. 헤더 필터 적용 (joinColumnMapping 사용 - 조인된 컬럼과 일치해야 함)
    if (Object.keys(headerFilters).length > 0) {
      result = result.filter((row) => {
        return Object.entries(headerFilters).every(([columnName, values]) => {
          if (values.size === 0) return true;

          // joinColumnMapping을 사용하여 조인된 컬럼명 확인
          const mappedColumnName = joinColumnMapping[columnName] || columnName;
          
          // 여러 가능한 컬럼명 시도 (mappedColumnName 우선)
          const cellValue = row[mappedColumnName] ?? row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()];
          const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue) : "";

          return values.has(cellStr);
        });
      });
    }

    // 3. 🆕 Filter Builder 적용
    if (filterGroups.length > 0) {
      result = result.filter((row) => {
        return filterGroups.every((group) => {
          const validConditions = group.conditions.filter(
            (c) => c.column && (c.operator === "isEmpty" || c.operator === "isNotEmpty" || c.value),
          );
          if (validConditions.length === 0) return true;

          const evaluateCondition = (value: any, condition: (typeof group.conditions)[0]): boolean => {
            const strValue = value !== null && value !== undefined ? String(value).toLowerCase() : "";
            const condValue = condition.value.toLowerCase();

            switch (condition.operator) {
              case "equals":
                return strValue === condValue;
              case "notEquals":
                return strValue !== condValue;
              case "contains":
                return strValue.includes(condValue);
              case "notContains":
                return !strValue.includes(condValue);
              case "startsWith":
                return strValue.startsWith(condValue);
              case "endsWith":
                return strValue.endsWith(condValue);
              case "greaterThan":
                return parseFloat(strValue) > parseFloat(condValue);
              case "lessThan":
                return parseFloat(strValue) < parseFloat(condValue);
              case "greaterOrEqual":
                return parseFloat(strValue) >= parseFloat(condValue);
              case "lessOrEqual":
                return parseFloat(strValue) <= parseFloat(condValue);
              case "isEmpty":
                return strValue === "" || value === null || value === undefined;
              case "isNotEmpty":
                return strValue !== "" && value !== null && value !== undefined;
              default:
                return true;
            }
          };

          if (group.logic === "AND") {
            return validConditions.every((cond) => evaluateCondition(row[cond.column], cond));
          } else {
            return validConditions.some((cond) => evaluateCondition(row[cond.column], cond));
          }
        });
      });
    }

    return result;
  }, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups, joinColumnMapping]);

  const [currentPage, setCurrentPage] = useState(1);
  const [totalPages, setTotalPages] = useState(0);
  const [totalItems, setTotalItems] = useState(0);
  const [pageInputValue, setPageInputValue] = useState<string>("1");
  const [searchTerm, setSearchTerm] = useState("");
  const [sortColumn, setSortColumn] = useState<string | null>(null);
  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
  const hasInitializedSort = useRef(false);
  const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
  const [tableLabel, setTableLabel] = useState<string>("");
  const pageSizeKey = useMemo(() => {
    if (!tableConfig.selectedTable) return null;
    if (currentTabId) return `pageSize_${currentTabId}_${tableConfig.selectedTable}`;
    return `pageSize_${tableConfig.selectedTable}`;
  }, [tableConfig.selectedTable, currentTabId]);

  const [localPageSize, setLocalPageSize] = useState<number>(() => {
    const key =
      currentTabId && tableConfig.selectedTable
        ? `pageSize_${currentTabId}_${tableConfig.selectedTable}`
        : tableConfig.selectedTable
          ? `pageSize_${tableConfig.selectedTable}`
          : null;
    if (key) {
      const val = sessionStorage.getItem(key);
      if (val) return Number(val);
    }
    return tableConfig.pagination?.pageSize || 20;
  });
  const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]);
  const [columnMeta, setColumnMeta] = useState<
    Record<string, { webType?: string; codeCategory?: string; inputType?: string; categoryRef?: string }>
  >({});
  // 🆕 엔티티 조인 테이블의 컬럼 메타데이터 (테이블명.컬럼명 → inputType)
  const [joinedColumnMeta, setJoinedColumnMeta] = useState<
    Record<string, { webType?: string; codeCategory?: string; inputType?: string }>
  >({});
  const [categoryMappings, setCategoryMappings] = useState<
    Record<string, Record<string, { label: string; color?: string }>>
  >({});
  const [categoryMappingsKey, setCategoryMappingsKey] = useState(0);
  const [searchValues, setSearchValues] = useState<Record<string, any>>({});
  const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
  const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
  const [refreshTrigger, setRefreshTrigger] = useState(0);
  // columnOrder는 상단에서 정의됨 (visibleColumns보다 먼저 필요)
  const columnRefs = useRef<Record<string, HTMLTableCellElement | null>>({});
  const [isAllSelected, setIsAllSelected] = useState(false);
  const hasInitializedWidths = useRef(false);
  const isResizing = useRef(false);

  // 필터 설정 관련 상태
  const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false);
  const [visibleFilterColumns, setVisibleFilterColumns] = useState<Set<string>>(new Set());

  // 🆕 키보드 네비게이션 관련 상태
  const [focusedCell, setFocusedCell] = useState<{ rowIndex: number; colIndex: number } | null>(null);
  const tableContainerRef = useRef<HTMLDivElement>(null);

  // 🆕 인라인 셀 편집 관련 상태
  const [editingCell, setEditingCell] = useState<{
    rowIndex: number;
    colIndex: number;
    columnName: string;
    originalValue: any;
  } | null>(null);
  const [editingValue, setEditingValue] = useState<string>("");
  const editInputRef = useRef<HTMLInputElement>(null);

  // 🆕 배치 편집 관련 상태
  const [editMode, setEditMode] = useState<"immediate" | "batch">("immediate"); // 편집 모드
  const [pendingChanges, setPendingChanges] = useState<
    Map<
      string,
      {
        rowIndex: number;
        columnName: string;
        originalValue: any;
        newValue: any;
        primaryKeyValue: any;
      }
    >
  >(new Map()); // key: `${rowIndex}-${columnName}`
  const [localEditedData, setLocalEditedData] = useState<Record<number, Record<string, any>>>({}); // 로컬 수정 데이터

  // 🆕 유효성 검사 관련 상태
  const [validationErrors, setValidationErrors] = useState<Map<string, string>>(new Map()); // key: `${rowIndex}-${columnName}`

  // 🆕 유효성 검사 규칙 타입
  type ValidationRule = {
    required?: boolean;
    min?: number;
    max?: number;
    minLength?: number;
    maxLength?: number;
    pattern?: RegExp;
    customMessage?: string;
    validate?: (value: any, row: any) => string | null; // 커스텀 검증 함수 (에러 메시지 또는 null)
  };

  // 🆕 Cascading Lookups 관련 상태
  const [cascadingOptions, setCascadingOptions] = useState<Record<string, { value: string; label: string }[]>>({});
  const [loadingCascading, setLoadingCascading] = useState<Record<string, boolean>>({});

  // 🆕 Multi-Level Headers (Column Bands) 타입
  type ColumnBand = {
    caption: string;
    columns: string[]; // 포함될 컬럼명 배열
  };

  // 그룹 설정 관련 상태
  const [groupByColumns, setGroupByColumns] = useState<string[]>([]);
  const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());

  // 🆕 그룹별 합산 설정 상태
  const [groupSumConfig, setGroupSumConfig] = useState<GroupSumConfig | null>(null);

  // 🆕 Master-Detail 관련 상태
  const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set()); // 확장된 행 키 목록
  const [detailData, setDetailData] = useState<Record<string, any[]>>({}); // 상세 데이터 캐시

  // 🆕 Drag & Drop 재정렬 관련 상태
  const [draggedRowIndex, setDraggedRowIndex] = useState<number | null>(null);
  const [dropTargetIndex, setDropTargetIndex] = useState<number | null>(null);
  const [isDragEnabled, setIsDragEnabled] = useState<boolean>((tableConfig as any).enableRowDrag ?? false);

  // 🆕 Virtual Scrolling 관련 상태
  const [isVirtualScrollEnabled] = useState<boolean>((tableConfig as any).virtualScroll ?? false);
  const [scrollTop, setScrollTop] = useState(0);
  const ROW_HEIGHT = 40; // 각 행의 높이 (픽셀)
  const OVERSCAN = 5; // 버퍼로 추가 렌더링할 행 수
  const scrollContainerRef = useRef<HTMLDivElement>(null);

  // 🆕 Column Reordering 관련 상태
  const [draggedColumnIndex, setDraggedColumnIndex] = useState<number | null>(null);
  const [dropTargetColumnIndex, setDropTargetColumnIndex] = useState<number | null>(null);
  const [isColumnDragEnabled] = useState<boolean>((tableConfig as any).enableColumnDrag ?? true);

  // 🆕 State Persistence: 통합 상태 키 (탭 ID 스코프 + sessionStorage)
  const tableStateKey = useMemo(() => {
    if (!tableConfig.selectedTable) return null;
    if (currentTabId) return `tableState_${currentTabId}_${tableConfig.selectedTable}`;
    return `tableState_${tableConfig.selectedTable}`;
  }, [tableConfig.selectedTable]);

  // 🆕 Real-Time Updates 관련 상태
  const [isRealTimeEnabled] = useState<boolean>((tableConfig as any).realTimeUpdates ?? false);
  const [wsConnectionStatus, setWsConnectionStatus] = useState<"connecting" | "connected" | "disconnected">(
    "disconnected",
  );
  const wsRef = useRef<WebSocket | null>(null);
  const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);

  // 🆕 Context Menu 관련 상태
  const [contextMenu, setContextMenu] = useState<{
    x: number;
    y: number;
    rowIndex: number;
    colIndex: number;
    row: any;
  } | null>(null);

  // 사용자 옵션 모달 관련 상태
  const [isTableOptionsOpen, setIsTableOptionsOpen] = useState(false);
  const [showGridLines, setShowGridLines] = useState(true);
  const [viewMode, setViewMode] = useState<"table" | "card" | "grouped-card">("table");
  const [frozenColumns, setFrozenColumns] = useState<string[]>([]);
  const [frozenColumnCount, setFrozenColumnCount] = useState<number>(0);

  // 🆕 Search Panel (통합 검색) 관련 상태
  const [globalSearchTerm, setGlobalSearchTerm] = useState("");
  const [isSearchPanelOpen, setIsSearchPanelOpen] = useState(false);
  const [searchHighlights, setSearchHighlights] = useState<Set<string>>(new Set()); // "rowIndex-colIndex" 형식

  // 🆕 Filter Builder (고급 필터) 관련 상태 추가
  const [isFilterBuilderOpen, setIsFilterBuilderOpen] = useState(false);
  const [activeFilterCount, setActiveFilterCount] = useState(0);

  // 🆕 연결된 필터 처리 (셀렉트박스 등 다른 컴포넌트 값으로 필터링)
  useEffect(() => {
    const linkedFilters = tableConfig.linkedFilters;

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

    // 연결된 소스 컴포넌트들의 값을 주기적으로 확인
    const checkLinkedFilters = () => {
      const newFilterValues: Record<string, any> = {};
      let hasChanges = false;

      linkedFilters.forEach((filter) => {
        if (filter.enabled === false) return;

        const sourceProvider = screenContext.getDataProvider(filter.sourceComponentId);
        if (sourceProvider) {
          const selectedData = sourceProvider.getSelectedData();
          if (selectedData && selectedData.length > 0) {
            const sourceField = filter.sourceField || "value";
            const value = selectedData[0][sourceField];

            if (value !== linkedFilterValues[filter.targetColumn]) {
              newFilterValues[filter.targetColumn] = value;
              hasChanges = true;
            } else {
              newFilterValues[filter.targetColumn] = linkedFilterValues[filter.targetColumn];
            }
          }
        }
      });

      if (hasChanges) {
        setLinkedFilterValues(newFilterValues);

        // searchValues에 연결된 필터 값 병합
        setSearchValues((prev) => ({
          ...prev,
          ...newFilterValues,
        }));

        // 첫 페이지로 이동
        setCurrentPage(1);
      }
    };

    // 초기 체크
    checkLinkedFilters();

    // 주기적으로 체크 (500ms마다)
    const intervalId = setInterval(checkLinkedFilters, 500);

    return () => {
      clearInterval(intervalId);
    };
  }, [screenContext, tableConfig.linkedFilters, linkedFilterValues]);

  // DataProvidable 인터페이스 구현
  const dataProvider: DataProvidable = {
    componentId: component.id,
    componentType: "table-list",
    tableName: tableConfig.selectedTable,

    getSelectedData: () => {
      // 🆕 필터링된 데이터에서 선택된 행만 반환 (우측에 추가된 항목 제외)
      const selectedData = filteredData.filter((row) => {
        const rowId = String(row.id || row[tableConfig.selectedTable + "_id"] || "");
        return selectedRows.has(rowId);
      });
      return selectedData;
    },

    getAllData: () => {
      // 🆕 필터링된 데이터 반환
      return filteredData;
    },

    clearSelection: () => {
      setSelectedRows(new Set());
      setIsAllSelected(false);
    },

    getEntityJoinColumns: () => {
      return (tableConfig.columns || [])
        .filter((col) => col.additionalJoinInfo)
        .map((col) => ({
          sourceTable: col.additionalJoinInfo!.sourceTable || tableConfig.selectedTable,
          sourceColumn: col.additionalJoinInfo!.sourceColumn,
          joinAlias: col.additionalJoinInfo!.joinAlias,
          referenceTable: col.additionalJoinInfo!.referenceTable,
        }));
    },
  };

  // DataReceivable 인터페이스 구현
  const dataReceiver: DataReceivable = {
    componentId: component.id,
    componentType: "table",

    receiveData: async (receivedData: any[], config: DataReceiverConfig) => {
      try {
        let newData: any[] = [];

        switch (config.mode) {
          case "append":
            // 기존 데이터에 추가
            newData = [...data, ...receivedData];
            break;

          case "replace":
            // 기존 데이터를 완전히 교체
            newData = receivedData;
            break;

          case "merge":
            // 기존 데이터와 병합 (ID 기반)
            const existingMap = new Map(data.map((item) => [item.id, item]));
            receivedData.forEach((item) => {
              if (item.id && existingMap.has(item.id)) {
                // 기존 데이터 업데이트
                existingMap.set(item.id, { ...existingMap.get(item.id), ...item });
              } else {
                // 새 데이터 추가
                existingMap.set(item.id || Date.now() + Math.random(), item);
              }
            });
            newData = Array.from(existingMap.values());
            break;
        }

        // 상태 업데이트
        setData(newData);

        // 총 아이템 수 업데이트
        setTotalItems(newData.length);
      } catch (error) {
        console.error("데이터 수신 실패:", error);
        throw error;
      }
    },

    getData: () => {
      return data;
    },
  };

  // 화면 컨텍스트에 데이터 제공자/수신자로 등록
  useEffect(() => {
    if (screenContext && component.id) {
      screenContext.registerDataProvider(component.id, dataProvider);
      screenContext.registerDataReceiver(component.id, dataReceiver);

      return () => {
        screenContext.unregisterDataProvider(component.id);
        screenContext.unregisterDataReceiver(component.id);
      };
    }
  }, [screenContext, component.id, data, selectedRows]);

  // 분할 패널 컨텍스트에 데이터 수신자로 등록
  // useSplitPanelPosition 훅으로 위치 가져오기 (중첩된 화면에서도 작동)
  const currentSplitPosition =
    splitPanelPosition || splitPanelContext?.getPositionByScreenId(screenId as number) || null;

  useEffect(() => {
    if (splitPanelContext && component.id && currentSplitPosition) {
      const splitPanelReceiver = {
        componentId: component.id,
        componentType: "table-list",
        receiveData: async (incomingData: any[], mode: "append" | "replace" | "merge") => {
          await dataReceiver.receiveData(incomingData, {
            targetComponentId: component.id,
            targetComponentType: "table-list",
            mode,
            mappingRules: [],
          });
        },
      };

      splitPanelContext.registerReceiver(currentSplitPosition, component.id, splitPanelReceiver);

      return () => {
        splitPanelContext.unregisterReceiver(currentSplitPosition, component.id);
      };
    }
  }, [splitPanelContext, component.id, currentSplitPosition, dataReceiver]);

  // 테이블 등록 (Context에 등록)
  const tableId = `table-list-${component.id}`;

  useEffect(() => {
    // tableConfig.columns를 직접 사용 (displayColumns는 비어있을 수 있음)
    const columnsToRegister = (tableConfig.columns || []).filter(
      (col) => col.visible !== false && col.columnName !== "__checkbox__",
    );

    if (!tableConfig.selectedTable || !columnsToRegister || columnsToRegister.length === 0) {
      return;
    }

    // 컬럼의 고유 값 조회 함수
    const getColumnUniqueValues = async (columnName: string) => {
      const { apiClient } = await import("@/lib/api/client");

      // 최고관리자가 특정 회사 프리뷰 시 해당 회사 카테고리만 필터링
      const filterParam = companyCode && companyCode !== "*"
        ? `?filterCompanyCode=${encodeURIComponent(companyCode)}`
        : "";

      // 1단계: 카테고리 API 시도 (columnMeta 무관하게 항상 시도)
      try {
        const response = await apiClient.get(`/table-categories/${tableConfig.selectedTable}/${columnName}/values${filterParam}`);
        if (response.data.success && response.data.data && response.data.data.length > 0) {
          return response.data.data.map((item: any) => ({
            value: item.valueCode,
            label: item.valueLabel,
          }));
        }
      } catch {
        // 카테고리 API 실패 시 다음 단계로
      }

      // 2단계: DISTINCT API (백엔드에서 category_values/code_info 라벨 변환 포함)
      try {
        const response = await apiClient.get(`/entity/${tableConfig.selectedTable}/distinct/${columnName}`);
        if (response.data.success && response.data.data && response.data.data.length > 0) {
          let options = response.data.data.map((item: any) => ({
            value: String(item.value),
            label: String(item.label),
          }));

          // 프론트엔드 카테고리 매핑으로 추가 라벨 변환
          const mapping = categoryMappings[columnName];
          if (mapping && Object.keys(mapping).length > 0) {
            options = options.map((opt) => ({
              value: opt.value,
              label: mapping[opt.value]?.label || opt.label,
            }));
          }

          return options;
        }
      } catch {
        // DISTINCT API 실패 시 다음 단계로
      }

      // 3단계: 현재 로드된 데이터에서 고유 값 추출 (최종 fallback)
      const uniqueValuesMap = new Map<string, string>();
      const mapping = categoryMappings[columnName];

      data.forEach((row) => {
        const value = row[columnName];
        if (value !== null && value !== undefined && value !== "") {
          const strValue = String(value);
          // _name 필드 또는 카테고리 매핑에서 라벨 가져오기
          const nameField = row[`${columnName}_name`];
          const mappedLabel = mapping?.[strValue]?.label;
          const label = mappedLabel || nameField || strValue;
          uniqueValuesMap.set(strValue, label);
        }
      });

      return Array.from(uniqueValuesMap.entries())
        .map(([value, label]) => ({ value, label }))
        .sort((a, b) => a.label.localeCompare(b.label));
    };

    const registration = {
      tableId,
      label: tableLabel || tableConfig.selectedTable,
      tableName: tableConfig.selectedTable,
      dataCount: totalItems || data.length, // 초기 데이터 건수 포함
      columns: columnsToRegister.map((col) => ({
        columnName: col.columnName || col.field,
        columnLabel: columnLabels[col.columnName] || col.displayName || col.label || col.columnName || col.field,
        inputType: columnMeta[col.columnName]?.inputType || "text",
        visible: col.visible !== false,
        width: columnWidths[col.columnName] || col.width || 150,
        sortable: col.sortable !== false,
        filterable: col.searchable !== false,
      })),
      onFilterChange: setFilters,
      onGroupChange: setGrouping,
      onColumnVisibilityChange: setColumnVisibility,
      getColumnUniqueValues, // 고유 값 조회 함수 등록
      onGroupSumChange: setGroupSumConfig, // 그룹별 합산 설정
      // 틀고정 컬럼 관련
      frozenColumnCount, // 현재 틀고정 컬럼 수
      onFrozenColumnCountChange: (count: number) => {
        setFrozenColumnCount(count);
        const visibleCols = columnsToRegister
          .filter((col) => col.visible !== false)
          .map((col) => col.columnName || col.field);
        setFrozenColumns(visibleCols.slice(0, count));
      },
      // 탭 관련 정보 (탭 내부의 테이블인 경우)
      parentTabId,
      parentTabsComponentId,
      screenId: screenId ? Number(screenId) : undefined,
    };

    registerTable(registration);

    return () => {
      unregisterTable(tableId);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    tableId,
    tableConfig.selectedTable,
    tableConfig.columns,
    columnLabels,
    columnMeta,
    categoryMappings,
    columnWidths,
    tableLabel,
    data,
    totalItems,
    registerTable,
  ]);

  // 🎯 초기 로드 시 localStorage에서 정렬 상태 불러오기 (없으면 defaultSort 적용)
  useEffect(() => {
    if (!tableConfig.selectedTable || !userId || hasInitializedSort.current) return;

    const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`;
    const savedSort = localStorage.getItem(storageKey);

    if (savedSort) {
      try {
        const { column, direction } = JSON.parse(savedSort);
        if (column && direction) {
          setSortColumn(column);
          setSortDirection(direction);
          hasInitializedSort.current = true;
          return;
        }
      } catch (error) {
        // 정렬 상태 복원 실패
      }
    }

    // localStorage에 저장된 정렬이 없으면 defaultSort 설정 적용
    if (tableConfig.defaultSort?.columnName) {
      setSortColumn(tableConfig.defaultSort.columnName);
      setSortDirection(tableConfig.defaultSort.direction || "asc");
      hasInitializedSort.current = true;
    }
  }, [tableConfig.selectedTable, tableConfig.defaultSort, userId]);

  // 🆕 초기 로드 시 localStorage에서 컬럼 순서 불러오기
  useEffect(() => {
    if (!tableConfig.selectedTable || !userId) return;

    const userKey = userId || "guest";
    const storageKey = `table_column_order_${tableConfig.selectedTable}_${userKey}`;
    const savedOrder = localStorage.getItem(storageKey);

    if (savedOrder) {
      try {
        const parsedOrder = JSON.parse(savedOrder);
        setColumnOrder(parsedOrder);

        // 부모 컴포넌트에 초기 컬럼 순서 전달
        if (onSelectedRowsChange && parsedOrder.length > 0) {

          // 초기 데이터도 함께 전달 (컬럼 순서대로 재정렬)
          const initialData = data.map((row: any) => {
            const reordered: any = {};
            parsedOrder.forEach((colName: string) => {
              if (colName in row) {
                reordered[colName] = row[colName];
              }
            });
            // 나머지 컬럼 추가
            Object.keys(row).forEach((key) => {
              if (!(key in reordered)) {
                reordered[key] = row[key];
              }
            });
            return reordered;
          });

          // 전역 저장소에 데이터 저장
          if (tableConfig.selectedTable) {
            // 컬럼 라벨 매핑 생성 (tableConfig.columns 사용 - visibleColumns는 아직 정의되지 않음)
            const cols = (tableConfig.columns || []).filter((col) => col.visible !== false);
            const labels: Record<string, string> = {};
            cols.forEach((col) => {
              labels[col.columnName] = columnLabels[col.columnName] || col.columnName;
            });

            tableDisplayStore.setTableData(
              tableConfig.selectedTable,
              initialData,
              parsedOrder.filter((col) => col !== "__checkbox__"),
              sortColumn,
              sortDirection,
              {
                filterConditions: Object.keys(searchValues).length > 0 ? searchValues : undefined,
                searchTerm: searchTerm || undefined,
                visibleColumns: cols.map((col) => col.columnName),
                columnLabels: labels,
                currentPage: currentPage,
                pageSize: localPageSize,
                totalItems: totalItems,
              },
            );
          }

          onSelectedRowsChange([], [], sortColumn, sortDirection, parsedOrder, initialData);
        }
      } catch (error) {
        console.error("❌ 컬럼 순서 파싱 실패:", error);
      }
    }
  }, [tableConfig.selectedTable, userId, data.length]); // data.length 추가 (데이터 로드 후 실행)

  const { optimizedConvertCode } = useEntityJoinOptimization(columnMeta, {
    enableBatchLoading: true,
    preloadCommonCodes: true,
    maxBatchSize: 5,
  });

  // ========================================
  // 컬럼 라벨 가져오기
  // ========================================

  const fetchColumnLabels = useCallback(async () => {
    if (!tableConfig.selectedTable) return;

    try {
      // 🔥 FIX: 캐시 키에 회사 코드 포함 (멀티테넌시 지원)
      const currentUser = JSON.parse(localStorage.getItem("currentUser") || "{}");
      const companyCode = currentUser.companyCode || "UNKNOWN";
      const cacheKey = `columns_${tableConfig.selectedTable}_${companyCode}`;
      const cached = tableColumnCache.get(cacheKey);
      if (cached && Date.now() - cached.timestamp < TABLE_CACHE_TTL) {
        const labels: Record<string, string> = {};
        const meta: Record<string, { webType?: string; codeCategory?: string; inputType?: string; categoryRef?: string }> = {};

        const inputTypeMap: Record<string, string> = {};
        const categoryRefMap: Record<string, string> = {};
        if (cached.inputTypes) {
          cached.inputTypes.forEach((col: any) => {
            inputTypeMap[col.columnName] = col.inputType;
            if (col.categoryRef) {
              categoryRefMap[col.columnName] = col.categoryRef;
            }
          });
        }

        cached.columns.forEach((col: any) => {
          labels[col.columnName] = col.displayName || col.comment || col.columnName;
          meta[col.columnName] = {
            webType: col.webType,
            codeCategory: col.codeCategory,
            inputType: inputTypeMap[col.columnName],
            categoryRef: categoryRefMap[col.columnName],
          };
        });

        setColumnLabels(labels);
        setColumnMeta(meta);
        return;
      }

      const columns = await tableTypeApi.getColumns(tableConfig.selectedTable);

      const inputTypes = await tableTypeApi.getColumnInputTypes(tableConfig.selectedTable);
      const inputTypeMap: Record<string, string> = {};
      const categoryRefMap: Record<string, string> = {};
      inputTypes.forEach((col: any) => {
        inputTypeMap[col.columnName] = col.inputType;
        if (col.categoryRef) {
          categoryRefMap[col.columnName] = col.categoryRef;
        }
      });

      tableColumnCache.set(cacheKey, {
        columns,
        inputTypes,
        timestamp: Date.now(),
      });

      const labels: Record<string, string> = {};
      const meta: Record<string, { webType?: string; codeCategory?: string; inputType?: string; categoryRef?: string }> = {};

      columns.forEach((col: any) => {
        labels[col.columnName] = col.displayName || col.comment || col.columnName;
        meta[col.columnName] = {
          webType: col.webType,
          codeCategory: col.codeCategory,
          inputType: inputTypeMap[col.columnName],
          categoryRef: categoryRefMap[col.columnName],
        };
      });

      setColumnLabels(labels);
      setColumnMeta(meta);
    } catch (error) {
      console.error("컬럼 라벨 가져오기 실패:", error);
    }
  }, [tableConfig.selectedTable]);

  // ========================================
  // 테이블 라벨 가져오기
  // ========================================

  const fetchTableLabel = useCallback(async () => {
    if (!tableConfig.selectedTable) return;

    try {
      const cacheKey = `table_info_${tableConfig.selectedTable}`;
      const cached = tableInfoCache.get(cacheKey);
      if (cached && Date.now() - cached.timestamp < TABLE_CACHE_TTL) {
        const tables = cached.tables || [];
        const tableInfo = tables.find((t: any) => t.tableName === tableConfig.selectedTable);
        const label =
          tableInfo?.displayName || (tableInfo as any)?.comment || tableInfo?.description || tableConfig.selectedTable;
        setTableLabel(label);
        return;
      }

      const tables = await tableTypeApi.getTables();

      tableInfoCache.set(cacheKey, {
        tables,
        timestamp: Date.now(),
      });

      const tableInfo = tables.find((t: any) => t.tableName === tableConfig.selectedTable);
      const label =
        tableInfo?.displayName || (tableInfo as any)?.comment || tableInfo?.description || tableConfig.selectedTable;
      setTableLabel(label);
    } catch (error) {
      console.error("테이블 라벨 가져오기 실패:", error);
    }
  }, [tableConfig.selectedTable]);

  // ========================================
  // 카테고리 값 매핑 로드
  // ========================================

  // 카테고리 컬럼 목록 추출 (useMemo로 최적화)
  const categoryColumns = useMemo(() => {
    return Object.entries(columnMeta)
      .filter(([_, meta]) => meta.inputType === "category")
      .map(([columnName, _]) => columnName);
  }, [columnMeta]);

  // 카테고리 매핑 로드 (columnMeta 변경 시 즉시 실행)
  useEffect(() => {
    const loadCategoryMappings = async () => {
      if (!tableConfig.selectedTable) {
        return;
      }

      try {
        const mappings: Record<string, Record<string, { label: string; color?: string }>> = {};
        const apiClient = (await import("@/lib/api/client")).apiClient;

        // 최고관리자가 특정 회사 프리뷰 시 해당 회사 카테고리만 필터링
        const filterCompanyParam = companyCode && companyCode !== "*"
          ? `&filterCompanyCode=${encodeURIComponent(companyCode)}`
          : "";

        // 트리 구조를 평탄화하는 헬퍼 함수 (메인 테이블 + 엔티티 조인 공통 사용)
        // valueCode만 키로 사용 (valueId까지 넣으면 같은 라벨이 2번 나옴)
        const flattenTree = (items: any[], mapping: Record<string, { label: string; color?: string }>) => {
          items.forEach((item: any) => {
            if (item.valueCode) {
              mapping[String(item.valueCode)] = {
                label: item.valueLabel,
                color: item.color,
              };
            }
            if (item.children && Array.isArray(item.children) && item.children.length > 0) {
              flattenTree(item.children, mapping);
            }
          });
        };

        for (const columnName of categoryColumns) {
          try {
            let targetTable = tableConfig.selectedTable;
            let targetColumn = columnName;

            // category_ref가 있으면 참조 테이블.컬럼 기준으로 조회
            const meta = columnMeta[columnName];
            if (meta?.categoryRef) {
              const refParts = meta.categoryRef.split(".");
              if (refParts.length === 2) {
                targetTable = refParts[0];
                targetColumn = refParts[1];
              }
            } else if (columnName.includes(".")) {
              // 엔티티 조인 컬럼 처리: "테이블명.컬럼명" 형태
              const parts = columnName.split(".");
              targetTable = parts[0];
              targetColumn = parts[1];
            }

            // 비활성화된 카테고리도 라벨로 표시하기 위해 includeInactive=true
            const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values?includeInactive=true${filterCompanyParam}`);

            if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
              const mapping: Record<string, { label: string; color?: string }> = {};
              flattenTree(response.data.data, mapping);

              if (Object.keys(mapping).length > 0) {
                mappings[columnName] = mapping;
              }
            }
          } catch {
            // 카테고리 값 로드 실패 - 무시
          }
        }

        // 🆕 엔티티 조인 컬럼의 inputType 정보 가져오기 및 카테고리 매핑 로드
        // 1. "테이블명.컬럼명" 형태의 조인 컬럼 추출
        const joinedColumns =
          tableConfig.columns?.filter((col) => col.columnName?.includes(".")).map((col) => col.columnName) || [];

        // 2. additionalJoinInfo가 있는 컬럼도 추출 (예: item_code_material → item_info.material)
        const additionalJoinColumns =
          tableConfig.columns
            ?.filter((col: any) => col.additionalJoinInfo?.referenceTable)
            .map((col: any) => ({
              columnName: col.columnName, // 예: item_code_material
              referenceTable: col.additionalJoinInfo.referenceTable, // 예: item_info
              // joinAlias에서 실제 컬럼명 추출 (item_code_material → material)
              actualColumn:
                col.additionalJoinInfo.joinAlias?.replace(`${col.additionalJoinInfo.sourceColumn}_`, "") ||
                col.columnName,
            })) || [];

        // 조인 테이블별로 그룹화
        const joinedTableColumns: Record<string, { columnName: string; actualColumn: string }[]> = {};

        // "테이블명.컬럼명" 형태 처리
        for (const joinedColumn of joinedColumns) {
          const parts = joinedColumn.split(".");
          if (parts.length !== 2) continue;

          const joinedTable = parts[0];
          const joinedColumnName = parts[1];

          if (!joinedTableColumns[joinedTable]) {
            joinedTableColumns[joinedTable] = [];
          }
          joinedTableColumns[joinedTable].push({
            columnName: joinedColumn,
            actualColumn: joinedColumnName,
          });
        }

        // additionalJoinInfo 형태 처리
        for (const col of additionalJoinColumns) {
          if (!joinedTableColumns[col.referenceTable]) {
            joinedTableColumns[col.referenceTable] = [];
          }
          joinedTableColumns[col.referenceTable].push({
            columnName: col.columnName, // 예: item_code_material
            actualColumn: col.actualColumn, // 예: material
          });
        }

        // 조인된 테이블별로 inputType 정보 가져오기
        const newJoinedColumnMeta: Record<string, { webType?: string; codeCategory?: string; inputType?: string }> = {};

        for (const [joinedTable, columns] of Object.entries(joinedTableColumns)) {
          try {
            // 조인 테이블의 컬럼 inputType 정보 가져오기 (이미 import된 tableTypeApi 사용)
            const inputTypes = await tableTypeApi.getColumnInputTypes(joinedTable);

            for (const col of columns) {
              const inputTypeInfo = inputTypes.find((it: any) => it.columnName === col.actualColumn);

              // 컬럼명 그대로 저장 (item_code_material 또는 item_info.material)
              newJoinedColumnMeta[col.columnName] = {
                inputType: inputTypeInfo?.inputType,
              };

              // inputType이 category인 경우 카테고리 매핑 로드
              if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) {
                try {
                  const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values?includeInactive=true${filterCompanyParam}`);

                  if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
                    const mapping: Record<string, { label: string; color?: string }> = {};
                    flattenTree(response.data.data, mapping);

                    if (Object.keys(mapping).length > 0) {
                      mappings[col.columnName] = mapping;
                    }
                  }
                } catch {
                  // 조인 테이블 카테고리 없음 - 무시
                }
              }
            }
          } catch {
            // 조인 테이블 inputType 로드 실패 - 무시
          }
        }

        // 조인 컬럼 메타데이터 상태 업데이트
        if (Object.keys(newJoinedColumnMeta).length > 0) {
          setJoinedColumnMeta(newJoinedColumnMeta);
        }

        // 🆕 카테고리 연쇄관계 매핑 로드 (category_value_cascading_mapping)
        try {
          const cascadingResponse = await apiClient.get(
            `/category-value-cascading/table/${tableConfig.selectedTable}/mappings`,
          );
          if (cascadingResponse.data.success && cascadingResponse.data.data) {
            const cascadingMappings = cascadingResponse.data.data;

            // 각 자식 컬럼에 대한 매핑 추가
            for (const [columnName, columnMappings] of Object.entries(
              cascadingMappings as Record<string, Array<{ code: string; label: string }>>,
            )) {
              if (!mappings[columnName]) {
                mappings[columnName] = {};
              }
              // 연쇄관계 매핑 추가
              for (const item of columnMappings) {
                mappings[columnName][item.code] = {
                  label: item.label,
                  color: undefined, // 연쇄관계는 색상 없음
                };
              }
            }
            // 카테고리 연쇄관계 매핑 로드 완료
          }
        } catch {
          // 연쇄관계 매핑이 없는 경우 무시
        }

        setCategoryMappings(mappings);
        if (Object.keys(mappings).length > 0) {
          setCategoryMappingsKey((prev) => prev + 1);
        }
      } catch {
        // 카테고리 매핑 로드 실패 - 무시
      }
    };

    loadCategoryMappings();
  }, [
    tableConfig.selectedTable,
    categoryColumns.length,
    JSON.stringify(categoryColumns),
    JSON.stringify(tableConfig.columns),
    columnMeta,
    companyCode,
  ]);

  // ========================================
  // 데이터 가져오기
  // ========================================

  const fetchTableDataInternal = useCallback(async () => {
    if (!tableConfig.selectedTable || isDesignMode) {
      setData([]);
      setTotalPages(0);
      setTotalItems(0);
      return;
    }

    setLoading(true);
    setError(null);

    try {
      const page = currentPage;
      const pageSize = localPageSize;
      // 🆕 sortColumn이 없으면 defaultSort 설정을 fallback으로 사용
      const sortBy = sortColumn || tableConfig.defaultSort?.columnName || undefined;
      const sortOrder = sortColumn ? sortDirection : (tableConfig.defaultSort?.direction || sortDirection);
      const search = searchTerm || undefined;

      // 🆕 연결 필터 값 가져오기 (분할 패널 내부일 때)
      const linkedFilterValues: Record<string, any> = {};
      let hasLinkedFiltersConfigured = false; // 연결 필터가 설정되어 있는지 여부
      let hasSelectedLeftData = false; // 좌측에서 데이터가 선택되었는지 여부

      if (splitPanelContext) {
        // 연결 필터 설정 여부 확인 (현재 테이블에 해당하는 필터가 있는지)
        const linkedFiltersConfig = splitPanelContext.linkedFilters || [];
        hasLinkedFiltersConfigured = linkedFiltersConfig.some(
          (filter) =>
            filter.targetColumn?.startsWith(tableConfig.selectedTable + ".") ||
            filter.targetColumn === tableConfig.selectedTable,
        );

        // 좌측 데이터 선택 여부 확인
        hasSelectedLeftData =
          splitPanelContext.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0;

        const allLinkedFilters = splitPanelContext.getLinkedFilterValues();

        // 현재 테이블에 해당하는 필터만 추출 (테이블명.컬럼명 형식에서)
        // 연결 필터는 코드 값이므로 정확한 매칭(equals)을 사용해야 함
        for (const [key, value] of Object.entries(allLinkedFilters)) {
          if (key.includes(".")) {
            const [tableName, columnName] = key.split(".");
            if (tableName === tableConfig.selectedTable) {
              // 연결 필터는 코드 값이므로 equals 연산자 사용
              linkedFilterValues[columnName] = { value, operator: "equals" };
              hasLinkedFiltersConfigured = true; // 이 테이블에 대한 필터가 있음
            }
          } else {
            // 테이블명 없이 컬럼명만 있는 경우 그대로 사용 (equals)
            linkedFilterValues[key] = { value, operator: "equals" };
          }
        }

        // 🆕 자동 컬럼 매칭: linkedFilters가 설정되어 있지 않아도
        // 우측 화면(splitPanelPosition === "right")이고 좌측 데이터가 선택되어 있으면
        // 동일한 컬럼명이 있는 경우 자동으로 필터링 적용
        if (
          splitPanelPosition === "right" &&
          hasSelectedLeftData &&
          Object.keys(linkedFilterValues).length === 0 &&
          !hasLinkedFiltersConfigured
        ) {
          const leftData = splitPanelContext.selectedLeftData!;
          const tableColumns = (tableConfig.columns || []).map((col) => col.columnName);

          // 좌측 데이터의 컬럼 중 현재 테이블에 동일한 컬럼이 있는지 확인
          for (const [colName, colValue] of Object.entries(leftData)) {
            // null, undefined, 빈 문자열 제외
            if (colValue === null || colValue === undefined || colValue === "") continue;
            // id, objid 등 기본 키는 제외 (너무 일반적인 컬럼명)
            if (colName === "id" || colName === "objid" || colName === "company_code") continue;

            // 현재 테이블에 동일한 컬럼이 있는지 확인
            if (tableColumns.includes(colName)) {
              // 자동 컬럼 매칭도 equals 연산자 사용
              linkedFilterValues[colName] = { value: colValue, operator: "equals" };
              hasLinkedFiltersConfigured = true;
            }
          }
        }
      }

      // 연결 필터가 설정되어 있지만 좌측에서 데이터가 선택되지 않은 경우
      // → 빈 데이터 표시 (모든 데이터를 보여주지 않음)
      if (hasLinkedFiltersConfigured && !hasSelectedLeftData) {
        setData([]);
        setTotalItems(0);
        setLoading(false);
        return;
      }

      // RelatedDataButtons 대상이지만 아직 버튼이 선택되지 않은 경우
      // → 빈 데이터 표시 (모든 데이터를 보여주지 않음)
      if (isRelatedButtonTarget && !relatedButtonFilter) {
        setData([]);
        setTotalItems(0);
        setLoading(false);
        return;
      }

      // RelatedDataButtons 필터 값 준비
      const relatedButtonFilterValues: Record<string, any> = {};
      if (relatedButtonFilter) {
        relatedButtonFilterValues[relatedButtonFilter.filterColumn] = {
          value: relatedButtonFilter.filterValue,
          operator: "equals",
        };
      }

      // 검색 필터, 연결 필터, RelatedDataButtons 필터 병합
      const filters = {
        ...(Object.keys(searchValues).length > 0 ? searchValues : {}),
        ...linkedFilterValues,
        ...relatedButtonFilterValues, // 🆕 RelatedDataButtons 필터 추가
      };
      const hasFilters = Object.keys(filters).length > 0;

      // 🆕 REST API 데이터 소스 처리
      const isRestApiTable =
        tableConfig.selectedTable.startsWith("restapi_") || tableConfig.selectedTable.startsWith("_restapi_");

      let response: any;

      if (isRestApiTable) {
        // REST API 데이터 소스인 경우
        const connectionIdMatch = tableConfig.selectedTable.match(/restapi_(\d+)/);
        const connectionId = connectionIdMatch ? parseInt(connectionIdMatch[1]) : null;

        if (connectionId) {
          // REST API 연결 정보 가져오기 및 데이터 조회
          const { ExternalRestApiConnectionAPI } = await import("@/lib/api/externalRestApiConnection");
          const restApiData = await ExternalRestApiConnectionAPI.fetchData(
            connectionId,
            undefined, // endpoint - 연결 정보에서 가져옴
            "response", // jsonPath - 기본값 response
          );

          response = {
            data: restApiData.rows || [],
            total: restApiData.total || restApiData.rows?.length || 0,
            totalPages: Math.ceil((restApiData.total || restApiData.rows?.length || 0) / pageSize),
          };
        } else {
          throw new Error("REST API 연결 ID를 찾을 수 없습니다.");
        }
      } else {
        // 일반 DB 테이블인 경우 (기존 로직)
        const entityJoinColumns = (tableConfig.columns || [])
          .filter((col) => col.additionalJoinInfo)
          .map((col) => ({
            sourceTable: col.additionalJoinInfo!.sourceTable,
            sourceColumn: col.additionalJoinInfo!.sourceColumn,
            joinAlias: col.additionalJoinInfo!.joinAlias,
            referenceTable: col.additionalJoinInfo!.referenceTable,
          }));

        // 🎯 화면별 엔티티 표시 설정 수집
        const screenEntityConfigs: Record<string, any> = {};
        (tableConfig.columns || [])
          .filter((col) => col.entityDisplayConfig && col.entityDisplayConfig.displayColumns?.length > 0)
          .forEach((col) => {
            screenEntityConfigs[col.columnName] = {
              displayColumns: col.entityDisplayConfig!.displayColumns,
              separator: col.entityDisplayConfig!.separator || " - ",
              sourceTable: col.entityDisplayConfig!.sourceTable || tableConfig.selectedTable,
              joinTable: col.entityDisplayConfig!.joinTable,
            };
          });

        // 🆕 제외 필터 처리 (다른 테이블에 이미 존재하는 데이터 제외)
        let excludeFilterParam: any = undefined;
        if (tableConfig.excludeFilter?.enabled) {
          const excludeConfig = tableConfig.excludeFilter;
          let filterValue: any = undefined;

          // 필터 값 소스에 따라 값 가져오기 (우선순위: formData > URL > 분할패널)
          if (excludeConfig.filterColumn && excludeConfig.filterValueField) {
            const fieldName = excludeConfig.filterValueField;

            // 1순위: props로 전달받은 formData에서 값 가져오기 (모달에서 사용)
            if (propFormData && propFormData[fieldName]) {
              filterValue = propFormData[fieldName];
            }
            // 2순위: URL 파라미터에서 값 가져오기
            else if (typeof window !== "undefined") {
              const urlParams = new URLSearchParams(window.location.search);
              filterValue = urlParams.get(fieldName);
            }
            // 3순위: 분할 패널 부모 데이터에서 값 가져오기
            if (!filterValue && splitPanelContext?.selectedLeftData) {
              filterValue = splitPanelContext.selectedLeftData[fieldName];
            }
          }

          if (filterValue || !excludeConfig.filterColumn) {
            excludeFilterParam = {
              enabled: true,
              referenceTable: excludeConfig.referenceTable,
              referenceColumn: excludeConfig.referenceColumn,
              sourceColumn: excludeConfig.sourceColumn,
              filterColumn: excludeConfig.filterColumn,
              filterValue: filterValue,
            };
          }
        }

        // 🎯 항상 entityJoinApi 사용 (writer 컬럼 자동 조인 지원)
        response = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, {
          page,
          size: pageSize,
          sortBy,
          sortOrder,
          search: hasFilters ? filters : undefined,
          enableEntityJoin: true,
          additionalJoinColumns: entityJoinColumns.length > 0 ? entityJoinColumns : undefined,
          screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // 🎯 화면별 엔티티 설정 전달
          dataFilter: tableConfig.dataFilter, // 🆕 데이터 필터 전달
          excludeFilter: excludeFilterParam, // 🆕 제외 필터 전달
          companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드 (최고 관리자만)
        });

        // 실제 데이터의 item_number만 추출하여 중복 확인
        const itemNumbers = (response.data || []).map((item: any) => item.item_number);
        const uniqueItemNumbers = [...new Set(itemNumbers)];

        // console.log("✅ [TableList] API 응답 받음");
        // console.log(`  - dataLength: ${response.data?.length || 0}`);
        // console.log(`  - total: ${response.total}`);
        // console.log(`  - itemNumbers: ${JSON.stringify(itemNumbers)}`);
        // console.log(`  - uniqueItemNumbers: ${JSON.stringify(uniqueItemNumbers)}`);
        // console.log(`  - isDuplicated: ${itemNumbers.length !== uniqueItemNumbers.length}`);

        setData(response.data || []);
        setTotalPages(response.totalPages || 0);
        setTotalItems(response.total || 0);
        setError(null);

        // 🎯 Store에 필터 조건 저장 (엑셀 다운로드용)
        // tableConfig.columns 사용 (visibleColumns는 이 시점에서 아직 정의되지 않을 수 있음)
        const cols = (tableConfig.columns || []).filter((col) => col.visible !== false);
        const labels: Record<string, string> = {};
        cols.forEach((col) => {
          labels[col.columnName] = columnLabels[col.columnName] || col.columnName;
        });

        tableDisplayStore.setTableData(
          tableConfig.selectedTable,
          response.data || [],
          cols.map((col) => col.columnName),
          sortBy,
          sortOrder,
          {
            filterConditions: filters,
            searchTerm: search,
            visibleColumns: cols.map((col) => col.columnName),
            columnLabels: labels,
            currentPage: page,
            pageSize: pageSize,
            totalItems: response.total || 0,
          },
        );
      }
    } catch (err: any) {
      console.error("데이터 가져오기 실패:", err);
      setData([]);
      setTotalPages(0);
      setTotalItems(0);
      setError(err.message || "데이터를 불러오지 못했습니다.");
    } finally {
      setLoading(false);
    }
  }, [
    tableConfig.selectedTable,
    tableConfig.columns,
    currentPage,
    localPageSize,
    sortColumn,
    sortDirection,
    searchTerm,
    searchValues,
    isDesignMode,
    // 🆕 우측 화면일 때만 selectedLeftData 변경에 반응 (좌측 테이블은 재조회 불필요)
    splitPanelPosition,
    currentSplitPosition,
    splitPanelContext?.selectedLeftData,
    // 🆕 RelatedDataButtons 필터 추가
    relatedButtonFilter,
    isRelatedButtonTarget,
    // 🆕 프리뷰용 회사 코드 오버라이드
    companyCode,
  ]);

  const fetchTableDataDebounced = useCallback(
    (...args: Parameters<typeof fetchTableDataInternal>) => {
      const key = `fetchData_${tableConfig.selectedTable}_${currentPage}_${sortColumn}_${sortDirection}`;
      return debouncedApiCall(key, fetchTableDataInternal, 300)(...args);
    },
    [fetchTableDataInternal, tableConfig.selectedTable, currentPage, sortColumn, sortDirection],
  );

  // ========================================
  // 이벤트 핸들러
  // ========================================

  const handlePageChange = (newPage: number) => {
    if (newPage < 1 || newPage > totalPages) return;
    setCurrentPage(newPage);
    if (tableConfig.pagination) {
      tableConfig.pagination.currentPage = newPage;
    }
    if (onConfigChange) {
      onConfigChange({ ...tableConfig, pagination: { ...tableConfig.pagination, currentPage: newPage } });
    }
  };

  useEffect(() => {
    setPageInputValue(String(currentPage));
  }, [currentPage]);

  const commitPageInput = () => {
    const parsed = parseInt(pageInputValue, 10);
    if (isNaN(parsed) || pageInputValue.trim() === "") {
      setPageInputValue(String(currentPage));
      return;
    }
    const clamped = Math.max(1, Math.min(parsed, totalPages || 1));
    if (clamped !== currentPage) {
      handlePageChange(clamped);
    }
    setPageInputValue(String(clamped));
  };

  const handleSort = (column: string) => {
    let newSortColumn = column;
    let newSortDirection: "asc" | "desc" = "asc";

    if (sortColumn === column) {
      newSortDirection = sortDirection === "asc" ? "desc" : "asc";
      setSortDirection(newSortDirection);
    } else {
      setSortColumn(column);
      setSortDirection("asc");
      newSortColumn = column;
      newSortDirection = "asc";
    }

    // 정렬 상태를 localStorage에 저장 (사용자별)
    if (tableConfig.selectedTable && userId) {
      const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`;
      try {
        localStorage.setItem(
          storageKey,
          JSON.stringify({
            column: newSortColumn,
            direction: newSortDirection,
          }),
        );
      } catch (error) {
        // 정렬 상태 저장 실패
      }
    }

    // 정렬 변경 시 선택 정보와 함께 정렬 정보도 전달
    if (onSelectedRowsChange) {
      const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index)));

      // 1단계: 데이터를 정렬
      const sortedData = [...data].sort((a, b) => {
        const aVal = a[newSortColumn];
        const bVal = b[newSortColumn];

        // null/undefined 처리
        if (aVal == null && bVal == null) return 0;
        if (aVal == null) return 1;
        if (bVal == null) return -1;

        // 숫자 비교 (문자열이어도 숫자로 변환 가능하면 숫자로 비교)
        const aNum = Number(aVal);
        const bNum = Number(bVal);

        // 둘 다 유효한 숫자이고, 원본 값이 빈 문자열이 아닌 경우
        if (!isNaN(aNum) && !isNaN(bNum) && aVal !== "" && bVal !== "") {
          return newSortDirection === "desc" ? bNum - aNum : aNum - bNum;
        }

        // 문자열 비교 (대소문자 구분 없이, 숫자 포함 문자열도 자연스럽게 정렬)
        const aStr = String(aVal).toLowerCase();
        const bStr = String(bVal).toLowerCase();

        // 자연스러운 정렬 (숫자 포함 문자열)
        const comparison = aStr.localeCompare(bStr, undefined, { numeric: true, sensitivity: "base" });
        return newSortDirection === "desc" ? -comparison : comparison;
      });

      // 2단계: 정렬된 데이터를 컬럼 순서대로 재정렬
      // tableConfig.columns 사용 (visibleColumns는 이 시점에서 아직 정의되지 않을 수 있음)
      const cols = (tableConfig.columns || []).filter((col) => col.visible !== false);
      const reorderedData = sortedData.map((row: any) => {
        const reordered: any = {};
        cols.forEach((col) => {
          if (col.columnName in row) {
            reordered[col.columnName] = row[col.columnName];
          }
        });
        // 나머지 컬럼 추가
        Object.keys(row).forEach((key) => {
          if (!(key in reordered)) {
            reordered[key] = row[key];
          }
        });
        return reordered;
      });

      onSelectedRowsChange(
        Array.from(selectedRows),
        selectedRowsData,
        newSortColumn,
        newSortDirection,
        columnOrder.length > 0 ? columnOrder : undefined,
        reorderedData,
      );

      // 전역 저장소에 정렬된 데이터 저장
      if (tableConfig.selectedTable) {
        const cleanColumnOrder = (columnOrder.length > 0 ? columnOrder : cols.map((c) => c.columnName)).filter(
          (col) => col !== "__checkbox__",
        );

        // 컬럼 라벨 정보도 함께 저장
        const labels: Record<string, string> = {};
        cols.forEach((col) => {
          labels[col.columnName] = columnLabels[col.columnName] || col.columnName;
        });

        tableDisplayStore.setTableData(
          tableConfig.selectedTable,
          reorderedData,
          cleanColumnOrder,
          newSortColumn,
          newSortDirection,
          {
            filterConditions: Object.keys(searchValues).length > 0 ? searchValues : undefined,
            searchTerm: searchTerm || undefined,
            visibleColumns: cols.map((col) => col.columnName),
            columnLabels: labels,
            currentPage: currentPage,
            pageSize: localPageSize,
            totalItems: totalItems,
          },
        );
      }
    } else {
      console.warn("⚠️ onSelectedRowsChange 콜백이 없습니다!");
    }
  };

  const handleSearchValueChange = (columnName: string, value: any) => {
    setSearchValues((prev) => ({ ...prev, [columnName]: value }));
  };

  const handleAdvancedSearch = () => {
    setCurrentPage(1);
    fetchTableDataDebounced();
  };

  const handleClearAdvancedFilters = useCallback(() => {
    // 상태를 초기화하고 useEffect로 데이터 새로고침
    setSearchValues({});
    setCurrentPage(1);

    // 강제로 데이터 새로고침 트리거
    setRefreshTrigger((prev) => prev + 1);
  }, [searchValues]);

  const handleRefresh = () => {
    fetchTableDataDebounced();
  };

  const getRowKey = (row: any, index: number) => {
    return row.id || row.uuid || `row-${index}`;
  };

  const handleRowSelection = (rowKey: string, checked: boolean) => {
    const isMultiSelect = tableConfig.checkbox?.multiple !== false;
    let newSelectedRows: Set<string>;

    if (isMultiSelect) {
      newSelectedRows = new Set(selectedRows);
      if (checked) {
        newSelectedRows.add(rowKey);
      } else {
        newSelectedRows.delete(rowKey);
      }
    } else {
      // 단일 선택: 기존 선택 해제 후 새 항목만 선택
      newSelectedRows = checked ? new Set([rowKey]) : new Set();
    }
    setSelectedRows(newSelectedRows);

    const selectedRowsData = data.filter((row, index) => newSelectedRows.has(getRowKey(row, index)));
    if (onSelectedRowsChange) {
      onSelectedRowsChange(Array.from(newSelectedRows), selectedRowsData, sortColumn || undefined, sortDirection);
    }
    if (onFormDataChange) {
      onFormDataChange({
        selectedRows: Array.from(newSelectedRows),
        selectedRowsData,
      });
    }

    // 🆕 리피터 컨테이너/집계 위젯 연동용 V2 이벤트 발생
    v2EventBus.emitSync(V2_EVENTS.TABLE_DATA_CHANGE, {
      tableName: tableConfig.selectedTable || "",
      data: selectedRowsData,
      totalCount: selectedRowsData.length,
      source: component.id || "table-list",
    });

    // dataBinding 연동용 window CustomEvent
    if (typeof window !== "undefined") {
      window.dispatchEvent(
        new CustomEvent("v2-table-selection", {
          detail: {
            tableName: tableConfig.selectedTable || "",
            data: selectedRowsData,
            source: component.id || "table-list",
          },
        })
      );
    }

    // 🆕 modalDataStore에 선택된 데이터 자동 저장 (테이블명 기반 dataSourceId)
    if (tableConfig.selectedTable && selectedRowsData.length > 0) {
      import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
        const modalItems = selectedRowsData.map((row, idx) => ({
          id: getRowKey(row, idx),
          originalData: row,
          additionalData: {},
        }));
        useModalDataStore.getState().setData(tableConfig.selectedTable!, modalItems);
      });
    } else if (tableConfig.selectedTable && selectedRowsData.length === 0) {
      // 선택 해제 시 데이터 제거
      import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
        useModalDataStore.getState().clearData(tableConfig.selectedTable!);
      });
    }

    const allRowsSelected = filteredData.every((row, index) => newSelectedRows.has(getRowKey(row, index)));
    setIsAllSelected(allRowsSelected && filteredData.length > 0);
  };

  const handleSelectAll = (checked: boolean) => {
    if (checked) {
      const allKeys = filteredData.map((row, index) => getRowKey(row, index));
      const newSelectedRows = new Set(allKeys);
      setSelectedRows(newSelectedRows);
      setIsAllSelected(true);

      if (onSelectedRowsChange) {
        onSelectedRowsChange(Array.from(newSelectedRows), filteredData, sortColumn || undefined, sortDirection);
      }
      if (onFormDataChange) {
        onFormDataChange({
          selectedRows: Array.from(newSelectedRows),
          selectedRowsData: filteredData,
        });
      }

      // 🆕 리피터 컨테이너/집계 위젯 연동용 V2 이벤트 발생
      v2EventBus.emitSync(V2_EVENTS.TABLE_DATA_CHANGE, {
        tableName: tableConfig.selectedTable || "",
        data: filteredData,
        totalCount: filteredData.length,
        source: component.id || "table-list",
      });

      // 🆕 modalDataStore에 전체 데이터 저장
      if (tableConfig.selectedTable && filteredData.length > 0) {
        import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
          const modalItems = filteredData.map((row, idx) => ({
            id: getRowKey(row, idx),
            originalData: row,
            additionalData: {},
          }));

          useModalDataStore.getState().setData(tableConfig.selectedTable!, modalItems);
        });
      }
    } else {
      setSelectedRows(new Set());
      setIsAllSelected(false);

      if (onSelectedRowsChange) {
        onSelectedRowsChange([], [], sortColumn || undefined, sortDirection);
      }
      if (onFormDataChange) {
        onFormDataChange({ selectedRows: [], selectedRowsData: [] });
      }

      // 🆕 리피터 컨테이너/집계 위젯 연동용 V2 이벤트 발생 (선택 해제)
      v2EventBus.emitSync(V2_EVENTS.TABLE_DATA_CHANGE, {
        tableName: tableConfig.selectedTable || "",
        data: [],
        totalCount: 0,
        source: component.id || "table-list",
      });

      // 🆕 modalDataStore 데이터 제거
      if (tableConfig.selectedTable) {
        import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
          useModalDataStore.getState().clearData(tableConfig.selectedTable!);
        });
      }
    }
  };

  const handleRowClick = (row: any, index: number, e: React.MouseEvent) => {
    // 체크박스 클릭은 무시 (이미 handleRowSelection에서 처리됨)
    const target = e.target as HTMLElement;
    if (target.closest('input[type="checkbox"]') || target.closest('button[role="checkbox"]')) {
      return;
    }

    // 행 선택/해제 토글
    const rowKey = getRowKey(row, index);
    const isCurrentlySelected = selectedRows.has(rowKey);

    handleRowSelection(rowKey, !isCurrentlySelected);

    // 🆕 분할 패널 컨텍스트에 선택된 데이터 저장 (좌측 화면인 경우)
    // disableAutoDataTransfer가 true이면 자동 전달 비활성화 (버튼 클릭으로만 전달)
    // currentSplitPosition을 사용하여 정확한 위치 확인 (splitPanelPosition이 없을 수 있음)
    const effectiveSplitPosition = splitPanelPosition || currentSplitPosition;

    if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
      if (!isCurrentlySelected) {
        // 선택된 경우: 데이터 저장
        splitPanelContext.setSelectedLeftData(row);
      } else {
        // 선택 해제된 경우: 데이터 초기화
        splitPanelContext.setSelectedLeftData(null);
      }
    }
  };

  // 🆕 셀 클릭 핸들러 (포커스 설정 + 행 선택/해제 토글)
  const handleCellClick = (rowIndex: number, colIndex: number, e: React.MouseEvent) => {
    e.stopPropagation();

    // 현재 편집 중인 셀을 클릭한 경우 포커스 이동 방지 (select 드롭다운 등이 닫히는 것 방지)
    if (editingCell?.rowIndex === rowIndex && editingCell?.colIndex === colIndex) {
      return;
    }

    setFocusedCell({ rowIndex, colIndex });
    tableContainerRef.current?.focus();

    const row = filteredData[rowIndex];
    if (!row) return;

    // 체크박스 컬럼은 Checkbox의 onCheckedChange에서 이미 처리되므로 스킵
    const column = visibleColumns[colIndex];
    if (column?.columnName === "__checkbox__") return;

    const rowKey = getRowKey(row, rowIndex);
    const isCurrentlySelected = selectedRows.has(rowKey);

    const effectiveSplitPosition = splitPanelPosition || currentSplitPosition;

    if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
      // 분할 패널 좌측: 단일 행 선택 모드
      if (!isCurrentlySelected) {
        setSelectedRows(new Set([rowKey]));
        setIsAllSelected(false);

        splitPanelContext.setSelectedLeftData(row);

        if (onSelectedRowsChange) {
          onSelectedRowsChange([rowKey], [row], sortColumn || undefined, sortDirection);
        }
        if (onFormDataChange) {
          onFormDataChange({ selectedRows: [rowKey], selectedRowsData: [row] });
        }
      }
    } else {
      // 일반 모드: 행 선택/해제 토글
      handleRowSelection(rowKey, !isCurrentlySelected);

      if (splitPanelContext && effectiveSplitPosition === "left") {
        if (!isCurrentlySelected) {
          splitPanelContext.setSelectedLeftData(row);
        } else {
          splitPanelContext.setSelectedLeftData(null);
        }
      }
    }
  };

  // 🆕 셀 더블클릭 핸들러 (편집 모드 진입) - visibleColumns 정의 후 사용
  const handleCellDoubleClick = useCallback(
    (rowIndex: number, colIndex: number, columnName: string, value: any) => {
      // 체크박스 컬럼은 편집 불가
      if (columnName === "__checkbox__") return;

      // 🆕 편집 불가 컬럼 체크
      const column = visibleColumns.find((col) => col.columnName === columnName);
      if (column?.editable === false) {
        toast.warning(`'${column.displayName || columnName}' 컬럼은 편집할 수 없습니다.`);
        return;
      }

      setEditingCell({ rowIndex, colIndex, columnName, originalValue: value });
      setEditingValue(value !== null && value !== undefined ? String(value) : "");
      setFocusedCell({ rowIndex, colIndex });
    },
    [visibleColumns],
  );

  // 🆕 편집 모드 진입 placeholder (실제 구현은 visibleColumns 정의 후)
  const startEditingRef = useRef<() => void>(() => {});

  // 🆕 각 컬럼의 고유값 목록 계산
  const columnUniqueValues = useMemo(() => {
    const result: Record<string, string[]> = {};

    if (data.length === 0) return result;

    (tableConfig.columns || []).forEach((column: { columnName: string }) => {
      if (column.columnName === "__checkbox__") return;

      const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName;
      const values = new Set<string>();

      data.forEach((row) => {
        const val = row[mappedColumnName];
        if (val !== null && val !== undefined && val !== "") {
          values.add(String(val));
        }
      });

      result[column.columnName] = Array.from(values).sort();
    });

    return result;
  }, [data, tableConfig.columns, joinColumnMapping]);

  // 🆕 헤더 필터 토글
  const toggleHeaderFilter = useCallback((columnName: string, value: string) => {
    setHeaderFilters((prev) => {
      const current = prev[columnName] || new Set<string>();
      const newSet = new Set(current);

      if (newSet.has(value)) {
        newSet.delete(value);
      } else {
        newSet.add(value);
      }

      return { ...prev, [columnName]: newSet };
    });
  }, []);

  // 🆕 헤더 필터 초기화
  const clearHeaderFilter = useCallback((columnName: string) => {
    setHeaderFilters((prev) => {
      const newFilters = { ...prev };
      delete newFilters[columnName];
      return newFilters;
    });
    setOpenFilterColumn(null);
  }, []);

  // 🆕 모든 헤더 필터 초기화
  const clearAllHeaderFilters = useCallback(() => {
    setHeaderFilters({});
    setOpenFilterColumn(null);
  }, []);

  // 🆕 데이터 요약 (Total Summaries) 설정
  // 형식: { columnName: { type: 'sum' | 'avg' | 'count' | 'min' | 'max', label?: string } }
  const summaryConfig = useMemo(() => {
    const config: Record<string, { type: string; label?: string }> = {};

    // tableConfig에서 summary 설정 읽기
    if (tableConfig.summaries) {
      tableConfig.summaries.forEach((summary: { columnName: string; type: string; label?: string }) => {
        config[summary.columnName] = { type: summary.type, label: summary.label };
      });
    }

    return config;
  }, [tableConfig.summaries]);

  // 🆕 요약 데이터 계산
  const summaryData = useMemo(() => {
    if (Object.keys(summaryConfig).length === 0 || data.length === 0) {
      return null;
    }

    const result: Record<string, { value: number | string; label: string }> = {};

    Object.entries(summaryConfig).forEach(([columnName, config]) => {
      const values = data
        .map((row) => {
          const mappedColumnName = joinColumnMapping[columnName] || columnName;
          const val = row[mappedColumnName];
          return typeof val === "number" ? val : parseFloat(val);
        })
        .filter((v) => !isNaN(v));

      let value: number | string = 0;
      let label = config.label || "";

      switch (config.type) {
        case "sum":
          value = values.reduce((acc, v) => acc + v, 0);
          label = label || "합계";
          break;
        case "avg":
          value = values.length > 0 ? values.reduce((acc, v) => acc + v, 0) / values.length : 0;
          label = label || "평균";
          break;
        case "count":
          value = data.length;
          label = label || "개수";
          break;
        case "min":
          value = values.length > 0 ? Math.min(...values) : 0;
          label = label || "최소";
          break;
        case "max":
          value = values.length > 0 ? Math.max(...values) : 0;
          label = label || "최대";
          break;
        default:
          value = 0;
      }

      result[columnName] = { value, label };
    });

    return result;
  }, [data, summaryConfig, joinColumnMapping]);

  // 🆕 편집 취소
  const cancelEditing = useCallback(() => {
    setEditingCell(null);
    setEditingValue("");
    tableContainerRef.current?.focus();
  }, []);

  // 🆕 편집 저장 (즉시 저장 또는 배치 저장)
  const saveEditing = useCallback(async () => {
    if (!editingCell) return;

    const { rowIndex, columnName, originalValue } = editingCell;
    const newValue = editingValue;

    // 값이 변경되지 않았으면 그냥 닫기
    if (String(originalValue ?? "") === newValue) {
      setCellValidationError(rowIndex, columnName, null); // 에러 초기화
      cancelEditing();
      return;
    }

    // 현재 행 데이터 가져오기
    const row = data[rowIndex];
    if (!row || !tableConfig.selectedTable) {
      cancelEditing();
      return;
    }

    // 🆕 유효성 검사 실행
    const validationError = validateValue(newValue === "" ? null : newValue, columnName, row);
    if (validationError) {
      setCellValidationError(rowIndex, columnName, validationError);
      toast.error(validationError);
      // 편집 상태 유지 (에러 수정 가능하도록)
      return;
    }
    // 유효성 통과 시 에러 초기화
    setCellValidationError(rowIndex, columnName, null);

    // 기본 키 필드 찾기 (id 또는 첫 번째 컬럼)
    const primaryKeyField = tableConfig.primaryKey || "id";
    const primaryKeyValue = row[primaryKeyField];

    if (primaryKeyValue === undefined || primaryKeyValue === null) {
      console.error("기본 키 값을 찾을 수 없습니다:", primaryKeyField);
      cancelEditing();
      return;
    }

    // 🆕 배치 모드: 변경사항을 pending에 저장
    if (editMode === "batch") {
      const changeKey = `${rowIndex}-${columnName}`;
      setPendingChanges((prev) => {
        const newMap = new Map(prev);
        newMap.set(changeKey, {
          rowIndex,
          columnName,
          originalValue,
          newValue: newValue === "" ? null : newValue,
          primaryKeyValue,
        });
        return newMap;
      });

      // 로컬 수정 데이터 업데이트 (화면 표시용)
      setLocalEditedData((prev) => ({
        ...prev,
        [rowIndex]: {
          ...(prev[rowIndex] || {}),
          [columnName]: newValue === "" ? null : newValue,
        },
      }));

      cancelEditing();
      return;
    }

    // 즉시 모드: 바로 저장
    try {
      const { apiClient } = await import("@/lib/api/client");

      await apiClient.put("/dynamic-form/update-field", {
        tableName: tableConfig.selectedTable,
        keyField: primaryKeyField,
        keyValue: primaryKeyValue,
        updateField: columnName,
        updateValue: newValue === "" ? null : newValue,
      });

      // 데이터 새로고침 트리거
      setRefreshTrigger((prev) => prev + 1);
    } catch (error) {
      // 셀 편집 저장 실패
    }

    cancelEditing();
  }, [
    editingCell,
    editingValue,
    data,
    tableConfig.selectedTable,
    tableConfig.primaryKey,
    cancelEditing,
    editMode,
    pendingChanges.size,
  ]);

  // 🆕 배치 저장: 모든 변경사항 한번에 저장
  const saveBatchChanges = useCallback(async () => {
    if (pendingChanges.size === 0) {
      toast.info("저장할 변경사항이 없습니다.");
      return;
    }

    try {
      const { apiClient } = await import("@/lib/api/client");
      const primaryKeyField = tableConfig.primaryKey || "id";

      // 모든 변경사항 저장
      const savePromises = Array.from(pendingChanges.values()).map((change) =>
        apiClient.put("/dynamic-form/update-field", {
          tableName: tableConfig.selectedTable,
          keyField: primaryKeyField,
          keyValue: change.primaryKeyValue,
          updateField: change.columnName,
          updateValue: change.newValue,
        }),
      );

      await Promise.all(savePromises);

      // 상태 초기화
      setPendingChanges(new Map());
      setLocalEditedData({});
      setRefreshTrigger((prev) => prev + 1);

      toast.success(`${pendingChanges.size}개의 변경사항이 저장되었습니다.`);
    } catch (error) {
      showErrorToast("데이터 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." });
    }
  }, [pendingChanges, tableConfig.selectedTable, tableConfig.primaryKey]);

  // 배치 취소: 모든 변경사항 롤백
  const cancelBatchChanges = useCallback(() => {
    if (pendingChanges.size === 0) return;

    setPendingChanges(new Map());
    setLocalEditedData({});
    toast.info("변경사항이 취소되었습니다.");
  }, [pendingChanges.size]);

  // 🆕 특정 셀이 수정되었는지 확인
  const isCellModified = useCallback(
    (rowIndex: number, columnName: string) => {
      return pendingChanges.has(`${rowIndex}-${columnName}`);
    },
    [pendingChanges],
  );

  // 🆕 수정된 셀 값 가져오기 (로컬 수정 데이터 우선)
  const getDisplayValue = useCallback(
    (row: any, rowIndex: number, columnName: string) => {
      const localValue = localEditedData[rowIndex]?.[columnName];
      if (localValue !== undefined) {
        return localValue;
      }
      return row[columnName];
    },
    [localEditedData],
  );

  // 🆕 유효성 검사 함수
  const validateValue = useCallback(
    (value: any, columnName: string, row: any): string | null => {
      // tableConfig.validation에서 컬럼별 규칙 가져오기
      const rules = (tableConfig as any).validation?.[columnName] as ValidationRule | undefined;
      if (!rules) return null;

      const strValue = value !== null && value !== undefined ? String(value) : "";
      const numValue = parseFloat(strValue);

      // 필수 검사
      if (rules.required && (!strValue || strValue.trim() === "")) {
        return rules.customMessage || "필수 입력 항목입니다.";
      }

      // 값이 비어있으면 다른 검사 스킵 (required가 아닌 경우)
      if (!strValue || strValue.trim() === "") return null;

      // 최소값 검사
      if (rules.min !== undefined && !isNaN(numValue) && numValue < rules.min) {
        return rules.customMessage || `최소값은 ${rules.min}입니다.`;
      }

      // 최대값 검사
      if (rules.max !== undefined && !isNaN(numValue) && numValue > rules.max) {
        return rules.customMessage || `최대값은 ${rules.max}입니다.`;
      }

      // 최소 길이 검사
      if (rules.minLength !== undefined && strValue.length < rules.minLength) {
        return rules.customMessage || `최소 ${rules.minLength}자 이상 입력해주세요.`;
      }

      // 최대 길이 검사
      if (rules.maxLength !== undefined && strValue.length > rules.maxLength) {
        return rules.customMessage || `최대 ${rules.maxLength}자까지 입력 가능합니다.`;
      }

      // 패턴 검사
      if (rules.pattern && !rules.pattern.test(strValue)) {
        return rules.customMessage || "입력 형식이 올바르지 않습니다.";
      }

      // 커스텀 검증
      if (rules.validate) {
        const customError = rules.validate(value, row);
        if (customError) return customError;
      }

      return null;
    },
    [tableConfig],
  );

  // 🆕 셀 유효성 에러 여부 확인
  const getCellValidationError = useCallback(
    (rowIndex: number, columnName: string): string | null => {
      return validationErrors.get(`${rowIndex}-${columnName}`) || null;
    },
    [validationErrors],
  );

  // 🆕 유효성 검사 에러 설정
  const setCellValidationError = useCallback((rowIndex: number, columnName: string, error: string | null) => {
    setValidationErrors((prev) => {
      const newMap = new Map(prev);
      const key = `${rowIndex}-${columnName}`;
      if (error) {
        newMap.set(key, error);
      } else {
        newMap.delete(key);
      }
      return newMap;
    });
  }, []);

  // 🆕 모든 유효성 에러 초기화
  const clearAllValidationErrors = useCallback(() => {
    setValidationErrors(new Map());
  }, []);

  // 🆕 Excel 내보내기 함수
  const exportToExcel = useCallback(
    (exportAll: boolean = true) => {
      try {
        // 내보낼 데이터 선택 (선택된 행만 또는 전체)
        let exportData: any[];
        if (exportAll) {
          exportData = filteredData;
        } else {
          // 선택된 행만 내보내기
          exportData = filteredData.filter((row, index) => {
            const rowKey = getRowKey(row, index);
            return selectedRows.has(rowKey);
          });
        }

        if (exportData.length === 0) {
          toast.error(exportAll ? "내보낼 데이터가 없습니다." : "선택된 행이 없습니다.");
          return;
        }

        // 컬럼 정보 가져오기 (체크박스 제외)
        const exportColumns = visibleColumns.filter((col) => col.columnName !== "__checkbox__");

        // 헤더 행 생성
        const headers = exportColumns.map((col) => columnLabels[col.columnName] || col.columnName);

        // 데이터 행 생성
        const rows = exportData.map((row) => {
          return exportColumns.map((col) => {
            const mappedColumnName = joinColumnMapping[col.columnName] || col.columnName;
            const value = row[mappedColumnName];

            // 카테고리 매핑된 값 처리
            if (categoryMappings[col.columnName] && value !== null && value !== undefined) {
              const mapping = categoryMappings[col.columnName][String(value)];
              if (mapping) {
                return mapping.label;
              }
            }

            // null/undefined 처리
            if (value === null || value === undefined) {
              return "";
            }

            return value;
          });
        });

        // 워크시트 생성
        const wsData = [headers, ...rows];
        const ws = XLSX.utils.aoa_to_sheet(wsData);

        // 컬럼 너비 자동 조정
        const colWidths = exportColumns.map((col, idx) => {
          const headerLength = headers[idx]?.length || 10;
          const maxDataLength = Math.max(...rows.map((row) => String(row[idx] ?? "").length));
          return { wch: Math.min(Math.max(headerLength, maxDataLength) + 2, 50) };
        });
        ws["!cols"] = colWidths;

        // 워크북 생성
        const wb = XLSX.utils.book_new();
        XLSX.utils.book_append_sheet(wb, ws, tableLabel || "데이터");

        // 파일명 생성
        const _en = new Date();
        const fileName = `${tableLabel || tableConfig.selectedTable || "export"}_${_en.getFullYear()}-${String(_en.getMonth() + 1).padStart(2, "0")}-${String(_en.getDate()).padStart(2, "0")}.xlsx`;

        // 파일 다운로드
        XLSX.writeFile(wb, fileName);

        toast.success(`${exportData.length}개 행이 Excel로 내보내기 되었습니다.`);
      } catch (error) {
        showErrorToast("Excel 파일 내보내기에 실패했습니다", error, { guidance: "데이터를 확인하고 다시 시도해 주세요." });
      }
    },
    [
      filteredData,
      selectedRows,
      visibleColumns,
      columnLabels,
      joinColumnMapping,
      categoryMappings,
      tableLabel,
      tableConfig.selectedTable,
      getRowKey,
    ],
  );

  // 🆕 행 확장/축소 토글
  const toggleRowExpand = useCallback(
    async (rowKey: string, row: any) => {
      setExpandedRows((prev) => {
        const newSet = new Set(prev);
        if (newSet.has(rowKey)) {
          newSet.delete(rowKey);
        } else {
          newSet.add(rowKey);
          // 상세 데이터 로딩 (아직 없는 경우)
          if (!detailData[rowKey] && (tableConfig as any).masterDetail?.detailTable) {
            loadDetailData(rowKey, row);
          }
        }
        return newSet;
      });
    },
    [detailData, tableConfig],
  );

  // 🆕 상세 데이터 로딩
  const loadDetailData = useCallback(
    async (rowKey: string, row: any) => {
      const masterDetailConfig = (tableConfig as any).masterDetail;
      if (!masterDetailConfig?.detailTable) return;

      try {
        const { apiClient } = await import("@/lib/api/client");

        // masterKey 값 가져오기
        const masterKeyField = masterDetailConfig.masterKey || "id";
        const masterKeyValue = row[masterKeyField];

        // 상세 테이블에서 데이터 조회
        const response = await apiClient.post(`/table-management/tables/${masterDetailConfig.detailTable}/data`, {
          page: 1,
          size: 100,
          search: {
            [masterDetailConfig.detailKey || masterKeyField]: masterKeyValue,
          },
          autoFilter: true,
        });

        const details = response.data?.data?.data || [];

        setDetailData((prev) => ({
          ...prev,
          [rowKey]: details,
        }));
      } catch (error) {
        setDetailData((prev) => ({
          ...prev,
          [rowKey]: [],
        }));
      }
    },
    [tableConfig],
  );

  // 🆕 모든 행 확장/축소
  const expandAllRows = useCallback(() => {
    if (expandedRows.size === filteredData.length) {
      // 모두 축소
      setExpandedRows(new Set());
    } else {
      // 모두 확장
      const allKeys = new Set(filteredData.map((row, index) => getRowKey(row, index)));
      setExpandedRows(allKeys);
    }
  }, [expandedRows.size, filteredData, getRowKey]);

  // 🆕 Multi-Level Headers: Band 정보 계산
  const columnBandsInfo = useMemo(() => {
    const bands = (tableConfig as any).columnBands as ColumnBand[] | undefined;
    if (!bands || bands.length === 0) return null;

    // 각 band의 시작 인덱스와 colspan 계산
    const bandInfo = bands
      .map((band) => {
        const visibleBandColumns = band.columns.filter((colName) =>
          visibleColumns.some((vc) => vc.columnName === colName),
        );

        const startIndex = visibleColumns.findIndex((vc) => visibleBandColumns.includes(vc.columnName));

        return {
          caption: band.caption,
          columns: visibleBandColumns,
          colSpan: visibleBandColumns.length,
          startIndex,
        };
      })
      .filter((b) => b.colSpan > 0);

    // Band에 포함되지 않은 컬럼 찾기
    const bandedColumns = new Set(bands.flatMap((b) => b.columns));
    const unbandedColumns = visibleColumns
      .map((vc, idx) => ({ columnName: vc.columnName, index: idx }))
      .filter((c) => !bandedColumns.has(c.columnName));

    return {
      bands: bandInfo,
      unbandedColumns,
      hasBands: bandInfo.length > 0,
    };
  }, [tableConfig, visibleColumns]);

  // 🆕 Cascading Lookups: 연계 드롭다운 옵션 로딩
  const loadCascadingOptions = useCallback(
    async (columnName: string, parentColumnName: string, parentValue: any) => {
      const cascadingConfig = (tableConfig as any).cascadingLookups?.[columnName];
      if (!cascadingConfig) return;

      const cacheKey = `${columnName}_${parentValue}`;

      // 이미 로딩 중이면 스킵
      if (loadingCascading[cacheKey]) return;

      // 이미 캐시된 데이터가 있으면 스킵
      if (cascadingOptions[cacheKey]) return;

      setLoadingCascading((prev) => ({ ...prev, [cacheKey]: true }));

      try {
        const { apiClient } = await import("@/lib/api/client");

        // API에서 연계 옵션 로딩
        const response = await apiClient.post(`/table-management/tables/${cascadingConfig.sourceTable}/data`, {
          page: 1,
          size: 1000,
          search: {
            [cascadingConfig.parentKeyField || parentColumnName]: parentValue,
          },
          autoFilter: true,
        });

        const items = response.data?.data?.data || [];
        const options = items.map((item: any) => ({
          value: item[cascadingConfig.valueField || "id"],
          label: item[cascadingConfig.labelField || "name"],
        }));

        setCascadingOptions((prev) => ({
          ...prev,
          [cacheKey]: options,
        }));
      } catch (error) {
        setCascadingOptions((prev) => ({
          ...prev,
          [cacheKey]: [],
        }));
      } finally {
        setLoadingCascading((prev) => ({ ...prev, [cacheKey]: false }));
      }
    },
    [tableConfig, cascadingOptions, loadingCascading],
  );

  // 🆕 Cascading Lookups: 특정 컬럼의 옵션 가져오기
  const getCascadingOptions = useCallback(
    (columnName: string, row: any): { value: string; label: string }[] => {
      const cascadingConfig = (tableConfig as any).cascadingLookups?.[columnName];
      if (!cascadingConfig) return [];

      const parentValue = row[cascadingConfig.parentColumn];
      if (parentValue === undefined || parentValue === null) return [];

      const cacheKey = `${columnName}_${parentValue}`;
      return cascadingOptions[cacheKey] || [];
    },
    [tableConfig, cascadingOptions],
  );

  // 🆕 Virtual Scrolling: virtualScrollInfo는 displayData 정의 이후로 이동됨 (아래 참조)

  // 🆕 Virtual Scrolling: 스크롤 핸들러
  const handleVirtualScroll = useCallback(
    (e: React.UIEvent<HTMLDivElement>) => {
      if (!isVirtualScrollEnabled) return;
      setScrollTop(e.currentTarget.scrollTop);
    },
    [isVirtualScrollEnabled],
  );

  // 🆕 State Persistence: 통합 상태 저장
  const saveTableState = useCallback(() => {
    if (!tableStateKey) return;

    const state = {
      columnWidths,
      columnOrder,
      sortColumn,
      sortDirection,
      groupByColumns,
      frozenColumns,
      frozenColumnCount, // 틀고정 컬럼 수 저장
      showGridLines,
      headerFilters: Object.fromEntries(
        Object.entries(headerFilters).map(([key, set]) => [key, Array.from(set as Set<string>)]),
      ),
      pageSize: localPageSize,
      timestamp: Date.now(),
    };

    try {
      sessionStorage.setItem(tableStateKey, JSON.stringify(state));
    } catch (error) {
      console.error("❌ 테이블 상태 저장 실패:", error);
    }
  }, [
    tableStateKey,
    columnWidths,
    columnOrder,
    sortColumn,
    sortDirection,
    groupByColumns,
    frozenColumns,
    frozenColumnCount,
    showGridLines,
    headerFilters,
    localPageSize,
  ]);

  // 🆕 State Persistence: 통합 상태 복원
  const loadTableState = useCallback(() => {
    if (!tableStateKey) return;

    try {
      const saved = sessionStorage.getItem(tableStateKey);
      if (!saved) return;

      const state = JSON.parse(saved);

      if (state.columnWidths) setColumnWidths(state.columnWidths);
      if (state.columnOrder) setColumnOrder(state.columnOrder);
      if (state.sortColumn !== undefined) setSortColumn(state.sortColumn);
      if (state.sortDirection) setSortDirection(state.sortDirection);
      if (state.groupByColumns) setGroupByColumns(state.groupByColumns);
      if (state.frozenColumns) {
        // 체크박스 컬럼은 frozen 대상에서 제외 (배경색 이중 적용 방지)
        const restoredFrozenColumns = (state.frozenColumns || []).filter((col: string) => col !== "__checkbox__");
        setFrozenColumns(restoredFrozenColumns);
      }
      if (state.frozenColumnCount !== undefined) setFrozenColumnCount(state.frozenColumnCount); // 틀고정 컬럼 수 복원
      if (state.showGridLines !== undefined) setShowGridLines(state.showGridLines);
      if (state.headerFilters) {
        const filters: Record<string, Set<string>> = {};
        Object.entries(state.headerFilters).forEach(([key, values]) => {
          filters[key] = new Set(values as string[]);
        });
        setHeaderFilters(filters);
      }
    } catch (error) {
      console.error("❌ 테이블 상태 복원 실패:", error);
    }
  }, [tableStateKey]);

  // 🆕 State Persistence: 상태 초기화
  const resetTableState = useCallback(() => {
    if (!tableStateKey) return;

    try {
      sessionStorage.removeItem(tableStateKey);
      setColumnWidths({});
      setColumnOrder([]);
      setSortColumn(null);
      setSortDirection("asc");
      setGroupByColumns([]);
      setFrozenColumns([]);
      setShowGridLines(true);
      setHeaderFilters({});
      toast.success("테이블 설정이 초기화되었습니다.");
    } catch (error) {
      console.error("❌ 테이블 상태 초기화 실패:", error);
    }
  }, [tableStateKey]);

  // 🆕 State Persistence: 컴포넌트 마운트 시 상태 복원
  useEffect(() => {
    loadTableState();
  }, [tableStateKey]); // loadTableState는 의존성에서 제외 (무한 루프 방지)

  // 🆕 Real-Time Updates: WebSocket 연결
  const connectWebSocket = useCallback(() => {
    if (!isRealTimeEnabled || !tableConfig.selectedTable) return;

    const wsUrl =
      (tableConfig as any).wsUrl ||
      `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}/ws/table/${tableConfig.selectedTable}`;

    try {
      setWsConnectionStatus("connecting");
      wsRef.current = new WebSocket(wsUrl);

      wsRef.current.onopen = () => {
        setWsConnectionStatus("connected");
      };

      wsRef.current.onmessage = (event) => {
        try {
          const message = JSON.parse(event.data);

          switch (message.type) {
            case "insert":
              // 새 데이터 추가
              setRefreshTrigger((prev) => prev + 1);
              toast.info("새 데이터가 추가되었습니다.");
              break;
            case "update":
              // 데이터 업데이트
              setRefreshTrigger((prev) => prev + 1);
              toast.info("데이터가 업데이트되었습니다.");
              break;
            case "delete":
              // 데이터 삭제
              setRefreshTrigger((prev) => prev + 1);
              toast.info("데이터가 삭제되었습니다.");
              break;
            case "refresh":
              // 전체 새로고침
              setRefreshTrigger((prev) => prev + 1);
              break;
            default:
              // 알 수 없는 메시지 타입
              break;
          }
        } catch (error) {
          // WebSocket 메시지 파싱 오류
        }
      };

      wsRef.current.onclose = () => {
        setWsConnectionStatus("disconnected");

        // 자동 재연결 (5초 후)
        if (isRealTimeEnabled) {
          reconnectTimeoutRef.current = setTimeout(() => {
            connectWebSocket();
          }, 5000);
        }
      };

      wsRef.current.onerror = () => {
        setWsConnectionStatus("disconnected");
      };
    } catch (error) {
      setWsConnectionStatus("disconnected");
    }
  }, [isRealTimeEnabled, tableConfig.selectedTable]);

  // 🆕 Real-Time Updates: 연결 관리
  useEffect(() => {
    if (isRealTimeEnabled) {
      connectWebSocket();
    }

    return () => {
      // 정리
      if (reconnectTimeoutRef.current) {
        clearTimeout(reconnectTimeoutRef.current);
      }
      if (wsRef.current) {
        wsRef.current.close();
        wsRef.current = null;
      }
    };
  }, [isRealTimeEnabled, tableConfig.selectedTable]);

  // 🆕 State Persistence: 상태 변경 시 자동 저장 (디바운스)
  useEffect(() => {
    const timeoutId = setTimeout(() => {
      saveTableState();
    }, 1000); // 1초 후 저장 (디바운스)

    return () => clearTimeout(timeoutId);
  }, [
    columnWidths,
    columnOrder,
    sortColumn,
    sortDirection,
    groupByColumns,
    frozenColumns,
    showGridLines,
    headerFilters,
  ]);

  // 🆕 Clipboard: 선택된 데이터 복사
  const handleCopy = useCallback(async () => {
    try {
      // 선택된 행 데이터 가져오기
      let copyData: any[];

      if (selectedRows.size > 0) {
        // 선택된 행만
        copyData = filteredData.filter((row, index) => {
          const rowKey = getRowKey(row, index);
          return selectedRows.has(rowKey);
        });
      } else if (focusedCell) {
        // 포커스된 셀만
        const row = filteredData[focusedCell.rowIndex];
        if (row) {
          const column = visibleColumns[focusedCell.colIndex];
          const value = row[column?.columnName];
          await navigator.clipboard.writeText(String(value ?? ""));
          toast.success("셀 복사됨");
          return;
        }
        return;
      } else {
        toast.info("복사할 데이터를 선택해주세요.");
        return;
      }

      // TSV 형식으로 변환 (탭으로 구분)
      const exportColumns = visibleColumns.filter((c) => c.columnName !== "__checkbox__");
      const headers = exportColumns.map((c) => columnLabels[c.columnName] || c.columnName);
      const rows = copyData.map((row) =>
        exportColumns
          .map((c) => {
            const value = row[c.columnName];
            return value !== null && value !== undefined ? String(value).replace(/\t/g, " ").replace(/\n/g, " ") : "";
          })
          .join("\t"),
      );

      const tsvContent = [headers.join("\t"), ...rows].join("\n");
      await navigator.clipboard.writeText(tsvContent);

      toast.success(`${copyData.length}행 복사됨`);
    } catch (error) {
      showErrorToast("클립보드 복사에 실패했습니다", error, { guidance: "브라우저 권한을 확인해 주세요." });
    }
  }, [selectedRows, filteredData, focusedCell, visibleColumns, columnLabels, getRowKey]);

  // 🆕 전체 행 선택
  const handleSelectAllRows = useCallback(() => {
    if (selectedRows.size === filteredData.length) {
      // 전체 해제
      setSelectedRows(new Set());
      setIsAllSelected(false);
    } else {
      // 전체 선택
      const allKeys = new Set(filteredData.map((row, index) => getRowKey(row, index)));
      setSelectedRows(allKeys);
      setIsAllSelected(true);
    }
  }, [selectedRows.size, filteredData, getRowKey]);

  // 🆕 Context Menu: 열기
  const handleContextMenu = useCallback((e: React.MouseEvent, rowIndex: number, colIndex: number, row: any) => {
    e.preventDefault();
    setContextMenu({
      x: e.clientX,
      y: e.clientY,
      rowIndex,
      colIndex,
      row,
    });
  }, []);

  // 🆕 Context Menu: 닫기
  const closeContextMenu = useCallback(() => {
    setContextMenu(null);
  }, []);

  // 🆕 Context Menu: 외부 클릭 시 닫기
  useEffect(() => {
    if (contextMenu) {
      const handleClick = () => closeContextMenu();
      document.addEventListener("click", handleClick);
      return () => document.removeEventListener("click", handleClick);
    }
  }, [contextMenu, closeContextMenu]);

  // 🆕 Search Panel: 통합 검색 실행
  const executeGlobalSearch = useCallback(
    (term: string) => {
      if (!term.trim()) {
        setSearchHighlights(new Set());
        return;
      }

      const lowerTerm = term.toLowerCase();
      const highlights = new Set<string>();

      filteredData.forEach((row, rowIndex) => {
        visibleColumns.forEach((col, colIndex) => {
          const value = row[col.columnName];
          if (value !== null && value !== undefined) {
            const strValue = String(value).toLowerCase();
            if (strValue.includes(lowerTerm)) {
              highlights.add(`${rowIndex}-${colIndex}`);
            }
          }
        });
      });

      setSearchHighlights(highlights);

      // 첫 번째 검색 결과로 포커스 이동
      if (highlights.size > 0) {
        const firstHighlight = Array.from(highlights)[0];
        const [rowIdx, colIdx] = firstHighlight.split("-").map(Number);
        setFocusedCell({ rowIndex: rowIdx, colIndex: colIdx });
        toast.success(`${highlights.size}개 검색 결과`);
      } else {
        toast.info("검색 결과가 없습니다");
      }
    },
    [filteredData, visibleColumns],
  );

  // 🆕 Search Panel: 다음 검색 결과로 이동
  const goToNextSearchResult = useCallback(() => {
    if (searchHighlights.size === 0) return;

    const highlightArray = Array.from(searchHighlights).sort((a, b) => {
      const [aRow, aCol] = a.split("-").map(Number);
      const [bRow, bCol] = b.split("-").map(Number);
      if (aRow !== bRow) return aRow - bRow;
      return aCol - bCol;
    });

    if (!focusedCell) {
      const [rowIdx, colIdx] = highlightArray[0].split("-").map(Number);
      setFocusedCell({ rowIndex: rowIdx, colIndex: colIdx });
      return;
    }

    const currentKey = `${focusedCell.rowIndex}-${focusedCell.colIndex}`;
    const currentIndex = highlightArray.indexOf(currentKey);
    const nextIndex = (currentIndex + 1) % highlightArray.length;
    const [rowIdx, colIdx] = highlightArray[nextIndex].split("-").map(Number);
    setFocusedCell({ rowIndex: rowIdx, colIndex: colIdx });
  }, [searchHighlights, focusedCell]);

  // 🆕 Search Panel: 이전 검색 결과로 이동
  const goToPrevSearchResult = useCallback(() => {
    if (searchHighlights.size === 0) return;

    const highlightArray = Array.from(searchHighlights).sort((a, b) => {
      const [aRow, aCol] = a.split("-").map(Number);
      const [bRow, bCol] = b.split("-").map(Number);
      if (aRow !== bRow) return aRow - bRow;
      return aCol - bCol;
    });

    if (!focusedCell) {
      const lastIdx = highlightArray.length - 1;
      const [rowIdx, colIdx] = highlightArray[lastIdx].split("-").map(Number);
      setFocusedCell({ rowIndex: rowIdx, colIndex: colIdx });
      return;
    }

    const currentKey = `${focusedCell.rowIndex}-${focusedCell.colIndex}`;
    const currentIndex = highlightArray.indexOf(currentKey);
    const prevIndex = currentIndex <= 0 ? highlightArray.length - 1 : currentIndex - 1;
    const [rowIdx, colIdx] = highlightArray[prevIndex].split("-").map(Number);
    setFocusedCell({ rowIndex: rowIdx, colIndex: colIdx });
  }, [searchHighlights, focusedCell]);

  // 🆕 Search Panel: 검색 초기화
  const clearGlobalSearch = useCallback(() => {
    setGlobalSearchTerm("");
    setSearchHighlights(new Set());
    setIsSearchPanelOpen(false);
  }, []);

  // 🆕 Filter Builder: 조건 추가
  const addFilterCondition = useCallback((groupId: string, defaultColumn?: string) => {
    setFilterGroups((prev) =>
      prev.map((group) =>
        group.id === groupId
          ? {
              ...group,
              conditions: [
                ...group.conditions,
                {
                  id: `cond-${Date.now()}`,
                  column: defaultColumn || "",
                  operator: "contains" as const,
                  value: "",
                },
              ],
            }
          : group,
      ),
    );
  }, []);

  // 🆕 Filter Builder: 조건 삭제
  const removeFilterCondition = useCallback((groupId: string, conditionId: string) => {
    setFilterGroups((prev) =>
      prev.map((group) =>
        group.id === groupId
          ? {
              ...group,
              conditions: group.conditions.filter((c) => c.id !== conditionId),
            }
          : group,
      ),
    );
  }, []);

  // 🆕 Filter Builder: 조건 업데이트
  const updateFilterCondition = useCallback(
    (groupId: string, conditionId: string, field: keyof FilterCondition, value: string) => {
      setFilterGroups((prev) =>
        prev.map((group) =>
          group.id === groupId
            ? {
                ...group,
                conditions: group.conditions.map((c) => (c.id === conditionId ? { ...c, [field]: value } : c)),
              }
            : group,
        ),
      );
    },
    [],
  );

  // 🆕 Filter Builder: 그룹 추가
  const addFilterGroup = useCallback((defaultColumn?: string) => {
    setFilterGroups((prev) => [
      ...prev,
      {
        id: `group-${Date.now()}`,
        logic: "AND" as const,
        conditions: [
          {
            id: `cond-${Date.now()}`,
            column: defaultColumn || "",
            operator: "contains" as const,
            value: "",
          },
        ],
      },
    ]);
  }, []);

  // 🆕 Filter Builder: 그룹 삭제
  const removeFilterGroup = useCallback((groupId: string) => {
    setFilterGroups((prev) => prev.filter((g) => g.id !== groupId));
  }, []);

  // 🆕 Filter Builder: 그룹 로직 변경
  const updateGroupLogic = useCallback((groupId: string, logic: "AND" | "OR") => {
    setFilterGroups((prev) => prev.map((group) => (group.id === groupId ? { ...group, logic } : group)));
  }, []);

  // 🆕 Filter Builder: 필터 적용
  const applyFilterBuilder = useCallback(() => {
    // 유효한 조건 개수 계산
    let validConditions = 0;
    filterGroups.forEach((group) => {
      group.conditions.forEach((cond) => {
        if (cond.column && (cond.operator === "isEmpty" || cond.operator === "isNotEmpty" || cond.value)) {
          validConditions++;
        }
      });
    });
    setActiveFilterCount(validConditions);
    setIsFilterBuilderOpen(false);
    toast.success(`${validConditions}개 필터 조건 적용됨`);
  }, [filterGroups]);

  // 🆕 Filter Builder: 필터 초기화
  const clearFilterBuilder = useCallback(() => {
    setFilterGroups([]);
    setActiveFilterCount(0);
    toast.info("필터 초기화됨");
  }, []);

  // 🆕 Filter Builder: 조건 평가 함수
  const evaluateCondition = useCallback((value: any, condition: FilterCondition): boolean => {
    const strValue = value !== null && value !== undefined ? String(value).toLowerCase() : "";
    const condValue = condition.value.toLowerCase();

    switch (condition.operator) {
      case "equals":
        return strValue === condValue;
      case "notEquals":
        return strValue !== condValue;
      case "contains":
        return strValue.includes(condValue);
      case "notContains":
        return !strValue.includes(condValue);
      case "startsWith":
        return strValue.startsWith(condValue);
      case "endsWith":
        return strValue.endsWith(condValue);
      case "greaterThan":
        return parseFloat(strValue) > parseFloat(condValue);
      case "lessThan":
        return parseFloat(strValue) < parseFloat(condValue);
      case "greaterOrEqual":
        return parseFloat(strValue) >= parseFloat(condValue);
      case "lessOrEqual":
        return parseFloat(strValue) <= parseFloat(condValue);
      case "isEmpty":
        return strValue === "" || value === null || value === undefined;
      case "isNotEmpty":
        return strValue !== "" && value !== null && value !== undefined;
      default:
        return true;
    }
  }, []);

  // 🆕 Filter Builder: 행이 필터 조건을 만족하는지 확인
  const rowPassesFilterBuilder = useCallback(
    (row: any): boolean => {
      if (filterGroups.length === 0) return true;

      // 모든 그룹이 AND로 연결됨 (그룹 간)
      return filterGroups.every((group) => {
        const validConditions = group.conditions.filter(
          (c) => c.column && (c.operator === "isEmpty" || c.operator === "isNotEmpty" || c.value),
        );
        if (validConditions.length === 0) return true;

        if (group.logic === "AND") {
          return validConditions.every((cond) => evaluateCondition(row[cond.column], cond));
        } else {
          return validConditions.some((cond) => evaluateCondition(row[cond.column], cond));
        }
      });
    },
    [filterGroups, evaluateCondition],
  );

  // 🆕 컬럼 드래그 시작
  const handleColumnDragStart = useCallback(
    (e: React.DragEvent<HTMLTableCellElement>, index: number) => {
      if (!isColumnDragEnabled) return;

      setDraggedColumnIndex(index);
      e.dataTransfer.effectAllowed = "move";
      e.dataTransfer.setData("text/plain", `col-${index}`);
    },
    [isColumnDragEnabled],
  );

  // 🆕 컬럼 드래그 오버
  const handleColumnDragOver = useCallback(
    (e: React.DragEvent<HTMLTableCellElement>, index: number) => {
      if (!isColumnDragEnabled || draggedColumnIndex === null) return;

      e.preventDefault();
      e.dataTransfer.dropEffect = "move";

      if (index !== draggedColumnIndex) {
        setDropTargetColumnIndex(index);
      }
    },
    [isColumnDragEnabled, draggedColumnIndex],
  );

  // 🆕 컬럼 드래그 종료
  const handleColumnDragEnd = useCallback(() => {
    setDraggedColumnIndex(null);
    setDropTargetColumnIndex(null);
  }, []);

  // 🆕 컬럼 드롭
  const handleColumnDrop = useCallback(
    (e: React.DragEvent<HTMLTableCellElement>, targetIndex: number) => {
      e.preventDefault();

      if (!isColumnDragEnabled || draggedColumnIndex === null || draggedColumnIndex === targetIndex) {
        handleColumnDragEnd();
        return;
      }

      // 컬럼 순서 변경
      const newOrder = [...(columnOrder.length > 0 ? columnOrder : visibleColumns.map((c) => c.columnName))];
      const [movedColumn] = newOrder.splice(draggedColumnIndex, 1);
      newOrder.splice(targetIndex, 0, movedColumn);

      setColumnOrder(newOrder);
      toast.info("컬럼 순서가 변경되었습니다.");

      handleColumnDragEnd();
    },
    [isColumnDragEnabled, draggedColumnIndex, columnOrder, visibleColumns, handleColumnDragEnd],
  );

  // 🆕 행 드래그 시작
  const handleRowDragStart = useCallback(
    (e: React.DragEvent<HTMLTableRowElement>, index: number) => {
      if (!isDragEnabled) return;

      setDraggedRowIndex(index);
      e.dataTransfer.effectAllowed = "move";
      e.dataTransfer.setData("text/plain", String(index));

      // 드래그 이미지 설정 (반투명)
      const dragImage = e.currentTarget.cloneNode(true) as HTMLElement;
      dragImage.style.opacity = "0.5";
      dragImage.style.position = "absolute";
      dragImage.style.top = "-1000px";
      document.body.appendChild(dragImage);
      e.dataTransfer.setDragImage(dragImage, 0, 0);
      setTimeout(() => document.body.removeChild(dragImage), 0);
    },
    [isDragEnabled],
  );

  // 🆕 행 드래그 오버
  const handleRowDragOver = useCallback(
    (e: React.DragEvent<HTMLTableRowElement>, index: number) => {
      if (!isDragEnabled || draggedRowIndex === null) return;

      e.preventDefault();
      e.dataTransfer.dropEffect = "move";

      if (index !== draggedRowIndex) {
        setDropTargetIndex(index);
      }
    },
    [isDragEnabled, draggedRowIndex],
  );

  // 🆕 행 드래그 종료
  const handleRowDragEnd = useCallback(() => {
    setDraggedRowIndex(null);
    setDropTargetIndex(null);
  }, []);

  // 🆕 행 드롭
  const handleRowDrop = useCallback(
    async (e: React.DragEvent<HTMLTableRowElement>, targetIndex: number) => {
      e.preventDefault();

      if (!isDragEnabled || draggedRowIndex === null || draggedRowIndex === targetIndex) {
        handleRowDragEnd();
        return;
      }

      try {
        // 로컬 데이터 재정렬
        const newData = [...filteredData];
        const [movedRow] = newData.splice(draggedRowIndex, 1);
        newData.splice(targetIndex, 0, movedRow);

        // 서버에 순서 저장 (order_index 필드가 있는 경우)
        const orderField = (tableConfig as any).orderField || "order_index";
        const hasOrderField = newData[0] && orderField in newData[0];

        if (hasOrderField && tableConfig.selectedTable) {
          const { apiClient } = await import("@/lib/api/client");
          const primaryKeyField = tableConfig.primaryKey || "id";

          // 영향받는 행들의 순서 업데이트
          const updates = newData.map((row, idx) => ({
            tableName: tableConfig.selectedTable,
            keyField: primaryKeyField,
            keyValue: row[primaryKeyField],
            updateField: orderField,
            updateValue: idx + 1,
          }));

          // 배치 업데이트
          await Promise.all(updates.map((update) => apiClient.put("/dynamic-form/update-field", update)));

          toast.success("순서가 변경되었습니다.");
          setRefreshTrigger((prev) => prev + 1);
        } else {
          // 로컬에서만 순서 변경 (저장 안함)
          toast.info("순서가 변경되었습니다. (로컬만)");
        }
      } catch (error) {
        toast.error("순서 변경 중 오류가 발생했습니다.");
      }

      handleRowDragEnd();
    },
    [isDragEnabled, draggedRowIndex, filteredData, tableConfig, handleRowDragEnd],
  );

  // 🆕 PDF 내보내기 (인쇄용 HTML 생성)
  const exportToPdf = useCallback(
    (exportAll: boolean = true) => {
      try {
        // 내보낼 데이터 선택
        let exportData: any[];
        if (exportAll) {
          exportData = filteredData;
        } else {
          exportData = filteredData.filter((row, index) => {
            const rowKey = getRowKey(row, index);
            return selectedRows.has(rowKey);
          });
        }

        if (exportData.length === 0) {
          toast.error(exportAll ? "내보낼 데이터가 없습니다." : "선택된 행이 없습니다.");
          return;
        }

        // 컬럼 정보 가져오기 (체크박스 제외)
        const exportColumns = visibleColumns.filter((col) => col.columnName !== "__checkbox__");

        // 인쇄용 HTML 생성
        const printContent = `
        <!DOCTYPE html>
        <html>
        <head>
          <meta charset="utf-8">
          <title>${tableLabel || tableConfig.selectedTable || "데이터"}</title>
          <style>
            * { margin: 0; padding: 0; box-sizing: border-box; }
            body { font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif; padding: 20px; }
            h1 { font-size: 18px; margin-bottom: 10px; text-align: center; }
            .info { font-size: 12px; color: #666; margin-bottom: 20px; text-align: center; }
            table { width: 100%; border-collapse: collapse; font-size: 11px; }
            th, td { border: 1px solid #ddd; padding: 6px 8px; text-align: left; }
            th { background-color: #f5f5f5; font-weight: 600; }
            tr:nth-child(even) { background-color: #fafafa; }
            .number { text-align: right; }
            @media print {
              body { padding: 0; }
              table { page-break-inside: auto; }
              tr { page-break-inside: avoid; page-break-after: auto; }
            }
          </style>
        </head>
        <body>
          <h1>${tableLabel || tableConfig.selectedTable || "데이터 목록"}</h1>
          <div class="info">
            출력일: ${new Date().toLocaleDateString("ko-KR")} | 
            총 ${exportData.length}건
          </div>
          <table>
            <thead>
              <tr>
                ${exportColumns.map((col) => `<th>${columnLabels[col.columnName] || col.columnName}</th>`).join("")}
              </tr>
            </thead>
            <tbody>
              ${exportData
                .map(
                  (row) => `
                <tr>
                  ${exportColumns
                    .map((col) => {
                      const mappedColumnName = joinColumnMapping[col.columnName] || col.columnName;
                      let value = row[mappedColumnName];

                      // 카테고리 매핑
                      if (categoryMappings[col.columnName] && value !== null && value !== undefined) {
                        const mapping = categoryMappings[col.columnName][String(value)];
                        if (mapping) value = mapping.label;
                      }

                      const meta = columnMeta[col.columnName];
                      const inputType = meta?.inputType || (col as any).inputType;
                      const isNumeric = inputType === "number" || inputType === "decimal";

                      return `<td class="${isNumeric ? "number" : ""}">${value ?? ""}</td>`;
                    })
                    .join("")}
                </tr>
              `,
                )
                .join("")}
            </tbody>
          </table>
        </body>
        </html>
      `;

        // 새 창에서 인쇄
        const printWindow = window.open("", "_blank");
        if (printWindow) {
          printWindow.document.write(printContent);
          printWindow.document.close();
          printWindow.onload = () => {
            printWindow.print();
          };
          toast.success("인쇄 창이 열렸습니다.");
        } else {
          toast.error("팝업이 차단되었습니다. 팝업을 허용해주세요.");
        }
      } catch (error) {
        console.error("❌ PDF 내보내기 실패:", error);
        showErrorToast("PDF 파일 내보내기에 실패했습니다", error, { guidance: "데이터를 확인하고 다시 시도해 주세요." });
      }
    },
    [
      filteredData,
      selectedRows,
      visibleColumns,
      columnLabels,
      joinColumnMapping,
      categoryMappings,
      columnMeta,
      tableLabel,
      tableConfig.selectedTable,
      getRowKey,
    ],
  );

  // 🆕 편집 중 키보드 핸들러 (간단 버전 - Tab 이동은 visibleColumns 정의 후 처리)
  const handleEditKeyDown = useCallback(
    (e: React.KeyboardEvent<HTMLInputElement>) => {
      switch (e.key) {
        case "Enter":
          e.preventDefault();
          saveEditing();
          break;
        case "Escape":
          e.preventDefault();
          cancelEditing();
          break;
        case "Tab":
          e.preventDefault();
          saveEditing();
          // Tab 이동은 편집 저장 후 테이블 키보드 핸들러에서 처리
          break;
      }
    },
    [saveEditing, cancelEditing],
  );

  // 🆕 편집 입력 필드가 나타나면 자동 포커스
  useEffect(() => {
    if (editingCell && editInputRef.current) {
      editInputRef.current.focus();
      // select()는 input 요소에서만 사용 가능 (select 요소에서는 사용 불가)
      if (typeof editInputRef.current.select === "function") {
        editInputRef.current.select();
      }
    }
  }, [editingCell]);

  // 🆕 포커스된 셀로 스크롤
  useEffect(() => {
    if (focusedCell && tableContainerRef.current) {
      const focusedCellElement = tableContainerRef.current.querySelector(
        `[data-row="${focusedCell.rowIndex}"][data-col="${focusedCell.colIndex}"]`,
      ) as HTMLElement;

      if (focusedCellElement) {
        focusedCellElement.scrollIntoView({ block: "nearest", inline: "nearest" });
      }
    }
  }, [focusedCell]);

  // 컬럼 드래그앤드롭 기능 제거됨 (테이블 옵션 모달에서 컬럼 순서 변경 가능)

  const handleClick = (e: React.MouseEvent) => {
    e.stopPropagation();
    onClick?.();
  };

  // ========================================
  // 컬럼 관련 (visibleColumns는 상단에서 정의됨)
  // ========================================

  // 🆕 visibleColumns가 변경될 때마다 현재 컬럼 순서를 부모에게 전달
  const lastColumnOrderRef = useRef<string>("");

  useEffect(() => {
    // console.log("🔍 [컬럼 순서 전달 useEffect] 실행됨:", {
    //   hasCallback: !!onSelectedRowsChange,
    //   visibleColumnsLength: visibleColumns.length,
    //   visibleColumnsNames: visibleColumns.map((c) => c.columnName),
    // });

    if (!onSelectedRowsChange) {
      // console.warn("⚠️ onSelectedRowsChange 콜백이 없습니다!");
      return;
    }

    if (visibleColumns.length === 0) {
      // console.warn("⚠️ visibleColumns가 비어있습니다!");
      return;
    }

    const currentColumnOrder = visibleColumns.map((col) => col.columnName).filter((name) => name !== "__checkbox__"); // 체크박스 컬럼 제외

    // console.log("🔍 [컬럼 순서] 체크박스 제외 후:", currentColumnOrder);

    // 컬럼 순서가 실제로 변경되었을 때만 전달 (무한 루프 방지)
    const columnOrderString = currentColumnOrder.join(",");
    // console.log("🔍 [컬럼 순서] 비교:", {
    //   current: columnOrderString,
    //   last: lastColumnOrderRef.current,
    //   isDifferent: columnOrderString !== lastColumnOrderRef.current,
    // });

    if (columnOrderString === lastColumnOrderRef.current) {
      // console.log("⏭️ 컬럼 순서 변경 없음, 전달 스킵");
      return;
    }

    lastColumnOrderRef.current = columnOrderString;
    // console.log("📊 현재 화면 컬럼 순서 전달:", currentColumnOrder);

    // 선택된 행 데이터 가져오기
    const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index)));

    // 화면에 표시된 데이터를 컬럼 순서대로 재정렬
    const reorderedData = data.map((row: any) => {
      const reordered: any = {};
      visibleColumns.forEach((col) => {
        if (col.columnName in row) {
          reordered[col.columnName] = row[col.columnName];
        }
      });
      // 나머지 컬럼 추가
      Object.keys(row).forEach((key) => {
        if (!(key in reordered)) {
          reordered[key] = row[key];
        }
      });
      return reordered;
    });

    onSelectedRowsChange(
      Array.from(selectedRows),
      selectedRowsData,
      sortColumn,
      sortDirection,
      currentColumnOrder,
      reorderedData,
    );
  }, [visibleColumns.length, visibleColumns.map((c) => c.columnName).join(",")]); // 의존성 단순화

  // 🆕 키보드 네비게이션 핸들러 (visibleColumns 정의 후에 배치)
  const handleTableKeyDown = useCallback(
    (e: React.KeyboardEvent<HTMLDivElement>) => {
      // 편집 중일 때는 테이블 키보드 핸들러 무시 (편집 입력에서 처리)
      if (editingCell) return;

      if (!focusedCell || data.length === 0) return;

      const { rowIndex, colIndex } = focusedCell;
      const maxRowIndex = data.length - 1;
      const maxColIndex = visibleColumns.length - 1;

      switch (e.key) {
        case "ArrowUp":
          e.preventDefault();
          if (rowIndex > 0) {
            setFocusedCell({ rowIndex: rowIndex - 1, colIndex });
          }
          break;
        case "ArrowDown":
          e.preventDefault();
          if (rowIndex < maxRowIndex) {
            setFocusedCell({ rowIndex: rowIndex + 1, colIndex });
          }
          break;
        case "ArrowLeft":
          e.preventDefault();
          if (colIndex > 0) {
            setFocusedCell({ rowIndex, colIndex: colIndex - 1 });
          }
          break;
        case "ArrowRight":
          e.preventDefault();
          if (colIndex < maxColIndex) {
            setFocusedCell({ rowIndex, colIndex: colIndex + 1 });
          }
          break;
        case "Enter":
          e.preventDefault();
          // 현재 행 선택/해제
          const enterRow = data[rowIndex];
          if (enterRow) {
            const rowKey = getRowKey(enterRow, rowIndex);
            const isCurrentlySelected = selectedRows.has(rowKey);
            handleRowSelection(rowKey, !isCurrentlySelected);
          }
          break;
        case " ": // Space
          e.preventDefault();
          // 체크박스 토글
          const spaceRow = data[rowIndex];
          if (spaceRow) {
            const currentRowKey = getRowKey(spaceRow, rowIndex);
            const isChecked = selectedRows.has(currentRowKey);
            handleRowSelection(currentRowKey, !isChecked);
          }
          break;
        case "F2":
          // 🆕 F2: 편집 모드 진입
          e.preventDefault();
          {
            const col = visibleColumns[colIndex];
            if (col && col.columnName !== "__checkbox__") {
              // 🆕 편집 불가 컬럼 체크
              if (col.editable === false) {
                toast.warning(`'${col.displayName || col.columnName}' 컬럼은 편집할 수 없습니다.`);
                break;
              }
              const row = data[rowIndex];
              const mappedCol = joinColumnMapping[col.columnName] || col.columnName;
              const val = row?.[mappedCol];
              setEditingCell({
                rowIndex,
                colIndex,
                columnName: col.columnName,
                originalValue: val,
              });
              setEditingValue(val !== null && val !== undefined ? String(val) : "");
            }
          }
          break;
        case "b":
        case "B":
          // 🆕 Ctrl+B: 배치 편집 모드 토글
          if (e.ctrlKey) {
            e.preventDefault();
            setEditMode((prev) => {
              const newMode = prev === "immediate" ? "batch" : "immediate";
              if (newMode === "immediate" && pendingChanges.size > 0) {
                // 즉시 모드로 전환 시 저장되지 않은 변경사항 경고
                const confirmDiscard = window.confirm(
                  `저장되지 않은 ${pendingChanges.size}개의 변경사항이 있습니다. 취소하시겠습니까?`,
                );
                if (confirmDiscard) {
                  setPendingChanges(new Map());
                  setLocalEditedData({});
                  toast.info("배치 편집 모드 종료");
                  return "immediate";
                }
                return "batch";
              }
              toast.info(newMode === "batch" ? "배치 편집 모드 시작 (Ctrl+B로 종료)" : "즉시 저장 모드");
              return newMode;
            });
          }
          break;
        case "s":
        case "S":
          // 🆕 Ctrl+S: 배치 저장
          if (e.ctrlKey && editMode === "batch") {
            e.preventDefault();
            saveBatchChanges();
          }
          break;
        case "c":
        case "C":
          // 🆕 Ctrl+C: 선택된 행/셀 복사
          if (e.ctrlKey) {
            e.preventDefault();
            handleCopy();
          }
          break;
        case "v":
        case "V":
          // 🆕 Ctrl+V: 붙여넣기 (편집 중인 경우만)
          if (e.ctrlKey && editingCell) {
            // 기본 동작 허용 (input에서 처리)
          }
          break;
        case "a":
        case "A":
          // 🆕 Ctrl+A: 전체 선택
          if (e.ctrlKey) {
            e.preventDefault();
            handleSelectAllRows();
          }
          break;
        case "f":
        case "F":
          // 🆕 Ctrl+F: 통합 검색 패널 열기
          if (e.ctrlKey) {
            e.preventDefault();
            setIsSearchPanelOpen(true);
          }
          break;
        case "F3":
          // 🆕 F3: 다음 검색 결과 / Shift+F3: 이전 검색 결과
          e.preventDefault();
          if (e.shiftKey) {
            goToPrevSearchResult();
          } else {
            goToNextSearchResult();
          }
          break;
        case "Home":
          e.preventDefault();
          if (e.ctrlKey) {
            // Ctrl+Home: 첫 번째 셀로
            setFocusedCell({ rowIndex: 0, colIndex: 0 });
          } else {
            // Home: 현재 행의 첫 번째 셀로
            setFocusedCell({ rowIndex, colIndex: 0 });
          }
          break;
        case "End":
          e.preventDefault();
          if (e.ctrlKey) {
            // Ctrl+End: 마지막 셀로
            setFocusedCell({ rowIndex: maxRowIndex, colIndex: maxColIndex });
          } else {
            // End: 현재 행의 마지막 셀로
            setFocusedCell({ rowIndex, colIndex: maxColIndex });
          }
          break;
        case "PageUp":
          e.preventDefault();
          // 10행 위로
          setFocusedCell({ rowIndex: Math.max(0, rowIndex - 10), colIndex });
          break;
        case "PageDown":
          e.preventDefault();
          // 10행 아래로
          setFocusedCell({ rowIndex: Math.min(maxRowIndex, rowIndex + 10), colIndex });
          break;
        case "Escape":
          e.preventDefault();
          // 포커스 해제
          setFocusedCell(null);
          break;
        case "Tab":
          e.preventDefault();
          if (e.shiftKey) {
            // Shift+Tab: 이전 셀
            if (colIndex > 0) {
              setFocusedCell({ rowIndex, colIndex: colIndex - 1 });
            } else if (rowIndex > 0) {
              setFocusedCell({ rowIndex: rowIndex - 1, colIndex: maxColIndex });
            }
          } else {
            // Tab: 다음 셀
            if (colIndex < maxColIndex) {
              setFocusedCell({ rowIndex, colIndex: colIndex + 1 });
            } else if (rowIndex < maxRowIndex) {
              setFocusedCell({ rowIndex: rowIndex + 1, colIndex: 0 });
            }
          }
          break;
        default:
          // 🆕 직접 타이핑으로 편집 모드 진입 (영문자, 숫자, 한글 등)
          if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) {
            const column = visibleColumns[colIndex];
            if (column && column.columnName !== "__checkbox__") {
              // 🆕 편집 불가 컬럼 체크
              if (column.editable === false) {
                toast.warning(`'${column.displayName || column.columnName}' 컬럼은 편집할 수 없습니다.`);
                break;
              }
              e.preventDefault();
              // 편집 시작 (현재 키를 초기값으로)
              const row = data[rowIndex];
              const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName;
              const value = row?.[mappedColumnName];

              setEditingCell({
                rowIndex,
                colIndex,
                columnName: column.columnName,
                originalValue: value,
              });
              setEditingValue(e.key); // 입력한 키로 시작
            }
          }
          break;
      }
    },
    [editingCell, focusedCell, data, visibleColumns, joinColumnMapping, selectedRows, getRowKey, handleRowSelection],
  );

  const getColumnWidth = (column: ColumnConfig) => {
    if (column.columnName === "__checkbox__") return 50;
    if (column.width) return column.width;

    switch (column.format) {
      case "date":
        return 120;
      case "number":
      case "currency":
        return 100;
      case "boolean":
        return 80;
      default:
        return 150;
    }
  };

  const renderCheckboxHeader = () => {
    if (!tableConfig.checkbox?.selectAll) return null;
    if (tableConfig.checkbox?.multiple === false) return null;

    return (
      <Checkbox
        checked={isAllSelected}
        onCheckedChange={handleSelectAll}
        aria-label="전체 선택"
        style={{
          width: 16,
          height: 16,
          borderWidth: 1.5,
          borderColor: isAllSelected ? "hsl(var(--primary))" : "hsl(var(--muted-foreground) / 0.5)",
        }}
      />
    );
  };

  const renderCheckboxCell = (row: any, index: number) => {
    const rowKey = getRowKey(row, index);
    const isChecked = selectedRows.has(rowKey);

    return (
      <Checkbox
        checked={isChecked}
        onCheckedChange={(checked) => handleRowSelection(rowKey, checked as boolean)}
        aria-label={`행 ${index + 1} 선택`}
        style={{
          width: 16,
          height: 16,
          borderWidth: 1.5,
          borderColor: isChecked ? "hsl(var(--primary))" : "hsl(var(--muted-foreground) / 0.5)",
        }}
      />
    );
  };

  const formatCellValue = useCallback(
    (value: any, column: ColumnConfig, rowData?: Record<string, any>) => {
      // 🎯 엔티티 컬럼 표시 설정이 있는 경우 - value가 null이어도 rowData에서 조합 가능
      // 이 체크를 가장 먼저 수행 (null 체크보다 앞에)
      if (column.entityDisplayConfig && rowData) {
        const displayColumns =
          column.entityDisplayConfig.displayColumns || (column.entityDisplayConfig as any).selectedColumns;
        const separator = column.entityDisplayConfig.separator;

        if (displayColumns && displayColumns.length > 0) {
          // 선택된 컬럼들의 값을 구분자로 조합
          const values = displayColumns
            .map((colName: string) => {
              // 🎯 백엔드 alias 규칙: ${sourceColumn}_${displayColumn}
              // 예: manager 컬럼에서 user_name 선택 시 → manager_user_name
              const joinedKey = `${column.columnName}_${colName}`;
              let cellValue = rowData[joinedKey];

              // fallback: 직접 컬럼명으로 시도 (기본 테이블 컬럼인 경우)
              if (cellValue === null || cellValue === undefined) {
                cellValue = rowData[colName];
              }

              if (cellValue === null || cellValue === undefined) return "";
              return String(cellValue);
            })
            .filter((v: string) => v !== ""); // 빈 값 제외

          const result = values.join(separator || " - ");
          if (result) {
            return result; // 결과가 있으면 반환
          }
          // 결과가 비어있으면 아래로 계속 진행 (원래 값 사용)
        }
      }

      // value가 null/undefined면 "-" 반환
      if (value === null || value === undefined) return "-";

      // 🎯 writer 컬럼 자동 변환: user_id -> user_name
      if (column.columnName === "writer" && rowData && rowData.writer_name) {
        return rowData.writer_name;
      }

      // 🆕 메인 테이블 메타 또는 조인 테이블 메타에서 정보 가져오기
      const meta = columnMeta[column.columnName] || joinedColumnMeta[column.columnName];

      // inputType 기반 포맷팅 (columnMeta에서 가져온 inputType 우선)
      const inputType = meta?.inputType || column.inputType;

      // 🖼️ 이미지 타입: 작은 썸네일 표시 (TableCellImage 컴포넌트 사용)
      if (inputType === "image" && value) {
        return <TableCellImage value={String(value)} />;
      }

      // 📎 첨부파일 타입: TableCellFile 컴포넌트로 렌더링 (objid, JSON 배열 모두 지원)
      const isAttachmentColumn =
        inputType === "file" ||
        inputType === "attachment" ||
        column.columnName === "attachments" ||
        column.columnName?.toLowerCase().includes("attachment") ||
        column.columnName?.toLowerCase().includes("file");

      if (isAttachmentColumn && value) {
        return <TableCellFile value={String(value)} />;
      }
      if (isAttachmentColumn && !value) {
        return <span className="text-muted-foreground text-xs">-</span>;
      }

      // 카테고리 타입: 배지로 표시 (배지 없음 옵션 지원, 다중 값 지원)
      if (inputType === "category") {
        if (!value) return "";

        // 🆕 엔티티 조인 컬럼의 경우 여러 형태로 매핑 찾기
        // 1. 원래 컬럼명 (item_info.material)
        // 2. 점(.) 뒤의 컬럼명만 (material)
        let mapping = categoryMappings[column.columnName];

        if (!mapping && column.columnName.includes(".")) {
          const simpleColumnName = column.columnName.split(".").pop();
          if (simpleColumnName) {
            mapping = categoryMappings[simpleColumnName];
          }
        }

        const { Badge } = require("@/components/ui/badge");

        // 다중 값 처리: 콤마로 구분된 값들을 분리
        const valueStr = String(value);
        const values = valueStr.includes(",")
          ? valueStr
              .split(",")
              .map((v) => v.trim())
              .filter((v) => v)
          : [valueStr];

        // 단일 값인 경우 (기존 로직)
        if (values.length === 1) {
          const categoryData = mapping?.[values[0]];
          const displayLabel = categoryData?.label || values[0];
          const displayColor = categoryData?.color;

          // 배지 없음 옵션: color가 없거나, "none"이거나, 매핑 데이터가 없으면 텍스트만 표시
          if (!displayColor || displayColor === "none" || !categoryData) {
            return <span className="text-sm">{displayLabel}</span>;
          }

          return (
            <Badge
              style={{
                backgroundColor: displayColor,
                borderColor: displayColor,
              }}
              className="text-white"
            >
              {displayLabel}
            </Badge>
          );
        }

        // 다중 값인 경우: 여러 배지 렌더링
        return (
          <div className="flex flex-nowrap gap-1 overflow-hidden">
            {values.map((val, idx) => {
              const categoryData = mapping?.[val];
              const displayLabel = categoryData?.label || val;
              const displayColor = categoryData?.color;

              // 배지 없음 옵션: color가 없거나, "none"이거나, 매핑 데이터가 없으면 텍스트만 표시
              if (!displayColor || displayColor === "none" || !categoryData) {
                return (
                  <span key={idx} className="shrink-0 whitespace-nowrap text-sm">
                    {displayLabel}
                    {idx < values.length - 1 && ", "}
                  </span>
                );
              }

              return (
                <Badge
                  key={idx}
                  style={{
                    backgroundColor: displayColor,
                    borderColor: displayColor,
                  }}
                  className="shrink-0 whitespace-nowrap text-white"
                >
                  {displayLabel}
                </Badge>
              );
            })}
          </div>
        );
      }

      // 코드 타입: 코드 값 → 코드명 변환
      if (inputType === "code" && meta?.codeCategory && value) {
        try {
          // optimizedConvertCode(categoryCode, codeValue) 순서 주의!
          const convertedValue = optimizedConvertCode(meta.codeCategory, value);
          // 변환에 성공했으면 변환된 코드명 반환
          if (convertedValue && convertedValue !== value) {
            return convertedValue;
          }
        } catch (error) {
          console.error(`코드 변환 실패: ${column.columnName}, 카테고리: ${meta.codeCategory}, 값: ${value}`, error);
        }
        // 변환 실패 시 원본 코드 값 반환
        return String(value);
      }

      // 날짜 타입 포맷팅 (yyyy-mm-dd)
      if (inputType === "date" || inputType === "datetime") {
        if (value) {
          try {
            const date = new Date(value);
            const year = date.getFullYear();
            const month = String(date.getMonth() + 1).padStart(2, "0");
            const day = String(date.getDate()).padStart(2, "0");
            return `${year}-${month}-${day}`;
          } catch {
            return String(value);
          }
        }
        return "-";
      }

      // 숫자 타입 포맷팅 (공통 formatNumber 사용)
      if (inputType === "number" || inputType === "decimal") {
        if (value !== null && value !== undefined && value !== "") {
          if (column.thousandSeparator !== false) {
            return centralFormatNumber(value);
          }
          const numValue = typeof value === "string" ? parseFloat(value) : value;
          return isNaN(numValue) ? String(value) : String(numValue);
        }
        return String(value);
      }

      switch (column.format) {
        case "number":
          if (value !== null && value !== undefined && value !== "") {
            if (column.thousandSeparator !== false) {
              return centralFormatNumber(value);
            }
            const numValue = typeof value === "string" ? parseFloat(value) : value;
            return isNaN(numValue) ? String(value) : String(numValue);
          }
          return String(value);
        case "date":
          if (value) {
            try {
              const date = new Date(value);
              const year = date.getFullYear();
              const month = String(date.getMonth() + 1).padStart(2, "0");
              const day = String(date.getDate()).padStart(2, "0");
              return `${year}-${month}-${day}`;
            } catch {
              return value;
            }
          }
          return "-";
        case "currency":
          if (value !== null && value !== undefined && value !== "") {
            return centralFormatCurrency(value);
          }
          return value;
        case "boolean":
          return value ? "예" : "아니오";
        default:
          return String(value);
      }
    },
    [columnMeta, joinedColumnMeta, optimizedConvertCode, categoryMappings],
  );

  // ========================================
  // useEffect 훅
  // ========================================

  // 필터 설정 sessionStorage 키 생성 (탭 ID 스코프)
  const filterSettingKey = useMemo(() => {
    if (!tableConfig.selectedTable) return null;
    const base = screenId
      ? `${tableConfig.selectedTable}_screen_${screenId}`
      : tableConfig.selectedTable;
    if (currentTabId) return `filterSettings_${currentTabId}_${base}`;
    return `filterSettings_${base}`;
  }, [tableConfig.selectedTable, screenId, currentTabId]);

  // 그룹 설정 sessionStorage 키 생성 (탭 ID 스코프)
  const groupSettingKey = useMemo(() => {
    if (!tableConfig.selectedTable) return null;
    const base = screenId
      ? `${tableConfig.selectedTable}_screen_${screenId}`
      : tableConfig.selectedTable;
    if (currentTabId) return `groupSettings_${currentTabId}_${base}`;
    return `groupSettings_${base}`;
  }, [tableConfig.selectedTable, screenId, currentTabId]);

  // 저장된 필터 설정 불러오기
  useEffect(() => {
    if (!filterSettingKey || visibleColumns.length === 0) return;

    try {
      const saved = sessionStorage.getItem(filterSettingKey);
      if (saved) {
        const savedFilters = JSON.parse(saved);
        setVisibleFilterColumns(new Set(savedFilters));
      } else {
        // 초기값: 빈 Set (아무것도 선택 안 함)
        setVisibleFilterColumns(new Set());
      }
    } catch (error) {
      console.error("필터 설정 불러오기 실패:", error);
      setVisibleFilterColumns(new Set());
    }
  }, [filterSettingKey, visibleColumns]);

  // 필터 설정 저장
  const saveFilterSettings = useCallback(() => {
    if (!filterSettingKey) return;

    try {
      sessionStorage.setItem(filterSettingKey, JSON.stringify(Array.from(visibleFilterColumns)));
      setIsFilterSettingOpen(false);
      toast.success("검색 필터 설정이 저장되었습니다");

      // 검색 값 초기화
      setSearchValues({});
    } catch (error) {
      console.error("필터 설정 저장 실패:", error);
      showErrorToast("필터 설정 저장에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." });
    }
  }, [filterSettingKey, visibleFilterColumns]);

  // 필터 컬럼 토글
  const toggleFilterVisibility = useCallback((columnName: string) => {
    setVisibleFilterColumns((prev) => {
      const newSet = new Set(prev);
      if (newSet.has(columnName)) {
        newSet.delete(columnName);
      } else {
        newSet.add(columnName);
      }
      return newSet;
    });
  }, []);

  // 전체 선택/해제
  const toggleAllFilters = useCallback(() => {
    const filterableColumns = visibleColumns.filter((col) => col.columnName !== "__checkbox__");
    const columnNames = filterableColumns.map((col) => col.columnName);

    if (visibleFilterColumns.size === columnNames.length) {
      // 전체 해제
      setVisibleFilterColumns(new Set());
    } else {
      // 전체 선택
      setVisibleFilterColumns(new Set(columnNames));
    }
  }, [visibleFilterColumns, visibleColumns]);

  // 표시할 필터 목록 (선택된 컬럼만)
  const activeFilters = useMemo(() => {
    return visibleColumns
      .filter((col) => col.columnName !== "__checkbox__" && visibleFilterColumns.has(col.columnName))
      .map((col) => ({
        columnName: col.columnName,
        label: columnLabels[col.columnName] || col.displayName || col.columnName,
        type: col.format || "text",
      }));
  }, [visibleColumns, visibleFilterColumns, columnLabels]);

  // 그룹 설정 자동 저장 (localStorage)
  useEffect(() => {
    if (!groupSettingKey) return;

    try {
      sessionStorage.setItem(groupSettingKey, JSON.stringify(groupByColumns));
    } catch (error) {
      console.error("그룹 설정 저장 실패:", error);
    }
  }, [groupSettingKey, groupByColumns]);

  // 그룹 컬럼 토글
  const toggleGroupColumn = useCallback((columnName: string) => {
    setGroupByColumns((prev) => {
      if (prev.includes(columnName)) {
        return prev.filter((col) => col !== columnName);
      } else {
        return [...prev, columnName];
      }
    });
  }, []);

  // 사용자 옵션 저장 핸들러
  const handleTableOptionsSave = useCallback(
    (config: {
      columns: Array<{ columnName: string; label: string; visible: boolean; width?: number; frozen?: boolean }>;
      showGridLines: boolean;
      viewMode: "table" | "card" | "grouped-card";
    }) => {
      // 컬럼 순서 업데이트
      const newColumnOrder = config.columns.map((col) => col.columnName);
      setColumnOrder(newColumnOrder);

      // 컬럼 너비 업데이트
      const newWidths: Record<string, number> = {};
      config.columns.forEach((col) => {
        if (col.width) {
          newWidths[col.columnName] = col.width;
        }
      });
      setColumnWidths(newWidths);

      // 틀고정 컬럼 업데이트
      const newFrozenColumns = config.columns.filter((col) => col.frozen).map((col) => col.columnName);
      setFrozenColumns(newFrozenColumns);

      // 그리드선 표시 업데이트
      setShowGridLines(config.showGridLines);

      // 보기 모드 업데이트
      setViewMode(config.viewMode);

      // 컬럼 표시/숨기기 업데이트
      const newDisplayColumns = displayColumns.map((col) => {
        const configCol = config.columns.find((c) => c.columnName === col.columnName);
        if (configCol) {
          return { ...col, visible: configCol.visible };
        }
        return col;
      });
      setDisplayColumns(newDisplayColumns);

      toast.success("테이블 옵션이 저장되었습니다");
    },
    [displayColumns],
  );

  // 그룹 펼치기/접기 토글
  const toggleGroupCollapse = useCallback((groupKey: string) => {
    setCollapsedGroups((prev) => {
      const newSet = new Set(prev);
      if (newSet.has(groupKey)) {
        newSet.delete(groupKey);
      } else {
        newSet.add(groupKey);
      }
      return newSet;
    });
  }, []);

  // 그룹 해제
  const clearGrouping = useCallback(() => {
    setGroupByColumns([]);
    setCollapsedGroups(new Set());
    if (groupSettingKey) {
      sessionStorage.removeItem(groupSettingKey);
    }
    toast.success("그룹이 해제되었습니다");
  }, [groupSettingKey]);

  // 데이터 그룹화
  const groupedData = useMemo((): GroupedData[] => {
    if (groupByColumns.length === 0 || filteredData.length === 0) return [];

    const grouped = new Map<string, any[]>();

    filteredData.forEach((item) => {
      // 그룹 키 생성: "통화:KRW > 단위:EA"
      const keyParts = groupByColumns.map((col) => {
        // 카테고리/엔티티 타입인 경우 _name 필드 사용
        const inputType = columnMeta?.[col]?.inputType;
        let displayValue = item[col];

        if (inputType === "category" || inputType === "entity" || inputType === "code") {
          // _name 필드가 있으면 사용 (예: division_name, writer_name)
          const nameField = `${col}_name`;
          if (item[nameField] !== undefined && item[nameField] !== null) {
            displayValue = item[nameField];
          }
        }

        const label = columnLabels[col] || col;
        return `${label}:${displayValue !== null && displayValue !== undefined ? displayValue : "-"}`;
      });
      const groupKey = keyParts.join(" > ");

      if (!grouped.has(groupKey)) {
        grouped.set(groupKey, []);
      }
      grouped.get(groupKey)!.push(item);
    });

    return Array.from(grouped.entries()).map(([groupKey, items]) => {
      const groupValues: Record<string, any> = {};
      groupByColumns.forEach((col) => {
        groupValues[col] = items[0]?.[col];
      });

      // 🆕 그룹별 소계 계산
      const groupSummary: Record<string, { sum: number; avg: number; count: number }> = {};

      // 숫자형 컬럼에 대해 소계 계산
      (tableConfig.columns || []).forEach((col: { columnName: string }) => {
        if (col.columnName === "__checkbox__") return;

        const colMeta = columnMeta?.[col.columnName];
        const inputType = colMeta?.inputType;
        const isNumeric = inputType === "number" || inputType === "decimal";

        if (isNumeric) {
          const values = items.map((item) => parseFloat(item[col.columnName])).filter((v) => !isNaN(v));

          if (values.length > 0) {
            const sum = values.reduce((a, b) => a + b, 0);
            groupSummary[col.columnName] = {
              sum,
              avg: sum / values.length,
              count: values.length,
            };
          }
        }
      });

      return {
        groupKey,
        groupValues,
        items,
        count: items.length,
        summary: groupSummary, // 🆕 그룹별 소계
      };
    });
  }, [data, groupByColumns, columnLabels, columnMeta, tableConfig.columns]);

  // 🆕 그룹별 합산된 데이터 계산 (FilterPanel에서 설정한 경우)
  const summedData = useMemo(() => {
    // 그룹핑이 비활성화되었거나 그룹 기준 컬럼이 없으면 원본 데이터 반환
    if (!groupSumConfig?.enabled || !groupSumConfig?.groupByColumn) {
      return filteredData;
    }

    const groupByColumn = groupSumConfig.groupByColumn;
    const groupMap = new Map<string, any>();

    // 조인 컬럼인지 확인하고 실제 키 추론
    const getActualKey = (columnName: string, item: any): string => {
      if (columnName.includes(".")) {
        const [refTable, fieldName] = columnName.split(".");
        const inferredSourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id");
        const exactKey = `${inferredSourceColumn}_${fieldName}`;
        if (item[exactKey] !== undefined) return exactKey;
        if (fieldName === "item_name" || fieldName === "name") {
          const aliasKey = `${inferredSourceColumn}_name`;
          if (item[aliasKey] !== undefined) return aliasKey;
        }
      }
      return columnName;
    };

    // 숫자 타입인지 확인하는 함수
    const isNumericValue = (value: any): boolean => {
      if (value === null || value === undefined || value === "") return false;
      const num = parseFloat(String(value));
      return !isNaN(num) && isFinite(num);
    };

    // 그룹핑 수행
    filteredData.forEach((item) => {
      const actualKey = getActualKey(groupByColumn, item);
      const groupValue = String(item[actualKey] || item[groupByColumn] || "");

      if (!groupMap.has(groupValue)) {
        // 첫 번째 항목을 기준으로 초기화
        groupMap.set(groupValue, { ...item, _groupCount: 1 });
      } else {
        const existing = groupMap.get(groupValue);
        existing._groupCount += 1;

        // 모든 키에 대해 숫자면 합산
        Object.keys(item).forEach((key) => {
          const value = item[key];
          if (isNumericValue(value) && key !== groupByColumn && !key.endsWith("_id") && !key.includes("code")) {
            const numValue = parseFloat(String(value));
            const existingValue = parseFloat(String(existing[key] || 0));
            existing[key] = existingValue + numValue;
          }
        });

        groupMap.set(groupValue, existing);
      }
    });

    const result = Array.from(groupMap.values());

    return result;
  }, [filteredData, groupSumConfig]);

  // 🆕 표시할 데이터: 합산 모드면 summedData, 아니면 filteredData
  const displayData = useMemo(() => {
    return groupSumConfig?.enabled ? summedData : filteredData;
  }, [groupSumConfig?.enabled, summedData, filteredData]);

  // 🆕 Virtual Scrolling: 보이는 행 범위 계산 (displayData 정의 이후에 위치)
  const virtualScrollInfo = useMemo(() => {
    const dataSource = displayData;
    if (!isVirtualScrollEnabled || dataSource.length === 0) {
      return {
        startIndex: 0,
        endIndex: dataSource.length,
        visibleData: dataSource,
        topSpacerHeight: 0,
        bottomSpacerHeight: 0,
        totalHeight: dataSource.length * ROW_HEIGHT,
      };
    }

    const containerHeight = scrollContainerRef.current?.clientHeight || 600;
    const totalRows = dataSource.length;
    const totalHeight = totalRows * ROW_HEIGHT;

    // 현재 보이는 행 범위 계산
    const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN);
    const visibleRowCount = Math.ceil(containerHeight / ROW_HEIGHT) + OVERSCAN * 2;
    const endIndex = Math.min(totalRows, startIndex + visibleRowCount);

    return {
      startIndex,
      endIndex,
      visibleData: dataSource.slice(startIndex, endIndex),
      topSpacerHeight: startIndex * ROW_HEIGHT,
      bottomSpacerHeight: (totalRows - endIndex) * ROW_HEIGHT,
      totalHeight,
    };
  }, [isVirtualScrollEnabled, displayData, scrollTop, ROW_HEIGHT, OVERSCAN]);

  // 저장된 그룹 설정 불러오기
  useEffect(() => {
    if (!groupSettingKey || visibleColumns.length === 0) return;

    try {
      const saved = sessionStorage.getItem(groupSettingKey);
      if (saved) {
        const savedGroups = JSON.parse(saved);
        setGroupByColumns(savedGroups);
      }
    } catch (error) {
      console.error("그룹 설정 불러오기 실패:", error);
    }
  }, [groupSettingKey, visibleColumns]);

  useEffect(() => {
    fetchColumnLabels();
    fetchTableLabel();
  }, [tableConfig.selectedTable, fetchColumnLabels, fetchTableLabel]);

  // 🆕 우측 화면일 때만 selectedLeftData 변경에 반응하도록 변수 생성
  const isRightPanel = splitPanelPosition === "right" || currentSplitPosition === "right";
  const selectedLeftDataForRightPanel = isRightPanel ? splitPanelContext?.selectedLeftData : null;

  useEffect(() => {
    // console.log("🔍 [TableList] useEffect 실행 - 데이터 조회 트리거", {
    //   isDesignMode,
    //   tableName: tableConfig.selectedTable,
    //   currentPage,
    //   sortColumn,
    //   sortDirection,
    // });

    if (!isDesignMode && tableConfig.selectedTable) {
      fetchTableDataDebounced();
    }
  }, [
    tableConfig.selectedTable,
    currentPage,
    localPageSize,
    sortColumn,
    sortDirection,
    searchTerm,
    searchValues, // 필터 값 변경 시에도 데이터 새로고침
    refreshKey,
    refreshTrigger, // 강제 새로고침 트리거
    isDesignMode,
    selectedLeftDataForRightPanel, // 🆕 우측 화면일 때만 좌측 데이터 선택 변경 시 데이터 새로고침
    // fetchTableDataDebounced 제거: useCallback 재생성으로 인한 무한 루프 방지
  ]);

  useEffect(() => {
    if (tableConfig.refreshInterval && !isDesignMode) {
      const interval = setInterval(() => {
        fetchTableDataDebounced();
      }, tableConfig.refreshInterval * 1000);

      return () => clearInterval(interval);
    }
  }, [tableConfig.refreshInterval, isDesignMode]);

  // 🆕 전역 테이블 새로고침 이벤트 리스너
  useEffect(() => {
    const handleRefreshTable = () => {
      if (tableConfig.selectedTable && !isDesignMode) {
        setRefreshTrigger((prev) => prev + 1);
      }
    };

    // V2 EventBus 구독 (레거시 어댑터가 window 이벤트도 브릿지)
    const unsubscribe = v2EventBus.subscribe(
      V2_EVENTS.TABLE_REFRESH,
      (payload) => {
        // 특정 테이블만 새로고침하거나 전체 새로고침
        if (!payload.tableName || payload.tableName === tableConfig.selectedTable) {
          handleRefreshTable();
        }
      },
      { componentId: component.id }
    );

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

    return () => {
      unsubscribe();
      window.removeEventListener("refreshTable", handleRefreshTable);
    };
  }, [tableConfig.selectedTable, isDesignMode, component.id]);

  // 테이블명 변경 시 전역 레지스트리에서 확인
  useEffect(() => {
    if (typeof window !== "undefined" && window.__relatedButtonsTargetTables && tableConfig.selectedTable) {
      const isTarget = window.__relatedButtonsTargetTables.has(tableConfig.selectedTable);
      if (isTarget) {
        setIsRelatedButtonTarget(true);
      }
    }
  }, [tableConfig.selectedTable]);

  // RelatedDataButtons 등록/해제 이벤트 리스너
  useEffect(() => {
    const handleRelatedButtonRegister = (event: CustomEvent) => {
      const { targetTable } = event.detail || {};
      if (targetTable === tableConfig.selectedTable) {
        setIsRelatedButtonTarget(true);
      }
    };

    const handleRelatedButtonUnregister = (event: CustomEvent) => {
      const { targetTable } = event.detail || {};
      if (targetTable === tableConfig.selectedTable) {
        setIsRelatedButtonTarget(false);
        setRelatedButtonFilter(null);
      }
    };

    // V2 EventBus 구독
    const unsubscribeRegister = v2EventBus.subscribe(
      V2_EVENTS.RELATED_BUTTON_REGISTER,
      (payload) => {
        if (payload.targetTables.includes(tableConfig.selectedTable || "")) {
          setIsRelatedButtonTarget(true);
        }
      },
      { componentId: component.id }
    );

    const unsubscribeUnregister = v2EventBus.subscribe(
      V2_EVENTS.RELATED_BUTTON_UNREGISTER,
      (payload) => {
        if (payload.buttonId) {
          setIsRelatedButtonTarget(false);
          setRelatedButtonFilter(null);
        }
      },
      { componentId: component.id }
    );

    // 레거시 이벤트도 계속 지원 (점진적 마이그레이션)
    window.addEventListener("related-button-register" as any, handleRelatedButtonRegister);
    window.addEventListener("related-button-unregister" as any, handleRelatedButtonUnregister);

    return () => {
      unsubscribeRegister();
      unsubscribeUnregister();
      window.removeEventListener("related-button-register" as any, handleRelatedButtonRegister);
      window.removeEventListener("related-button-unregister" as any, handleRelatedButtonUnregister);
    };
  }, [tableConfig.selectedTable, component.id]);

  // RelatedDataButtons 선택 이벤트 리스너 (버튼 선택 시 테이블 필터링)
  useEffect(() => {
    const handleRelatedButtonSelect = (event: CustomEvent) => {
      const { targetTable, filterColumn, filterValue } = event.detail || {};

      // 이 테이블이 대상 테이블인지 확인
      if (targetTable === tableConfig.selectedTable) {
        // filterValue가 null이면 선택 해제 (빈 상태)
        if (filterValue === null || filterValue === undefined) {
          setRelatedButtonFilter(null);
          setIsRelatedButtonTarget(true); // 대상으로 등록은 유지
        } else {
          setRelatedButtonFilter({ filterColumn, filterValue });
          setIsRelatedButtonTarget(true);
        }
      }
    };

    // V2 EventBus 구독
    const unsubscribeSelect = v2EventBus.subscribe(
      V2_EVENTS.RELATED_BUTTON_SELECT,
      (payload) => {
        if (payload.tableName === tableConfig.selectedTable) {
          if (!payload.selectedData || payload.selectedData.length === 0) {
            setRelatedButtonFilter(null);
            setIsRelatedButtonTarget(true);
          } else {
            // 첫 번째 선택된 데이터의 ID를 필터로 사용
            const firstItem = payload.selectedData[0];
            if (firstItem?.id) {
              setRelatedButtonFilter({ filterColumn: "id", filterValue: firstItem.id });
            }
            setIsRelatedButtonTarget(true);
          }
        }
      },
      { componentId: component.id }
    );

    // 레거시 이벤트도 계속 지원 (점진적 마이그레이션)
    window.addEventListener("related-button-select" as any, handleRelatedButtonSelect);

    return () => {
      unsubscribeSelect();
      window.removeEventListener("related-button-select" as any, handleRelatedButtonSelect);
    };
  }, [tableConfig.selectedTable, component.id]);

  // 🆕 relatedButtonFilter 변경 시 데이터 다시 로드
  useEffect(() => {
    if (!isDesignMode) {
      // relatedButtonFilter가 있으면 데이터 로드, null이면 빈 상태 (setRefreshTrigger로 트리거)
      // RelatedDataButtons 상태 변경
      setRefreshTrigger((prev) => prev + 1);
    }
  }, [relatedButtonFilter, isDesignMode]);

  // 🎯 컬럼 너비 자동 계산 (내용 기반)
  const calculateOptimalColumnWidth = useCallback(
    (columnName: string, displayName: string): number => {
      // 기본 너비 설정
      const MIN_WIDTH = 100;
      const MAX_WIDTH = 400;
      const PADDING = 48; // 좌우 패딩 + 여유 공간
      const HEADER_PADDING = 60; // 헤더 추가 여유 (정렬 아이콘 등)

      // 헤더 텍스트 너비 계산 (대략 8px per character)
      const headerWidth = (displayName?.length || columnName.length) * 10 + HEADER_PADDING;

      // 데이터 셀 너비 계산 (상위 50개 샘플링)
      const sampleSize = Math.min(50, data.length);
      let maxDataWidth = headerWidth;

      for (let i = 0; i < sampleSize; i++) {
        const cellValue = data[i]?.[columnName];
        if (cellValue !== null && cellValue !== undefined) {
          const cellText = String(cellValue);
          // 숫자는 좁게, 텍스트는 넓게 계산
          const isNumber = !isNaN(Number(cellValue)) && cellValue !== "";
          const charWidth = isNumber ? 8 : 9;
          const cellWidth = cellText.length * charWidth + PADDING;
          maxDataWidth = Math.max(maxDataWidth, cellWidth);
        }
      }

      // 최소/최대 범위 내로 제한
      return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.ceil(maxDataWidth)));
    },
    [data],
  );

  // 🎯 localStorage에서 컬럼 너비 불러오기 및 초기 계산
  useEffect(() => {
    if (!hasInitializedWidths.current && visibleColumns.length > 0 && data.length > 0) {
      const timer = setTimeout(() => {
        const storageKey =
          tableConfig.selectedTable && userId ? `table_column_widths_${tableConfig.selectedTable}_${userId}` : null;

        // 1. localStorage에서 저장된 너비 불러오기
        let savedWidths: Record<string, number> = {};
        if (storageKey) {
          try {
            const saved = localStorage.getItem(storageKey);
            if (saved) {
              savedWidths = JSON.parse(saved);
            }
          } catch (error) {
            console.error("컬럼 너비 불러오기 실패:", error);
          }
        }

        // 2. 자동 계산 또는 저장된 너비 적용
        const newWidths: Record<string, number> = {};
        let hasAnyWidth = false;

        visibleColumns.forEach((column) => {
          // 체크박스 컬럼은 제외 (고정 48px)
          if (column.columnName === "__checkbox__") return;

          // 저장된 너비가 있으면 우선 사용
          if (savedWidths[column.columnName]) {
            newWidths[column.columnName] = savedWidths[column.columnName];
            hasAnyWidth = true;
          } else {
            // 저장된 너비가 없으면 자동 계산
            const optimalWidth = calculateOptimalColumnWidth(
              column.columnName,
              columnLabels[column.columnName] || column.displayName,
            );
            newWidths[column.columnName] = optimalWidth;
            hasAnyWidth = true;
          }
        });

        if (hasAnyWidth) {
          setColumnWidths(newWidths);
          hasInitializedWidths.current = true;
        }
      }, 150); // DOM 렌더링 대기

      return () => clearTimeout(timer);
    }
  }, [visibleColumns, data, tableConfig.selectedTable, userId, calculateOptimalColumnWidth, columnLabels]);

  // ========================================
  // 페이지네이션 JSX
  // ========================================

  const paginationJSX = useMemo(() => {
    if (!tableConfig.pagination?.enabled || isDesignMode) return null;

    // 페이지 크기 변경 핸들러
    const handlePageSizeChange = (newSize: number) => {
      setLocalPageSize(newSize);
      setCurrentPage(1);
      if (pageSizeKey) {
        sessionStorage.setItem(pageSizeKey, String(newSize));
      }
      if (onConfigChange) {
        onConfigChange({
          ...tableConfig,
          pagination: { ...tableConfig.pagination, pageSize: newSize, currentPage: 1 },
        });
      }
    };

    return (
      <div className="border-border bg-background relative flex h-14 w-full flex-shrink-0 items-center justify-center border-t-2 px-4 sm:h-[60px] sm:px-6">
        {/* 좌측: 페이지 크기 입력 */}
        <div className="absolute left-2 flex items-center gap-1 sm:left-6 sm:gap-2">
          <span className="text-muted-foreground hidden text-xs sm:inline">표시:</span>
          <input
            type="number"
            min={1}
            max={10000}
            value={localPageSize}
            onChange={(e) => {
              const value = Math.min(10000, Math.max(1, Number(e.target.value) || 1));
              handlePageSizeChange(value);
            }}
            onBlur={(e) => {
              // 포커스 잃을 때 유효 범위로 조정
              const value = Math.min(10000, Math.max(1, Number(e.target.value) || 10));
              handlePageSizeChange(value);
            }}
            className="border-input bg-background focus:ring-ring h-7 w-14 rounded-md border px-2 text-center text-xs focus:ring-1 focus:outline-none sm:h-8 sm:w-16"
          />
          <span className="text-muted-foreground text-xs">개</span>
        </div>

        {/* 중앙 페이지네이션 컨트롤 */}
        <div className="flex items-center gap-2 sm:gap-4">
          <Button
            variant="outline"
            size="sm"
            onClick={() => handlePageChange(1)}
            disabled={currentPage === 1 || loading}
            className="h-8 w-8 p-0 sm:h-9 sm:w-auto sm:px-3"
          >
            <ChevronsLeft className="h-3 w-3 sm:h-4 sm:w-4" />
          </Button>
          <Button
            variant="outline"
            size="sm"
            onClick={() => handlePageChange(currentPage - 1)}
            disabled={currentPage === 1 || loading}
            className="h-8 w-8 p-0 sm:h-9 sm:w-auto sm:px-3"
          >
            <ChevronLeft className="h-3 w-3 sm:h-4 sm:w-4" />
          </Button>

          <div className="flex items-center gap-1 sm:gap-2">
            <input
              type="text"
              inputMode="numeric"
              value={pageInputValue}
              onChange={(e) => setPageInputValue(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === "Enter") {
                  commitPageInput();
                  (e.target as HTMLInputElement).blur();
                }
              }}
              onBlur={commitPageInput}
              onFocus={(e) => e.target.select()}
              disabled={loading}
              className="border-input bg-background focus:ring-ring h-7 w-10 rounded-md border px-1 text-center text-xs font-medium focus:ring-1 focus:outline-none sm:h-8 sm:w-12 sm:text-sm"
            />
            <span className="text-muted-foreground text-xs sm:text-sm">/</span>
            <span className="text-foreground text-xs font-medium sm:text-sm">
              {totalPages || 1}
            </span>
          </div>

          <Button
            variant="outline"
            size="sm"
            onClick={() => handlePageChange(currentPage + 1)}
            disabled={currentPage >= totalPages || loading}
            className="h-8 w-8 p-0 sm:h-9 sm:w-auto sm:px-3"
          >
            <ChevronRight className="h-3 w-3 sm:h-4 sm:w-4" />
          </Button>
          <Button
            variant="outline"
            size="sm"
            onClick={() => handlePageChange(totalPages)}
            disabled={currentPage >= totalPages || loading}
            className="h-8 w-8 p-0 sm:h-9 sm:w-auto sm:px-3"
          >
            <ChevronsRight className="h-3 w-3 sm:h-4 sm:w-4" />
          </Button>
        </div>

        {/* 우측 버튼 그룹 */}
        <div className="absolute right-2 flex items-center gap-1 sm:right-6">
          {/* 🆕 내보내기 버튼 (Excel/PDF) */}
          <Popover>
            <PopoverTrigger asChild>
              <Button variant="ghost" size="sm" className="h-8 w-8 p-0 sm:h-9 sm:w-auto sm:px-3" title="내보내기">
                <Download className="h-3 w-3" />
              </Button>
            </PopoverTrigger>
            <PopoverContent className="w-56 p-2" align="end">
              <div className="flex flex-col gap-1">
                <div className="text-muted-foreground px-2 py-1 text-xs font-medium">Excel</div>
                <Button variant="ghost" size="sm" className="justify-start text-xs" onClick={() => exportToExcel(true)}>
                  <FileSpreadsheet className="mr-2 h-3 w-3 text-green-600" />
                  전체 Excel 내보내기
                </Button>
                <Button
                  variant="ghost"
                  size="sm"
                  className="justify-start text-xs"
                  onClick={() => exportToExcel(false)}
                  disabled={selectedRows.size === 0}
                >
                  <FileSpreadsheet className="mr-2 h-3 w-3 text-green-600" />
                  선택 항목만 ({selectedRows.size}개)
                </Button>
                <div className="border-border my-1 border-t" />
                <div className="text-muted-foreground px-2 py-1 text-xs font-medium">PDF/인쇄</div>
                <Button variant="ghost" size="sm" className="justify-start text-xs" onClick={() => exportToPdf(true)}>
                  <FileText className="mr-2 h-3 w-3 text-red-600" />
                  전체 PDF 내보내기
                </Button>
                <Button
                  variant="ghost"
                  size="sm"
                  className="justify-start text-xs"
                  onClick={() => exportToPdf(false)}
                  disabled={selectedRows.size === 0}
                >
                  <FileText className="mr-2 h-3 w-3 text-red-600" />
                  선택 항목만 ({selectedRows.size}개)
                </Button>
              </div>
            </PopoverContent>
          </Popover>

          {/* 새로고침 버튼 (하단 페이지네이션) */}
          {(tableConfig.toolbar?.showPaginationRefresh ?? true) && (
            <Button
              variant="ghost"
              size="sm"
              onClick={handleRefresh}
              disabled={loading}
              className="h-8 w-8 p-0 sm:h-9 sm:w-auto sm:px-3"
            >
              <RefreshCw className={cn("h-3 w-3", loading && "animate-spin")} />
            </Button>
          )}
        </div>
      </div>
    );
  }, [
    tableConfig.pagination,
    tableConfig.toolbar?.showPaginationRefresh,
    isDesignMode,
    currentPage,
    totalPages,
    totalItems,
    loading,
    selectedRows.size,
    exportToExcel,
    exportToPdf,
    localPageSize,
    onConfigChange,
    tableConfig,
    pageInputValue,
  ]);

  // ========================================
  // 렌더링
  // ========================================

  const domProps = {
    onClick: handleClick,
    onDragStart: isDesignMode ? onDragStart : undefined,
    onDragEnd: isDesignMode ? onDragEnd : undefined,
    draggable: isDesignMode,
    className: cn("w-full h-full", className, isDesignMode && "cursor-move"), // customer-item-mapping과 동일
    style: componentStyle,
  };

  // 카드 모드
  if (tableConfig.displayMode === "card" && !isDesignMode) {
    return (
      <div {...domProps}>
        {loading ? (
          <div className="flex h-40 items-center justify-center">
            <span className="text-muted-foreground text-sm">로딩 중...</span>
          </div>
        ) : error ? (
          <div className="flex h-40 items-center justify-center">
            <span className="text-destructive text-sm">{error}</span>
          </div>
        ) : (
          <CardModeRenderer
            data={data}
            cardConfig={
              tableConfig.cardConfig || {
                idColumn: visibleColumns[0]?.columnName || "id",
                titleColumn: visibleColumns[0]?.columnName || "",
                cardsPerRow: 3,
                cardSpacing: 16,
                showActions: false,
              }
            }
            visibleColumns={visibleColumns}
            onRowClick={handleRowClick}
            selectedRows={Array.from(selectedRows)}
          />
        )}
        {paginationJSX}
      </div>
    );
  }

  // SingleTableWithSticky 모드
  if (tableConfig.stickyHeader && !isDesignMode) {
    return (
      <div {...domProps}>
        {/* 필터 헤더는 TableSearchWidget으로 이동 */}

        {/* 그룹 표시 배지 */}
        {groupByColumns.length > 0 && (
          <div className="border-border bg-muted/30 border-b px-4 py-1.5 sm:px-6">
            <div className="flex items-center gap-2 text-xs sm:text-sm">
              <span className="text-muted-foreground">그룹:</span>
              <div className="flex flex-wrap items-center gap-2">
                {groupByColumns.map((col, idx) => (
                  <span key={col} className="flex items-center">
                    {idx > 0 && <span className="text-muted-foreground mx-1">→</span>}
                    <span className="bg-primary/10 text-primary rounded px-2 py-1 text-xs font-medium">
                      {columnLabels[col] || col}
                    </span>
                  </span>
                ))}
              </div>
              <button
                onClick={clearGrouping}
                className="hover:bg-destructive/10 text-destructive ml-auto rounded p-1"
                title="그룹 해제"
              >
                <X className="h-4 w-4" />
              </button>
            </div>
          </div>
        )}

        <div style={{ flex: 1, overflow: "hidden" }}>
          <SingleTableWithSticky
            data={data}
            columns={visibleColumns}
            loading={loading}
            error={error}
            sortColumn={sortColumn}
            sortDirection={sortDirection}
            onSort={handleSort}
            tableConfig={tableConfig}
            isDesignMode={isDesignMode}
            isAllSelected={isAllSelected}
            handleSelectAll={handleSelectAll}
            handleRowClick={handleRowClick}
            columnLabels={columnLabels}
            renderCheckboxHeader={renderCheckboxHeader}
            renderCheckboxCell={renderCheckboxCell}
            formatCellValue={(value: any, format?: string, columnName?: string, rowData?: Record<string, any>) => {
              const column = visibleColumns.find((c) => c.columnName === columnName);
              return column ? formatCellValue(value, column, rowData) : String(value);
            }}
            getColumnWidth={getColumnWidth}
            containerWidth={calculatedWidth}
            onCellDoubleClick={handleCellDoubleClick}
            editingCell={editingCell}
            editingValue={editingValue}
            onEditingValueChange={setEditingValue}
            onEditKeyDown={handleEditKeyDown}
            onEditSave={saveEditing}
            editInputRef={editInputRef}
            columnMeta={columnMeta}
            categoryMappings={categoryMappings}
          />
        </div>

        {paginationJSX}
      </div>
    );
  }

  // 일반 테이블 모드 (네이티브 HTML 테이블)
  return (
    <>
      <div {...domProps}>
        {/* 필터 헤더는 TableSearchWidget으로 이동 */}

        {/* 🆕 DevExpress 스타일 기능 툴바 */}
        <div className="border-border bg-muted/20 flex flex-wrap items-center gap-1 border-b px-2 py-1.5 sm:gap-2 sm:px-4 sm:py-2">
          {/* 편집 모드 토글 */}
          {(tableConfig.toolbar?.showEditMode ?? false) && (
            <div className="border-border flex items-center gap-1 border-r pr-2">
              <Button
                variant={editMode === "batch" ? "default" : "ghost"}
                size="sm"
                onClick={() => setEditMode(editMode === "batch" ? "immediate" : "batch")}
                className="h-7 text-xs"
                title="배치 편집 모드 (Ctrl+B)"
              >
                <Edit className="mr-1 h-3 w-3" />
                {editMode === "batch" ? "배치 모드" : "즉시 저장"}
              </Button>
            </div>
          )}

          {/* 내보내기 버튼들 */}
          {((tableConfig.toolbar?.showExcel ?? false) || (tableConfig.toolbar?.showPdf ?? false)) && (
            <div className="border-border flex items-center gap-1 border-r pr-2">
              {(tableConfig.toolbar?.showExcel ?? false) && (
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={() => exportToExcel(true)}
                  className="h-7 text-xs"
                  title="Excel 내보내기"
                >
                  <FileSpreadsheet className="mr-1 h-3 w-3 text-green-600" />
                  Excel
                </Button>
              )}
              {(tableConfig.toolbar?.showPdf ?? false) && (
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={() => exportToPdf(true)}
                  className="h-7 text-xs"
                  title="PDF 내보내기"
                >
                  <FileText className="mr-1 h-3 w-3 text-red-600" />
                  PDF
                </Button>
              )}
            </div>
          )}

          {/* 복사 버튼 */}
          {(tableConfig.toolbar?.showCopy ?? false) && (
            <div className="border-border flex items-center gap-1 border-r pr-2">
              <Button
                variant="ghost"
                size="sm"
                onClick={handleCopy}
                disabled={selectedRows.size === 0 && !focusedCell}
                className="h-7 text-xs"
                title="복사 (Ctrl+C)"
              >
                <Copy className="mr-1 h-3 w-3" />
                복사
              </Button>
            </div>
          )}

          {/* 선택 정보 - 숨김 처리 */}

          {/* 🆕 통합 검색 패널 */}
          {(tableConfig.toolbar?.showSearch ?? false) && (
            <div className="border-border flex items-center gap-1 border-r pr-2">
              {isSearchPanelOpen ? (
                <div className="flex items-center gap-1">
                  <input
                    type="text"
                    value={globalSearchTerm}
                    onChange={(e) => setGlobalSearchTerm(e.target.value)}
                    onKeyDown={(e) => {
                      if (e.key === "Enter") {
                        executeGlobalSearch(globalSearchTerm);
                      } else if (e.key === "Escape") {
                        clearGlobalSearch();
                      } else if (e.key === "F3" || (e.key === "g" && (e.ctrlKey || e.metaKey))) {
                        e.preventDefault();
                        if (e.shiftKey) {
                          goToPrevSearchResult();
                        } else {
                          goToNextSearchResult();
                        }
                      }
                    }}
                    placeholder="검색어 입력... (Enter)"
                    className="border-input bg-background focus:ring-primary h-7 w-32 rounded border px-2 text-xs focus:ring-1 focus:outline-none sm:w-48"
                    autoFocus
                  />
                  {searchHighlights.size > 0 && (
                    <span className="text-muted-foreground text-xs">{searchHighlights.size}개</span>
                  )}
                  <Button
                    variant="ghost"
                    size="sm"
                    onClick={goToPrevSearchResult}
                    disabled={searchHighlights.size === 0}
                    className="h-6 w-6 p-0"
                    title="이전 (Shift+F3)"
                  >
                    <ChevronLeft className="h-3 w-3" />
                  </Button>
                  <Button
                    variant="ghost"
                    size="sm"
                    onClick={goToNextSearchResult}
                    disabled={searchHighlights.size === 0}
                    className="h-6 w-6 p-0"
                    title="다음 (F3)"
                  >
                    <ChevronRight className="h-3 w-3" />
                  </Button>
                  <Button
                    variant="ghost"
                    size="sm"
                    onClick={clearGlobalSearch}
                    className="h-6 w-6 p-0"
                    title="닫기 (Esc)"
                  >
                    <X className="h-3 w-3" />
                  </Button>
                </div>
              ) : (
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={() => setIsSearchPanelOpen(true)}
                  className="h-7 text-xs"
                  title="통합 검색 (Ctrl+F)"
                >
                  <Filter className="mr-1 h-3 w-3" />
                  검색
                </Button>
              )}
            </div>
          )}

          {/* 🆕 Filter Builder (고급 필터) 버튼 */}
          {(tableConfig.toolbar?.showFilter ?? false) && (
            <div className="border-border flex items-center gap-1 border-r pr-2">
              <Button
                variant={activeFilterCount > 0 ? "default" : "ghost"}
                size="sm"
                onClick={() => setIsFilterBuilderOpen(true)}
                className="h-7 text-xs"
                title="고급 필터"
              >
                <Layers className="mr-1 h-3 w-3" />
                필터
                {activeFilterCount > 0 && (
                  <span className="ml-1 rounded-full bg-white/20 px-1.5 text-[10px]">{activeFilterCount}</span>
                )}
              </Button>
              {activeFilterCount > 0 && (
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={clearFilterBuilder}
                  className="h-6 w-6 p-0"
                  title="필터 초기화"
                >
                  <X className="h-3 w-3" />
                </Button>
              )}
            </div>
          )}

          {/* 새로고침 (상단) */}
          {(tableConfig.toolbar?.showRefresh ?? false) && (
            <div className="ml-auto flex items-center gap-1">
              <Button
                variant="ghost"
                size="sm"
                onClick={handleRefresh}
                disabled={loading}
                className="h-7 text-xs"
                title="새로고침"
              >
                <RefreshCw className={cn("mr-1 h-3 w-3", loading && "animate-spin")} />
                새로고침
              </Button>
            </div>
          )}
        </div>

        {/* 필터 칩 바 */}
        {filterGroups.length > 0 && filterGroups.some(g => g.conditions.some(c => c.column && c.value)) && (
          <div className="border-border bg-muted/30 flex items-center gap-2 border-b px-4 py-1.5">
            {filterGroups.flatMap(group =>
              group.conditions
                .filter(c => c.column && c.value)
                .map(condition => {
                  const label = columnLabels[condition.column] || condition.column;
                  const opLabel = condition.operator === "equals" ? "=" : condition.operator === "contains" ? "⊃" : condition.operator === "notEquals" ? "≠" : condition.operator === "startsWith" ? "^" : condition.operator === "endsWith" ? "$" : condition.operator === "greaterThan" ? ">" : condition.operator === "lessThan" ? "<" : condition.operator;
                  return (
                    <span
                      key={condition.id}
                      className="border-border bg-background text-muted-foreground inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-[10px] font-semibold"
                    >
                      {label} {opLabel} {condition.value}
                      <button
                        onClick={() => removeFilterCondition(group.id, condition.id)}
                        className="hover:text-destructive ml-0.5 leading-none transition-colors"
                      >
                        <X className="h-3 w-3" />
                      </button>
                    </span>
                  );
                })
            )}
            <button
              onClick={clearFilterBuilder}
              className="text-muted-foreground hover:text-foreground ml-auto text-[9px] font-semibold transition-colors"
            >
              전체 초기화
            </button>
          </div>
        )}

        {/* 배치 편집 툴바 */}
        {(editMode === "batch" || pendingChanges.size > 0) && (
          <div className="border-border flex items-center justify-between border-b bg-amber-50 px-4 py-2 sm:px-6 dark:bg-amber-950/30">
            <div className="flex items-center gap-3 text-xs sm:text-sm">
              <span className="rounded bg-amber-100 px-2 py-1 font-medium text-amber-700 dark:bg-amber-900/50 dark:text-amber-300">
                배치 편집 모드
              </span>
              {pendingChanges.size > 0 && (
                <span className="text-amber-600 dark:text-amber-400">{pendingChanges.size}개 변경사항</span>
              )}
            </div>
            <div className="flex items-center gap-2">
              <Button
                variant="outline"
                size="sm"
                onClick={cancelBatchChanges}
                disabled={pendingChanges.size === 0}
                className="h-7 text-xs"
              >
                취소
              </Button>
              <Button
                variant="default"
                size="sm"
                onClick={saveBatchChanges}
                disabled={pendingChanges.size === 0}
                className="h-7 text-xs"
              >
                저장 ({pendingChanges.size})
              </Button>
            </div>
          </div>
        )}

        {/* 그룹 표시 배지 */}
        {groupByColumns.length > 0 && (
          <div className="border-border bg-muted/30 border-b px-4 py-2 sm:px-6">
            <div className="flex items-center gap-2 text-xs sm:text-sm">
              <span className="text-muted-foreground">그룹:</span>
              <div className="flex flex-wrap items-center gap-2">
                {groupByColumns.map((col, idx) => (
                  <span key={col} className="flex items-center">
                    {idx > 0 && <span className="text-muted-foreground mx-1">→</span>}
                    <span className="bg-primary/10 text-primary rounded px-2 py-1 text-xs font-medium">
                      {columnLabels[col] || col}
                    </span>
                  </span>
                ))}
              </div>
              <button
                onClick={clearGrouping}
                className="hover:bg-destructive/10 text-destructive ml-auto rounded p-1"
                title="그룹 해제"
              >
                <X className="h-4 w-4" />
              </button>
            </div>
          </div>
        )}

        {/* 테이블 컨테이너 - 키보드 네비게이션 지원 */}
        <div
          ref={tableContainerRef}
          className="flex flex-1 flex-col focus:outline-none"
          style={{
            width: "100%",
            height: "100%",
            overflow: "hidden",
          }}
          tabIndex={0}
          onKeyDown={handleTableKeyDown}
          role="grid"
          aria-label="데이터 테이블"
        >
          {/* 스크롤 영역 */}
          <div
            ref={scrollContainerRef}
            className="bg-background flex-1"
            style={{
              position: "relative",
              width: "100%",
              height: "100%",
              overflow: "auto",
            }}
            onScroll={handleVirtualScroll}
          >
            {/* 테이블 */}
            <table
              className={cn("table-mobile-fixed", !showGridLines && "hide-grid")}
              style={{
                borderCollapse: "collapse",
                width: "100%",
                tableLayout: "fixed",
              }}
            >
              {/* 헤더 (sticky) */}
              <thead
                className="sticky z-50"
                style={{
                  position: "sticky",
                  top: 0,
                  zIndex: 50,
                  backgroundColor: "hsl(var(--background))",
                }}
              >
                {/* 🆕 Multi-Level Headers (Column Bands) */}
                {columnBandsInfo?.hasBands && (
                  <tr
                    className="border-border/60 bg-muted/40 h-8 border-b sm:h-10"
                    style={{ backgroundColor: "hsl(var(--muted) / 0.4)" }}
                  >
                    {visibleColumns.map((column, colIdx) => {
                      // 이 컬럼이 속한 band 찾기
                      const band = columnBandsInfo.bands.find(
                        (b) => b.columns.includes(column.columnName) && b.startIndex === colIdx,
                      );

                      // band의 첫 번째 컬럼인 경우에만 렌더링
                      if (band) {
                        return (
                          <th
                            key={`band-${column.columnName}`}
                            colSpan={band.colSpan}
                            className="border-primary/10 border-r px-2 py-1 text-center text-xs font-semibold sm:px-4 sm:text-sm"
                          >
                            {band.caption}
                          </th>
                        );
                      }

                      // band에 속하지 않은 컬럼 (개별 표시)
                      const isInAnyBand = columnBandsInfo.bands.some((b) => b.columns.includes(column.columnName));
                      if (!isInAnyBand) {
                        return (
                          <th
                            key={`noband-${column.columnName}`}
                            rowSpan={2}
                            className="border-primary/10 border-r px-2 py-1 text-center text-xs font-semibold sm:px-4 sm:text-sm"
                          >
                            {columnLabels[column.columnName] || column.columnName}
                          </th>
                        );
                      }

                      // band의 중간 컬럼은 렌더링하지 않음
                      return null;
                    })}
                  </tr>
                )}
                <tr
                  className="bg-muted/80 h-10 border-b border-border/60 sm:h-12"
                  style={{
                    backgroundColor: "hsl(var(--muted) / 0.4)",
                  }}
                >
                  {visibleColumns.map((column, columnIndex) => {
                    const columnWidth = columnWidths[column.columnName];
                    const isFrozen = frozenColumns.includes(column.columnName);
                    const frozenIndex = frozenColumns.indexOf(column.columnName);

                    // 틀고정된 컬럼의 left 위치 계산
                    let leftPosition = 0;
                    if (isFrozen && frozenIndex > 0) {
                      for (let i = 0; i < frozenIndex; i++) {
                        const frozenCol = frozenColumns[i];
                        // 체크박스 컬럼은 48px 고정
                        const frozenColWidth = frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150;
                        leftPosition += frozenColWidth;
                      }
                    }

                    // 🆕 Column Reordering 상태
                    const isColumnDragging = draggedColumnIndex === columnIndex;
                    const isColumnDropTarget = dropTargetColumnIndex === columnIndex;

                    return (
                      <th
                        key={column.columnName}
                        ref={(el) => (columnRefs.current[column.columnName] = el)}
                        className={cn(
                          "group text-muted-foreground relative h-8 overflow-hidden text-[10px] font-bold uppercase tracking-[0.04em] text-ellipsis whitespace-nowrap select-none sm:h-10 sm:text-xs",
                          column.columnName === "__checkbox__" ? "px-0 py-1" : "px-3 py-2",
                          column.sortable !== false &&
                            column.columnName !== "__checkbox__" &&
                            "hover:text-foreground hover:bg-muted/50 cursor-pointer transition-colors",
                          sortColumn === column.columnName && "!text-primary",
                          isFrozen && "sticky z-40 shadow-[2px_0_4px_rgba(0,0,0,0.1)]",
                          // 🆕 Column Reordering 스타일
                          isColumnDragEnabled &&
                            column.columnName !== "__checkbox__" &&
                            "cursor-grab active:cursor-grabbing",
                          isColumnDragging && "bg-primary/20 opacity-50",
                          isColumnDropTarget && "border-l-primary border-l-4",
                        )}
                        style={{
                          textAlign: column.columnName === "__checkbox__" ? "center" : "center",
                          width:
                            column.columnName === "__checkbox__"
                              ? "48px"
                              : columnWidth
                                ? `${columnWidth}px`
                                : undefined,
                          minWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
                          maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
                          userSelect: "none",
                          backgroundColor: "hsl(var(--muted) / 0.4)",
                          ...(isFrozen && { left: `${leftPosition}px` }),
                        }}
                        // 🆕 Column Reordering 이벤트
                        draggable={isColumnDragEnabled && column.columnName !== "__checkbox__"}
                        onDragStart={(e) => handleColumnDragStart(e, columnIndex)}
                        onDragOver={(e) => handleColumnDragOver(e, columnIndex)}
                        onDragEnd={handleColumnDragEnd}
                        onDrop={(e) => handleColumnDrop(e, columnIndex)}
                        onClick={() => {
                          if (isResizing.current) return;
                          if (column.sortable !== false && column.columnName !== "__checkbox__") {
                            handleSort(column.columnName);
                          }
                        }}
                      >
                        {column.columnName === "__checkbox__" ? (
                          renderCheckboxHeader()
                        ) : (
                          <div style={{ display: "flex", alignItems: "center", gap: "4px", justifyContent: "center" }}>
                            {isColumnDragEnabled && (
                              <GripVertical className="absolute left-0.5 top-1/2 h-3 w-3 -translate-y-1/2 opacity-0 transition-opacity group-hover:opacity-40" />
                            )}
                            <span>{columnLabels[column.columnName] || column.displayName}</span>
                            {column.sortable !== false && sortColumn === column.columnName && (
                              <span className="text-primary">{sortDirection === "asc" ? "↑" : "↓"}</span>
                            )}
                            {/* 🆕 헤더 필터 버튼 */}
                            {tableConfig.headerFilter !== false &&
                              columnUniqueValues[column.columnName]?.length > 0 && (
                                <Popover
                                  open={openFilterColumn === column.columnName}
                                  onOpenChange={(open) => setOpenFilterColumn(open ? column.columnName : null)}
                                >
                                  <PopoverTrigger asChild>
                                    <button
                                      onClick={(e) => {
                                        e.stopPropagation();
                                        setOpenFilterColumn(
                                          openFilterColumn === column.columnName ? null : column.columnName,
                                        );
                                      }}
                                      className={cn(
                                        "hover:bg-primary/20 ml-1 rounded p-0.5 transition-colors",
                                        headerFilters[column.columnName]?.size > 0 && "text-primary bg-primary/10",
                                      )}
                                      title="필터"
                                    >
                                      <Filter className="h-3 w-3" />
                                    </button>
                                  </PopoverTrigger>
                                  <PopoverContent
                                    className="w-48 p-2"
                                    align="start"
                                    onClick={(e) => e.stopPropagation()}
                                  >
                                    <div className="space-y-2">
                                      <div className="flex items-center justify-between border-b pb-2">
                                        <span className="text-xs font-medium">
                                          필터: {columnLabels[column.columnName] || column.displayName}
                                        </span>
                                        {headerFilters[column.columnName]?.size > 0 && (
                                          <button
                                            onClick={() => clearHeaderFilter(column.columnName)}
                                            className="text-destructive text-xs hover:underline"
                                          >
                                            초기화
                                          </button>
                                        )}
                                      </div>
                                      <div className="max-h-48 space-y-1 overflow-y-auto">
                                        {columnUniqueValues[column.columnName]?.slice(0, 50).map((val) => {
                                          const isSelected = headerFilters[column.columnName]?.has(val);
                                          return (
                                            <div
                                              key={val}
                                              className={cn(
                                                "hover:bg-muted flex cursor-pointer items-center gap-2 rounded px-2 py-1 text-xs",
                                                isSelected && "bg-primary/10",
                                              )}
                                              onClick={() => toggleHeaderFilter(column.columnName, val)}
                                            >
                                              <div
                                                className={cn(
                                                  "flex h-4 w-4 items-center justify-center rounded border",
                                                  isSelected ? "bg-primary border-primary" : "border-input",
                                                )}
                                              >
                                                {isSelected && <Check className="text-primary-foreground h-3 w-3" />}
                                              </div>
                                              <span className="truncate">{val || "(빈 값)"}</span>
                                            </div>
                                          );
                                        })}
                                        {(columnUniqueValues[column.columnName]?.length || 0) > 50 && (
                                          <div className="text-muted-foreground px-2 py-1 text-xs">
                                            ...외 {(columnUniqueValues[column.columnName]?.length || 0) - 50}개
                                          </div>
                                        )}
                                      </div>
                                    </div>
                                  </PopoverContent>
                                </Popover>
                              )}
                          </div>
                        )}
                        {/* 리사이즈 핸들 (체크박스 제외) */}
                        {columnIndex < visibleColumns.length - 1 && column.columnName !== "__checkbox__" && (
                          <div
                            className="absolute top-0 right-0 z-20 h-full w-2 cursor-col-resize hover:bg-blue-500"
                            style={{ marginRight: "-4px", paddingLeft: "4px", paddingRight: "4px" }}
                            onClick={(e) => e.stopPropagation()} // 정렬 클릭 방지
                            onMouseDown={(e) => {
                              e.preventDefault();
                              e.stopPropagation();

                              const thElement = columnRefs.current[column.columnName];
                              if (!thElement) return;

                              isResizing.current = true;

                              const startX = e.clientX;
                              const startWidth = columnWidth || thElement.offsetWidth;

                              // 드래그 중 텍스트 선택 방지
                              document.body.style.userSelect = "none";
                              document.body.style.cursor = "col-resize";

                              const handleMouseMove = (moveEvent: MouseEvent) => {
                                moveEvent.preventDefault();

                                const diff = moveEvent.clientX - startX;
                                const newWidth = Math.max(80, startWidth + diff);

                                // 직접 DOM 스타일 변경 (리렌더링 없음)
                                if (thElement) {
                                  thElement.style.width = `${newWidth}px`;
                                }
                              };

                              const handleMouseUp = () => {
                                // 최종 너비를 state에 저장
                                if (thElement) {
                                  const finalWidth = Math.max(80, thElement.offsetWidth);
                                  setColumnWidths((prev) => {
                                    const newWidths = { ...prev, [column.columnName]: finalWidth };

                                    // 🎯 localStorage에 컬럼 너비 저장 (사용자별)
                                    if (tableConfig.selectedTable && userId) {
                                      const storageKey = `table_column_widths_${tableConfig.selectedTable}_${userId}`;
                                      try {
                                        localStorage.setItem(storageKey, JSON.stringify(newWidths));
                                      } catch (error) {
                                        console.error("컬럼 너비 저장 실패:", error);
                                      }
                                    }

                                    return newWidths;
                                  });
                                }

                                // 텍스트 선택 복원
                                document.body.style.userSelect = "";
                                document.body.style.cursor = "";

                                // 약간의 지연 후 리사이즈 플래그 해제 (클릭 이벤트가 먼저 처리되지 않도록)
                                setTimeout(() => {
                                  isResizing.current = false;
                                }, 100);

                                document.removeEventListener("mousemove", handleMouseMove);
                                document.removeEventListener("mouseup", handleMouseUp);
                              };

                              document.addEventListener("mousemove", handleMouseMove);
                              document.addEventListener("mouseup", handleMouseUp);
                            }}
                          />
                        )}
                      </th>
                    );
                  })}
                </tr>
              </thead>

              {/* 바디 (스크롤) */}
              <tbody key={`tbody-${categoryMappingsKey}`} style={{ position: "relative" }}>
                {loading ? (
                  <tr>
                    <td colSpan={visibleColumns.length} className="p-12 text-center">
                      <div className="flex flex-col items-center gap-3">
                        <RefreshCw className="text-muted-foreground h-8 w-8 animate-spin" />
                        <div className="text-muted-foreground text-sm font-medium">로딩 중...</div>
                      </div>
                    </td>
                  </tr>
                ) : error ? (
                  <tr>
                    <td colSpan={visibleColumns.length} className="p-12 text-center">
                      <div className="flex flex-col items-center gap-3">
                        <div className="text-destructive text-sm font-medium">오류 발생</div>
                        <div className="text-muted-foreground text-xs">{error}</div>
                      </div>
                    </td>
                  </tr>
                ) : data.length === 0 ? (
                  <tr>
                    <td colSpan={visibleColumns.length} className="p-12 text-center">
                      <div className="flex flex-col items-center gap-3">
                        <TableIcon className="text-muted-foreground/50 h-12 w-12" />
                        <div className="text-muted-foreground text-sm font-medium">데이터가 없습니다</div>
                        <div className="text-muted-foreground text-xs">
                          조건을 변경하거나 새로운 데이터를 추가해보세요
                        </div>
                      </div>
                    </td>
                  </tr>
                ) : groupByColumns.length > 0 && groupedData.length > 0 ? (
                  // 그룹화된 렌더링
                  groupedData.map((group) => {
                    const isCollapsed = collapsedGroups.has(group.groupKey);
                    return (
                      <React.Fragment key={group.groupKey}>
                        {/* 그룹 헤더 */}
                        <tr>
                          <td
                            colSpan={visibleColumns.length}
                            className="bg-muted/50 border-border sticky top-[48px] z-[5] border-b"
                            style={{ top: "48px" }}
                          >
                            <div
                              className="hover:bg-muted flex cursor-pointer items-center gap-3 p-3"
                              onClick={() => toggleGroupCollapse(group.groupKey)}
                            >
                              {isCollapsed ? (
                                <ChevronRight className="h-4 w-4 flex-shrink-0" />
                              ) : (
                                <ChevronDown className="h-4 w-4 flex-shrink-0" />
                              )}
                              <span className="flex-1 text-sm font-medium">{group.groupKey}</span>
                              <span className="text-muted-foreground text-xs">({group.count}건)</span>
                            </div>
                          </td>
                        </tr>
                        {/* 그룹 데이터 */}
                        {!isCollapsed &&
                          group.items.map((row, index) => (
                            <tr
                              key={index}
                              className={cn(
                                "hover:bg-accent cursor-pointer border-b border-border/50 transition-[background] duration-75",
                                index % 2 === 0 ? "bg-background" : "bg-muted/20",
                              )}
                              onClick={(e) => handleRowClick(row, index, e)}
                            >
                              {visibleColumns.map((column, colIndex) => {
                                const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName;
                                const cellValue = row[mappedColumnName];

                                const meta = columnMeta[column.columnName];
                                const inputType = meta?.inputType || column.inputType;
                                const isNumeric = inputType === "number" || inputType === "decimal";

                                const isFrozen = frozenColumns.includes(column.columnName);
                                const frozenIndex = frozenColumns.indexOf(column.columnName);

                                // 틀고정된 컬럼의 left 위치 계산
                                let leftPosition = 0;
                                if (isFrozen && frozenIndex > 0) {
                                  for (let i = 0; i < frozenIndex; i++) {
                                    const frozenCol = frozenColumns[i];
                                    // 체크박스 컬럼은 48px 고정
                                    const frozenColWidth =
                                      frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150;
                                    leftPosition += frozenColWidth;
                                  }
                                }

                                return (
                                  <td
                                    key={column.columnName}
                                    className={cn(
                                      "text-foreground text-[11px] font-normal",
                                      inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap max-w-[170px]",
                                      column.columnName === "__checkbox__"
                                        ? "px-0 py-[7px] text-center"
                                        : "px-3 py-[7px]",
                                      isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]",
                                      (inputType === "code" || inputType === "category") && "font-mono text-[10px] text-primary font-medium",
                                      isNumeric && "tabular-nums",
                                    )}
                                    style={{
                                      textAlign:
                                        column.columnName === "__checkbox__"
                                          ? "center"
                                          : isNumeric
                                            ? "right"
                                            : column.align || "left",
                                      width:
                                        column.columnName === "__checkbox__"
                                          ? "48px"
                                          : `${100 / visibleColumns.length}%`,
                                      minWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
                                      maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
                                  ...(isFrozen && {
                                    left: `${leftPosition}px`,
                                    backgroundColor: index % 2 === 0
                                      ? "hsl(var(--background))"
                                      : "hsl(var(--muted) / 0.2)",
                                  }),
                                }}
                              >
                                    {column.columnName === "__checkbox__"
                                      ? renderCheckboxCell(row, index)
                                      : formatCellValue(cellValue, column, row)}
                              </td>
                                );
                              })}
                            </tr>
                          ))}
                        {/* 🆕 그룹별 소계 행 */}
                        {!isCollapsed && group.summary && Object.keys(group.summary).length > 0 && (
                          <tr className="bg-muted/30 border-primary/20 border-b-2">
                            {visibleColumns.map((column, colIndex) => {
                              const summary = group.summary?.[column.columnName];
                              const meta = columnMeta[column.columnName];
                              const inputType = meta?.inputType || (column as any).inputType;
                              const isNumeric = inputType === "number" || inputType === "decimal";

                              if (colIndex === 0 && column.columnName === "__checkbox__") {
                                return (
                                  <td key={column.columnName} className="px-2 py-1 sm:px-4">
                                    <span className="text-muted-foreground text-xs font-medium">소계</span>
                                  </td>
                                );
                              }

                              if (colIndex === 0 && column.columnName !== "__checkbox__") {
                                return (
                                  <td key={column.columnName} className="px-2 py-1 sm:px-4">
                                    <span className="text-muted-foreground text-xs font-medium">
                                      소계 ({group.count}건)
                                    </span>
                                  </td>
                                );
                              }

                              if (summary) {
                                return (
                                  <td
                                    key={column.columnName}
                                    className="px-2 py-1 text-xs font-semibold sm:px-4"
                                    style={{ textAlign: isNumeric ? "right" : "left" }}
                                  >
                                    {summary.sum.toLocaleString()}
                                  </td>
                                );
                              }

                              return <td key={column.columnName} className="px-2 py-1 sm:px-4" />;
                            })}
                          </tr>
                        )}
                      </React.Fragment>
                    );
                  })
                ) : (
                  // 일반 렌더링 (그룹 없음) - 키보드 네비게이션 지원
                  <>
                    {/* 🆕 Virtual Scrolling: Top Spacer */}
                    {isVirtualScrollEnabled && virtualScrollInfo.topSpacerHeight > 0 && (
                      <tr style={{ height: `${virtualScrollInfo.topSpacerHeight}px` }}>
                        <td colSpan={visibleColumns.length} />
                      </tr>
                    )}
                    {/* 데이터 행 렌더링 - 🆕 합산 모드면 displayData 사용 */}
                    {(isVirtualScrollEnabled ? virtualScrollInfo.visibleData : displayData).map((row, idx) => {
                      // Virtual Scrolling에서는 실제 인덱스 계산
                      const index = isVirtualScrollEnabled ? virtualScrollInfo.startIndex + idx : idx;
                      const rowKey = getRowKey(row, index);
                      const isRowSelected = selectedRows.has(rowKey);
                      const isRowFocused = focusedCell?.rowIndex === index;

                      // 🆕 Drag & Drop 상태
                      const isDragging = draggedRowIndex === index;
                      const isDropTarget = dropTargetIndex === index;

                      return (
                        <tr
                          key={index}
                          className={cn(
                            "hover:bg-accent cursor-pointer border-b border-border/50 transition-[background] duration-75",
                            index % 2 === 0 ? "bg-background" : "bg-muted/20",
                            isRowSelected && "!bg-primary/10 hover:!bg-primary/15",
                            isRowFocused && "bg-accent/50",
                            isDragEnabled && "cursor-grab active:cursor-grabbing",
                            isDragging && "bg-muted opacity-50",
                            isDropTarget && "border-t-primary border-t-2",
                          )}
                          onClick={(e) => handleRowClick(row, index, e)}
                          role="row"
                          aria-selected={isRowSelected}
                          // 🆕 Drag & Drop 이벤트
                          draggable={isDragEnabled}
                          onDragStart={(e) => handleRowDragStart(e, index)}
                          onDragOver={(e) => handleRowDragOver(e, index)}
                          onDragEnd={handleRowDragEnd}
                          onDrop={(e) => handleRowDrop(e, index)}
                        >
                          {visibleColumns.map((column, colIndex) => {
                            const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName;
                            // 🆕 배치 편집: 로컬 수정 데이터 우선 표시
                            const cellValue =
                              editMode === "batch"
                                ? getDisplayValue(row, index, mappedColumnName)
                                : row[mappedColumnName];

                            const meta = columnMeta[column.columnName];
                            const inputType = meta?.inputType || column.inputType;
                            const isNumeric = inputType === "number" || inputType === "decimal";

                            const isFrozen = frozenColumns.includes(column.columnName);
                            const frozenIndex = frozenColumns.indexOf(column.columnName);

                            // 셀 포커스 상태
                            const isCellFocused = focusedCell?.rowIndex === index && focusedCell?.colIndex === colIndex;

                            // 🆕 배치 편집: 수정된 셀 여부
                            const isModified = isCellModified(index, mappedColumnName);

                            // 🆕 유효성 검사 에러
                            const cellValidationError = getCellValidationError(index, mappedColumnName);

                            // 🆕 검색 하이라이트 여부
                            const isSearchHighlighted = searchHighlights.has(`${index}-${colIndex}`);

                            // 틀고정된 컬럼의 left 위치 계산
                            let leftPosition = 0;
                            if (isFrozen && frozenIndex > 0) {
                              for (let i = 0; i < frozenIndex; i++) {
                                const frozenCol = frozenColumns[i];
                                // 체크박스 컬럼은 48px 고정
                                const frozenColWidth =
                                  frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150;
                                leftPosition += frozenColWidth;
                              }
                            }

                            return (
                              <td
                                key={column.columnName}
                                data-row={index}
                                data-col={colIndex}
                                className={cn(
                                  "text-foreground text-[11px] font-normal",
                                  inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap max-w-[170px]",
                                  column.columnName === "__checkbox__" ? "px-0 py-[7px] text-center" : "px-3 py-[7px]",
                                  isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]",
                                  isCellFocused && !editingCell && "bg-primary/5 shadow-[inset_0_0_0_2px_hsl(var(--primary))]",
                                  editingCell?.rowIndex === index && editingCell?.colIndex === colIndex && "p-0",
                                  isModified && !cellValidationError && "bg-amber-100 dark:bg-amber-900/40",
                                  cellValidationError && "bg-red-50 shadow-[inset_0_0_0_2px_hsl(0_84%_60%)] dark:bg-red-950/40",
                                  isSearchHighlighted && !isCellFocused && "bg-yellow-200 dark:bg-yellow-700/50",
                                  column.editable === false && "bg-muted/10 dark:bg-muted/10",
                                  // 코드 컬럼: mono 폰트 + primary 색상
                                  (inputType === "code" || inputType === "category") && "font-mono text-[10px] text-primary font-medium",
                                  // 숫자 컬럼: tabular-nums 오른쪽 정렬
                                  isNumeric && "tabular-nums",
                                )}
                                // 🆕 유효성 에러 툴팁
                                title={cellValidationError || undefined}
                                style={{
                                  textAlign:
                                    column.columnName === "__checkbox__"
                                      ? "center"
                                      : isNumeric
                                        ? "right"
                                        : column.align || "left",
                                  width:
                                    column.columnName === "__checkbox__" ? "48px" : `${100 / visibleColumns.length}%`,
                                  minWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
                                  maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
                                  ...(isFrozen && {
                                    left: `${leftPosition}px`,
                                    backgroundColor: index % 2 === 0
                                      ? "hsl(var(--background))"
                                      : "hsl(var(--muted) / 0.2)",
                                  }),
                                }}
                                onClick={(e) => handleCellClick(index, colIndex, e)}
                                onDoubleClick={() =>
                                  handleCellDoubleClick(index, colIndex, column.columnName, cellValue)
                                }
                                onContextMenu={(e) => handleContextMenu(e, index, colIndex, row)}
                                role="gridcell"
                                tabIndex={isCellFocused ? 0 : -1}
                              >
                                {/* 🆕 인라인 편집 모드 */}
                                {editingCell?.rowIndex === index && editingCell?.colIndex === colIndex
                                  ? // 🆕 Cascading Lookups: 드롭다운 또는 일반 입력
                                    (() => {
                                      const cascadingConfig = (tableConfig as any).cascadingLookups?.[
                                        column.columnName
                                      ];
                                      const options = cascadingConfig
                                        ? getCascadingOptions(column.columnName, row)
                                        : [];

                                      // 부모 값이 변경되면 옵션 로딩
                                      if (cascadingConfig && options.length === 0) {
                                        const parentValue = row[cascadingConfig.parentColumn];
                                        if (parentValue !== undefined && parentValue !== null) {
                                          loadCascadingOptions(
                                            column.columnName,
                                            cascadingConfig.parentColumn,
                                            parentValue,
                                          );
                                        }
                                      }

                                      // 카테고리/코드 타입이거나 Cascading Lookup인 경우 드롭다운
                                      const colMeta = columnMeta[column.columnName];
                                      const isCategoryType =
                                        colMeta?.inputType === "category" || colMeta?.inputType === "code";
                                      const hasCategoryOptions =
                                        categoryMappings[column.columnName] &&
                                        Object.keys(categoryMappings[column.columnName]).length > 0;

                                      if (cascadingConfig || (isCategoryType && hasCategoryOptions)) {
                                        const selectOptions = cascadingConfig
                                          ? options
                                          : Object.entries(categoryMappings[column.columnName] || {}).map(
                                              ([value, info]) => ({
                                                value,
                                                label: info.label,
                                              }),
                                            );

                                        return (
                                          <select
                                            ref={editInputRef as any}
                                            value={editingValue}
                                            onChange={(e) => setEditingValue(e.target.value)}
                                            onKeyDown={handleEditKeyDown}
                                            onBlur={saveEditing}
                                            className="border-primary bg-background h-8 w-full shrink-0 border-2 px-2 py-1 text-xs focus:outline-none sm:px-4 sm:py-1.5 sm:text-sm"
                                            autoFocus
                                          >
                                            <option value="">선택하세요</option>
                                            {selectOptions.map((opt) => (
                                              <option key={opt.value} value={opt.value}>
                                                {opt.label}
                                              </option>
                                            ))}
                                          </select>
                                        );
                                      }

                                      // 날짜 타입: 캘린더 피커
                                      const isDateType = colMeta?.inputType === "date" || colMeta?.inputType === "datetime";
                                      if (isDateType) {
                                        const { InlineCellDatePicker } = require("@/components/screen/filters/InlineCellDatePicker");
                                        return (
                                          <InlineCellDatePicker
                                            value={editingValue}
                                            onChange={setEditingValue}
                                            onSave={saveEditing}
                                            onKeyDown={handleEditKeyDown}
                                            inputRef={editInputRef}
                                          />
                                        );
                                      }

                                      // 일반 입력 필드 (행 높이 유지: h-8 고정)
                                      return (
                                        <input
                                          ref={editInputRef}
                                          type={isNumeric ? "number" : "text"}
                                          value={editingValue}
                                          onChange={(e) => setEditingValue(e.target.value)}
                                          onKeyDown={handleEditKeyDown}
                                          onBlur={saveEditing}
                                          className="border-primary bg-background h-8 w-full shrink-0 border-2 px-2 py-1 text-xs focus:outline-none sm:px-4 sm:py-1.5 sm:text-sm"
                                          style={{
                                            textAlign: isNumeric ? "right" : column.align || "left",
                                          }}
                                        />
                                      );
                                    })()
                                  : column.columnName === "__checkbox__"
                                    ? renderCheckboxCell(row, index)
                                    : (cellValue === null || cellValue === undefined || cellValue === "")
                                      ? <span className="text-muted-foreground/50">-</span>
                                      : formatCellValue(cellValue, column, row)}
                              </td>
                            );
                          })}
                        </tr>
                      );
                    })}
                    {/* 🆕 Virtual Scrolling: Bottom Spacer */}
                    {isVirtualScrollEnabled && virtualScrollInfo.bottomSpacerHeight > 0 && (
                      <tr style={{ height: `${virtualScrollInfo.bottomSpacerHeight}px` }}>
                        <td colSpan={visibleColumns.length} />
                      </tr>
                    )}
                  </>
                )}
              </tbody>

              {/* 🆕 데이터 요약 (Total Summaries) */}
              {summaryData && Object.keys(summaryData).length > 0 && (
                <tfoot className="bg-muted/80 border-primary/20 sticky bottom-0 z-10 border-t-2">
                  <tr>
                    {visibleColumns.map((column, colIndex) => {
                      const summary = summaryData[column.columnName];
                      const columnWidth = columnWidths[column.columnName];
                      const isFrozen = frozenColumns.includes(column.columnName);
                      const frozenIndex = frozenColumns.indexOf(column.columnName);

                      // 틀고정된 컬럼의 left 위치 계산
                      let leftPosition = 0;
                      if (isFrozen && frozenIndex > 0) {
                        for (let i = 0; i < frozenIndex; i++) {
                          const frozenCol = frozenColumns[i];
                          // 체크박스 컬럼은 48px 고정
                          const frozenColWidth = frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150;
                          leftPosition += frozenColWidth;
                        }
                      }

                      const meta = columnMeta[column.columnName];
                      const inputType = meta?.inputType || (column as any).inputType;
                      const isNumeric = inputType === "number" || inputType === "decimal";

                      return (
                        <td
                          key={column.columnName}
                          className={cn(
                            "text-foreground text-xs font-semibold sm:text-sm",
                            column.columnName === "__checkbox__" ? "px-0 py-2" : "px-2 py-2 sm:px-4",
                            isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]",
                          )}
                          style={{
                            textAlign: isNumeric ? "right" : column.align || "left",
                            width:
                              column.columnName === "__checkbox__"
                                ? "48px"
                                : columnWidth
                                  ? `${columnWidth}px`
                                  : undefined,
                            ...(isFrozen && {
                              left: `${leftPosition}px`,
                              backgroundColor: "hsl(var(--muted) / 0.4)",
                            }),
                          }}
                        >
                          {summary ? (
                            <div className="flex flex-col">
                              <span className="text-muted-foreground text-[10px] sm:text-xs">{summary.label}</span>
                              <span className="text-primary font-bold">
                                {typeof summary.value === "number"
                                  ? summary.value.toLocaleString("ko-KR", {
                                      maximumFractionDigits: 2,
                                    })
                                  : summary.value}
                              </span>
                            </div>
                          ) : colIndex === 0 ? (
                            <span className="text-muted-foreground text-xs">요약</span>
                          ) : null}
                        </td>
                      );
                    })}
                  </tr>
                </tfoot>
              )}
            </table>
          </div>
        </div>

        {/* 페이지네이션 */}
        {paginationJSX}
      </div>

      {/* 필터 설정 다이얼로그 */}
      <Dialog open={isFilterSettingOpen} onOpenChange={setIsFilterSettingOpen}>
        <DialogContent className="max-w-[95vw] sm:max-w-[500px]">
          <DialogHeader>
            <DialogTitle className="text-base sm:text-lg">검색 필터 설정</DialogTitle>
            <DialogDescription className="text-xs sm:text-sm">
              검색 필터로 사용할 컬럼을 선택하세요. 선택한 컬럼의 검색 입력 필드가 표시됩니다.
            </DialogDescription>
          </DialogHeader>

          <div className="space-y-3 sm:space-y-4">
            {/* 전체 선택/해제 */}
            <div className="bg-muted/50 flex items-center gap-3 rounded border p-3">
              <Checkbox
                id="select-all-filters"
                checked={
                  visibleFilterColumns.size ===
                    visibleColumns.filter((col) => col.columnName !== "__checkbox__").length &&
                  visibleColumns.filter((col) => col.columnName !== "__checkbox__").length > 0
                }
                onCheckedChange={toggleAllFilters}
              />
              <Label htmlFor="select-all-filters" className="flex-1 cursor-pointer text-xs font-semibold sm:text-sm">
                전체 선택/해제
              </Label>
              <span className="text-muted-foreground text-xs">
                {visibleFilterColumns.size} / {visibleColumns.filter((col) => col.columnName !== "__checkbox__").length}
                개
              </span>
            </div>

            {/* 컬럼 목록 */}
            <div className="max-h-[50vh] space-y-2 overflow-y-auto rounded border p-2">
              {visibleColumns
                .filter((col) => col.columnName !== "__checkbox__")
                .map((col) => (
                  <div key={col.columnName} className="hover:bg-muted/50 flex items-center gap-3 rounded p-2">
                    <Checkbox
                      id={`filter-${col.columnName}`}
                      checked={visibleFilterColumns.has(col.columnName)}
                      onCheckedChange={() => toggleFilterVisibility(col.columnName)}
                    />
                    <Label
                      htmlFor={`filter-${col.columnName}`}
                      className="flex-1 cursor-pointer text-xs font-normal sm:text-sm"
                    >
                      {columnLabels[col.columnName] || col.displayName || col.columnName}
                    </Label>
                  </div>
                ))}
            </div>

            {/* 선택된 컬럼 개수 안내 */}
            <div className="text-muted-foreground bg-muted/30 rounded p-3 text-center text-xs">
              {visibleFilterColumns.size === 0 ? (
                <span>검색 필터를 사용하려면 최소 1개 이상의 컬럼을 선택하세요</span>
              ) : (
                <span>
                  총 <span className="text-primary font-semibold">{visibleFilterColumns.size}개</span>의 검색 필터가
                  표시됩니다
                </span>
              )}
            </div>
          </div>

          <DialogFooter className="gap-2 sm:gap-0">
            <Button
              variant="outline"
              onClick={() => setIsFilterSettingOpen(false)}
              className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
            >
              취소
            </Button>
            <Button onClick={saveFilterSettings} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
              저장
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      {/* 🆕 Context Menu (우클릭 메뉴) */}
      {contextMenu && (
        <div
          className="bg-popover text-popover-foreground fixed z-[9999] min-w-[160px] rounded-md border shadow-md"
          style={{
            left: contextMenu.x,
            top: contextMenu.y,
          }}
          onClick={(e) => e.stopPropagation()}
        >
          <div className="p-1">
            {/* 셀 복사 */}
            <button
              className="hover:bg-accent hover:text-accent-foreground flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm"
              onClick={() => {
                const col = visibleColumns[contextMenu.colIndex];
                if (col) {
                  const value = contextMenu.row[col.columnName];
                  navigator.clipboard.writeText(String(value ?? ""));
                  toast.success("셀 값이 복사되었습니다");
                }
                closeContextMenu();
              }}
            >
              <Copy className="h-4 w-4" />셀 복사
            </button>

            {/* 행 복사 */}
            <button
              className="hover:bg-accent hover:text-accent-foreground flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm"
              onClick={() => {
                const rowData = visibleColumns.map((col) => String(contextMenu.row[col.columnName] ?? "")).join("\t");
                navigator.clipboard.writeText(rowData);
                toast.success("행 데이터가 복사되었습니다");
                closeContextMenu();
              }}
            >
              <ClipboardCopy className="h-4 w-4" />행 복사
            </button>

            <div className="bg-border my-1 h-px" />

            {/* 셀 편집 */}
            {(() => {
              const col = visibleColumns[contextMenu.colIndex];
              const isEditable = col?.editable !== false && col?.columnName !== "__checkbox__";
              return (
                <button
                  className={`flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm ${
                    isEditable ? "hover:bg-accent hover:text-accent-foreground" : "cursor-not-allowed opacity-50"
                  }`}
                  onClick={() => {
                    if (!isEditable) {
                      toast.warning(`'${col?.displayName || col?.columnName}' 컬럼은 편집할 수 없습니다.`);
                      closeContextMenu();
                      return;
                    }
                    if (col) {
                      setEditingCell({
                        rowIndex: contextMenu.rowIndex,
                        colIndex: contextMenu.colIndex,
                        columnName: col.columnName,
                        originalValue: contextMenu.row[col.columnName],
                      });
                      setEditingValue(String(contextMenu.row[col.columnName] ?? ""));
                    }
                    closeContextMenu();
                  }}
                >
                  <Edit className="h-4 w-4" />셀 편집 {!isEditable && "(잠김)"}
                </button>
              );
            })()}

            {/* 행 선택/해제 */}
            <button
              className="hover:bg-accent hover:text-accent-foreground flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm"
              onClick={() => {
                const rowKey = getRowKey(contextMenu.row, contextMenu.rowIndex);
                setSelectedRows((prev) => {
                  const next = new Set(prev);
                  if (next.has(rowKey)) {
                    next.delete(rowKey);
                  } else {
                    next.add(rowKey);
                  }
                  return next;
                });
                closeContextMenu();
              }}
            >
              <CheckSquare className="h-4 w-4" />
              {selectedRows.has(getRowKey(contextMenu.row, contextMenu.rowIndex)) ? "선택 해제" : "행 선택"}
            </button>

            <div className="bg-border my-1 h-px" />

            {/* 행 삭제 */}
            <button
              className="hover:bg-destructive hover:text-destructive-foreground flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-red-600"
              onClick={async () => {
                if (confirm("이 행을 삭제하시겠습니까?")) {
                  try {
                    const rowId = contextMenu.row.id || contextMenu.row.uuid;
                    if (!rowId) {
                      toast.error("삭제할 행의 ID를 찾을 수 없습니다");
                      closeContextMenu();
                      return;
                    }
                    const tableName = tableConfig.selectedTable;
                    if (!tableName) {
                      toast.error("테이블명을 찾을 수 없습니다");
                      closeContextMenu();
                      return;
                    }
                    await tableTypeApi.deleteTableData(tableName, { ids: [String(rowId)] });
                    toast.success("행이 삭제되었습니다");
                    handleRefresh();
                  } catch (error) {
                    console.error("삭제 오류:", error);
                    toast.error("삭제 중 오류가 발생했습니다");
                  }
                }
                closeContextMenu();
              }}
            >
              <Trash2 className="h-4 w-4" />행 삭제
            </button>
          </div>
        </div>
      )}

      {/* 🆕 Filter Builder 모달 */}
      <Dialog open={isFilterBuilderOpen} onOpenChange={setIsFilterBuilderOpen}>
        <DialogContent className="max-w-[95vw] sm:max-w-[700px]">
          <DialogHeader>
            <DialogTitle className="text-base sm:text-lg">고급 필터</DialogTitle>
            <DialogDescription className="text-xs sm:text-sm">
              여러 조건을 조합하여 데이터를 필터링합니다.
            </DialogDescription>
          </DialogHeader>

          <div className="max-h-[60vh] space-y-4 overflow-y-auto">
            {filterGroups.length === 0 ? (
              <div className="text-muted-foreground py-8 text-center text-sm">
                필터 조건이 없습니다. 아래 버튼을 클릭하여 조건을 추가하세요.
              </div>
            ) : (
              filterGroups.map((group, groupIndex) => (
                <div key={group.id} className="rounded-lg border p-4">
                  <div className="mb-3 flex items-center justify-between">
                    <div className="flex items-center gap-2">
                      <span className="text-muted-foreground text-xs">조건 그룹 {groupIndex + 1}</span>
                      <select
                        value={group.logic}
                        onChange={(e) => updateGroupLogic(group.id, e.target.value as "AND" | "OR")}
                        className="border-input bg-background h-7 rounded border px-2 text-xs"
                      >
                        <option value="AND">AND (모두 만족)</option>
                        <option value="OR">OR (하나라도 만족)</option>
                      </select>
                    </div>
                    <Button
                      variant="ghost"
                      size="sm"
                      onClick={() => removeFilterGroup(group.id)}
                      className="h-6 w-6 p-0 text-red-500 hover:text-red-700"
                    >
                      <X className="h-4 w-4" />
                    </Button>
                  </div>

                  <div className="space-y-2">
                    {group.conditions.map((condition) => (
                      <div key={condition.id} className="flex items-center gap-2">
                        {/* 컬럼 선택 */}
                        <select
                          value={condition.column}
                          onChange={(e) => updateFilterCondition(group.id, condition.id, "column", e.target.value)}
                          className="border-input bg-background h-8 w-32 rounded border px-2 text-xs sm:w-40"
                        >
                          {visibleColumns
                            .filter((c) => c.columnName !== "__checkbox__")
                            .map((col) => (
                              <option key={col.columnName} value={col.columnName}>
                                {columnLabels[col.columnName] || col.displayName || col.columnName}
                              </option>
                            ))}
                        </select>

                        {/* 연산자 선택 */}
                        <select
                          value={condition.operator}
                          onChange={(e) => updateFilterCondition(group.id, condition.id, "operator", e.target.value)}
                          className="border-input bg-background h-8 w-28 rounded border px-2 text-xs sm:w-36"
                        >
                          <option value="contains">포함</option>
                          <option value="notContains">포함하지 않음</option>
                          <option value="equals">같음</option>
                          <option value="notEquals">같지 않음</option>
                          <option value="startsWith">시작</option>
                          <option value="endsWith">끝</option>
                          <option value="greaterThan">보다 큼</option>
                          <option value="lessThan">보다 작음</option>
                          <option value="greaterOrEqual">이상</option>
                          <option value="lessOrEqual">이하</option>
                          <option value="isEmpty">비어있음</option>
                          <option value="isNotEmpty">비어있지 않음</option>
                        </select>

                        {/* 값 입력 (isEmpty/isNotEmpty가 아닌 경우만) */}
                        {condition.operator !== "isEmpty" && condition.operator !== "isNotEmpty" && (
                          <input
                            type="text"
                            value={condition.value}
                            onChange={(e) => updateFilterCondition(group.id, condition.id, "value", e.target.value)}
                            placeholder="값 입력"
                            className="border-input bg-background h-8 flex-1 rounded border px-2 text-xs"
                          />
                        )}

                        {/* 조건 삭제 */}
                        <Button
                          variant="ghost"
                          size="sm"
                          onClick={() => removeFilterCondition(group.id, condition.id)}
                          className="h-6 w-6 p-0 text-red-500 hover:text-red-700"
                          disabled={group.conditions.length === 1}
                        >
                          <X className="h-3 w-3" />
                        </Button>
                      </div>
                    ))}
                  </div>

                  {/* 조건 추가 버튼 */}
                  <Button
                    variant="ghost"
                    size="sm"
                    onClick={() => addFilterCondition(group.id, visibleColumns[0]?.columnName)}
                    className="mt-2 h-7 text-xs"
                  >
                    + 조건 추가
                  </Button>
                </div>
              ))
            )}

            {/* 그룹 추가 버튼 */}
            <Button
              variant="outline"
              size="sm"
              onClick={() => addFilterGroup(visibleColumns[0]?.columnName)}
              className="w-full"
            >
              + 필터 그룹 추가
            </Button>
          </div>

          <DialogFooter className="gap-2 sm:gap-0">
            <Button
              variant="outline"
              onClick={clearFilterBuilder}
              className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
            >
              초기화
            </Button>
            <Button
              variant="outline"
              onClick={() => setIsFilterBuilderOpen(false)}
              className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
            >
              취소
            </Button>
            <Button onClick={applyFilterBuilder} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
              적용
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      {/* 테이블 옵션 모달 */}
      <TableOptionsModal
        isOpen={isTableOptionsOpen}
        onClose={() => setIsTableOptionsOpen(false)}
        columns={visibleColumns.map((col) => ({
          columnName: col.columnName,
          label: columnLabels[col.columnName] || col.displayName || col.columnName,
          visible: col.visible !== false,
          width: columnWidths[col.columnName],
          frozen: frozenColumns.includes(col.columnName),
        }))}
        onSave={handleTableOptionsSave}
        tableName={tableConfig.selectedTable || "table"}
        userId={userId}
      />
    </>
  );
};

export const TableListWrapper: React.FC<TableListComponentProps> = (props) => {
  return (
    <V2ErrorBoundary
      componentId={props.component?.id || "table-list"}
      componentType="v2-table-list"
      fallbackStyle="compact"
    >
      <TableListComponent {...props} />
    </V2ErrorBoundary>
  );
};
