"use client";

/**
 * pop-string-list 설정 패널 (Stepper/Wizard 방식)
 *
 * 6단계 순차 진행:
 * 1) 모드 선택 (리스트/카드)
 * 2) 헤더 설정
 * 3) 오버플로우 설정
 * 4) 데이터 선택 (테이블 + 컬럼 통합)
 * 5) 조인 설정 (선택)
 * 6-A) 리스트 컬럼 배치 (리스트 모드)
 * 6-B) 카드 그리드 디자이너 (카드 모드)
 */

import { useState, useEffect, useRef, useCallback, Fragment } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Check, ChevronsUpDown, ChevronLeft, ChevronRight, Plus, Minus, Trash2 } from "lucide-react";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command";
import { cn } from "@/lib/utils";
import type {
  PopStringListConfig,
  StringListDisplayMode,
  ListColumnConfig,
  CardGridConfig,
  CardCellDefinition,
} from "./types";
import type { CardListDataSource, CardColumnJoin } from "../types";
import {
  fetchTableList,
  fetchTableColumns,
  type TableInfo,
  type ColumnInfo,
} from "../pop-dashboard/utils/dataFetcher";

// ===== Props =====

interface ConfigPanelProps {
  config: PopStringListConfig | undefined;
  onUpdate: (config: PopStringListConfig) => void;
}

// ===== 기본 설정값 =====

const DEFAULT_CONFIG: PopStringListConfig = {
  displayMode: "list",
  header: { enabled: true, label: "" },
  overflow: { visibleRows: 5, mode: "loadMore", showExpandButton: true, loadMoreCount: 5, maxExpandRows: 50, pageSize: 5, paginationStyle: "bottom" },
  dataSource: { tableName: "" },
  listColumns: [],
  cardGrid: undefined,
};

// Stepper 단계 정의
const STEP_LABELS = [
  "모드 선택",
  "헤더 설정",
  "오버플로우",
  "데이터 선택",
  "조인 설정",
  "레이아웃",
] as const;

const TOTAL_STEPS = STEP_LABELS.length;

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

