"use client";

import React, { useState, useEffect, useRef, useMemo, useCallback } from "react";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ChevronDown, Check, GripVertical } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import { RepeaterColumnConfig } from "./types";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";

// @dnd-kit imports
import {
  DndContext,
  closestCenter,
  KeyboardSensor,
  PointerSensor,
  useSensor,
  useSensors,
  DragEndEvent,
} from "@dnd-kit/core";
import {
  SortableContext,
  sortableKeyboardCoordinates,
  verticalListSortingStrategy,
  useSortable,
  arrayMove,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";

// SortableRow 컴포넌트 - 드래그 가능한 테이블 행
interface SortableRowProps {
  id: string;
  children: (props: {
    attributes: React.HTMLAttributes<HTMLElement>;
    listeners: React.HTMLAttributes<HTMLElement> | undefined;
    isDragging: boolean;
  }) => React.ReactNode;
  className?: string;
}

function SortableRow({ id, children, className }: SortableRowProps) {
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });

  const style: React.CSSProperties = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.5 : 1,
    backgroundColor: isDragging ? "#f0f9ff" : undefined,
  };

  return (
    <tr ref={setNodeRef} style={style} className={className}>
      {children({ attributes, listeners, isDragging })}
    </tr>
  );
}

interface RepeaterTableProps {
  columns: RepeaterColumnConfig[];
  data: any[];
  onDataChange: (newData: any[]) => void;
  onRowChange: (index: number, newRow: any) => void;
  onRowDelete: (index: number) => void;
  // 동적 데이터 소스 관련
  activeDataSources?: Record<string, string>; // 컬럼별 현재 활성화된 데이터 소스 ID
  onDataSourceChange?: (columnField: string, optionId: string) => void; // 데이터 소스 변경 콜백
  // 체크박스 선택 관련
  selectedRows: Set<number>; // 선택된 행 인덱스
  onSelectionChange: (selectedRows: Set<number>) => void; // 선택 변경 콜백
  // 균등 분배 트리거
  equalizeWidthsTrigger?: number; // 값이 변경되면 균등 분배 실행
  // 🆕 카테고리 라벨 변환용
  categoryColumns?: string[]; // 카테고리 타입 컬럼명 목록
  categoryLabelMap?: Record<string, string>; // 카테고리 코드 → 라벨 매핑
}