export function PopStringListConfigPanel({ config, onUpdate }: ConfigPanelProps) {
  const [step, setStep] = useState(0);
  const [tables, setTables] = useState<TableInfo[]>([]);
  const [columns, setColumns] = useState<ColumnInfo[]>([]);
  const [selectedColumns, setSelectedColumns] = useState<string[]>([]);

  // 설정값 (undefined 대비 기본값 병합)
  const cfg: PopStringListConfig = {
    ...DEFAULT_CONFIG,
    ...config,
    header: { ...DEFAULT_CONFIG.header, ...config?.header },
    overflow: { ...DEFAULT_CONFIG.overflow, ...config?.overflow },
    dataSource: { ...DEFAULT_CONFIG.dataSource, ...config?.dataSource },
  };

  // 설정 업데이트 헬퍼
  const update = (partial: Partial<PopStringListConfig>) => {
    onUpdate({ ...cfg, ...partial });
  };

  // 테이블 목록 로드
  useEffect(() => {
    fetchTableList()
      .then(setTables)
      .catch(() => setTables([])); // 네트워크 오류 시 빈 배열
  }, []);

  // 테이블 변경 시 컬럼 로드
  useEffect(() => {
    if (!cfg.dataSource.tableName) {
      setColumns([]);
      return;
    }
    fetchTableColumns(cfg.dataSource.tableName)
      .then(setColumns)
      .catch(() => setColumns([])); // 네트워크 오류 시 빈 배열
  }, [cfg.dataSource.tableName]);

  // 선택된 컬럼 복원 (config에 저장된 값 우선)
  useEffect(() => {
    if (cfg.selectedColumns && cfg.selectedColumns.length > 0) {
      setSelectedColumns(cfg.selectedColumns);
    } else if (cfg.displayMode === "list" && cfg.listColumns) {
      setSelectedColumns(cfg.listColumns.map((c) => c.columnName));
    } else if (cfg.displayMode === "card" && cfg.cardGrid) {
      setSelectedColumns((cfg.cardGrid.cells || []).map((c) => c.columnName));
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [cfg.dataSource.tableName]); // 테이블 변경 시에만 복원

  // 다음/이전 단계
  const canGoNext = (): boolean => {
    switch (step) {
      case 0: return true; // 모드 선택 (기본값 있음)
      case 1: return true; // 헤더 (선택사항)
      case 2: return true; // 오버플로우 (기본값 있음)
      case 3: return !!cfg.dataSource.tableName && selectedColumns.length > 0; // 테이블 + 컬럼
      case 4: return true; // 조인 (선택사항)
      case 5: return true; // 레이아웃
      default: return false;
    }
  };

  const goNext = () => {
    if (step < TOTAL_STEPS - 1 && canGoNext()) setStep(step + 1);
  };

  const goPrev = () => {
    if (step > 0) setStep(step - 1);
  };

  return (
    <div className="flex flex-col gap-3">
      {/* Stepper 인디케이터 */}
      <div className="flex items-center gap-1">
        {STEP_LABELS.map((label, i) => (
          <button
            key={i}
            onClick={() => setStep(i)}
            className={`flex-1 cursor-pointer rounded-sm py-0.5 text-center text-[10px] transition-colors ${
              i === step
                ? "bg-primary text-primary-foreground font-medium"
                : i < step
                  ? "bg-primary/20 text-primary"
                  : "bg-muted text-muted-foreground hover:bg-muted/80"
            }`}
            title={label}
          >
            {i + 1}
          </button>
        ))}
      </div>
      <div className="text-xs font-medium">{STEP_LABELS[step]}</div>

      {/* 단계별 컨텐츠 */}
      <div className="min-h-[120px]">
        {step === 0 && (
          <StepModeSelect
            displayMode={cfg.displayMode}
            onChange={(displayMode) => update({ displayMode })}
          />
        )}
        {step === 1 && (
          <StepHeader
            header={cfg.header}
            onChange={(header) => update({ header })}
          />
        )}
        {step === 2 && (
          <StepOverflow
            overflow={cfg.overflow}
            onChange={(overflow) => update({ overflow })}
          />
        )}
        {step === 3 && (
          <StepDataSelect
            tables={tables}
            tableName={cfg.dataSource.tableName}
            onTableChange={(tableName) => {
              setSelectedColumns([]);
              update({
                dataSource: { ...cfg.dataSource, tableName },
                selectedColumns: [],
                listColumns: [],
                cardGrid: undefined,
              });
            }}
            columns={columns}
            selectedColumns={selectedColumns}
            onColumnsChange={(cols) => {
              setSelectedColumns(cols);
              if (cfg.displayMode === "list") {
                const currentList = cfg.listColumns || [];
                // 기존 리스트에서: 체크 해제된 메인 컬럼만 제거
                // 조인 컬럼 (이름에 "."이 포함)은 항상 보존
                const preserved = currentList.filter(
                  (lc) => cols.includes(lc.columnName) || lc.columnName.includes(".")
                );
                // 새로 체크된 메인 컬럼만 리스트 끝에 추가
                const existingNames = new Set(preserved.map((lc) => lc.columnName));
                const added = cols
                  .filter((colName) => !existingNames.has(colName))
                  .map((colName) => ({ columnName: colName, label: colName } as ListColumnConfig));
                update({ selectedColumns: cols, listColumns: [...preserved, ...added] });
              } else {
                update({ selectedColumns: cols });
              }
            }}
          />
        )}
        {step === 4 && (
          <StepJoinConfig
            dataSource={cfg.dataSource}
            tables={tables}
            mainColumns={columns}
            onChange={(dataSource) => {
              // 조인 변경 후: 유효한 조인 컬럼명 셋 계산
              const validJoinColNames = new Set(
                (dataSource.joins || []).flatMap((j) =>
                  (j.selectedTargetColumns || []).map((col) => `${j.targetTable}.${col}`)
                )
              );
              // listColumns에서 고아 조인 컬럼 제거 + alternateColumns 정리
              const currentList = cfg.listColumns || [];
              const cleanedList = currentList
                .filter((lc) => {
                  if (!lc.columnName.includes(".")) return true; // 메인 컬럼: 유지
                  return validJoinColNames.has(lc.columnName); // 조인 컬럼: 유효한 것만
                })
                .map((lc) => {
                  const alts = lc.alternateColumns;
                  if (!alts) return lc;
                  const cleanedAlts = alts.filter((a) => {
                    if (!a.includes(".")) return true; // 메인 컬럼: 유지
                    return validJoinColNames.has(a); // 조인 컬럼: 유효한 것만
                  });
                  return {
                    ...lc,
                    alternateColumns: cleanedAlts.length > 0 ? cleanedAlts : undefined,
                  };
                });
              update({ dataSource, listColumns: cleanedList });
            }}
          />
        )}
        {step === 5 &&
          (cfg.displayMode === "list" ? (
            <StepListLayout
              listColumns={cfg.listColumns || []}
              availableColumns={columns.filter((c) =>
                selectedColumns.includes(c.name)
              )}
              joinedColumns={
                // 조인에서 선택된 대상 컬럼들을 {테이블명.컬럼명} 형태로 수집
                (cfg.dataSource.joins || []).flatMap((j) =>
                  (j.selectedTargetColumns || []).map((col) => ({
                    name: `${j.targetTable}.${col}`,
                    displayName: col,
                    sourceTable: j.targetTable,
                  }))
                )
              }
              onChange={(listColumns) => update({ listColumns })}
            />
          ) : (
            <StepCardDesigner
              cardGrid={cfg.cardGrid}
              columns={columns}
              selectedColumns={selectedColumns}
              onChange={(cardGrid) => update({ cardGrid })}
            />
          ))}
      </div>

      {/* 이전/다음 버튼 */}
      <div className="flex justify-between">
        <Button
          variant="outline"
          size="sm"
          onClick={goPrev}
          disabled={step === 0}
          className="h-7 text-xs"
        >
          <ChevronLeft className="mr-1 h-3 w-3" />
          이전
        </Button>
        <span className="text-[10px] text-muted-foreground self-center">
          {step + 1} / {TOTAL_STEPS}
        </span>
        <Button
          variant="outline"
          size="sm"
          onClick={goNext}
          disabled={step === TOTAL_STEPS - 1 || !canGoNext()}
          className="h-7 text-xs"
        >
          다음
          <ChevronRight className="ml-1 h-3 w-3" />
        </Button>
      </div>
    </div>
  );
}

// ===== STEP 0: 모드 선택 =====

function StepModeSelect({
  displayMode,
  onChange,
}: {
  displayMode: StringListDisplayMode;
  onChange: (mode: StringListDisplayMode) => void;
}) {
  return (
    <div className="flex gap-2">
      <button
        onClick={() => onChange("list")}
        className={`flex-1 rounded-md border p-3 text-center transition-colors ${
          displayMode === "list"
            ? "border-primary bg-primary/10 text-primary"
            : "border-border hover:border-primary/50"
        }`}
      >
        <div className="text-sm font-medium">리스트</div>
        <div className="text-[10px] text-muted-foreground mt-1">
          엑셀형 행/열 테이블
        </div>
      </button>
      <button
        onClick={() => onChange("card")}
        className={`flex-1 rounded-md border p-3 text-center transition-colors ${
          displayMode === "card"
            ? "border-primary bg-primary/10 text-primary"
            : "border-border hover:border-primary/50"
        }`}
      >
        <div className="text-sm font-medium">카드</div>
        <div className="text-[10px] text-muted-foreground mt-1">
          셀 병합 가능한 카드
        </div>
      </button>
    </div>
  );
}

// ===== STEP 1: 헤더 설정 =====

function StepHeader({
  header,
  onChange,
}: {
  header: PopStringListConfig["header"];
  onChange: (header: PopStringListConfig["header"]) => void;
}) {
  return (
    <div className="space-y-3">
      <div className="flex items-center justify-between">
        <Label className="text-xs">헤더 표시</Label>
        <Switch
          checked={header.enabled}
          onCheckedChange={(enabled) => onChange({ ...header, enabled })}
        />
      </div>
      {header.enabled && (
        <div>
          <Label className="text-xs">헤더 라벨</Label>
          <Input
            value={header.label || ""}
            onChange={(e) => onChange({ ...header, label: e.target.value })}
            placeholder="리스트 제목 입력"
            className="mt-1 h-8 text-xs"
          />
        </div>
      )}
    </div>
  );
}

// ===== STEP 2: 오버플로우 설정 =====

function StepOverflow({
  overflow,
  onChange,
}: {
  overflow: PopStringListConfig["overflow"];
  onChange: (overflow: PopStringListConfig["overflow"]) => void;
}) {
  const mode = overflow.mode || "loadMore";

  return (
    <div className="space-y-3">
      <div>
        <Label className="text-xs">기본 표시 행 수</Label>
        <Input
          type="number"
          min={1}
          max={50}
          value={overflow.visibleRows}
          onChange={(e) =>
            onChange({ ...overflow, visibleRows: Number(e.target.value) || 5 })
          }
          className="mt-1 h-8 text-xs"
        />
      </div>

      <div>
        <Label className="text-xs">오버플로우 방식</Label>
        <Select
          value={mode}
          onValueChange={(v) =>
            onChange({ ...overflow, mode: v as "loadMore" | "pagination" })
          }
        >
          <SelectTrigger className="mt-1 h-8 text-xs">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="loadMore" className="text-xs">
              더보기 (점진 확장)
            </SelectItem>
            <SelectItem value="pagination" className="text-xs">
              페이지네이션
            </SelectItem>
          </SelectContent>
        </Select>
      </div>

      {mode === "loadMore" && (
        <>
          <div className="flex items-center justify-between">
            <Label className="text-xs">더보기 버튼</Label>
            <Switch
              checked={overflow.showExpandButton}
              onCheckedChange={(showExpandButton) =>
                onChange({ ...overflow, showExpandButton })
              }
            />
          </div>
          {overflow.showExpandButton && (
            <>
              <div>
                <Label className="text-xs">더보기 추가 행 수</Label>
                <Input
                  type="number"
                  min={1}
                  max={50}
                  value={overflow.loadMoreCount ?? 5}
                  onChange={(e) =>
                    onChange({
                      ...overflow,
                      loadMoreCount: Number(e.target.value) || 5,
                    })
                  }
                  className="mt-1 h-8 text-xs"
                />
                <p className="text-muted-foreground mt-1 text-[10px]">
                  클릭할 때마다 추가로 표시할 행 수
                </p>
              </div>
              <div>
                <Label className="text-xs">최대 확장 행 수</Label>
                <Input
                  type="number"
                  min={overflow.visibleRows}
                  max={200}
                  value={overflow.maxExpandRows}
                  onChange={(e) =>
                    onChange({
                      ...overflow,
                      maxExpandRows: Number(e.target.value) || 50,
                    })
                  }
                  className="mt-1 h-8 text-xs"
                />
              </div>
            </>
          )}
        </>
      )}

      {mode === "pagination" && (
        <>
          <div>
            <Label className="text-xs">페이지당 행 수</Label>
            <Input
              type="number"
              min={1}
              max={100}
              value={overflow.pageSize ?? overflow.visibleRows}
              onChange={(e) =>
                onChange({
                  ...overflow,
                  pageSize: Number(e.target.value) || 5,
                })
              }
              className="mt-1 h-8 text-xs"
            />
          </div>
          <div>
            <Label className="text-xs">네비게이션 스타일</Label>
            <Select
              value={overflow.paginationStyle || "bottom"}
              onValueChange={(v) =>
                onChange({
                  ...overflow,
                  paginationStyle: v as "bottom" | "side",
                })
              }
            >
              <SelectTrigger className="mt-1 h-8 text-xs">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="bottom" className="text-xs">
                  하단 페이지 표시
                </SelectItem>
                <SelectItem value="side" className="text-xs">
                  좌우 화살표
                </SelectItem>
              </SelectContent>
            </Select>
            <p className="text-muted-foreground mt-1 text-[10px]">
              {(overflow.paginationStyle || "bottom") === "bottom"
                ? "컴포넌트 하단에 이전/다음 버튼과 페이지 번호 표시"
                : "컴포넌트 좌우에 화살표 버튼 표시"}
            </p>
          </div>
        </>
      )}
    </div>
  );
}

// ===== STEP 3: 데이터 선택 (테이블 + 컬럼 통합) =====

function StepDataSelect({
  tables,
  tableName,
  onTableChange,
  columns,
  selectedColumns,
  onColumnsChange,
}: {
  tables: TableInfo[];
  tableName: string;
  onTableChange: (tableName: string) => void;
  columns: ColumnInfo[];
  selectedColumns: string[];
  onColumnsChange: (selected: string[]) => void;
}) {
  const [open, setOpen] = useState(false);

  const selectedDisplay = tableName
    ? tables.find((t) => t.tableName === tableName)?.displayName || tableName
    : "";

  const toggleColumn = (colName: string) => {
    if (selectedColumns.includes(colName)) {
      onColumnsChange(selectedColumns.filter((c) => c !== colName));
    } else {
      onColumnsChange([...selectedColumns, colName]);
    }
  };

  return (
    <div className="space-y-3">
      {/* 테이블 선택 */}
      <div>
        <Label className="text-xs">데이터 테이블</Label>
        <Popover open={open} onOpenChange={setOpen}>
          <PopoverTrigger asChild>
            <Button
              variant="outline"
              role="combobox"
              aria-expanded={open}
              className="mt-1 h-8 w-full justify-between text-xs font-normal"
            >
              {tableName ? selectedDisplay : "테이블 검색 / 선택"}
              <ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
            </Button>
          </PopoverTrigger>
          <PopoverContent
            className="p-0"
            style={{ width: "var(--radix-popover-trigger-width)" }}
            align="start"
          >
            <Command>
              <CommandInput
                placeholder="테이블명 검색 (한글/영어)"
                className="text-xs"
              />
              <CommandList>
                <CommandEmpty className="py-3 text-center text-xs text-muted-foreground">
                  검색 결과가 없습니다
                </CommandEmpty>
                <CommandGroup>
                  <CommandItem
                    value="__none__"
                    onSelect={() => {
                      onTableChange("");
                      setOpen(false);
                    }}
                    className="text-xs"
                  >
                    <Check
                      className={cn(
                        "mr-2 h-3 w-3",
                        !tableName ? "opacity-100" : "opacity-0"
                      )}
                    />
                    선택 안 함
                  </CommandItem>
                  {tables.map((t) => (
                    <CommandItem
                      key={t.tableName}
                      value={`${t.tableName} ${t.displayName || ""}`}
                      onSelect={() => {
                        onTableChange(t.tableName);
                        setOpen(false);
                      }}
                      className="text-xs"
                    >
                      <Check
                        className={cn(
                          "mr-2 h-3 w-3",
                          tableName === t.tableName
                            ? "opacity-100"
                            : "opacity-0"
                        )}
                      />
                      <div className="flex flex-col">
                        <span>{t.displayName || t.tableName}</span>
                        {t.displayName && t.displayName !== t.tableName && (
                          <span className="text-[10px] text-muted-foreground">
                            {t.tableName}
                          </span>
                        )}
                      </div>
                    </CommandItem>
                  ))}
                </CommandGroup>
              </CommandList>
            </Command>
          </PopoverContent>
        </Popover>
      </div>

      {/* 컬럼 선택 (테이블 선택 후 표시) */}
      {tableName && columns.length > 0 && (
        <div>
          <Label className="text-xs">
            사용할 컬럼 ({selectedColumns.length}개 선택됨)
          </Label>
          <div className="mt-1 max-h-[160px] space-y-0.5 overflow-auto rounded border p-1">
            {columns.map((col) => (
              <label
                key={col.name}
                className="flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-muted/50"
              >
                <input
                  type="checkbox"
                  checked={selectedColumns.includes(col.name)}
                  onChange={() => toggleColumn(col.name)}
                  className="h-3 w-3"
                />
                <span className="text-xs">{col.name}</span>
                <span className="ml-auto text-[10px] text-muted-foreground">
                  {col.type}
                </span>
              </label>
            ))}
          </div>
        </div>
      )}

      {tableName && columns.length === 0 && (
        <p className="text-[10px] text-muted-foreground">
          컬럼 로딩 중...
        </p>
      )}
    </div>
  );
}

// ===== STEP 4: 조인 설정 (UX 개선 - 자동매칭 + 타입필터링) =====

// DB 타입을 짧은 약어로 변환
const shortType = (t: string): string => {
  const lower = t.toLowerCase();
  if (lower.includes("character varying") || lower === "varchar") return "varchar";
  if (lower === "text") return "text";
  if (lower.includes("timestamp")) return "timestamp";
  if (lower === "integer" || lower === "int4") return "int";
  if (lower === "bigint" || lower === "int8") return "bigint";
  if (lower === "numeric" || lower === "decimal") return "numeric";
  if (lower === "boolean" || lower === "bool") return "bool";
  if (lower === "date") return "date";
  if (lower === "uuid") return "uuid";
  if (lower === "jsonb" || lower === "json") return "json";
  return t.length > 12 ? t.slice(0, 10) + ".." : t;
};

// 조인 항목 하나를 관리하는 서브 컴포넌트
function JoinItem({
  join,
  index,
  tables,
  mainColumns,
  mainTableName,
  onUpdate,
  onRemove,
}: {
  join: CardColumnJoin;
  index: number;
  tables: TableInfo[];
  mainColumns: ColumnInfo[];
  mainTableName: string;
  onUpdate: (partial: Partial<CardColumnJoin>) => void;
  onRemove: () => void;
}) {
  const [targetColumns, setTargetColumns] = useState<ColumnInfo[]>([]);
  const [tableOpen, setTableOpen] = useState(false);
  const [loading, setLoading] = useState(false);

  // 대상 테이블 변경 시 컬럼 로딩
  useEffect(() => {
    if (!join.targetTable) {
      setTargetColumns([]);
      return;
    }
    setLoading(true);
    fetchTableColumns(join.targetTable)
      .then(setTargetColumns)
      .catch(() => setTargetColumns([]))
      .finally(() => setLoading(false));
  }, [join.targetTable]);

  // 자동 매칭: 이름 + 타입이 모두 같은 컬럼 쌍 찾기
  const autoMatches = mainColumns.filter((mc) =>
    targetColumns.some((tc) => tc.name === mc.name && tc.type === mc.type)
  );

  // 현재 연결된 쌍이 자동매칭 항목인지 확인
  const isAutoMatch =
    join.sourceColumn !== "" &&
    join.sourceColumn === join.targetColumn &&
    autoMatches.some((m) => m.name === join.sourceColumn);

  // 수동 매칭: 소스 컬럼 선택 시 같은 타입의 대상 컬럼만 필터
  const compatibleTargetCols = join.sourceColumn
    ? targetColumns.filter((tc) => {
        const srcCol = mainColumns.find((mc) => mc.name === join.sourceColumn);
        return srcCol ? tc.type === srcCol.type : true;
      })
    : targetColumns;

  // 메인 테이블 제외한 테이블 목록
  const selectableTables = tables.filter((t) => t.tableName !== mainTableName);

  // 연결 조건이 설정되었는지 여부
  const hasJoinCondition = join.sourceColumn !== "" && join.targetColumn !== "";

  // 선택된 대상 컬럼 관리 (연결 조건 컬럼은 제외한 나머지)
  const selectedTargetCols = join.selectedTargetColumns || [];

  // 가져올 수 있는 대상 컬럼 (연결 조건으로 사용된 컬럼 제외)
  const pickableTargetCols = targetColumns.filter(
    (tc) => tc.name !== join.targetColumn
  );

  const toggleTargetCol = (colName: string) => {
    const prev = selectedTargetCols;
    const next = prev.includes(colName)
      ? prev.filter((c) => c !== colName)
      : [...prev, colName];
    onUpdate({ selectedTargetColumns: next });
  };

  return (
    <div className="space-y-2 rounded border p-2">
      {/* 헤더 */}
      <div className="flex items-center justify-between">
        <span className="text-[10px] font-medium">연결 #{index + 1}</span>
        <Button variant="ghost" size="sm" onClick={onRemove} className="h-5 w-5 p-0">
          <Trash2 className="h-3 w-3 text-destructive" />
        </Button>
      </div>

      {/* 대상 테이블 선택 (검색 가능 Combobox) */}
      <div>
        <span className="text-[9px] text-muted-foreground">연결할 테이블</span>
        <Popover open={tableOpen} onOpenChange={setTableOpen}>
          <PopoverTrigger asChild>
            <Button
              variant="outline"
              role="combobox"
              aria-expanded={tableOpen}
              className="h-7 w-full justify-between text-[10px]"
            >
              {join.targetTable || "테이블 선택..."}
              <ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
            </Button>
          </PopoverTrigger>
          <PopoverContent
            className="p-0"
            style={{ width: "var(--radix-popover-trigger-width)" }}
            align="start"
          >
            <Command>
              <CommandInput placeholder="테이블 검색..." className="text-[10px]" />
              <CommandList>
                <CommandEmpty className="py-2 text-center text-[10px]">
                  테이블을 찾을 수 없습니다
                </CommandEmpty>
                <CommandGroup>
                  {selectableTables.map((t) => (
                    <CommandItem
                      key={t.tableName}
                      value={`${t.tableName} ${t.displayName || ""} ${t.description || ""}`}
                      onSelect={() => {
                        onUpdate({
                          targetTable: t.tableName,
                          sourceColumn: "",
                          targetColumn: "",
                          selectedTargetColumns: [],
                        });
                        setTableOpen(false);
                      }}
                      className="text-[10px]"
                    >
                      <Check
                        className={cn(
                          "mr-1 h-3 w-3",
                          join.targetTable === t.tableName ? "opacity-100" : "opacity-0"
                        )}
                      />
                      <div className="flex flex-col">
                        <span>{t.tableName}</span>
                        {(t.displayName || t.description) && (
                          <span className="text-[8px] text-muted-foreground">
                            {t.displayName || t.description}
                          </span>
                        )}
                      </div>
                    </CommandItem>
                  ))}
                </CommandGroup>
              </CommandList>
            </Command>
          </PopoverContent>
        </Popover>
      </div>

      {/* 대상 테이블 선택 후 컬럼 매칭 영역 */}
      {join.targetTable && (
        <>
          {loading ? (
            <p className="text-[9px] text-muted-foreground">컬럼 불러오는 중...</p>
          ) : (
            <>
              {/* 자동 매칭 결과 - 테이블 헤더 + 컬럼명만 표시 */}
              {autoMatches.length > 0 && (
                <div>
                  <span className="text-[9px] font-medium text-muted-foreground">
                    연결 조건 선택
                  </span>
                  {/* 테이블명 헤더 */}
                  <div className="mt-1 flex items-center gap-1 px-1.5 text-[8px] font-medium text-muted-foreground">
                    <div className="w-3.5 shrink-0" />
                    <span className="flex-1 truncate">{mainTableName}</span>
                    <span className="w-3 shrink-0" />
                    <span className="flex-1 truncate">{join.targetTable}</span>
                    <span className="w-12 shrink-0" />
                  </div>
                  {/* 매칭 행 */}
                  <div className="space-y-0.5">
                    {autoMatches.map((mc) => {
                      const isSelected =
                        join.sourceColumn === mc.name && join.targetColumn === mc.name;
                      return (
                        <button
                          key={mc.name}
                          type="button"
                          onClick={() => {
                            if (isSelected) {
                              onUpdate({ sourceColumn: "", targetColumn: "" });
                            } else {
                              onUpdate({
                                sourceColumn: mc.name,
                                targetColumn: mc.name,
                              });
                            }
                          }}
                          className={cn(
                            "flex w-full items-center gap-1 rounded px-1.5 py-0.5 text-left text-[10px] transition-colors",
                            isSelected
                              ? "bg-primary/10 text-primary"
                              : "hover:bg-muted"
                          )}
                        >
                          <div
                            className={cn(
                              "flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-sm border",
                              isSelected
                                ? "border-primary bg-primary"
                                : "border-muted-foreground/30"
                            )}
                          >
                            {isSelected && (
                              <Check className="h-2.5 w-2.5 text-primary-foreground" />
                            )}
                          </div>
                          <span className="flex-1 truncate">{mc.name}</span>
                          <span className="w-3 shrink-0 text-center text-muted-foreground">=</span>
                          <span className="flex-1 truncate">{mc.name}</span>
                          <span className="w-12 shrink-0 text-right text-[8px] text-muted-foreground">
                            {shortType(mc.type)}
                          </span>
                        </button>
                      );
                    })}
                  </div>
                </div>
              )}

              {autoMatches.length === 0 && (
                <p className="text-[9px] text-muted-foreground">
                  이름이 같은 컬럼이 없습니다. 아래에서 직접 지정하세요.
                </p>
              )}

              {/* 수동 매칭 (고급) */}
              {!isAutoMatch && (
                <div>
                  <span className="text-[9px] font-medium text-muted-foreground">
                    직접 지정
                  </span>
                  <div className="mt-1 flex items-center gap-1">
                    <Select
                      value={join.sourceColumn || ""}
                      onValueChange={(v) =>
                        onUpdate({ sourceColumn: v, targetColumn: "" })
                      }
                    >
                      <SelectTrigger className="h-7 flex-1 text-[10px]">
                        <SelectValue placeholder="메인 컬럼" />
                      </SelectTrigger>
                      <SelectContent>
                        {mainColumns.map((mc) => (
                          <SelectItem key={mc.name} value={mc.name} className="text-[10px]">
                            {mc.name}
                            <span className="ml-1 text-[8px] text-muted-foreground">
                              ({shortType(mc.type)})
                            </span>
                          </SelectItem>
                        ))}
                      </SelectContent>
                    </Select>

                    <span className="shrink-0 text-[10px] text-muted-foreground">=</span>

                    <Select
                      value={join.targetColumn || ""}
                      onValueChange={(v) => onUpdate({ targetColumn: v })}
                      disabled={!join.sourceColumn}
                    >
                      <SelectTrigger className="h-7 flex-1 text-[10px]">
                        <SelectValue
                          placeholder={
                            !join.sourceColumn ? "먼저 메인 선택" : "대상 컬럼"
                          }
                        />
                      </SelectTrigger>
                      <SelectContent>
                        {compatibleTargetCols.length > 0 ? (
                          compatibleTargetCols.map((tc) => (
                            <SelectItem key={tc.name} value={tc.name} className="text-[10px]">
                              {tc.name}
                              <span className="ml-1 text-[8px] text-muted-foreground">
                                ({shortType(tc.type)})
                              </span>
                            </SelectItem>
                          ))
                        ) : (
                          <SelectItem value="__none__" disabled className="text-[10px]">
                            호환 가능한 컬럼 없음
                          </SelectItem>
                        )}
                      </SelectContent>
                    </Select>
                  </div>
                </div>
              )}
            </>
          )}

          {/* 표시 방식 (JOIN 타입) - 자연어 + 설명 */}
          <div>
            <span className="text-[9px] font-medium text-muted-foreground">표시 방식</span>
            <div className="mt-1 flex gap-1">
              <button
                type="button"
                onClick={() => onUpdate({ joinType: "LEFT" })}
                className={cn(
                  "flex-1 rounded border px-2 py-1 transition-colors",
                  join.joinType === "LEFT"
                    ? "border-primary bg-primary/10 text-primary"
                    : "border-border hover:bg-muted"
                )}
              >
                <div className="text-[10px] font-medium">일치하지 않아도 표시</div>
                <div className="text-[7px] text-muted-foreground">
                  연결 데이터 없으면 빈칸
                </div>
              </button>
              <button
                type="button"
                onClick={() => onUpdate({ joinType: "INNER" })}
                className={cn(
                  "flex-1 rounded border px-2 py-1 transition-colors",
                  join.joinType === "INNER"
                    ? "border-primary bg-primary/10 text-primary"
                    : "border-border hover:bg-muted"
                )}
              >
                <div className="text-[10px] font-medium">일치하는 행만</div>
                <div className="text-[7px] text-muted-foreground">
                  연결 데이터 없으면 숨김
                </div>
              </button>
            </div>
          </div>

          {/* 가져올 컬럼 선택 (연결 조건 설정 후 활성화) */}
          {hasJoinCondition && !loading && (
            <div>
              <span className="text-[9px] font-medium text-muted-foreground">
                가져올 컬럼 ({selectedTargetCols.length}개 선택)
              </span>
              {pickableTargetCols.length > 0 ? (
                <div className="mt-1 max-h-[120px] space-y-0.5 overflow-y-auto">
                  {pickableTargetCols.map((tc) => {
                    const isChecked = selectedTargetCols.includes(tc.name);
                    return (
                      <button
                        key={tc.name}
                        type="button"
                        onClick={() => toggleTargetCol(tc.name)}
                        className={cn(
                          "flex w-full items-center gap-1.5 rounded px-1.5 py-0.5 text-left text-[10px] transition-colors",
                          isChecked ? "bg-primary/10 text-primary" : "hover:bg-muted"
                        )}
                      >
                        <div
                          className={cn(
                            "flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-sm border",
                            isChecked
                              ? "border-primary bg-primary"
                              : "border-muted-foreground/30"
                          )}
                        >
                          {isChecked && (
                            <Check className="h-2.5 w-2.5 text-primary-foreground" />
                          )}
                        </div>
                        <span className="flex-1 truncate">{tc.name}</span>
                        <span className="shrink-0 text-[8px] text-muted-foreground">
                          {shortType(tc.type)}
                        </span>
                      </button>
                    );
                  })}
                </div>
              ) : (
                <p className="mt-1 text-[9px] text-muted-foreground">
                  가져올 수 있는 컬럼이 없습니다
                </p>
              )}
            </div>
          )}
        </>
      )}
    </div>
  );
}

function StepJoinConfig({
  dataSource,
  tables,
  mainColumns,
  onChange,
}: {
  dataSource: CardListDataSource;
  tables: TableInfo[];
  mainColumns: ColumnInfo[];
  onChange: (dataSource: CardListDataSource) => void;
}) {
  const joins = dataSource.joins || [];

  const addJoin = () => {
    const newJoin: CardColumnJoin = {
      targetTable: "",
      joinType: "LEFT",
      sourceColumn: "",
      targetColumn: "",
    };
    onChange({ ...dataSource, joins: [...joins, newJoin] });
  };

  const removeJoin = (index: number) => {
    const next = joins.filter((_, i) => i !== index);
    onChange({ ...dataSource, joins: next });
  };

  const updateJoin = (index: number, partial: Partial<CardColumnJoin>) => {
    const next = joins.map((j, i) =>
      i === index ? { ...j, ...partial } : j
    );
    onChange({ ...dataSource, joins: next });
  };

  return (
    <div className="space-y-2">
      <p className="text-[10px] text-muted-foreground">
        다른 테이블의 데이터를 연결하여 함께 표시할 수 있습니다 (선택사항)
      </p>
      {joins.map((join, i) => (
        <JoinItem
          key={i}
          join={join}
          index={i}
          tables={tables}
          mainColumns={mainColumns}
          mainTableName={dataSource.tableName}
          onUpdate={(partial) => updateJoin(i, partial)}
          onRemove={() => removeJoin(i)}
        />
      ))}
      <Button
        variant="outline"
        size="sm"
        onClick={addJoin}
        className="h-7 w-full text-xs"
      >
        <Plus className="mr-1 h-3 w-3" />
        테이블 연결 추가
      </Button>
    </div>
  );
}

// ===== STEP 6-A: 리스트 컬럼 배치 =====

// 조인 테이블 컬럼 정보
interface JoinedColumnInfo {
  name: string; // "테이블명.컬럼명" 형태
  displayName: string; // 컬럼명만
  sourceTable: string; // 테이블명
}

function StepListLayout({
  listColumns,
  availableColumns,
  joinedColumns,
  onChange,
}: {
  listColumns: ListColumnConfig[];
  availableColumns: ColumnInfo[];
  joinedColumns: JoinedColumnInfo[];
  onChange: (listColumns: ListColumnConfig[]) => void;
}) {
  const widthBarRef = useRef<HTMLDivElement>(null);
  const isDraggingRef = useRef(false);
  const columnsRef = useRef(listColumns);
  columnsRef.current = listColumns;
  const [dragIdx, setDragIdx] = useState<number | null>(null);
  const [dragOverIdx, setDragOverIdx] = useState<number | null>(null);
  // 드래그 핸들에서만 draggable 활성화 (Select/Input 충돌 방지)
  const [draggableRow, setDraggableRow] = useState<number | null>(null);
  // 컬럼 전환 설정 펼침 인덱스
  const [expandedAltIdx, setExpandedAltIdx] = useState<number | null>(null);

  // 리스트에 현재 포함된 컬럼명 셋
  const listColumnNames = new Set(listColumns.map((c) => c.columnName));

  // 추가 가능한 컬럼: (메인 + 조인) 중 현재 리스트에 없는 것
  const addableColumns = [
    ...availableColumns
      .filter((c) => !listColumnNames.has(c.name))
      .map((c) => ({ value: c.name, label: c.name, source: "main" as const })),
    ...joinedColumns
      .filter((c) => !listColumnNames.has(c.name))
      .map((c) => ({
        value: c.name,
        label: `${c.displayName} (${c.sourceTable})`,
        source: "join" as const,
      })),
  ];

  // 컬럼 추가 (독립 헤더로 추가 시 다른 컬럼의 alternateColumns에서 제거)
  const addColumn = (columnValue: string) => {
    const joinCol = joinedColumns.find((c) => c.name === columnValue);
    const newCol: ListColumnConfig = {
      columnName: columnValue,
      label: joinCol?.displayName || columnValue,
    };
    // 다른 컬럼의 alternateColumns에서 이 컬럼 제거 (독립 헤더가 되므로)
    const cleaned = listColumns.map((col) => {
      const alts = col.alternateColumns;
      if (!alts || !alts.includes(columnValue)) return col;
      const newAlts = alts.filter((a) => a !== columnValue);
      return { ...col, alternateColumns: newAlts.length > 0 ? newAlts : undefined };
    });
    onChange([...cleaned, newCol]);
  };

  // 컬럼 삭제 (리스트에서만 삭제, STEP 3 체크 유지)
  const removeColumn = (index: number) => {
    const next = listColumns.filter((_, i) => i !== index);
    onChange(next);
    // 펼침 인덱스 초기화 (삭제로 인덱스가 밀리므로)
    setExpandedAltIdx(null);
  };

  // 전환 후보: (메인 + 조인) - 자기 자신 - 리스트에 독립 헤더로 있는 것
  const getAlternateCandidates = (currentColumnName: string) => {
    return [
      ...availableColumns
        .filter((c) => c.name !== currentColumnName && !listColumnNames.has(c.name))
        .map((c) => ({ value: c.name, label: c.name, source: "main" as const })),
      ...joinedColumns
        .filter((c) => c.name !== currentColumnName && !listColumnNames.has(c.name))
        .map((c) => ({
          value: c.name,
          label: c.displayName,
          source: "join" as const,
          sourceTable: c.sourceTable,
        })),
    ];
  };

  const updateColumn = (index: number, partial: Partial<ListColumnConfig>) => {
    const next = listColumns.map((col, i) =>
      i === index ? { ...col, ...partial } : col
    );
    onChange(next);
  };

  // 너비 드래그 핸들러
  const handleWidthDrag = useCallback(
    (e: React.MouseEvent, dividerIndex: number) => {
      e.preventDefault();
      isDraggingRef.current = true;
      const startX = e.clientX;
      const bar = widthBarRef.current;
      if (!bar) return;
      const barWidth = bar.offsetWidth;
      if (barWidth === 0) return;
      const cols = columnsRef.current;
      const startFrs = cols.map((c) => {
        const num = parseFloat(c.width || "1");
        return isNaN(num) || num <= 0 ? 1 : num;
      });
      const totalFr = startFrs.reduce((a, b) => a + b, 0);

      const onMove = (moveEvent: MouseEvent) => {
        const delta = moveEvent.clientX - startX;
        const frDelta = (delta / barWidth) * totalFr;
        const newFrs = [...startFrs];
        newFrs[dividerIndex] = Math.max(0.3, startFrs[dividerIndex] + frDelta);
        newFrs[dividerIndex + 1] = Math.max(
          0.3,
          startFrs[dividerIndex + 1] - frDelta
        );
        const next = columnsRef.current.map((col, i) => ({
          ...col,
          width: `${Math.round(newFrs[i] * 10) / 10}fr`,
        }));
        onChange(next);
      };
      const onUp = () => {
        isDraggingRef.current = false;
        document.removeEventListener("mousemove", onMove);
        document.removeEventListener("mouseup", onUp);
      };
      document.addEventListener("mousemove", onMove);
      document.addEventListener("mouseup", onUp);
    },
    [onChange]
  );

  // 순서 드래그앤드롭 - 핸들에서 mousedown 시에만 draggable 활성화
  const handleDragStart = (e: React.DragEvent, idx: number) => {
    e.dataTransfer.effectAllowed = "move";
    e.dataTransfer.setData("text/plain", String(idx));
    setDragIdx(idx);
  };

  const handleDragOver = (e: React.DragEvent, idx: number) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = "move";
    setDragOverIdx(idx);
  };

  const handleDrop = (e: React.DragEvent, idx: number) => {
    e.preventDefault();
    if (dragIdx === null || dragIdx === idx) {
      setDragIdx(null);
      setDragOverIdx(null);
      setDraggableRow(null);
      return;
    }
    const next = [...listColumns];
    const [moved] = next.splice(dragIdx, 1);
    next.splice(idx, 0, moved);
    onChange(next);
    setDragIdx(null);
    setDragOverIdx(null);
    setDraggableRow(null);
  };

  const handleDragEnd = () => {
    setDragIdx(null);
    setDragOverIdx(null);
    setDraggableRow(null);
  };

  if (listColumns.length === 0 && addableColumns.length === 0) {
    return (
      <p className="text-xs text-muted-foreground">
        컬럼을 먼저 선택하세요
      </p>
    );
  }

  return (
    <div className="space-y-2">
      {/* 컬럼 너비 드래그 바 */}
      <div
        ref={widthBarRef}
        className="flex h-6 select-none overflow-hidden rounded border"
      >
        {listColumns.map((col, i) => {
          const fr = parseFloat(col.width || "1") || 1;
          return (
            <Fragment key={col.columnName}>
              <div
                className="flex items-center justify-center bg-muted/30 text-[7px] text-muted-foreground"
                style={{ flex: fr }}
              >
                {col.label || col.columnName}
              </div>
              {i < listColumns.length - 1 && (
                <div
                  className="w-1 shrink-0 cursor-col-resize bg-border transition-colors hover:bg-primary"
                  onMouseDown={(e) => handleWidthDrag(e, i)}
                  title="드래그하여 너비 조정"
                />
              )}
            </Fragment>
          );
        })}
      </div>

      {/* 컬럼별 설정 (드래그 순서 + 컬럼 선택 + 라벨 + 정렬) */}
      <div className="space-y-1">
        {listColumns.map((col, i) => (
          <Fragment key={col.columnName}>
          <div
            draggable={draggableRow === i}
            onDragStart={(e) => handleDragStart(e, i)}
            onDragOver={(e) => handleDragOver(e, i)}
            onDrop={(e) => handleDrop(e, i)}
            onDragEnd={handleDragEnd}
            className={cn(
              "flex items-center gap-1 rounded px-1 py-0.5 transition-colors",
              dragIdx === i && "opacity-40",
              dragOverIdx === i && dragIdx !== i && "bg-primary/10 border-t-2 border-primary"
            )}
          >
            {/* 드래그 핸들 - mousedown 시에만 행 draggable 활성화 */}
            <div
              className="flex w-4 shrink-0 cursor-grab flex-col items-center justify-center active:cursor-grabbing"
              onMouseDown={() => setDraggableRow(i)}
              onMouseUp={() => setDraggableRow(null)}
            >
              <div className="flex flex-col gap-[2px]">
                <div className="h-[1.5px] w-2.5 rounded bg-muted-foreground/40" />
                <div className="h-[1.5px] w-2.5 rounded bg-muted-foreground/40" />
                <div className="h-[1.5px] w-2.5 rounded bg-muted-foreground/40" />
              </div>
            </div>

            {/* 컬럼 선택 드롭다운 (메인 + 조인 테이블 컬럼) */}
            <Select
              value={col.columnName}
              onValueChange={(v) => {
                const colInfo = availableColumns.find((c) => c.name === v);
                const joinInfo = joinedColumns.find((c) => c.name === v);
                updateColumn(i, {
                  columnName: v,
                  label: colInfo?.name || joinInfo?.displayName || v,
                  alternateColumns: undefined, // 컬럼 변경 시 전환 옵션 초기화
                });
              }}
            >
              <SelectTrigger className="h-7 w-24 text-[10px]">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                {availableColumns.length > 0 && (
                  <>
                    {availableColumns.map((ac) => (
                      <SelectItem
                        key={ac.name}
                        value={ac.name}
                        className="text-[10px]"
                      >
                        {ac.name}
                      </SelectItem>
                    ))}
                  </>
                )}
                {joinedColumns.length > 0 && (
                  <>
                    {joinedColumns.map((jc) => (
                      <SelectItem
                        key={jc.name}
                        value={jc.name}
                        className="text-[10px]"
                      >
                        <span>{jc.displayName}</span>
                        <span className="ml-1 text-[8px] text-muted-foreground">
                          ({jc.sourceTable})
                        </span>
                      </SelectItem>
                    ))}
                  </>
                )}
              </SelectContent>
            </Select>

            {/* 라벨 */}
            <Input
              value={col.label}
              onChange={(e) => updateColumn(i, { label: e.target.value })}
              placeholder="라벨"
              className="h-7 flex-1 text-[10px]"
            />

            {/* 정렬 */}
            <Select
              value={col.align || "left"}
              onValueChange={(v) =>
                updateColumn(i, { align: v as ListColumnConfig["align"] })
              }
            >
              <SelectTrigger className="h-7 w-14 text-[10px]">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="left" className="text-[10px]">
                  좌
                </SelectItem>
                <SelectItem value="center" className="text-[10px]">
                  중
                </SelectItem>
                <SelectItem value="right" className="text-[10px]">
                  우
                </SelectItem>
              </SelectContent>
            </Select>

            {/* 컬럼 전환 버튼 (전환 후보가 있을 때만) */}
            {getAlternateCandidates(col.columnName).length > 0 && (
              <button
                type="button"
                onClick={() =>
                  setExpandedAltIdx(expandedAltIdx === i ? null : i)
                }
                className={cn(
                  "flex h-7 w-7 shrink-0 items-center justify-center rounded text-[10px] transition-colors",
                  (col.alternateColumns || []).length > 0
                    ? "bg-primary/10 text-primary"
                    : "text-muted-foreground hover:bg-muted"
                )}
                title="컬럼 전환 설정"
              >
                <ChevronsUpDown className="h-3 w-3" />
              </button>
            )}

            {/* 컬럼 삭제 버튼 */}
            <button
              type="button"
              onClick={() => removeColumn(i)}
              className="flex h-7 w-7 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive transition-colors"
              title="컬럼 삭제"
            >
              <Trash2 className="h-3 w-3" />
            </button>
          </div>

          {/* 전환 가능 컬럼 (펼침 시만 표시, 메인+조인 중 리스트 미포함분) */}
          {expandedAltIdx === i && (() => {
            const candidates = getAlternateCandidates(col.columnName);
            if (candidates.length === 0) return null;
            return (
              <div className="ml-5 flex flex-wrap items-center gap-1 pb-1">
                <span className="shrink-0 text-[8px] text-muted-foreground">전환:</span>
                {candidates.map((cand) => {
                  const alts = col.alternateColumns || [];
                  const isAlt = alts.includes(cand.value);
                  return (
                    <button
                      key={cand.value}
                      type="button"
                      onClick={() => {
                        const newAlts = isAlt
                          ? alts.filter((a) => a !== cand.value)
                          : [...alts, cand.value];
                        updateColumn(i, {
                          alternateColumns: newAlts.length > 0 ? newAlts : undefined,
                        });
                      }}
                      className={cn(
                        "rounded border px-1.5 py-0.5 text-[8px] transition-colors",
                        isAlt
                          ? "border-primary bg-primary/10 text-primary"
                          : "border-border hover:bg-muted"
                      )}
                    >
                      {cand.label}
                      {cand.source === "join" && (
                        <span className="ml-0.5 text-[7px] text-muted-foreground">*</span>
                      )}
                    </button>
                  );
                })}
              </div>
            );
          })()}
        </Fragment>
        ))}
      </div>

      {/* 컬럼 추가 */}
      {addableColumns.length > 0 && (
        <Select onValueChange={addColumn}>
          <SelectTrigger className="h-7 w-full text-[10px]">
            <SelectValue placeholder="+ 컬럼 추가..." />
          </SelectTrigger>
          <SelectContent>
            {addableColumns.some((c) => c.source === "main") && (
              <>
                <SelectItem value="__main_header__" disabled className="text-[8px] font-medium text-muted-foreground">
                  메인 테이블
                </SelectItem>
                {addableColumns
                  .filter((c) => c.source === "main")
                  .map((c) => (
                    <SelectItem key={c.value} value={c.value} className="text-[10px]">
                      {c.label}
                    </SelectItem>
                  ))}
              </>
            )}
            {addableColumns.some((c) => c.source === "join") && (
              <>
                <SelectItem value="__join_header__" disabled className="text-[8px] font-medium text-muted-foreground">
                  조인 테이블
                </SelectItem>
                {addableColumns
                  .filter((c) => c.source === "join")
                  .map((c) => (
                    <SelectItem key={c.value} value={c.value} className="text-[10px]">
                      {c.label}
                    </SelectItem>
                  ))}
              </>
            )}
          </SelectContent>
        </Select>
      )}

      <p className="text-[8px] text-muted-foreground">
        행을 드래그하여 순서 변경 | 상단 바 경계를 드래그하여 너비 조정
      </p>
    </div>
  );
}

// ===== STEP 6-B: 시각적 카드 그리드 디자이너 =====

// fr 문자열을 숫자로 파싱 (예: "2fr" -> 2, "1fr" -> 1)
const parseFr = (v: string): number => {
  const num = parseFloat(v);
  return isNaN(num) || num <= 0 ? 1 : num;
};

// 카드 그리드 반응형 안전 제약
// - 6열 초과: 모바일(320px)에서 셀 30px 미만 → 텍스트 깨짐
// - 6행 초과: 카드 1장 높이 과도 → 스크롤 과다
// - gap 16px 초과: 셀 공간 부족
// - fr 0.3 미만: 셀 보이지 않음
const GRID_LIMITS = {
  cols: { min: 1, max: 6 },
  rows: { min: 1, max: 6 },
  gap: { min: 0, max: 16 },
  minFr: 0.3,
} as const;

// 행 높이 기본값 (px 기반 고정 높이)
const DEFAULT_ROW_HEIGHT = 32;
const MIN_ROW_HEIGHT = 24;

// px 문자열에서 숫자 추출 (예: "32px" → 32)
const parsePx = (v: string): number => {
  const num = parseInt(v);
  return isNaN(num) || num < MIN_ROW_HEIGHT ? DEFAULT_ROW_HEIGHT : num;
};

// fr → px 마이그레이션 (기존 저장 데이터 호환)
const migrateRowHeight = (v: string): string => {
  if (!v || v.endsWith("fr")) {
    return `${Math.round(parseFr(v) * DEFAULT_ROW_HEIGHT)}px`;
  }
  if (v.endsWith("px")) return v;
  // 단위 없는 숫자인 경우
  const num = parseInt(v);
  return `${isNaN(num) || num < MIN_ROW_HEIGHT ? DEFAULT_ROW_HEIGHT : num}px`;
};

function StepCardDesigner({
  cardGrid,
  columns,
  selectedColumns,
  onChange,
}: {
  cardGrid: CardGridConfig | undefined;
  columns: ColumnInfo[];
  selectedColumns: string[];
  onChange: (cardGrid: CardGridConfig) => void;
}) {
  // 셀에서 컬럼 선택 시 사용자가 선택한 컬럼만 표시
  const availableColumns = columns.filter((c) =>
    selectedColumns.includes(c.name)
  );
  const [selectedCellId, setSelectedCellId] = useState<string | null>(null);
  const [mergeMode, setMergeMode] = useState(false);
  const [mergeCellKeys, setMergeCellKeys] = useState<Set<string>>(new Set());
  const widthBarRef = useRef<HTMLDivElement>(null);
  const rowBarRef = useRef<HTMLDivElement>(null);
  const gridRef = useRef<HTMLDivElement>(null);
  const gridConfigRef = useRef<CardGridConfig | undefined>(undefined);
  const isDraggingRef = useRef(false);
  const [gridLines, setGridLines] = useState<{
    colLines: number[];
    rowLines: number[];
  }>({ colLines: [], rowLines: [] });

  // 기본 카드 그리드 (rowHeights는 px 기반 고정 높이)
  const rawGrid: CardGridConfig = cardGrid || {
    rows: 1,
    cols: 1,
    colWidths: ["1fr"],
    rowHeights: [`${DEFAULT_ROW_HEIGHT}px`],
    gap: 4,
    showBorder: true,
    cells: [],
  };

  // 기존 fr 데이터 → px 자동 마이그레이션 + 길이 정규화
  const migratedRowHeights = (
    rawGrid.rowHeights || Array(rawGrid.rows).fill(`${DEFAULT_ROW_HEIGHT}px`)
  ).map(migrateRowHeight);

  // colWidths/rowHeights 배열 길이와 cols/rows 수 불일치 보정
  const safeColWidths = rawGrid.colWidths || [];
  const normalizedColWidths =
    safeColWidths.length >= rawGrid.cols
      ? safeColWidths.slice(0, rawGrid.cols)
      : [
          ...safeColWidths,
          ...Array(rawGrid.cols - safeColWidths.length).fill("1fr"),
        ];
  const normalizedRowHeights =
    migratedRowHeights.length >= rawGrid.rows
      ? migratedRowHeights.slice(0, rawGrid.rows)
      : [
          ...migratedRowHeights,
          ...Array(rawGrid.rows - migratedRowHeights.length).fill(
            `${DEFAULT_ROW_HEIGHT}px`
          ),
        ];

  const grid: CardGridConfig = {
    ...rawGrid,
    colWidths: normalizedColWidths,
    rowHeights: normalizedRowHeights,
  };

  gridConfigRef.current = grid;

  const updateGrid = (partial: Partial<CardGridConfig>) => {
    onChange({ ...grid, ...partial });
  };

  // ---- 점유 맵 ----

  const buildOccupationMap = (): Record<string, string> => {
    const map: Record<string, string> = {};
    grid.cells.forEach((cell) => {
      const rs = Number(cell.rowSpan) || 1;
      const cs = Number(cell.colSpan) || 1;
      for (let r = cell.row; r < cell.row + rs; r++) {
        for (let c = cell.col; c < cell.col + cs; c++) {
          map[`${r}-${c}`] = cell.id;
        }
      }
    });
    return map;
  };

  const occupationMap = buildOccupationMap();

  const getCellByOrigin = (r: number, c: number) =>
    grid.cells.find((cell) => cell.row === r && cell.col === c);

  // ---- 셀 CRUD ----

  const addCellAt = (row: number, col: number) => {
    const newCell: CardCellDefinition = {
      id: `cell-${Date.now()}`,
      row,
      col,
      rowSpan: 1,
      colSpan: 1,
      columnName: "",
      type: "text",
    };
    updateGrid({ cells: [...grid.cells, newCell] });
    setSelectedCellId(newCell.id);
  };

  const removeCell = (id: string) => {
    updateGrid({ cells: grid.cells.filter((c) => c.id !== id) });
    if (selectedCellId === id) setSelectedCellId(null);
  };

  const updateCell = (id: string, partial: Partial<CardCellDefinition>) => {
    updateGrid({
      cells: grid.cells.map((c) => (c.id === id ? { ...c, ...partial } : c)),
    });
  };

  // ---- 병합 모드 ----

  const toggleMergeMode = () => {
    if (mergeMode) {
      setMergeMode(false);
      setMergeCellKeys(new Set());
    } else {
      setMergeMode(true);
      setMergeCellKeys(new Set());
      setSelectedCellId(null);
    }
  };

  const toggleMergeCell = (row: number, col: number) => {
    const key = `${row}-${col}`;
    if (occupationMap[key]) return; // 점유된 위치 무시
    const next = new Set(mergeCellKeys);
    if (next.has(key)) {
      next.delete(key);
    } else {
      next.add(key);
    }
    setMergeCellKeys(next);
  };

  const validateMergeSelection = (): {
    minRow: number;
    maxRow: number;
    minCol: number;
    maxCol: number;
  } | null => {
    if (mergeCellKeys.size < 2) return null;
    const positions = Array.from(mergeCellKeys).map((key) => {
      const [r, c] = key.split("-").map(Number);
      return { row: r, col: c };
    });
    const minRow = Math.min(...positions.map((p) => p.row));
    const maxRow = Math.max(...positions.map((p) => p.row));
    const minCol = Math.min(...positions.map((p) => p.col));
    const maxCol = Math.max(...positions.map((p) => p.col));
    const expectedCount = (maxRow - minRow + 1) * (maxCol - minCol + 1);
    if (mergeCellKeys.size !== expectedCount) return null;
    for (const key of mergeCellKeys) {
      if (occupationMap[key]) return null;
    }
    return { minRow, maxRow, minCol, maxCol };
  };

  const confirmMerge = () => {
    const bbox = validateMergeSelection();
    if (!bbox) return;
    const newCell: CardCellDefinition = {
      id: `cell-${Date.now()}`,
      row: bbox.minRow,
      col: bbox.minCol,
      rowSpan: bbox.maxRow - bbox.minRow + 1,
      colSpan: bbox.maxCol - bbox.minCol + 1,
      columnName: "",
      type: "text",
    };
    updateGrid({ cells: [...grid.cells, newCell] });
    setSelectedCellId(newCell.id);
    setMergeMode(false);
    setMergeCellKeys(new Set());
  };

  const cancelMerge = () => {
    setMergeMode(false);
    setMergeCellKeys(new Set());
  };

  const mergeValid = validateMergeSelection();

  // ---- 셀 분할 ----

  // 칸 나누기 (좌/우 분할 = 열 방향)
  const splitCellHorizontally = (cell: CardCellDefinition) => {
    const cs = Number(cell.colSpan) || 1;
    const rs = Number(cell.rowSpan) || 1;

    if (cs >= 2) {
      // colSpan 2 이상: 그리드 변경 없이 셀만 분할
      const leftSpan = Math.ceil(cs / 2);
      const rightSpan = cs - leftSpan;
      const newCell: CardCellDefinition = {
        id: `cell-${Date.now()}`,
        row: cell.row,
        col: cell.col + leftSpan,
        rowSpan: rs,
        colSpan: rightSpan,
        columnName: "",
        type: "text",
      };
      const updatedCells = grid.cells.map((c) =>
        c.id === cell.id ? { ...c, colSpan: leftSpan } : c
      );
      updateGrid({ cells: [...updatedCells, newCell] });
      setSelectedCellId(newCell.id);
    } else {
      // colSpan 1: 새 열 삽입하여 분할
      if (grid.cols >= GRID_LIMITS.cols.max) return;
      const insertPos = cell.col + 1;
      const updatedCells = grid.cells.map((c) => {
        if (c.id === cell.id) return c; // 원본 유지
        const cEnd = c.col + (Number(c.colSpan) || 1) - 1;
        if (c.col >= insertPos) return { ...c, col: c.col + 1 };
        if (cEnd >= insertPos)
          return { ...c, colSpan: (Number(c.colSpan) || 1) + 1 };
        return c;
      });
      const newCell: CardCellDefinition = {
        id: `cell-${Date.now()}`,
        row: cell.row,
        col: insertPos,
        rowSpan: rs,
        colSpan: 1,
        columnName: "",
        type: "text",
      };
      // 열 너비: 기존 열을 반으로 분할
      const colIdx = cell.col - 1;
      if (colIdx < 0 || colIdx >= grid.colWidths.length) return; // 범위 초과 방어
      const currentFr = parseFr(grid.colWidths[colIdx]);
      const halfFr = Math.max(GRID_LIMITS.minFr, currentFr / 2);
      const frStr = `${Math.round(halfFr * 10) / 10}fr`;
      const newWidths = [...grid.colWidths];
      newWidths[colIdx] = frStr;
      newWidths.splice(colIdx + 1, 0, frStr);
      updateGrid({
        cols: grid.cols + 1,
        colWidths: newWidths,
        cells: [...updatedCells, newCell],
      });
      setSelectedCellId(newCell.id);
    }
  };

  // 줄 나누기 (위/아래 분할 = 행 방향)
  const splitCellVertically = (cell: CardCellDefinition) => {
    const rs = Number(cell.rowSpan) || 1;
    const cs = Number(cell.colSpan) || 1;
    const heights = grid.rowHeights || Array(grid.rows).fill(`${DEFAULT_ROW_HEIGHT}px`);

    if (rs >= 2) {
      // rowSpan 2 이상: 그리드 변경 없이 셀만 분할
      const topSpan = Math.ceil(rs / 2);
      const bottomSpan = rs - topSpan;
      const newCell: CardCellDefinition = {
        id: `cell-${Date.now()}`,
        row: cell.row + topSpan,
        col: cell.col,
        rowSpan: bottomSpan,
        colSpan: cs,
        columnName: "",
        type: "text",
      };
      const updatedCells = grid.cells.map((c) =>
        c.id === cell.id ? { ...c, rowSpan: topSpan } : c
      );
      updateGrid({ cells: [...updatedCells, newCell] });
      setSelectedCellId(newCell.id);
    } else {
      // rowSpan 1: 새 행 삽입하여 분할 (기존 행 높이 유지, 새 행은 기본 높이)
      if (grid.rows >= GRID_LIMITS.rows.max) return;
      const insertPos = cell.row + 1;
      const updatedCells = grid.cells.map((c) => {
        if (c.id === cell.id) return c;
        const cEnd = c.row + (Number(c.rowSpan) || 1) - 1;
        if (c.row >= insertPos) return { ...c, row: c.row + 1 };
        if (cEnd >= insertPos)
          return { ...c, rowSpan: (Number(c.rowSpan) || 1) + 1 };
        return c;
      });
      const newCell: CardCellDefinition = {
        id: `cell-${Date.now()}`,
        row: insertPos,
        col: cell.col,
        rowSpan: 1,
        colSpan: cs,
        columnName: "",
        type: "text",
      };
      // 기존 행 높이 유지, 새 행은 기본 px 높이로 삽입
      const rowIdx = cell.row - 1;
      const newHeights = [...heights];
      newHeights.splice(rowIdx + 1, 0, `${DEFAULT_ROW_HEIGHT}px`);
      updateGrid({
        rows: grid.rows + 1,
        rowHeights: newHeights,
        cells: [...updatedCells, newCell],
      });
      setSelectedCellId(newCell.id);
    }
  };

  // ---- 클릭 핸들러 ----

  const handleEmptyCellClick = (row: number, col: number) => {
    if (mergeMode) {
      toggleMergeCell(row, col);
    } else {
      addCellAt(row, col);
    }
  };

  const handleCellClick = (cell: CardCellDefinition) => {
    if (mergeMode) return; // 병합 모드에서 기존 셀 클릭 무시
    setSelectedCellId(selectedCellId === cell.id ? null : cell.id);
  };

  // ---- 열 너비 드래그 (상단 바 - 일괄) ----

  const handleColDragStart = useCallback(
    (e: React.MouseEvent, dividerIndex: number) => {
      e.preventDefault();
      isDraggingRef.current = true;
      const startX = e.clientX;
      const bar = widthBarRef.current;
      if (!bar) return;
      const barWidth = bar.offsetWidth;
      if (barWidth === 0) return; // 0으로 나누기 방어
      const currentGrid = gridConfigRef.current;
      if (!currentGrid) return;
      const startFrs = (currentGrid.colWidths || []).map(parseFr);
      const totalFr = startFrs.reduce((a, b) => a + b, 0);

      const onMove = (moveEvent: MouseEvent) => {
        const delta = moveEvent.clientX - startX;
        const frDelta = (delta / barWidth) * totalFr;
        const newFrs = [...startFrs];
        newFrs[dividerIndex] = Math.max(
          GRID_LIMITS.minFr,
          startFrs[dividerIndex] + frDelta
        );
        newFrs[dividerIndex + 1] = Math.max(
          GRID_LIMITS.minFr,
          startFrs[dividerIndex + 1] - frDelta
        );
        const newWidths = newFrs.map(
          (fr) => `${Math.round(fr * 10) / 10}fr`
        );
        onChange({ ...currentGrid, colWidths: newWidths });
      };
      const onUp = () => {
        isDraggingRef.current = false;
        document.removeEventListener("mousemove", onMove);
        document.removeEventListener("mouseup", onUp);
      };
      document.addEventListener("mousemove", onMove);
      document.addEventListener("mouseup", onUp);
    },
    [onChange]
  );

  // ---- 행 높이 드래그 (좌측 바 - 일괄) ----

  const handleRowDragStart = useCallback(
    (e: React.MouseEvent, dividerIndex: number) => {
      e.preventDefault();
      isDraggingRef.current = true;
      const startY = e.clientY;
      const currentGrid = gridConfigRef.current;
      if (!currentGrid) return;
      // px 기반: 픽셀 델타를 직접 적용 (fr 변환 불필요 → 안정적)
      const heights = (
        currentGrid.rowHeights ||
        Array(currentGrid.rows).fill(`${DEFAULT_ROW_HEIGHT}px`)
      ).map(parsePx);
      if (dividerIndex < 0 || dividerIndex + 1 >= heights.length) return;

      const onMove = (moveEvent: MouseEvent) => {
        const delta = moveEvent.clientY - startY;
        const newHeights = [...heights];
        newHeights[dividerIndex] = Math.max(
          MIN_ROW_HEIGHT,
          heights[dividerIndex] + delta
        );
        newHeights[dividerIndex + 1] = Math.max(
          MIN_ROW_HEIGHT,
          heights[dividerIndex + 1] - delta
        );
        const newRowHeights = newHeights.map((h) => `${Math.round(h)}px`);
        onChange({ ...currentGrid, rowHeights: newRowHeights });
      };
      const onUp = () => {
        isDraggingRef.current = false;
        document.removeEventListener("mousemove", onMove);
        document.removeEventListener("mouseup", onUp);
      };
      document.addEventListener("mousemove", onMove);
      document.addEventListener("mouseup", onUp);
    },
    [onChange]
  );

  // ---- 내부 셀 경계 드래그 (개별) ----

  // 그리드 라인 위치 측정 (ResizeObserver)
  useEffect(() => {
    const gridEl = gridRef.current;
    if (!gridEl) return;

    const measure = () => {
      if (isDraggingRef.current) return;
      const style = window.getComputedStyle(gridEl);
      const colSizes = style.gridTemplateColumns
        .split(" ")
        .map(parseFloat)
        .filter((v) => !isNaN(v));
      const rowSizes = style.gridTemplateRows
        .split(" ")
        .map(parseFloat)
        .filter((v) => !isNaN(v));
      const gapSize =
        parseFloat(style.gap) || parseFloat(style.columnGap) || 0;

      const colLines: number[] = [];
      let x = 0;
      for (let i = 0; i < colSizes.length - 1; i++) {
        x += colSizes[i] + gapSize;
        colLines.push(x - gapSize / 2);
      }

      const rowLines: number[] = [];
      let y = 0;
      for (let i = 0; i < rowSizes.length - 1; i++) {
        y += rowSizes[i] + gapSize;
        rowLines.push(y - gapSize / 2);
      }

      setGridLines({ colLines, rowLines });
    };

    const observer = new ResizeObserver(measure);
    observer.observe(gridEl);
    measure();

    return () => observer.disconnect();
    // 배열 참조가 매 렌더 변경되므로, join으로 안정적인 값 비교
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [grid.colWidths.join(","), grid.rowHeights?.join(","), grid.gap, grid.cols, grid.rows]);

  // 경계선 가시성 (병합 셀 내부는 숨김)
  const isColLineVisible = (lineIdx: number): boolean => {
    const leftCol = lineIdx + 1;
    const rightCol = lineIdx + 2;
    for (let r = 1; r <= grid.rows; r++) {
      const left = occupationMap[`${r}-${leftCol}`];
      const right = occupationMap[`${r}-${rightCol}`];
      if (left !== right) return true;
      if (!left && !right) return true;
    }
    return false;
  };

  const isRowLineVisible = (lineIdx: number): boolean => {
    const topRow = lineIdx + 1;
    const bottomRow = lineIdx + 2;
    for (let c = 1; c <= grid.cols; c++) {
      const top = occupationMap[`${topRow}-${c}`];
      const bottom = occupationMap[`${bottomRow}-${c}`];
      if (top !== bottom) return true;
      if (!top && !bottom) return true;
    }
    return false;
  };

  // 내부 열 경계 드래그
  const handleInternalColDrag = useCallback(
    (e: React.MouseEvent, lineIdx: number) => {
      e.preventDefault();
      e.stopPropagation();
      isDraggingRef.current = true;
      const startX = e.clientX;
      const gridEl = gridRef.current;
      if (!gridEl) return;
      const gridWidth = gridEl.offsetWidth;
      if (gridWidth === 0) return; // 0으로 나누기 방어
      const currentGrid = gridConfigRef.current;
      if (!currentGrid) return;
      const startFrs = (currentGrid.colWidths || []).map(parseFr);
      const totalFr = startFrs.reduce((a, b) => a + b, 0);

      const onMove = (moveEvent: MouseEvent) => {
        const delta = moveEvent.clientX - startX;
        const frDelta = (delta / gridWidth) * totalFr;
        const newFrs = [...startFrs];
        newFrs[lineIdx] = Math.max(
          GRID_LIMITS.minFr,
          startFrs[lineIdx] + frDelta
        );
        newFrs[lineIdx + 1] = Math.max(
          GRID_LIMITS.minFr,
          startFrs[lineIdx + 1] - frDelta
        );
        const newWidths = newFrs.map(
          (fr) => `${Math.round(fr * 10) / 10}fr`
        );
        onChange({ ...currentGrid, colWidths: newWidths });
      };
      const onUp = () => {
        isDraggingRef.current = false;
        document.removeEventListener("mousemove", onMove);
        document.removeEventListener("mouseup", onUp);
      };
      document.addEventListener("mousemove", onMove);
      document.addEventListener("mouseup", onUp);
    },
    [onChange]
  );

  // 내부 행 경계 드래그 (px 기반 직접 조정)
  const handleInternalRowDrag = useCallback(
    (e: React.MouseEvent, lineIdx: number) => {
      e.preventDefault();
      e.stopPropagation();
      isDraggingRef.current = true;
      const startY = e.clientY;
      const currentGrid = gridConfigRef.current;
      if (!currentGrid) return;
      // px 기반: 픽셀 델타를 직접 적용
      const heights = (
        currentGrid.rowHeights ||
        Array(currentGrid.rows).fill(`${DEFAULT_ROW_HEIGHT}px`)
      ).map(parsePx);
      if (lineIdx < 0 || lineIdx + 1 >= heights.length) return;

      const onMove = (moveEvent: MouseEvent) => {
        const delta = moveEvent.clientY - startY;
        const newHeights = [...heights];
        newHeights[lineIdx] = Math.max(
          MIN_ROW_HEIGHT,
          heights[lineIdx] + delta
        );
        newHeights[lineIdx + 1] = Math.max(
          MIN_ROW_HEIGHT,
          heights[lineIdx + 1] - delta
        );
        const newRowHeights = newHeights.map((h) => `${Math.round(h)}px`);
        onChange({ ...currentGrid, rowHeights: newRowHeights });
      };
      const onUp = () => {
        isDraggingRef.current = false;
        document.removeEventListener("mousemove", onMove);
        document.removeEventListener("mouseup", onUp);
      };
      document.addEventListener("mousemove", onMove);
      document.addEventListener("mouseup", onUp);
    },
    [onChange]
  );

  // ---- 선택된 셀 ----

  const selectedCell = selectedCellId
    ? grid.cells.find((c) => c.id === selectedCellId)
    : null;

  useEffect(() => {
    if (selectedCellId && !grid.cells.find((c) => c.id === selectedCellId)) {
      setSelectedCellId(null);
    }
  }, [grid.cells, selectedCellId]);

  // ---- 그리드 위치 ----

  const gridPositions: { row: number; col: number }[] = [];
  for (let r = 1; r <= grid.rows; r++) {
    for (let c = 1; c <= grid.cols; c++) {
      gridPositions.push({ row: r, col: c });
    }
  }

  const rowHeightsArr = grid.rowHeights || Array(grid.rows).fill(`${DEFAULT_ROW_HEIGHT}px`);

  // ---- 바 그룹핑 (병합 셀 내부 경계는 하나로 묶음) ----

  type BarGroup = { startIdx: number; count: number; totalFr: number };

  const colGroups: BarGroup[] = (() => {
    const groups: BarGroup[] = [];
    if (grid.colWidths.length === 0) return groups; // 빈 배열 방어
    let cur: BarGroup = {
      startIdx: 0,
      count: 1,
      totalFr: parseFr(grid.colWidths[0]),
    };
    for (let i = 0; i < grid.cols - 1; i++) {
      if (isColLineVisible(i)) {
        groups.push(cur);
        cur = {
          startIdx: i + 1,
          count: 1,
          totalFr: parseFr(grid.colWidths[i + 1]),
        };
      } else {
        cur.count++;
        cur.totalFr += parseFr(grid.colWidths[i + 1]);
      }
    }
    groups.push(cur);
    return groups;
  })();

  const rowGroups: BarGroup[] = (() => {
    const groups: BarGroup[] = [];
    if (rowHeightsArr.length === 0) return groups; // 빈 배열 방어
    // totalFr 필드를 px 값의 합산으로 사용 (flex 비율로 활용)
    let cur: BarGroup = {
      startIdx: 0,
      count: 1,
      totalFr: parsePx(rowHeightsArr[0]),
    };
    for (let i = 0; i < grid.rows - 1; i++) {
      if (isRowLineVisible(i)) {
        groups.push(cur);
        cur = {
          startIdx: i + 1,
          count: 1,
          totalFr: parsePx(rowHeightsArr[i + 1]),
        };
      } else {
        cur.count++;
        cur.totalFr += parsePx(rowHeightsArr[i + 1]);
      }
    }
    groups.push(cur);
    return groups;
  })();

  return (
    <div className="space-y-2">
      {/* 인라인 툴바: 보더 + 간격 + 병합 + 나누기 */}
      <div className="flex flex-wrap items-center gap-1.5 rounded border bg-muted/30 px-2 py-1">
        <div className="flex items-center gap-1.5">
          <button
            onClick={() => updateGrid({ showBorder: !grid.showBorder })}
            className={cn(
              "h-5 rounded border px-1.5 text-[9px] transition-colors",
              grid.showBorder
                ? "border-primary bg-primary text-primary-foreground"
                : "border-border bg-background text-muted-foreground"
            )}
          >
            보더
          </button>
          <div className="flex items-center gap-0.5">
            <span className="text-[9px] text-muted-foreground">간격</span>
            <Button
              variant="ghost"
              size="sm"
              onClick={() =>
                updateGrid({
                  gap: Math.max(GRID_LIMITS.gap.min, grid.gap - 2),
                })
              }
              disabled={grid.gap <= GRID_LIMITS.gap.min}
              className="h-5 w-5 p-0"
            >
              <Minus className="h-2.5 w-2.5" />
            </Button>
            <span className="w-6 text-center text-[9px]">{grid.gap}px</span>
            <Button
              variant="ghost"
              size="sm"
              onClick={() =>
                updateGrid({
                  gap: Math.min(GRID_LIMITS.gap.max, grid.gap + 2),
                })
              }
              disabled={grid.gap >= GRID_LIMITS.gap.max}
              className="h-5 w-5 p-0"
            >
              <Plus className="h-2.5 w-2.5" />
            </Button>
          </div>
        </div>
        <button
          onClick={toggleMergeMode}
          className={cn(
            "h-5 rounded border px-1.5 text-[9px] transition-colors",
            mergeMode
              ? "border-primary bg-primary text-primary-foreground"
              : "border-border bg-background text-muted-foreground"
          )}
        >
          병합
        </button>
        <div className="mx-0.5 h-3 w-px bg-border" />
        <Button
          variant="outline"
          size="sm"
          onClick={() => {
            if (!selectedCell) return;
            splitCellHorizontally(selectedCell);
          }}
          disabled={
            !selectedCell ||
            (grid.cols >= GRID_LIMITS.cols.max &&
              (Number(selectedCell?.colSpan) || 1) <= 1)
          }
          className="h-5 px-1.5 text-[9px]"
        >
          칸 나누기
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() => {
            if (!selectedCell) return;
            splitCellVertically(selectedCell);
          }}
          disabled={
            !selectedCell ||
            (grid.rows >= GRID_LIMITS.rows.max &&
              (Number(selectedCell?.rowSpan) || 1) <= 1)
          }
          className="h-5 px-1.5 text-[9px]"
        >
          줄 나누기
        </Button>
      </div>

      {/* 병합 모드 안내 */}
      {mergeMode && (
        <div className="flex items-center gap-1 rounded border border-primary/50 bg-primary/5 px-2 py-1">
          <span className="flex-1 text-[9px] text-primary">
            {mergeCellKeys.size > 0
              ? `${mergeCellKeys.size}칸 선택됨${mergeValid ? " (병합 가능)" : " (직사각형으로 선택)"}`
              : "빈 셀을 클릭하여 선택"}
          </span>
          <Button
            variant="default"
            size="sm"
            onClick={confirmMerge}
            disabled={!mergeValid}
            className="h-5 px-2 text-[9px]"
          >
            확정
          </Button>
          <Button
            variant="ghost"
            size="sm"
            onClick={cancelMerge}
            className="h-5 px-1.5 text-[9px]"
          >
            취소
          </Button>
        </div>
      )}

      {/* 열 너비 드래그 바 (일괄 조정, 병합 트랙 묶음) */}
      <div className="flex items-center gap-1">
        <div className="w-4 shrink-0" />
        <div
          ref={widthBarRef}
          className="flex h-5 flex-1 select-none overflow-hidden rounded border"
        >
          {colGroups.map((group, gi) => (
            <Fragment key={gi}>
              <div
                className="flex items-center justify-center bg-muted/30 text-[7px] text-muted-foreground"
                style={{ flex: group.totalFr }}
              >
                {group.count > 1
                  ? `${Math.round(group.totalFr * 10) / 10}fr`
                  : grid.colWidths[group.startIdx]}
              </div>
              {gi < colGroups.length - 1 && (
                <div
                  className="w-1 shrink-0 cursor-col-resize bg-border transition-colors hover:bg-primary"
                  onMouseDown={(e) =>
                    handleColDragStart(e, group.startIdx + group.count - 1)
                  }
                  title="드래그하여 열 너비 일괄 조정"
                />
              )}
            </Fragment>
          ))}
        </div>
      </div>

      {/* 메인: 행 높이 바 (왼쪽) + 그리드 (오른쪽) */}
      <div className="flex gap-1">
        {/* 행 높이 드래그 바 (일괄 조정, 병합 트랙 묶음) */}
        <div
          ref={rowBarRef}
          className="flex w-4 shrink-0 select-none flex-col overflow-hidden rounded border"
        >
          {rowGroups.map((group, gi) => (
            <Fragment key={gi}>
              <div
                className="flex items-center justify-center bg-muted/30 text-[6px] text-muted-foreground"
                style={{ flex: group.totalFr }}
                title={
                  group.count > 1
                    ? `${Math.round(group.totalFr)}px`
                    : rowHeightsArr[group.startIdx]
                }
              >
                {Math.round(group.totalFr)}
              </div>
              {gi < rowGroups.length - 1 && (
                <div
                  className="h-1 shrink-0 cursor-row-resize bg-border transition-colors hover:bg-primary"
                  onMouseDown={(e) =>
                    handleRowDragStart(e, group.startIdx + group.count - 1)
                  }
                  title="드래그하여 행 높이 일괄 조정"
                />
              )}
            </Fragment>
          ))}
        </div>

        {/* 인터랙티브 그리드 + 내부 드래그 오버레이 */}
        <div className="relative flex-1">
          <div
            ref={gridRef}
            className="rounded border bg-muted/10 p-0.5"
            style={{
              display: "grid",
              gridTemplateColumns:
                grid.colWidths.length > 0
                  ? grid.colWidths
                      .map((w) => `minmax(30px, ${w})`)
                      .join(" ")
                  : "1fr",
              gridTemplateRows: rowHeightsArr.join(" "),
              gap: `${Number(grid.gap) || 0}px`,
            }}
          >
            {gridPositions.map(({ row, col }) => {
              const cellAtOrigin = getCellByOrigin(row, col);
              const occupiedBy = occupationMap[`${row}-${col}`];
              const isMergeSelected = mergeCellKeys.has(`${row}-${col}`);

              // span으로 점유된 위치
              if (occupiedBy && !cellAtOrigin) return null;

              // 셀 원점
              if (cellAtOrigin) {
                const isSelected = selectedCellId === cellAtOrigin.id;
                return (
                  <div
                    key={`${row}-${col}`}
                    className={cn(
                      "flex cursor-pointer items-center justify-center rounded p-0.5 transition-all",
                      isSelected
                        ? "bg-primary/20 ring-2 ring-primary"
                        : "bg-primary/10 hover:bg-primary/15"
                    )}
                    style={{
                      gridColumn: `${col} / span ${Number(cellAtOrigin.colSpan) || 1}`,
                      gridRow: `${row} / span ${Number(cellAtOrigin.rowSpan) || 1}`,
                      border: grid.showBorder
                        ? "1px solid hsl(var(--border))"
                        : "none",
                    }}
                    onClick={() => handleCellClick(cellAtOrigin)}
                  >
                    <div className="flex flex-col items-center gap-0.5 overflow-hidden text-center">
                      <span className="truncate text-[8px] font-medium text-primary">
                        {cellAtOrigin.columnName || "미지정"}
                      </span>
                      <span className="text-[6px] text-muted-foreground">
                        {cellAtOrigin.type}
                      </span>
                    </div>
                  </div>
                );
              }

              // 빈 위치
              return (
                <div
                  key={`${row}-${col}`}
                  className={cn(
                    "flex cursor-pointer items-center justify-center rounded border border-dashed transition-colors",
                    isMergeSelected
                      ? "border-primary bg-primary/20 text-primary"
                      : mergeMode
                        ? "border-primary/40 text-primary/40 hover:border-primary hover:bg-primary/10 hover:text-primary"
                        : "border-muted-foreground/30 text-muted-foreground/40 hover:border-primary/50 hover:bg-primary/5 hover:text-primary/60"
                  )}
                  style={{
                    gridColumn: `${col} / span 1`,
                    gridRow: `${row} / span 1`,
                  }}
                  onClick={() => handleEmptyCellClick(row, col)}
                  title={
                    mergeMode ? "클릭하여 병합 선택" : "클릭하여 셀 추가"
                  }
                >
                  {isMergeSelected ? (
                    <Check className="h-3 w-3" />
                  ) : (
                    <Plus className="h-3 w-3" />
                  )}
                </div>
              );
            })}
          </div>

          {/* 내부 경계 드래그 오버레이 (개별 조정) */}
          <div
            className="pointer-events-none absolute inset-0"
            style={{ padding: "2px" }}
          >
            {gridLines.colLines.map((x, i) => {
              if (!isColLineVisible(i)) return null;
              return (
                <div
                  key={`col-h-${i}`}
                  className="pointer-events-auto absolute top-0 bottom-0 z-10 cursor-col-resize transition-colors hover:bg-primary/30"
                  style={{ left: `${x - 3}px`, width: "6px" }}
                  onMouseDown={(e) => handleInternalColDrag(e, i)}
                  title="드래그하여 열 너비 개별 조정"
                />
              );
            })}
            {gridLines.rowLines.map((y, i) => {
              if (!isRowLineVisible(i)) return null;
              return (
                <div
                  key={`row-h-${i}`}
                  className="pointer-events-auto absolute left-0 right-0 z-10 cursor-row-resize transition-colors hover:bg-primary/30"
                  style={{ top: `${y - 3}px`, height: "6px" }}
                  onMouseDown={(e) => handleInternalRowDrag(e, i)}
                  title="드래그하여 행 높이 개별 조정"
                />
              );
            })}
          </div>
        </div>
      </div>

      {/* 선택된 셀 설정 패널 */}
      {selectedCell && !mergeMode && (
        <div className="space-y-2 rounded border bg-muted/20 p-2">
          <div className="flex items-center justify-between">
            <span className="text-[10px] font-medium">
              셀 (행{selectedCell.row} 열{selectedCell.col}
              {((Number(selectedCell.colSpan) || 1) > 1 ||
                (Number(selectedCell.rowSpan) || 1) > 1) &&
                `, ${Number(selectedCell.colSpan) || 1}x${Number(selectedCell.rowSpan) || 1}`}
              )
            </span>
            <Button
              variant="ghost"
              size="sm"
              onClick={() => removeCell(selectedCell.id)}
              className="h-5 w-5 p-0"
            >
              <Trash2 className="h-3 w-3 text-destructive" />
            </Button>
          </div>

          {/* 컬럼 + 타입 */}
          <div className="flex gap-1">
            <Select
              value={selectedCell.columnName || "none"}
              onValueChange={(v) =>
                updateCell(selectedCell.id, {
                  columnName: v === "none" ? "" : v,
                })
              }
            >
              <SelectTrigger className="h-7 flex-1 text-[10px]">
                <SelectValue placeholder="컬럼" />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="none" className="text-[10px]">
                  미지정
                </SelectItem>
                {availableColumns.map((col) => (
                  <SelectItem
                    key={col.name}
                    value={col.name}
                    className="text-[10px]"
                  >
                    {col.name}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
            <Select
              value={selectedCell.type}
              onValueChange={(v) =>
                updateCell(selectedCell.id, {
                  type: v as CardCellDefinition["type"],
                })
              }
            >
              <SelectTrigger className="h-7 w-20 text-[10px]">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="text" className="text-[10px]">
                  텍스트
                </SelectItem>
                <SelectItem value="image" className="text-[10px]">
                  이미지
                </SelectItem>
                <SelectItem value="badge" className="text-[10px]">
                  배지
                </SelectItem>
                <SelectItem value="button" className="text-[10px]">
                  버튼
                </SelectItem>
              </SelectContent>
            </Select>
          </div>

          {/* 라벨 + 라벨 위치 */}
          <div className="flex gap-1">
            <Input
              value={selectedCell.label || ""}
              onChange={(e) =>
                updateCell(selectedCell.id, { label: e.target.value })
              }
              placeholder="라벨 (선택)"
              className="h-7 flex-1 text-[10px]"
            />
            <Select
              value={selectedCell.labelPosition || "top"}
              onValueChange={(v) =>
                updateCell(selectedCell.id, {
                  labelPosition: v as "top" | "left",
                })
              }
            >
              <SelectTrigger className="h-7 w-16 text-[10px]">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="top" className="text-[10px]">
                  위
                </SelectItem>
                <SelectItem value="left" className="text-[10px]">
                  왼쪽
                </SelectItem>
              </SelectContent>
            </Select>
          </div>

          {/* 글자 크기 + 가로 정렬 + 세로 정렬 */}
          <div className="flex gap-1">
            <Select
              value={selectedCell.fontSize || "md"}
              onValueChange={(v) =>
                updateCell(selectedCell.id, {
                  fontSize: v as "sm" | "md" | "lg",
                })
              }
            >
              <SelectTrigger className="h-7 flex-1 text-[10px]">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="sm" className="text-[10px]">
                  작게
                </SelectItem>
                <SelectItem value="md" className="text-[10px]">
                  보통
                </SelectItem>
                <SelectItem value="lg" className="text-[10px]">
                  크게
                </SelectItem>
              </SelectContent>
            </Select>
            <Select
              value={selectedCell.align || "left"}
              onValueChange={(v) =>
                updateCell(selectedCell.id, {
                  align: v as "left" | "center" | "right",
                })
              }
            >
              <SelectTrigger className="h-7 flex-1 text-[10px]">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="left" className="text-[10px]">
                  좌측
                </SelectItem>
                <SelectItem value="center" className="text-[10px]">
                  중앙
                </SelectItem>
                <SelectItem value="right" className="text-[10px]">
                  우측
                </SelectItem>
              </SelectContent>
            </Select>
            <Select
              value={selectedCell.verticalAlign || "top"}
              onValueChange={(v) =>
                updateCell(selectedCell.id, {
                  verticalAlign: v as "top" | "middle" | "bottom",
                })
              }
            >
              <SelectTrigger className="h-7 flex-1 text-[10px]">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="top" className="text-[10px]">
                  상단
                </SelectItem>
                <SelectItem value="middle" className="text-[10px]">
                  중간
                </SelectItem>
                <SelectItem value="bottom" className="text-[10px]">
                  하단
                </SelectItem>
              </SelectContent>
            </Select>
          </div>

        </div>
      )}

      {/* 반응형 안내 */}
      <p className="text-[8px] text-muted-foreground">
        {grid.cols}열 x {grid.rows}행 (최대 {GRID_LIMITS.cols.max}x
        {GRID_LIMITS.rows.max}) | 상단/좌측: 일괄 | 셀 경계: 개별 조정
      </p>
    </div>
  );
}