export function RepeaterTable({
  columns,
  data,
  onDataChange,
  onRowChange,
  onRowDelete,
  activeDataSources = {},
  onDataSourceChange,
  selectedRows,
  onSelectionChange,
  equalizeWidthsTrigger,
  categoryColumns = [],
  categoryLabelMap = {},
}: RepeaterTableProps) {
  // 히든 컬럼을 제외한 표시 가능한 컬럼만 필터링
  const visibleColumns = useMemo(() => columns.filter((col) => !col.hidden), [columns]);

  // 🆕 카테고리 옵션 상태 (categoryRef별로 로드된 옵션)
  const [categoryOptionsMap, setCategoryOptionsMap] = useState<Record<string, { value: string; label: string }[]>>({});

  // 🆕 카테고리 옵션 로드
  // categoryRef 형식: "tableName.columnName" (예: "item_info.material")
  useEffect(() => {
    const loadCategoryOptions = async () => {
      // category 타입이면서 categoryRef가 있는 컬럼들 찾기
      const categoryColumns = visibleColumns.filter((col) => col.type === "category");

      const categoryRefs = categoryColumns
        .filter((col) => col.categoryRef)
        .map((col) => col.categoryRef!)
        .filter((ref, index, self) => self.indexOf(ref) === index); // 중복 제거

      if (categoryRefs.length === 0) {
        return;
      }

      for (const categoryRef of categoryRefs) {
        if (categoryOptionsMap[categoryRef]) {
          continue;
        }

        try {
          // categoryRef를 tableName.columnName 형식으로 파싱
          const parts = categoryRef.split(".");
          let tableName: string;
          let columnName: string;

          if (parts.length >= 2) {
            // "tableName.columnName" 형식
            tableName = parts[0];
            columnName = parts.slice(1).join("."); // 컬럼명에 .이 포함될 수 있음
          } else {
            // 단일 값인 경우 컬럼명만 있다고 가정 (테이블명 불명)
            console.warn(`카테고리 참조 형식 오류 (${categoryRef}): tableName.columnName 형식이어야 합니다`);
            continue;
          }

          const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values?includeInactive=true`);

          if (response.data?.success && response.data.data) {
            const options = response.data.data.map((item: any) => ({
              value: item.valueCode || item.value_code,
              label: item.valueLabel || item.value_label || item.displayValue || item.display_value || item.label,
            }));
            setCategoryOptionsMap((prev) => ({
              ...prev,
              [categoryRef]: options,
            }));
          } else {
            console.warn(`⚠️ [RepeaterTable] ${categoryRef} API 응답이 success가 아니거나 data가 없음`);
          }
        } catch (error) {
          console.error(`카테고리 옵션 로드 실패 (${categoryRef}):`, error);
        }
      }
    };

    loadCategoryOptions();
  }, [visibleColumns]);

  // 컨테이너 ref - 실제 너비 측정용
  const containerRef = useRef<HTMLDivElement>(null);

  // 초기 균등 분배 실행 여부 (마운트 시 한 번만 실행)
  const initializedRef = useRef(false);

  // 편집 가능한 컬럼 인덱스 목록 (방향키 네비게이션용)
  const editableColIndices = useMemo(
    () => visibleColumns.reduce<number[]>((acc, col, idx) => {
      if (col.editable && !col.calculated) acc.push(idx);
      return acc;
    }, []),
    [visibleColumns],
  );

  // 방향키로 리피터 셀 간 이동
  const handleArrowNavigation = useCallback(
    (e: React.KeyboardEvent<HTMLDivElement>) => {
      const key = e.key;
      if (!["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(key)) return;

      const target = e.target as HTMLElement;
      const cell = target.closest("[data-repeater-row]") as HTMLElement | null;
      if (!cell) return;

      const row = Number(cell.dataset.repeaterRow);
      const col = Number(cell.dataset.repeaterCol);
      if (isNaN(row) || isNaN(col)) return;

      // 텍스트 입력 중 좌/우 방향키는 커서 이동에 사용하므로 무시
      if ((key === "ArrowLeft" || key === "ArrowRight") && target.tagName === "INPUT") {
        const input = target as HTMLInputElement;
        const len = input.value?.length ?? 0;
        const pos = input.selectionStart ?? 0;
        // 커서가 끝에 있을 때만 오른쪽 이동, 처음에 있을 때만 왼쪽 이동
        if (key === "ArrowRight" && pos < len) return;
        if (key === "ArrowLeft" && pos > 0) return;
      }

      let nextRow = row;
      let nextColPos = editableColIndices.indexOf(col);

      switch (key) {
        case "ArrowUp":
          nextRow = Math.max(0, row - 1);
          break;
        case "ArrowDown":
          nextRow = Math.min(data.length - 1, row + 1);
          break;
        case "ArrowLeft":
          nextColPos = Math.max(0, nextColPos - 1);
          break;
        case "ArrowRight":
          nextColPos = Math.min(editableColIndices.length - 1, nextColPos + 1);
          break;
      }

      const nextCol = editableColIndices[nextColPos];
      if (nextRow === row && nextCol === col) return;

      e.preventDefault();

      const selector = `[data-repeater-row="${nextRow}"][data-repeater-col="${nextCol}"]`;
      const nextCell = containerRef.current?.querySelector(selector) as HTMLElement | null;
      if (!nextCell) return;

      const focusable = nextCell.querySelector<HTMLElement>(
        'input:not([disabled]), select:not([disabled]), [role="combobox"]:not([disabled]), button:not([disabled])',
      );
      if (focusable) {
        focusable.focus();
        if (focusable.tagName === "INPUT") {
          (focusable as HTMLInputElement).select();
        }
      }
    },
    [editableColIndices, data.length],
  );

  // DnD 센서 설정
  const sensors = useSensors(
    useSensor(PointerSensor, {
      activationConstraint: {
        distance: 8, // 8px 이동해야 드래그 시작 (클릭과 구분)
      },
    }),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    }),
  );

  // 드래그 종료 핸들러
  const handleDragEnd = (event: DragEndEvent) => {
    const { active, over } = event;

    if (over && active.id !== over.id) {
      const oldIndex = data.findIndex((_, idx) => `row-${idx}` === active.id);
      const newIndex = data.findIndex((_, idx) => `row-${idx}` === over.id);

      if (oldIndex !== -1 && newIndex !== -1) {
        const newData = arrayMove(data, oldIndex, newIndex);
        onDataChange(newData);

        // 선택된 행 인덱스도 업데이트
        if (selectedRows.size > 0) {
          const newSelectedRows = new Set<number>();
          selectedRows.forEach((oldIdx) => {
            if (oldIdx === oldIndex) {
              newSelectedRows.add(newIndex);
            } else if (oldIdx > oldIndex && oldIdx <= newIndex) {
              newSelectedRows.add(oldIdx - 1);
            } else if (oldIdx < oldIndex && oldIdx >= newIndex) {
              newSelectedRows.add(oldIdx + 1);
            } else {
              newSelectedRows.add(oldIdx);
            }
          });
          onSelectionChange(newSelectedRows);
        }
      }
    }
  };

  const [editingCell, setEditingCell] = useState<{
    rowIndex: number;
    field: string;
  } | null>(null);

  // 동적 데이터 소스 Popover 열림 상태
  const [openPopover, setOpenPopover] = useState<string | null>(null);

  // 컬럼 너비 상태 관리
  const [columnWidths, setColumnWidths] = useState<Record<string, number>>(() => {
    const widths: Record<string, number> = {};
    columns
      .filter((col) => !col.hidden)
      .forEach((col) => {
        widths[col.field] = col.width ? parseInt(col.width) : 120;
      });
    return widths;
  });

  // 기본 너비 저장 (리셋용)
  const defaultWidths = React.useMemo(() => {
    const widths: Record<string, number> = {};
    visibleColumns.forEach((col) => {
      widths[col.field] = col.width ? parseInt(col.width) : 120;
    });
    return widths;
  }, [visibleColumns]);

  // 리사이즈 상태
  const [resizing, setResizing] = useState<{ field: string; startX: number; startWidth: number } | null>(null);

  // 리사이즈 핸들러
  const handleMouseDown = (e: React.MouseEvent, field: string) => {
    e.preventDefault();
    setResizing({
      field,
      startX: e.clientX,
      startWidth: columnWidths[field] || 120,
    });
  };

  // 컨테이너 가용 너비 계산
  const getAvailableWidth = (): number => {
    if (!containerRef.current) return 800;
    const containerWidth = containerRef.current.offsetWidth;
    // 드래그 핸들(32px) + 체크박스 컬럼(40px) + border(2px)
    return containerWidth - 74;
  };

  // 텍스트 너비 계산 (한글/영문/숫자 혼합 고려)
  const measureTextWidth = (text: string): number => {
    if (!text) return 0;
    let width = 0;
    for (const char of text) {
      if (/[가-힣]/.test(char)) {
        width += 15; // 한글 (text-xs 12px 기준)
      } else if (/[a-zA-Z]/.test(char)) {
        width += 9; // 영문
      } else if (/[0-9]/.test(char)) {
        width += 8; // 숫자
      } else if (/[_\-.]/.test(char)) {
        width += 6; // 특수문자
      } else if (/[\(\)]/.test(char)) {
        width += 6; // 괄호
      } else {
        width += 8; // 기타
      }
    }
    return width;
  };

  // 해당 컬럼의 가장 긴 글자 너비 계산
  // equalWidth: 균등 분배 시 너비 (값이 없는 컬럼의 최소값으로 사용)
  const calculateColumnContentWidth = (field: string, equalWidth: number): number => {
    const column = visibleColumns.find((col) => col.field === field);
    if (!column) return equalWidth;

    // 날짜 필드는 110px (yyyy-MM-dd)
    if (column.type === "date") {
      return 110;
    }

    // 해당 컬럼에 값이 있는지 확인
    let hasValue = false;
    let maxDataWidth = 0;

    data.forEach((row) => {
      const value = row[field];
      if (value !== undefined && value !== null && value !== "") {
        hasValue = true;
        let displayText = String(value);

        if (typeof value === "number") {
          displayText = value.toLocaleString();
        }

        const textWidth = measureTextWidth(displayText) + 20; // padding
        maxDataWidth = Math.max(maxDataWidth, textWidth);
      }
    });

    // 값이 없으면 균등 분배 너비 사용
    if (!hasValue) {
      return equalWidth;
    }

    // 헤더 텍스트 너비 (동적 데이터 소스가 있으면 headerLabel 사용)
    let headerText = column.label || field;
    if (column.dynamicDataSource?.enabled && column.dynamicDataSource.options.length > 0) {
      const activeOptionId = activeDataSources[field] || column.dynamicDataSource.defaultOptionId;
      const activeOption =
        column.dynamicDataSource.options.find((opt) => opt.id === activeOptionId) ||
        column.dynamicDataSource.options[0];
      if (activeOption?.headerLabel) {
        headerText = activeOption.headerLabel;
      }
    }
    const headerWidth = measureTextWidth(headerText) + 32; // padding + 드롭다운 아이콘

    // 헤더와 데이터 중 큰 값 사용
    return Math.max(headerWidth, maxDataWidth);
  };

  // 헤더 더블클릭: 해당 컬럼만 글자 너비에 맞춤
  const handleDoubleClick = (field: string) => {
    const availableWidth = getAvailableWidth();
    const equalWidth = Math.max(60, Math.floor(availableWidth / visibleColumns.length));
    const contentWidth = calculateColumnContentWidth(field, equalWidth);
    setColumnWidths((prev) => ({
      ...prev,
      [field]: contentWidth,
    }));
  };

  // 균등 분배: 컬럼 수로 테이블 너비를 균등 분배
  const applyEqualizeWidths = () => {
    const availableWidth = getAvailableWidth();
    const equalWidth = Math.max(60, Math.floor(availableWidth / visibleColumns.length));

    const newWidths: Record<string, number> = {};
    visibleColumns.forEach((col) => {
      newWidths[col.field] = equalWidth;
    });

    setColumnWidths(newWidths);
  };

  // 자동 맞춤: 각 컬럼을 글자 너비에 맞추고, 컨테이너보다 작으면 남는 공간 분배
  const applyAutoFitWidths = () => {
    if (visibleColumns.length === 0) return;

    // 균등 분배 너비 계산 (값이 없는 컬럼의 최소값)
    const availableWidth = getAvailableWidth();
    const equalWidth = Math.max(60, Math.floor(availableWidth / visibleColumns.length));

    // 1. 각 컬럼의 글자 너비 계산 (값이 없으면 균등 분배 너비 사용)
    const newWidths: Record<string, number> = {};
    visibleColumns.forEach((col) => {
      newWidths[col.field] = calculateColumnContentWidth(col.field, equalWidth);
    });

    // 2. 컨테이너 너비와 비교
    const totalContentWidth = Object.values(newWidths).reduce((sum, w) => sum + w, 0);

    // 3. 컨테이너보다 작으면 남는 공간을 균등 분배 (테이블 꽉 참 유지)
    if (totalContentWidth < availableWidth) {
      const extraSpace = availableWidth - totalContentWidth;
      const extraPerColumn = Math.floor(extraSpace / visibleColumns.length);
      visibleColumns.forEach((col) => {
        newWidths[col.field] += extraPerColumn;
      });
    }
    // 컨테이너보다 크면 그대로 (스크롤 생성됨)

    setColumnWidths(newWidths);
  };

  // 초기 마운트 시 균등 분배 적용
  useEffect(() => {
    if (initializedRef.current) return;
    if (!containerRef.current || visibleColumns.length === 0) return;

    const timer = setTimeout(() => {
      applyEqualizeWidths();
      initializedRef.current = true;
    }, 100);

    return () => clearTimeout(timer);
  }, [visibleColumns]);

  // 🆕 트리거 변경 시 자동으로 컬럼 너비 조정 (데이터 기반)
  useEffect(() => {
    if (equalizeWidthsTrigger === undefined || equalizeWidthsTrigger === 0) return;
    if (!containerRef.current || visibleColumns.length === 0) return;

    // 데이터가 있으면 데이터 기반 자동 맞춤, 없으면 균등 분배
    const timer = setTimeout(() => {
      if (data.length > 0) {
        applyAutoFitWidths();
      } else {
        applyEqualizeWidths();
      }
    }, 50);

    return () => clearTimeout(timer);
  }, [equalizeWidthsTrigger, data.length]);

  useEffect(() => {
    if (!resizing) return;

    const handleMouseMove = (e: MouseEvent) => {
      if (!resizing) return;
      const diff = e.clientX - resizing.startX;
      const newWidth = Math.max(60, resizing.startWidth + diff);
      setColumnWidths((prev) => ({
        ...prev,
        [resizing.field]: newWidth,
      }));
    };

    const handleMouseUp = () => {
      setResizing(null);
    };

    document.addEventListener("mousemove", handleMouseMove);
    document.addEventListener("mouseup", handleMouseUp);

    return () => {
      document.removeEventListener("mousemove", handleMouseMove);
      document.removeEventListener("mouseup", handleMouseUp);
    };
  }, [resizing, visibleColumns, data]);

  // 데이터 변경 감지 (필요시 활성화)
  // useEffect(() => {
  //   console.log("📊 RepeaterTable 데이터 업데이트:", data.length, "개 행");
  // }, [data]);

  const handleCellEdit = (rowIndex: number, field: string, value: any) => {
    const newRow = { ...data[rowIndex], [field]: value };
    onRowChange(rowIndex, newRow);
  };

  // 전체 선택 체크박스 핸들러
  const handleSelectAll = (checked: boolean) => {
    if (checked) {
      // 모든 행 선택
      const allIndices = new Set(data.map((_, index) => index));
      onSelectionChange(allIndices);
    } else {
      // 전체 해제
      onSelectionChange(new Set());
    }
  };

  // 개별 행 선택 핸들러
  const handleRowSelect = (rowIndex: number, checked: boolean) => {
    const newSelection = new Set(selectedRows);
    if (checked) {
      newSelection.add(rowIndex);
    } else {
      newSelection.delete(rowIndex);
    }
    onSelectionChange(newSelection);
  };

  // 전체 선택 상태 계산
  const isAllSelected = data.length > 0 && selectedRows.size === data.length;
  const isIndeterminate = selectedRows.size > 0 && selectedRows.size < data.length;

  const renderCell = (row: any, column: RepeaterColumnConfig, rowIndex: number) => {
    const isEditing = editingCell?.rowIndex === rowIndex && editingCell?.field === column.field;
    const value = row[column.field];

    // 카테고리/셀렉트 라벨 변환 함수
    const getCategoryDisplayValue = (val: any): string => {
      if (!val || typeof val !== "string") return val || "-";

      // select 타입 컬럼의 selectOptions에서 라벨 찾기
      if (column.selectOptions && column.selectOptions.length > 0) {
        const matchedOption = column.selectOptions.find((opt) => opt.value === val);
        if (matchedOption) return matchedOption.label;
      }

      const fieldName = column.field.replace(/^_display_/, "");
      const isCategoryColumn = categoryColumns.includes(fieldName);

      // categoryLabelMap에 직접 매핑이 있으면 바로 변환
      if (categoryLabelMap[val]) return categoryLabelMap[val];

      // 카테고리 컬럼이 아니면 원래 값 반환
      if (!isCategoryColumn) return val;

      // 콤마 구분된 다중 값 처리
      const codes = val
        .split(",")
        .map((c: string) => c.trim())
        .filter(Boolean);
      const labels = codes.map((code: string) => categoryLabelMap[code] || code);
      return labels.join(", ");
    };

    // 🆕 40자 초과 시 ... 처리 및 툴팁 표시 함수
    const truncateText = (text: string, maxLength: number = 40): { truncated: string; isTruncated: boolean } => {
      if (!text || text.length <= maxLength) {
        return { truncated: text || "-", isTruncated: false };
      }
      return { truncated: text.substring(0, maxLength) + "...", isTruncated: true };
    };

    // 계산 필드는 편집 불가
    if (column.calculated || !column.editable) {
      // 숫자 포맷팅 함수: 정수/소수점 자동 구분
      const formatNumber = (val: any): string => {
        if (val === undefined || val === null || val === "") return "0";
        const num = typeof val === "number" ? val : parseFloat(val);
        if (isNaN(num)) return "0";
        // 정수면 소수점 없이, 소수면 소수점 유지
        if (Number.isInteger(num)) {
          return num.toLocaleString("ko-KR");
        } else {
          return num.toLocaleString("ko-KR");
        }
      };

      // 🆕 카테고리 타입이면 라벨로 변환하여 표시
      const displayValue = column.type === "number" ? formatNumber(value) : getCategoryDisplayValue(value);

      // 🆕 40자 초과 시 ... 처리 및 툴팁
      const { truncated, isTruncated } = truncateText(String(displayValue));

      return (
        <div className="truncate px-2 py-1" title={isTruncated ? String(displayValue) : undefined}>
          {truncated}
        </div>
      );
    }

    // 편집 가능한 필드
    switch (column.type) {
      case "number":
        // 숫자 표시: 정수/소수점 자동 구분
        const displayValue = (() => {
          if (value === undefined || value === null || value === "") return "";
          const num = typeof value === "number" ? value : parseFloat(value);
          if (isNaN(num)) return "";
          return num.toString();
        })();

        return (
          <Input
            type="text"
            inputMode="numeric"
            value={displayValue}
            onChange={(e) => {
              const val = e.target.value;
              // 숫자와 소수점만 허용
              if (val === "" || /^-?\d*\.?\d*$/.test(val)) {
                handleCellEdit(rowIndex, column.field, val === "" ? 0 : parseFloat(val) || 0);
              }
            }}
            className="h-8 w-full min-w-0 rounded-none border-gray-200 text-right text-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
          />
        );

      case "date":
        // ISO 형식(2025-11-23T00:00:00.000Z)을 yyyy-mm-dd로 변환
        const formatDateValue = (val: any): string => {
          if (!val) return "";
          // 이미 yyyy-mm-dd 형식이면 그대로 반환
          if (typeof val === "string" && /^\d{4}-\d{2}-\d{2}$/.test(val)) {
            return val;
          }
          // ISO 형식이면 날짜 부분만 추출
          if (typeof val === "string" && val.includes("T")) {
            return val.split("T")[0];
          }
          // Date 객체이면 변환
          if (val instanceof Date) {
            return val.toISOString().split("T")[0];
          }
          return String(val);
        };

        return (
          <input
            type="date"
            value={formatDateValue(value)}
            onChange={(e) => handleCellEdit(rowIndex, column.field, e.target.value)}
            onClick={(e) => (e.target as HTMLInputElement).showPicker?.()}
            className="h-8 w-full min-w-0 cursor-pointer rounded-none border border-gray-200 bg-white px-2 text-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-inner-spin-button]:hidden"
          />
        );

      case "select":
        return (
          <Select value={value || ""} onValueChange={(newValue) => handleCellEdit(rowIndex, column.field, newValue)}>
            <SelectTrigger className="h-8 w-full min-w-0 rounded-none border-gray-200 text-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-500">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {column.selectOptions
                ?.filter((option) => option.value && option.value !== "")
                .map((option) => (
                  <SelectItem key={option.value} value={option.value}>
                    {option.label}
                  </SelectItem>
                ))}
            </SelectContent>
          </Select>
        );

      case "category": {
        // 🆕 카테고리 타입: categoryRef로 로드된 옵션 사용
        const options = column.categoryRef ? categoryOptionsMap[column.categoryRef] || [] : [];
        return (
          <Select value={value || ""} onValueChange={(newValue) => handleCellEdit(rowIndex, column.field, newValue)}>
            <SelectTrigger className="h-8 w-full min-w-0 rounded-none border-gray-200 text-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-500">
              <SelectValue placeholder="선택..." />
            </SelectTrigger>
            <SelectContent>
              {options.map((option) => (
                <SelectItem key={option.value} value={option.value}>
                  {option.label}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        );
      }

      default: // text
        return (
          <Input
            type="text"
            value={value || ""}
            onChange={(e) => handleCellEdit(rowIndex, column.field, e.target.value)}
            className="h-8 w-full min-w-0 rounded-none border-gray-200 text-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
          />
        );
    }
  };

  // 드래그 아이템 ID 목록
  const sortableItems = data.map((_, idx) => `row-${idx}`);

  return (
    <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
      <div ref={containerRef} className="flex h-full flex-col border border-gray-200 bg-white" onKeyDown={handleArrowNavigation}>
        <div className="min-h-0 flex-1 overflow-x-auto overflow-y-auto">
          <table
            className="border-collapse text-xs"
            style={{
              width: `max(100%, ${Object.values(columnWidths).reduce((sum, w) => sum + w, 0) + 74}px)`,
            }}
          >
            <thead className="sticky top-0 z-20 bg-gray-50">
              <tr>
                {/* 드래그 핸들 헤더 - 좌측 고정 */}
                <th
                  key="header-drag"
                  className="sticky left-0 z-30 w-8 border-r border-b border-gray-200 bg-gray-50 px-1 py-2 text-center font-medium text-gray-700"
                >
                  <span className="sr-only">순서</span>
                </th>
                {/* 체크박스 헤더 - 좌측 고정 */}
                <th
                  key="header-checkbox"
                  className="sticky left-8 z-30 w-10 border-r border-b border-gray-200 bg-gray-50 px-3 py-2 text-center font-medium text-gray-700"
                >
                  <Checkbox
                    checked={isAllSelected}
                    // @ts-expect-error - indeterminate는 HTML 속성
                    data-indeterminate={isIndeterminate}
                    onCheckedChange={handleSelectAll}
                    className={cn("border-gray-400", isIndeterminate && "data-[state=checked]:bg-primary")}
                  />
                </th>
                {visibleColumns.map((col, colIndex) => {
                  const hasDynamicSource = col.dynamicDataSource?.enabled && col.dynamicDataSource.options.length > 0;
                  const activeOptionId = activeDataSources[col.field] || col.dynamicDataSource?.defaultOptionId;
                  const activeOption = hasDynamicSource
                    ? col.dynamicDataSource!.options.find((opt) => opt.id === activeOptionId) ||
                      col.dynamicDataSource!.options[0]
                    : null;

                  return (
                    <th
                      key={`header-col-${col.field || colIndex}`}
                      className="group relative cursor-pointer border-r border-b border-gray-200 px-3 py-2 text-left font-medium whitespace-nowrap text-gray-700 select-none"
                      style={{ width: `${columnWidths[col.field]}px` }}
                      onDoubleClick={() => handleDoubleClick(col.field)}
                      title="더블클릭하여 글자 너비에 맞춤"
                    >
                      <div className="pointer-events-none flex items-center justify-between">
                        <div className="pointer-events-auto flex items-center gap-1">
                          {hasDynamicSource ? (
                            <Popover
                              open={openPopover === col.field}
                              onOpenChange={(open) => setOpenPopover(open ? col.field : null)}
                            >
                              <PopoverTrigger asChild>
                                <button
                                  type="button"
                                  className={cn(
                                    "inline-flex items-center gap-1 transition-colors hover:text-blue-600",
                                    "-mx-1 rounded px-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500",
                                  )}
                                >
                                  {/* 컬럼명 - 선택된 옵션라벨 형식으로 표시 */}
                                  <span>
                                    {activeOption?.headerLabel || `${col.label} - ${activeOption?.label || ""}`}
                                  </span>
                                  <ChevronDown className="h-3 w-3 opacity-60" />
                                </button>
                              </PopoverTrigger>
                              <PopoverContent className="w-auto min-w-[160px] p-1" align="start" sideOffset={4}>
                                <div className="text-muted-foreground mb-1 border-b px-2 py-1 text-[10px]">
                                  데이터 소스 선택
                                </div>
                                {col.dynamicDataSource!.options.map((option) => (
                                  <button
                                    key={option.id}
                                    type="button"
                                    onClick={() => {
                                      onDataSourceChange?.(col.field, option.id);
                                      setOpenPopover(null);
                                      // 옵션 변경 시 해당 컬럼 너비 재계산
                                      if (option.headerLabel) {
                                        const newHeaderWidth = measureTextWidth(option.headerLabel) + 32;
                                        setColumnWidths((prev) => ({
                                          ...prev,
                                          [col.field]: Math.max(prev[col.field] || 60, newHeaderWidth),
                                        }));
                                      }
                                    }}
                                    className={cn(
                                      "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs",
                                      "hover:bg-accent hover:text-accent-foreground transition-colors",
                                      "focus-visible:bg-accent focus:outline-none",
                                      activeOption?.id === option.id && "bg-accent/50",
                                    )}
                                  >
                                    <Check
                                      className={cn(
                                        "h-3 w-3",
                                        activeOption?.id === option.id ? "opacity-100" : "opacity-0",
                                      )}
                                    />
                                    <span>{option.label}</span>
                                  </button>
                                ))}
                              </PopoverContent>
                            </Popover>
                          ) : (
                            <>
                              {col.label}
                              {col.required && <span className="ml-1 text-red-500">*</span>}
                            </>
                          )}
                        </div>
                        {/* 리사이즈 핸들 */}
                        <div
                          className="pointer-events-auto absolute top-0 right-0 bottom-0 w-1 cursor-col-resize opacity-0 transition-opacity group-hover:opacity-100 hover:bg-blue-500"
                          onMouseDown={(e) => handleMouseDown(e, col.field)}
                          title="드래그하여 너비 조정"
                        />
                      </div>
                    </th>
                  );
                })}
              </tr>
            </thead>
            <SortableContext items={sortableItems} strategy={verticalListSortingStrategy}>
              <tbody className="bg-white">
                {data.length === 0 ? (
                  <tr key="empty-row">
                    <td
                      key="empty-cell"
                      colSpan={visibleColumns.length + 2}
                      className="border-b border-gray-200 px-4 py-8 text-center text-gray-500"
                    >
                      추가된 항목이 없습니다
                    </td>
                  </tr>
                ) : (
                  data.map((row, rowIndex) => (
                    <SortableRow
                      key={`row-${rowIndex}`}
                      id={`row-${rowIndex}`}
                      className={cn(
                        "transition-colors hover:bg-blue-50/50",
                        selectedRows.has(rowIndex) && "bg-blue-50",
                      )}
                    >
                      {({ attributes, listeners, isDragging }) => (
                        <>
                          {/* 드래그 핸들 - 좌측 고정 */}
                          <td
                            key={`drag-${rowIndex}`}
                            className={cn(
                              "sticky left-0 z-10 border-r border-b border-gray-200 px-1 py-1 text-center",
                              selectedRows.has(rowIndex) ? "bg-blue-50" : "bg-white",
                            )}
                          >
                            <button
                              type="button"
                              className={cn(
                                "cursor-grab rounded p-1 transition-colors hover:bg-gray-100",
                                isDragging && "cursor-grabbing",
                              )}
                              {...attributes}
                              {...listeners}
                            >
                              <GripVertical className="h-4 w-4 text-gray-400" />
                            </button>
                          </td>
                          {/* 체크박스 - 좌측 고정 */}
                          <td
                            key={`check-${rowIndex}`}
                            className={cn(
                              "sticky left-8 z-10 border-r border-b border-gray-200 px-3 py-1 text-center",
                              selectedRows.has(rowIndex) ? "bg-blue-50" : "bg-white",
                            )}
                          >
                            <Checkbox
                              checked={selectedRows.has(rowIndex)}
                              onCheckedChange={(checked) => handleRowSelect(rowIndex, !!checked)}
                              className="border-gray-400"
                            />
                          </td>
                          {/* 데이터 컬럼들 */}
                          {visibleColumns.map((col, colIndex) => (
                            <td
                              key={`${rowIndex}-${col.field || colIndex}`}
                              className="overflow-hidden border-r border-b border-gray-200 px-1 py-1"
                              style={{
                                width: `${columnWidths[col.field]}px`,
                                maxWidth: `${columnWidths[col.field]}px`,
                              }}
                              data-repeater-row={rowIndex}
                              data-repeater-col={colIndex}
                            >
                              {renderCell(row, col, rowIndex)}
                            </td>
                          ))}
                        </>
                      )}
                    </SortableRow>
                  ))
                )}
              </tbody>
            </SortableContext>
          </table>
        </div>
      </div>
    </DndContext>
  );
}
