"use client";

/**
 * pop-card-list-v2 설정 패널 (3탭)
 *
 * 탭 1: 데이터 — 테이블/컬럼 선택, 조인, 정렬
 * 탭 2: 카드 디자인 — 열 수, 시각적 그리드 디자이너, 셀 클릭 시 타입별 상세 인라인
 * 탭 3: 동작 — 카드 선택 동작, 오버플로우, 카트
 */

import { useState, useEffect, useRef, useCallback, useMemo, 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,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Check, ChevronsUpDown, Plus, Minus, Trash2, ChevronDown, ChevronRight } 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 {
  PopCardListV2Config,
  CardGridConfigV2,
  CardCellDefinitionV2,
  CardCellType,
  CardListDataSource,
  CardColumnJoin,
  CardSortConfig,
  V2OverflowConfig,
  V2CardClickAction,
  V2CardClickModalConfig,
  ActionButtonUpdate,
  TimelineDataSource,
  StatusValueMapping,
  TimelineStatusSemantic,
  SelectModeButtonConfig,
  ActionButtonDef,
  ActionButtonShowCondition,
  ActionButtonClickAction,
} from "../types";
import type { ButtonVariant } from "../pop-button";
import {
  fetchTableList,
  fetchTableColumns,
  type TableInfo,
  type ColumnInfo,
} from "../pop-dashboard/utils/dataFetcher";

// ===== 컬럼 옵션 그룹 =====

interface ColumnOptionGroup {
  groupLabel: string;
  options: { value: string; label: string }[];
}

function renderColumnOptionGroups(groups: ColumnOptionGroup[]) {
  if (groups.length <= 1) {
    return groups.flatMap((g) =>
      g.options.map((o) => (
        <SelectItem key={o.value} value={o.value} className="text-[10px]">{o.label}</SelectItem>
      ))
    );
  }
  return groups
    .filter((g) => g.options.length > 0)
    .map((g) => (
      <SelectGroup key={g.groupLabel}>
        <SelectLabel className="text-[9px] font-semibold text-muted-foreground px-2 py-1">{g.groupLabel}</SelectLabel>
        {g.options.map((o) => (
          <SelectItem key={o.value} value={o.value} className="text-[10px]">{o.label}</SelectItem>
        ))}
      </SelectGroup>
    ));
}

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

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

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

const V2_DEFAULT_CONFIG: PopCardListV2Config = {
  dataSource: { tableName: "" },
  cardGrid: {
    rows: 1,
    cols: 1,
    colWidths: ["1fr"],
    rowHeights: ["32px"],
    gap: 4,
    showCellBorder: true,
    cells: [],
  },
  gridColumns: 3,
  cardGap: 8,
  scrollDirection: "vertical",
  overflow: { mode: "loadMore", visibleCount: 6, loadMoreCount: 6 },
  cardClickAction: "none",
};

// ===== 탭 정의 =====

type V2ConfigTab = "data" | "design" | "actions";

const TAB_LABELS: { id: V2ConfigTab; label: string }[] = [
  { id: "data", label: "데이터" },
  { id: "design", label: "카드 디자인" },
  { id: "actions", label: "동작" },
];

// ===== 셀 타입 라벨 =====

const V2_CELL_TYPE_LABELS: Record<CardCellType, { label: string; group: string }> = {
  text: { label: "텍스트", group: "기본" },
  field: { label: "필드 (라벨+값)", group: "기본" },
  image: { label: "이미지", group: "기본" },
  badge: { label: "배지", group: "기본" },
  button: { label: "버튼", group: "동작" },
  "number-input": { label: "숫자 입력", group: "입력" },
  "cart-button": { label: "담기 버튼", group: "입력" },
  "package-summary": { label: "포장 요약", group: "요약" },
  "status-badge": { label: "상태 배지", group: "표시" },
  timeline: { label: "타임라인", group: "표시" },
  "footer-status": { label: "하단 상태", group: "표시" },
  "action-buttons": { label: "액션 버튼", group: "동작" },
};

const CELL_TYPE_GROUPS = ["기본", "표시", "입력", "동작", "요약"] as const;

// ===== 그리드 유틸 =====

const parseFr = (v: string): number => {
  const num = parseFloat(v);
  return isNaN(num) || num <= 0 ? 1 : num;
};

const GRID_LIMITS = {
  cols: { min: 1, max: 6 },
  rows: { min: 1, max: 6 },
  gap: { min: 0, max: 16 },
  minFr: 0.3,
} as const;

const DEFAULT_ROW_HEIGHT = 32;
const MIN_ROW_HEIGHT = 24;

const parsePx = (v: string): number => {
  const num = parseInt(v);
  return isNaN(num) || num < MIN_ROW_HEIGHT ? DEFAULT_ROW_HEIGHT : num;
};

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`;
};

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 "ts";
  if (lower === "integer" || lower === "int4") return "int";
  if (lower === "bigint" || lower === "int8") return "bigint";
  if (lower === "numeric" || lower === "decimal") return "num";
  if (lower === "boolean" || lower === "bool") return "bool";
  if (lower === "date") return "date";
  if (lower === "jsonb" || lower === "json") return "json";
  return t.length > 8 ? t.slice(0, 6) + ".." : t;
};

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

export function PopCardListV2ConfigPanel({ config, onUpdate }: ConfigPanelProps) {
  const [tab, setTab] = useState<V2ConfigTab>("data");
  const [tables, setTables] = useState<TableInfo[]>([]);
  const [columns, setColumns] = useState<ColumnInfo[]>([]);
  const [selectedColumns, setSelectedColumns] = useState<string[]>([]);

  const cfg: PopCardListV2Config = {
    ...V2_DEFAULT_CONFIG,
    ...config,
    dataSource: { ...V2_DEFAULT_CONFIG.dataSource, ...config?.dataSource },
    cardGrid: { ...V2_DEFAULT_CONFIG.cardGrid, ...config?.cardGrid },
    overflow: { ...V2_DEFAULT_CONFIG.overflow, ...config?.overflow } as V2OverflowConfig,
  };

  const update = (partial: Partial<PopCardListV2Config>) => {
    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]);

  useEffect(() => {
    if (cfg.selectedColumns && cfg.selectedColumns.length > 0) {
      setSelectedColumns(cfg.selectedColumns);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [cfg.dataSource.tableName]);

  return (
    <div className="flex flex-col gap-3">
      {/* 탭 바 */}
      <div className="flex gap-1 rounded-md border bg-muted/30 p-0.5">
        {TAB_LABELS.map((t) => (
          <button
            key={t.id}
            onClick={() => setTab(t.id)}
            className={cn(
              "flex-1 rounded-sm py-1 text-center text-[10px] font-medium transition-colors",
              tab === t.id
                ? "bg-background text-foreground shadow-sm"
                : "text-muted-foreground hover:text-foreground"
            )}
          >
            {t.label}
          </button>
        ))}
      </div>

      {/* 탭 컨텐츠 */}
      {tab === "data" && (
        <TabData
          cfg={cfg}
          tables={tables}
          columns={columns}
          selectedColumns={selectedColumns}
          onTableChange={(tableName) => {
            setSelectedColumns([]);
            update({
              dataSource: { ...cfg.dataSource, tableName },
              selectedColumns: [],
              cardGrid: { ...cfg.cardGrid, cells: [] },
            });
          }}
          onColumnsChange={(cols) => {
            setSelectedColumns(cols);
            update({ selectedColumns: cols });
          }}
          onDataSourceChange={(dataSource) => update({ dataSource })}
          onSortChange={(sort) =>
            update({ dataSource: { ...cfg.dataSource, sort } })
          }
        />
      )}

      {tab === "design" && (
        <TabCardDesign
          cfg={cfg}
          columns={columns}
          selectedColumns={selectedColumns}
          tables={tables}
          onGridChange={(cardGrid) => update({ cardGrid })}
          onGridColumnsChange={(gridColumns) => update({ gridColumns })}
          onCardGapChange={(cardGap) => update({ cardGap })}
        />
      )}

      {tab === "actions" && (
        <TabActions
          cfg={cfg}
          onUpdate={update}
          columns={columns}
        />
      )}
    </div>
  );
}

// ===== 탭 1: 데이터 =====

function TabData({
  cfg,
  tables,
  columns,
  selectedColumns,
  onTableChange,
  onColumnsChange,
  onDataSourceChange,
  onSortChange,
}: {
  cfg: PopCardListV2Config;
  tables: TableInfo[];
  columns: ColumnInfo[];
  selectedColumns: string[];
  onTableChange: (tableName: string) => void;
  onColumnsChange: (cols: string[]) => void;
  onDataSourceChange: (ds: CardListDataSource) => void;
  onSortChange: (sort: CardSortConfig[] | undefined) => void;
}) {
  const [tableOpen, setTableOpen] = useState(false);
  const ds = cfg.dataSource;

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

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

  const sort = ds.sort?.[0];

  return (
    <div className="space-y-3">
      {/* 테이블 선택 */}
      <div>
        <Label className="text-xs">데이터 테이블</Label>
        <Popover open={tableOpen} onOpenChange={setTableOpen}>
          <PopoverTrigger asChild>
            <Button
              variant="outline"
              role="combobox"
              aria-expanded={tableOpen}
              className="mt-1 h-8 w-full justify-between text-xs font-normal"
            >
              {ds.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(""); setTableOpen(false); }}
                    className="text-xs"
                  >
                    <Check className={cn("mr-2 h-3 w-3", !ds.tableName ? "opacity-100" : "opacity-0")} />
                    선택 안 함
                  </CommandItem>
                  {tables.map((t) => (
                    <CommandItem
                      key={t.tableName}
                      value={`${t.tableName} ${t.displayName || ""}`}
                      onSelect={() => { onTableChange(t.tableName); setTableOpen(false); }}
                      className="text-xs"
                    >
                      <Check className={cn("mr-2 h-3 w-3", ds.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>

      {/* 컬럼 선택 */}
      {ds.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">
                  {shortType(col.type)}
                </span>
              </label>
            ))}
          </div>
        </div>
      )}

      {/* 조인 설정 (접이식) */}
      {ds.tableName && (
        <JoinSection
          dataSource={ds}
          tables={tables}
          mainColumns={columns}
          onChange={onDataSourceChange}
        />
      )}

      {/* 정렬 */}
      {ds.tableName && columns.length > 0 && (
        <div>
          <Label className="text-xs">정렬 기준</Label>
          <div className="mt-1 flex gap-1">
            <Select
              value={sort?.column || "__none__"}
              onValueChange={(v) =>
                onSortChange(v && v !== "__none__" ? [{ column: v, direction: sort?.direction || "desc" }] : undefined)
              }
            >
              <SelectTrigger className="h-7 flex-1 text-[10px]">
                <SelectValue placeholder="정렬 컬럼" />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="__none__" className="text-[10px]">없음</SelectItem>
                {columns.map((c) => (
                  <SelectItem key={c.name} value={c.name} className="text-[10px]">{c.name}</SelectItem>
                ))}
              </SelectContent>
            </Select>
            {sort?.column && (
              <Select
                value={sort.direction}
                onValueChange={(v) =>
                  onSortChange([{ column: sort.column, direction: v as "asc" | "desc" }])
                }
              >
                <SelectTrigger className="h-7 w-20 text-[10px]">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="asc" className="text-[10px]">오름차순</SelectItem>
                  <SelectItem value="desc" className="text-[10px]">내림차순</SelectItem>
                </SelectContent>
              </Select>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

// ===== 조인 섹션 =====

function JoinSection({
  dataSource,
  tables,
  mainColumns,
  onChange,
}: {
  dataSource: CardListDataSource;
  tables: TableInfo[];
  mainColumns: ColumnInfo[];
  onChange: (ds: CardListDataSource) => void;
}) {
  const [expanded, setExpanded] = useState((dataSource.joins?.length || 0) > 0);
  const joins = dataSource.joins || [];

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

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

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

  return (
    <div>
      <button
        type="button"
        onClick={() => setExpanded(!expanded)}
        className="flex w-full items-center gap-1 text-xs font-medium"
      >
        <span className={cn("transition-transform", expanded && "rotate-90")}>
          {">"}
        </span>
        조인 설정 ({joins.length}개)
      </button>
      {expanded && (
        <div className="mt-2 space-y-2">
          <p className="text-[10px] text-muted-foreground">
            다른 테이블의 데이터를 연결하여 함께 표시 (선택사항)
          </p>
          {joins.map((join, i) => (
            <JoinItemV2
              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>
      )}
    </div>
  );
}

// ===== 조인 아이템 =====

function JoinItemV2({
  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);

  useEffect(() => {
    if (!join.targetTable) { setTargetColumns([]); return; }
    fetchTableColumns(join.targetTable)
      .then(setTargetColumns)
      .catch(() => setTargetColumns([]));
  }, [join.targetTable]);

  const autoMatches = mainColumns.filter((mc) =>
    targetColumns.some((tc) => tc.name === mc.name && tc.type === mc.type)
  );

  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 next = selectedTargetCols.includes(colName)
      ? selectedTargetCols.filter((c) => c !== colName)
      : [...selectedTargetCols, 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>

      {/* 대상 테이블 */}
      <Popover open={tableOpen} onOpenChange={setTableOpen}>
        <PopoverTrigger asChild>
          <Button variant="outline" role="combobox" 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 || ""}`}
                    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")} />
                    {t.tableName}
                  </CommandItem>
                ))}
              </CommandGroup>
            </CommandList>
          </Command>
        </PopoverContent>
      </Popover>

      {/* 자동 매칭 */}
      {join.targetTable && autoMatches.length > 0 && (
        <div className="space-y-0.5">
          <span className="text-[9px] text-muted-foreground">연결 조건</span>
          {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-[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="text-[8px] text-muted-foreground">{shortType(mc.type)}</span>
              </button>
            );
          })}
        </div>
      )}

      {/* 수동 매칭 */}
      {join.targetTable && autoMatches.length === 0 && (
        <div className="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}</SelectItem>
              ))}
            </SelectContent>
          </Select>
          <span className="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="대상" /></SelectTrigger>
            <SelectContent>
              {targetColumns.map((tc) => (
                <SelectItem key={tc.name} value={tc.name} className="text-[10px]">{tc.name}</SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>
      )}

      {/* 표시 방식 */}
      {join.targetTable && (
        <div className="flex gap-1">
          {(["LEFT", "INNER"] as const).map((jt) => (
            <button
              key={jt}
              type="button"
              onClick={() => onUpdate({ joinType: jt })}
              className={cn(
                "flex-1 rounded border px-2 py-1 text-[10px] transition-colors",
                join.joinType === jt ? "border-primary bg-primary/10 text-primary" : "border-border hover:bg-muted"
              )}
            >
              {jt === "LEFT" ? "빈칸 허용" : "일치만"}
            </button>
          ))}
        </div>
      )}

      {/* 가져올 컬럼 */}
      {hasJoinCondition && pickableTargetCols.length > 0 && (
        <div>
          <span className="text-[9px] text-muted-foreground">가져올 컬럼 ({selectedTargetCols.length}개)</span>
          <div className="mt-1 max-h-[100px] 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-[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="text-[8px] text-muted-foreground">{shortType(tc.type)}</span>
                </button>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}

// ===== 탭 2: 카드 디자인 =====

function TabCardDesign({
  cfg,
  columns,
  selectedColumns,
  tables,
  onGridChange,
  onGridColumnsChange,
  onCardGapChange,
}: {
  cfg: PopCardListV2Config;
  columns: ColumnInfo[];
  selectedColumns: string[];
  tables: TableInfo[];
  onGridChange: (g: CardGridConfigV2) => void;
  onGridColumnsChange: (n: number) => void;
  onCardGapChange: (n: number) => void;
}) {
  const availableColumns = columns.filter((c) => selectedColumns.includes(c.name));
  const joinedColumns = (cfg.dataSource.joins || []).flatMap((j) =>
    (j.selectedTargetColumns || []).map((col) => ({
      name: `${j.targetTable}.${col}`,
      displayName: col,
      sourceTable: j.targetTable,
    }))
  );

  const [processColumns, setProcessColumns] = useState<ColumnInfo[]>([]);
  const timelineCell = cfg.cardGrid.cells.find((c) => c.type === "timeline" && c.timelineSource?.processTable);
  const processTableName = timelineCell?.timelineSource?.processTable || "";
  useEffect(() => {
    if (!processTableName) { setProcessColumns([]); return; }
    fetchTableColumns(processTableName)
      .then(setProcessColumns)
      .catch(() => setProcessColumns([]));
  }, [processTableName]);

  const columnOptionGroups: ColumnOptionGroup[] = [
    {
      groupLabel: `메인 (${cfg.dataSource.tableName || "테이블"})`,
      options: availableColumns.map((c) => ({ value: c.name, label: c.name })),
    },
    ...(joinedColumns.length > 0
      ? [{
          groupLabel: "조인",
          options: joinedColumns.map((c) => ({ value: c.name, label: `${c.displayName} (${c.sourceTable})` })),
        }]
      : []),
    ...(processColumns.length > 0
      ? [{
          groupLabel: `공정 (${processTableName})`,
          options: processColumns.map((c) => ({ value: `__process_${c.name}`, label: c.name })),
        }]
      : []),
  ];
  const allColumnOptions = columnOptionGroups.flatMap((g) => g.options);

  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 gridRef = useRef<HTMLDivElement>(null);
  const gridConfigRef = useRef<CardGridConfigV2 | undefined>(undefined);
  const isDraggingRef = useRef(false);
  const [gridLines, setGridLines] = useState<{ colLines: number[]; rowLines: number[] }>({ colLines: [], rowLines: [] });

  // 그리드 정규화
  const rawGrid = cfg.cardGrid;
  const migratedRowHeights = (rawGrid.rowHeights || Array(rawGrid.rows).fill(`${DEFAULT_ROW_HEIGHT}px`)).map(migrateRowHeight);
  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: CardGridConfigV2 = {
    ...rawGrid,
    colWidths: normalizedColWidths,
    rowHeights: normalizedRowHeights,
  };
  gridConfigRef.current = grid;

  const updateGrid = (partial: Partial<CardGridConfigV2>) => {
    onGridChange({ ...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: CardCellDefinitionV2 = {
      id: `cell-${Date.now()}`,
      row, col, rowSpan: 1, colSpan: 1,
      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<CardCellDefinitionV2>) => {
    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 validateMerge = (): { minRow: number; maxRow: number; minCol: number; maxCol: number } | null => {
    if (mergeCellKeys.size < 2) return null;
    const positions = Array.from(mergeCellKeys).map((k) => { const [r, c] = k.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));
    if (mergeCellKeys.size !== (maxRow - minRow + 1) * (maxCol - minCol + 1)) return null;
    for (const key of mergeCellKeys) { if (occupationMap[key]) return null; }
    return { minRow, maxRow, minCol, maxCol };
  };

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

  // 셀 분할
  const splitCellHorizontally = (cell: CardCellDefinitionV2) => {
    const cs = Number(cell.colSpan) || 1;
    const rs = Number(cell.rowSpan) || 1;
    if (cs >= 2) {
      const leftSpan = Math.ceil(cs / 2);
      const newCell: CardCellDefinitionV2 = { id: `cell-${Date.now()}`, row: cell.row, col: cell.col + leftSpan, rowSpan: rs, colSpan: cs - leftSpan, type: "text" };
      const updatedCells = grid.cells.map((c) => c.id === cell.id ? { ...c, colSpan: leftSpan } : c);
      updateGrid({ cells: [...updatedCells, newCell] });
      setSelectedCellId(newCell.id);
    } else {
      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: CardCellDefinitionV2 = { id: `cell-${Date.now()}`, row: cell.row, col: insertPos, rowSpan: rs, colSpan: 1, 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: CardCellDefinitionV2) => {
    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) {
      const topSpan = Math.ceil(rs / 2);
      const newCell: CardCellDefinitionV2 = { id: `cell-${Date.now()}`, row: cell.row + topSpan, col: cell.col, rowSpan: rs - topSpan, colSpan: cs, type: "text" };
      const updatedCells = grid.cells.map((c) => c.id === cell.id ? { ...c, rowSpan: topSpan } : c);
      updateGrid({ cells: [...updatedCells, newCell] });
      setSelectedCellId(newCell.id);
    } else {
      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: CardCellDefinitionV2 = { id: `cell-${Date.now()}`, row: insertPos, col: cell.col, rowSpan: 1, colSpan: cs, type: "text" };
      const newHeights = [...heights];
      newHeights.splice(cell.row - 1 + 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: CardCellDefinitionV2) => {
    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;
    const currentGrid = gridConfigRef.current;
    if (!currentGrid) return;
    const startFrs = (currentGrid.colWidths || []).map(parseFr);
    const totalFr = startFrs.reduce((a, b) => a + b, 0);

    const onMove = (me: MouseEvent) => {
      const delta = me.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);
      onGridChange({ ...currentGrid, colWidths: newFrs.map((fr) => `${Math.round(fr * 10) / 10}fr`) });
    };
    const onUp = () => { isDraggingRef.current = false; document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); };
    document.addEventListener("mousemove", onMove);
    document.addEventListener("mouseup", onUp);
  }, [onGridChange]);

  // 행 높이 드래그
  const handleRowDragStart = useCallback((e: React.MouseEvent, dividerIndex: number) => {
    e.preventDefault();
    isDraggingRef.current = true;
    const startY = e.clientY;
    const currentGrid = gridConfigRef.current;
    if (!currentGrid) return;
    const heights = (currentGrid.rowHeights || Array(currentGrid.rows).fill(`${DEFAULT_ROW_HEIGHT}px`)).map(parsePx);
    if (dividerIndex < 0 || dividerIndex + 1 >= heights.length) return;

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

  // 내부 셀 경계 드래그
  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) || 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();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [grid.colWidths.join(","), grid.rowHeights?.join(","), grid.gap, grid.cols, grid.rows]);

  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;
    const currentGrid = gridConfigRef.current;
    if (!currentGrid) return;
    const startFrs = (currentGrid.colWidths || []).map(parseFr);
    const totalFr = startFrs.reduce((a, b) => a + b, 0);
    const onMove = (me: MouseEvent) => {
      const delta = me.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);
      onGridChange({ ...currentGrid, colWidths: newFrs.map((fr) => `${Math.round(fr * 10) / 10}fr`) });
    };
    const onUp = () => { isDraggingRef.current = false; document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); };
    document.addEventListener("mousemove", onMove); document.addEventListener("mouseup", onUp);
  }, [onGridChange]);

  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;
    const heights = (currentGrid.rowHeights || Array(currentGrid.rows).fill(`${DEFAULT_ROW_HEIGHT}px`)).map(parsePx);
    if (lineIdx < 0 || lineIdx + 1 >= heights.length) return;
    const onMove = (me: MouseEvent) => {
      const delta = me.clientY - startY;
      const newH = [...heights];
      newH[lineIdx] = Math.max(MIN_ROW_HEIGHT, heights[lineIdx] + delta);
      newH[lineIdx + 1] = Math.max(MIN_ROW_HEIGHT, heights[lineIdx + 1] - delta);
      onGridChange({ ...currentGrid, rowHeights: newH.map((h) => `${Math.round(h)}px`) });
    };
    const onUp = () => { isDraggingRef.current = false; document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); };
    document.addEventListener("mousemove", onMove); document.addEventListener("mouseup", onUp);
  }, [onGridChange]);

  // 경계선 가시성
  const isColLineVisible = (lineIdx: number): boolean => {
    const leftCol = lineIdx + 1, rightCol = lineIdx + 2;
    for (let r = 1; r <= grid.rows; r++) {
      const left = occupationMap[`${r}-${leftCol}`], right = occupationMap[`${r}-${rightCol}`];
      if (left !== right || (!left && !right)) return true;
    }
    return false;
  };
  const isRowLineVisible = (lineIdx: number): boolean => {
    const topRow = lineIdx + 1, bottomRow = lineIdx + 2;
    for (let c = 1; c <= grid.cols; c++) {
      const top = occupationMap[`${topRow}-${c}`], bottom = occupationMap[`${bottomRow}-${c}`];
      if (top !== bottom || (!top && !bottom)) return true;
    }
    return false;
  };

  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 mergeValid = validateMerge();

  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;
    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 items-center gap-3">
        <div className="flex items-center gap-1">
          <span className="text-[9px] text-muted-foreground">열 수</span>
          <Button variant="ghost" size="sm" onClick={() => onGridColumnsChange(Math.max(1, (cfg.gridColumns || 3) - 1))} className="h-5 w-5 p-0"><Minus className="h-2.5 w-2.5" /></Button>
          <span className="w-4 text-center text-[10px]">{cfg.gridColumns || 3}</span>
          <Button variant="ghost" size="sm" onClick={() => onGridColumnsChange(Math.min(6, (cfg.gridColumns || 3) + 1))} className="h-5 w-5 p-0"><Plus className="h-2.5 w-2.5" /></Button>
        </div>
        <div className="flex items-center gap-1">
          <span className="text-[9px] text-muted-foreground">카드 간격</span>
          <Button variant="ghost" size="sm" onClick={() => onCardGapChange(Math.max(0, (cfg.cardGap || 8) - 4))} className="h-5 w-5 p-0"><Minus className="h-2.5 w-2.5" /></Button>
          <span className="w-6 text-center text-[9px]">{cfg.cardGap || 8}px</span>
          <Button variant="ghost" size="sm" onClick={() => onCardGapChange(Math.min(24, (cfg.cardGap || 8) + 4))} className="h-5 w-5 p-0"><Plus className="h-2.5 w-2.5" /></Button>
        </div>
      </div>

      {/* 인라인 툴바 */}
      <div className="flex flex-wrap items-center gap-1.5 rounded border bg-muted/30 px-2 py-1">
        <button
          onClick={() => updateGrid({ showCellBorder: !grid.showCellBorder })}
          className={cn("h-5 rounded border px-1.5 text-[9px] transition-colors", grid.showCellBorder ? "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>
        <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) 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) 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={() => { setMergeMode(false); setMergeCellKeys(new Set()); }} 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)} />
              )}
            </Fragment>
          ))}
        </div>
      </div>

      {/* 행 높이 바 + 그리드 */}
      <div className="flex gap-1">
        <div 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 }}>{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)} />
              )}
            </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}`);
              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.showCellBorder ? "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 || cellAtOrigin.label || "미지정"}</span>
                      <span className="text-[6px] text-muted-foreground">{V2_CELL_TYPE_LABELS[cellAtOrigin.type]?.label || 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" : "border-muted-foreground/30 text-muted-foreground/40 hover:border-primary/50 hover:bg-primary/5"
                  )}
                  style={{ gridColumn: `${col} / span 1`, gridRow: `${row} / span 1` }}
                  onClick={() => handleEmptyCellClick(row, col)}
                >
                  {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={`ch-${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)} />;
            })}
            {gridLines.rowLines.map((y, i) => {
              if (!isRowLineVisible(i)) return null;
              return <div key={`rh-${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)} />;
            })}
          </div>
        </div>
      </div>

      <p className="text-[8px] text-muted-foreground">
        {grid.cols}열 x {grid.rows}행 (최대 {GRID_LIMITS.cols.max}x{GRID_LIMITS.rows.max})
      </p>

      {/* 선택된 셀 설정 패널 */}
      {selectedCell && !mergeMode && (
        <CellDetailEditor
          cell={selectedCell}
          allCells={grid.cells}
          allColumnOptions={allColumnOptions}
          columnOptionGroups={columnOptionGroups}
          columns={columns}
          selectedColumns={selectedColumns}
          tables={tables}
          dataSource={cfg.dataSource}
          onUpdate={(partial) => updateCell(selectedCell.id, partial)}
          onRemove={() => removeCell(selectedCell.id)}
        />
      )}
    </div>
  );
}

// ===== 셀 상세 에디터 (타입별 인라인) =====

function CellDetailEditor({
  cell,
  allCells,
  allColumnOptions,
  columnOptionGroups,
  columns,
  selectedColumns,
  tables,
  dataSource,
  onUpdate,
  onRemove,
}: {
  cell: CardCellDefinitionV2;
  allCells: CardCellDefinitionV2[];
  allColumnOptions: { value: string; label: string }[];
  columnOptionGroups: ColumnOptionGroup[];
  columns: ColumnInfo[];
  selectedColumns: string[];
  tables: TableInfo[];
  dataSource: CardListDataSource;
  onUpdate: (partial: Partial<CardCellDefinitionV2>) => void;
  onRemove: () => void;
}) {
  const availableTableOptions = useMemo(() => {
    const opts: { value: string; label: string }[] = [];
    if (dataSource.tableName) {
      opts.push({ value: dataSource.tableName, label: `${dataSource.tableName} (메인)` });
    }
    for (const j of dataSource.joins || []) {
      if (j.targetTable) {
        opts.push({ value: j.targetTable, label: `${j.targetTable} (조인)` });
      }
    }
    const added = new Set(opts.map((o) => o.value));
    for (const c of allCells) {
      const pt = c.timelineSource?.processTable;
      if (pt && !added.has(pt)) {
        opts.push({ value: pt, label: `${pt} (타임라인)` });
        added.add(pt);
      }
    }
    return opts;
  }, [dataSource, allCells]);

  return (
    <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">
          셀 (행{cell.row} 열{cell.col}
          {((Number(cell.colSpan) || 1) > 1 || (Number(cell.rowSpan) || 1) > 1) && `, ${Number(cell.colSpan) || 1}x${Number(cell.rowSpan) || 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>

      {/* 컬럼 + 타입 */}
      <div className="flex gap-1">
        {cell.type !== "action-buttons" && (
          <Select value={cell.columnName || "none"} onValueChange={(v) => onUpdate({ columnName: v === "none" ? "" : v })}>
            <SelectTrigger className="h-7 flex-1 text-[10px]"><SelectValue placeholder="컬럼" /></SelectTrigger>
            <SelectContent>
              <SelectItem value="none" className="text-[10px]">미지정</SelectItem>
              {renderColumnOptionGroups(columnOptionGroups)}
            </SelectContent>
          </Select>
        )}
        <Select value={cell.type} onValueChange={(v) => onUpdate({ type: v as CardCellType })}>
          <SelectTrigger className={cn("h-7 text-[10px]", cell.type === "action-buttons" ? "flex-1" : "w-24")}><SelectValue /></SelectTrigger>
          <SelectContent>
            {CELL_TYPE_GROUPS.map((group) => {
              const types = (Object.entries(V2_CELL_TYPE_LABELS) as [CardCellType, { label: string; group: string }][]).filter(([, v]) => v.group === group);
              if (types.length === 0) return null;
              return (
                <Fragment key={group}>
                  <SelectItem value={`__group_${group}__`} disabled className="text-[8px] font-medium text-muted-foreground">{group}</SelectItem>
                  {types.map(([key, v]) => (
                    <SelectItem key={key} value={key} className="text-[10px]">{v.label}</SelectItem>
                  ))}
                </Fragment>
              );
            })}
          </SelectContent>
        </Select>
      </div>

      {/* 라벨 + 위치 */}
      <div className="flex gap-1">
        <Input value={cell.label || ""} onChange={(e) => onUpdate({ label: e.target.value })} placeholder="라벨 (선택)" className="h-7 flex-1 text-[10px]" />
        <Select value={cell.labelPosition || "top"} onValueChange={(v) => onUpdate({ 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={cell.fontSize || "md"} onValueChange={(v) => onUpdate({ fontSize: v as CardCellDefinitionV2["fontSize"] })}>
          <SelectTrigger className="h-7 flex-1 text-[10px]"><SelectValue /></SelectTrigger>
          <SelectContent>
            <SelectItem value="xs" className="text-[10px]">아주 작게</SelectItem>
            <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={cell.align || "left"} onValueChange={(v) => onUpdate({ align: v as CardCellDefinitionV2["align"] })}>
          <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={cell.verticalAlign || "top"} onValueChange={(v) => onUpdate({ verticalAlign: v as CardCellDefinitionV2["verticalAlign"] })}>
          <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>

      {/* 타입별 상세 설정 */}
      {cell.type === "status-badge" && <StatusMappingEditor cell={cell} allCells={allCells} onUpdate={onUpdate} />}
      {cell.type === "timeline" && <TimelineConfigEditor cell={cell} allColumnOptions={allColumnOptions} tables={tables} onUpdate={onUpdate} />}
      {cell.type === "action-buttons" && <ActionButtonsEditor cell={cell} allCells={allCells} allColumnOptions={allColumnOptions} columnOptionGroups={columnOptionGroups} availableTableOptions={availableTableOptions} onUpdate={onUpdate} />}
      {cell.type === "footer-status" && <FooterStatusEditor cell={cell} allColumnOptions={allColumnOptions} columnOptionGroups={columnOptionGroups} onUpdate={onUpdate} />}
      {cell.type === "field" && <FieldConfigEditor cell={cell} allColumnOptions={allColumnOptions} columnOptionGroups={columnOptionGroups} onUpdate={onUpdate} />}
      {cell.type === "number-input" && (
        <div className="space-y-1">
          <span className="text-[9px] font-medium text-muted-foreground">숫자 입력 설정</span>
          <div className="flex gap-1">
            <Input value={cell.inputUnit || ""} onChange={(e) => onUpdate({ inputUnit: e.target.value })} placeholder="단위 (EA)" className="h-7 flex-1 text-[10px]" />
            <Select value={cell.limitColumn || "__none__"} onValueChange={(v) => onUpdate({ limitColumn: v === "__none__" ? undefined : v })}>
              <SelectTrigger className="h-7 flex-1 text-[10px]"><SelectValue placeholder="제한 컬럼" /></SelectTrigger>
              <SelectContent>
                <SelectItem value="__none__" className="text-[10px]">없음</SelectItem>
                {renderColumnOptionGroups(columnOptionGroups)}
              </SelectContent>
            </Select>
          </div>
        </div>
      )}
      {cell.type === "cart-button" && (
        <div className="space-y-1">
          <span className="text-[9px] font-medium text-muted-foreground">담기 버튼 설정</span>
          <div className="flex gap-1">
            <Input value={cell.cartLabel || ""} onChange={(e) => onUpdate({ cartLabel: e.target.value })} placeholder="담기" className="h-7 flex-1 text-[10px]" />
            <Input value={cell.cartCancelLabel || ""} onChange={(e) => onUpdate({ cartCancelLabel: e.target.value })} placeholder="취소" className="h-7 flex-1 text-[10px]" />
          </div>
        </div>
      )}
    </div>
  );
}

// ===== 상태 배지 매핑 에디터 =====

const SEMANTIC_COLORS: Record<string, string> = {
  pending: "#64748b", active: "#3b82f6", done: "#10b981",
};

function StatusMappingEditor({
  cell,
  allCells,
  onUpdate,
}: {
  cell: CardCellDefinitionV2;
  allCells: CardCellDefinitionV2[];
  onUpdate: (partial: Partial<CardCellDefinitionV2>) => void;
}) {
  const statusMap = cell.statusMap || [];

  const timelineCell = allCells.find(
    (c) => c.type === "timeline" && c.timelineSource?.statusMappings?.length,
  );
  const hasTimeline = !!timelineCell;

  const loadFromTimeline = () => {
    const src = timelineCell?.timelineSource;
    if (!src?.statusMappings) return;
    const partial: Partial<CardCellDefinitionV2> = {
      statusMap: src.statusMappings.map((m) => ({
        value: m.dbValue,
        label: m.label,
        color: SEMANTIC_COLORS[m.semantic] || "#6b7280",
      })),
    };
    if (src.statusColumn) {
      partial.column = src.statusColumn;
    }
    onUpdate(partial);
  };

  const addMapping = () => {
    onUpdate({ statusMap: [...statusMap, { value: "", label: "", color: "#6b7280" }] });
  };

  const updateMapping = (index: number, partial: Partial<{ value: string; label: string; color: string }>) => {
    onUpdate({ statusMap: statusMap.map((m, i) => (i === index ? { ...m, ...partial } : m)) });
  };

  const removeMapping = (index: number) => {
    onUpdate({ statusMap: statusMap.filter((_, i) => i !== index) });
  };

  return (
    <div className="space-y-1">
      <div className="flex items-center justify-between">
        <span className="text-[9px] font-medium text-muted-foreground">상태값-색상 매핑</span>
        <div className="flex gap-1">
          {hasTimeline && (
            <Button variant="ghost" size="sm" onClick={loadFromTimeline} className="h-5 px-1.5 text-[9px]">
              타임라인 연동
            </Button>
          )}
          <Button variant="ghost" size="sm" onClick={addMapping} className="h-5 px-1.5 text-[9px]">
            <Plus className="mr-0.5 h-3 w-3" />추가
          </Button>
        </div>
      </div>
      {statusMap.map((m, i) => (
        <div key={i} className="flex items-center gap-1">
          <Input value={m.value} onChange={(e) => updateMapping(i, { value: e.target.value })} placeholder="값" className="h-6 flex-1 text-[10px]" />
          <Input value={m.label} onChange={(e) => updateMapping(i, { label: e.target.value })} placeholder="라벨" className="h-6 flex-1 text-[10px]" />
          <input type="color" value={m.color} onChange={(e) => updateMapping(i, { color: e.target.value })} className="h-6 w-8 cursor-pointer rounded border" />
          <Button variant="ghost" size="sm" onClick={() => removeMapping(i)} className="h-5 w-5 p-0">
            <Trash2 className="h-3 w-3 text-destructive" />
          </Button>
        </div>
      ))}
    </div>
  );
}

// ===== 타임라인 설정 =====

function TimelineConfigEditor({
  cell,
  allColumnOptions,
  tables,
  onUpdate,
}: {
  cell: CardCellDefinitionV2;
  allColumnOptions: { value: string; label: string }[];
  tables: TableInfo[];
  onUpdate: (partial: Partial<CardCellDefinitionV2>) => void;
}) {
  const src = cell.timelineSource || { processTable: "", foreignKey: "", seqColumn: "", nameColumn: "", statusColumn: "" };
  const [processColumns, setProcessColumns] = useState<ColumnInfo[]>([]);
  const [tableOpen, setTableOpen] = useState(false);

  useEffect(() => {
    if (!src.processTable) { setProcessColumns([]); return; }
    fetchTableColumns(src.processTable)
      .then(setProcessColumns)
      .catch(() => setProcessColumns([]));
  }, [src.processTable]);

  const updateSource = (partial: Partial<TimelineDataSource>) => {
    onUpdate({ timelineSource: { ...src, ...partial } });
  };

  const colOptions = processColumns.map((c) => ({ value: c.name, label: c.name }));

  return (
    <div className="space-y-2">
      <span className="text-[9px] font-medium text-muted-foreground">하위 데이터 소스</span>

      {/* 하위 테이블 선택 */}
      <div>
        <Label className="text-[9px] text-muted-foreground">하위 테이블</Label>
        <Popover open={tableOpen} onOpenChange={setTableOpen}>
          <PopoverTrigger asChild>
            <Button variant="outline" role="combobox" className="mt-0.5 h-7 w-full justify-between text-[10px] font-normal">
              {src.processTable || "테이블 선택"}
              <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="text-[10px]">테이블을 찾을 수 없습니다.</CommandEmpty>
                <CommandGroup>
                  {tables.map((t) => (
                    <CommandItem
                      key={t.tableName}
                      value={`${t.tableName} ${t.displayName || ""}`}
                      onSelect={() => {
                        updateSource({ processTable: t.tableName, foreignKey: "", seqColumn: "", nameColumn: "", statusColumn: "" });
                        setTableOpen(false);
                      }}
                      className="text-[10px]"
                    >
                      <Check className={cn("mr-1 h-3 w-3", src.processTable === t.tableName ? "opacity-100" : "opacity-0")} />
                      {t.displayName || t.tableName}
                    </CommandItem>
                  ))}
                </CommandGroup>
              </CommandList>
            </Command>
          </PopoverContent>
        </Popover>
      </div>

      {/* 컬럼 매핑 (하위 테이블 선택 후) */}
      {src.processTable && processColumns.length > 0 && (
        <div className="space-y-1">
          <Label className="text-[9px] text-muted-foreground">컬럼 매핑</Label>
          <div className="grid grid-cols-2 gap-1">
            <div>
              <span className="text-[8px] text-muted-foreground">연결 FK</span>
              <Select value={src.foreignKey || "__none__"} onValueChange={(v) => updateSource({ foreignKey: v === "__none__" ? "" : v })}>
                <SelectTrigger className="h-6 text-[9px]"><SelectValue placeholder="선택" /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__">미선택</SelectItem>
                  {colOptions.map((c) => <SelectItem key={c.value} value={c.value}>{c.label}</SelectItem>)}
                </SelectContent>
              </Select>
            </div>
            <div>
              <span className="text-[8px] text-muted-foreground">순서</span>
              <Select value={src.seqColumn || "__none__"} onValueChange={(v) => updateSource({ seqColumn: v === "__none__" ? "" : v })}>
                <SelectTrigger className="h-6 text-[9px]"><SelectValue placeholder="선택" /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__">미선택</SelectItem>
                  {colOptions.map((c) => <SelectItem key={c.value} value={c.value}>{c.label}</SelectItem>)}
                </SelectContent>
              </Select>
            </div>
            <div>
              <span className="text-[8px] text-muted-foreground">표시명</span>
              <Select value={src.nameColumn || "__none__"} onValueChange={(v) => updateSource({ nameColumn: v === "__none__" ? "" : v })}>
                <SelectTrigger className="h-6 text-[9px]"><SelectValue placeholder="선택" /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__">미선택</SelectItem>
                  {colOptions.map((c) => <SelectItem key={c.value} value={c.value}>{c.label}</SelectItem>)}
                </SelectContent>
              </Select>
            </div>
            <div>
              <span className="text-[8px] text-muted-foreground">상태</span>
              <Select value={src.statusColumn || "__none__"} onValueChange={(v) => updateSource({ statusColumn: v === "__none__" ? "" : v })}>
                <SelectTrigger className="h-6 text-[9px]"><SelectValue placeholder="선택" /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__">미선택</SelectItem>
                  {colOptions.map((c) => <SelectItem key={c.value} value={c.value}>{c.label}</SelectItem>)}
                </SelectContent>
              </Select>
            </div>
          </div>
        </div>
      )}

      {/* 상태 값 매핑 (동적 배열) */}
      {src.processTable && src.statusColumn && (
        <StatusMappingsEditor
          mappings={src.statusMappings || []}
          onChange={(mappings) => updateSource({ statusMappings: mappings })}
        />
      )}

      {/* 구분선 */}
      <div className="border-t pt-2">
        <span className="text-[9px] font-medium text-muted-foreground">표시 옵션</span>
      </div>

      <div className="flex items-center gap-1">
        <span className="w-20 shrink-0 text-[9px] text-muted-foreground">최대 표시 수</span>
        <Input
          type="number"
          min={3}
          max={10}
          value={cell.visibleCount || 5}
          onChange={(e) => onUpdate({ visibleCount: parseInt(e.target.value) || 5 })}
          className="h-7 w-16 text-[10px]"
        />
        <Select
          value={cell.timelinePriority || "before"}
          onValueChange={(v) => onUpdate({ timelinePriority: v as "before" | "after" })}
        >
          <SelectTrigger className="h-7 w-[72px] text-[10px]">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="before">전 우선</SelectItem>
            <SelectItem value="after">후 우선</SelectItem>
          </SelectContent>
        </Select>
      </div>
      <div className="flex items-center gap-1">
        <Label className="w-20 shrink-0 text-[9px] text-muted-foreground">현재 항목 강조</Label>
        <Switch
          checked={cell.currentHighlight !== false}
          onCheckedChange={(v) => onUpdate({ currentHighlight: v })}
        />
      </div>
      <div className="flex items-center gap-1">
        <Label className="w-20 shrink-0 text-[9px] text-muted-foreground">클릭시 상세</Label>
        <Switch
          checked={cell.showDetailModal !== false}
          onCheckedChange={(v) => onUpdate({ showDetailModal: v })}
        />
        <span className="text-[8px] text-muted-foreground">전체 목록 모달</span>
      </div>
    </div>
  );
}

// ===== 상태 값 매핑 에디터 (동적 배열) =====

const SEMANTIC_OPTIONS: { value: TimelineStatusSemantic; label: string }[] = [
  { value: "pending", label: "대기" },
  { value: "active", label: "진행" },
  { value: "done", label: "완료" },
];

const DEFAULT_STATUS_MAPPINGS: StatusValueMapping[] = [
  { dbValue: "waiting", label: "대기", semantic: "pending" },
  { dbValue: "accepted", label: "접수", semantic: "active" },
  { dbValue: "in_progress", label: "진행중", semantic: "active" },
  { dbValue: "completed", label: "완료", semantic: "done" },
];

function StatusMappingsEditor({
  mappings,
  onChange,
}: {
  mappings: StatusValueMapping[];
  onChange: (mappings: StatusValueMapping[]) => void;
}) {
  const addMapping = () => {
    onChange([...mappings, { dbValue: "", label: "", semantic: "pending" }]);
  };

  const updateMapping = (index: number, partial: Partial<StatusValueMapping>) => {
    onChange(mappings.map((m, i) => (i === index ? { ...m, ...partial } : m)));
  };

  const removeMapping = (index: number) => {
    onChange(mappings.filter((_, i) => i !== index));
  };

  const applyDefaults = () => {
    onChange([...DEFAULT_STATUS_MAPPINGS]);
  };

  return (
    <div className="space-y-1">
      <div className="flex items-center justify-between">
        <Label className="text-[9px] text-muted-foreground">상태 값 매핑</Label>
        <div className="flex gap-1">
          {mappings.length === 0 && (
            <Button variant="ghost" size="sm" onClick={applyDefaults} className="h-5 px-1.5 text-[9px]">
              기본값 적용
            </Button>
          )}
          <Button variant="ghost" size="sm" onClick={addMapping} className="h-5 px-1.5 text-[9px]">
            <Plus className="mr-0.5 h-3 w-3" />추가
          </Button>
        </div>
      </div>
      <p className="text-[8px] text-muted-foreground">DB 값, 화면 라벨, 의미(대기/진행/완료)를 매핑합니다.</p>
      {mappings.map((m, i) => (
        <div key={i} className="flex items-center gap-1">
          <Input
            value={m.dbValue}
            onChange={(e) => updateMapping(i, { dbValue: e.target.value })}
            placeholder="DB 값"
            className="h-6 flex-1 text-[10px]"
          />
          <Input
            value={m.label}
            onChange={(e) => updateMapping(i, { label: e.target.value })}
            placeholder="라벨"
            className="h-6 flex-1 text-[10px]"
          />
          <Select value={m.semantic} onValueChange={(v) => updateMapping(i, { semantic: v as TimelineStatusSemantic })}>
            <SelectTrigger className="h-6 w-16 text-[10px]"><SelectValue /></SelectTrigger>
            <SelectContent>
              {SEMANTIC_OPTIONS.map((o) => (
                <SelectItem key={o.value} value={o.value} className="text-[10px]">{o.label}</SelectItem>
              ))}
            </SelectContent>
          </Select>
          <label className="flex shrink-0 cursor-pointer items-center gap-0.5" title="기준 상태 (타임라인 강조 + 상태 판별 기준)">
            <input
              type="radio"
              name="isDerived"
              checked={!!m.isDerived}
              onChange={() => {
                const wasChecked = !!m.isDerived;
                onChange(mappings.map((item, idx) => ({
                  ...item,
                  isDerived: idx === i && !wasChecked ? true : undefined,
                })));
              }}
              className="h-3 w-3"
            />
            <span className="text-[8px] text-muted-foreground">기준</span>
          </label>
          <Button variant="ghost" size="sm" onClick={() => removeMapping(i)} className="h-5 w-5 p-0">
            <Trash2 className="h-3 w-3 text-destructive" />
          </Button>
        </div>
      ))}
    </div>
  );
}

// ===== 액션 버튼 에디터 (버튼 중심 구조) =====

function ActionButtonsEditor({
  cell,
  allCells,
  allColumnOptions,
  columnOptionGroups,
  availableTableOptions,
  onUpdate,
}: {
  cell: CardCellDefinitionV2;
  allCells: CardCellDefinitionV2[];
  allColumnOptions: { value: string; label: string }[];
  columnOptionGroups: ColumnOptionGroup[];
  availableTableOptions: { value: string; label: string }[];
  onUpdate: (partial: Partial<CardCellDefinitionV2>) => void;
}) {
  const buttons = cell.actionButtons || [];

  const statusOptions = useMemo(() => {
    const timelineCell = allCells.find(
      (c) => c.type === "timeline" && c.timelineSource?.statusMappings?.length,
    );
    return timelineCell?.timelineSource?.statusMappings?.map((m) => ({
      value: m.dbValue,
      label: m.label,
    })) || [];
  }, [allCells]);

  const updateButtons = (newBtns: ActionButtonDef[]) => {
    onUpdate({ actionButtons: newBtns });
  };

  const addButton = () => {
    updateButtons([...buttons, {
      label: "",
      variant: "default" as ButtonVariant,
      showCondition: { type: "always" },
      clickAction: { type: "immediate" },
    }]);
  };

  const updateBtn = (idx: number, partial: Partial<ActionButtonDef>) => {
    updateButtons(buttons.map((b, i) => (i === idx ? { ...b, ...partial } : b)));
  };

  const removeBtn = (idx: number) => {
    updateButtons(buttons.filter((_, i) => i !== idx));
  };

  const updateCondition = (idx: number, partial: Partial<ActionButtonShowCondition>) => {
    const btn = buttons[idx];
    updateBtn(idx, { showCondition: { ...(btn.showCondition || { type: "always" }), ...partial } });
  };

  // 다중 액션 체이닝 지원: clickActions 배열 우선, 없으면 clickAction 단일값 폴백
  const getActions = (btn: ActionButtonDef): ActionButtonClickAction[] => {
    if (btn.clickActions && btn.clickActions.length > 0) return btn.clickActions;
    return [btn.clickAction];
  };

  const setActions = (bIdx: number, actions: ActionButtonClickAction[]) => {
    updateBtn(bIdx, { clickActions: actions, clickAction: actions[0] || { type: "immediate" } });
  };

  const updateAction = (idx: number, aIdx: number, partial: Partial<ActionButtonClickAction>) => {
    const actions = [...getActions(buttons[idx])];
    actions[aIdx] = { ...actions[aIdx], ...partial };
    setActions(idx, actions);
  };

  const addAction = (bIdx: number) => {
    const actions = [...getActions(buttons[bIdx]), { type: "immediate" as const }];
    setActions(bIdx, actions);
  };

  const removeAction = (bIdx: number, aIdx: number) => {
    const actions = getActions(buttons[bIdx]).filter((_, i) => i !== aIdx);
    if (actions.length === 0) actions.push({ type: "immediate" as const });
    setActions(bIdx, actions);
  };

  const addActionUpdate = (bIdx: number, aIdx: number) => {
    const actions = getActions(buttons[bIdx]);
    const a = actions[aIdx];
    updateAction(bIdx, aIdx, { updates: [...(a.updates || []), { column: "", value: "", valueType: "static" as const }] });
  };

  const updateActionUpdate = (bIdx: number, aIdx: number, uIdx: number, partial: Partial<ActionButtonUpdate>) => {
    const a = getActions(buttons[bIdx])[aIdx];
    updateAction(bIdx, aIdx, { updates: (a.updates || []).map((u, i) => (i === uIdx ? { ...u, ...partial } : u)) });
  };

  const removeActionUpdate = (bIdx: number, aIdx: number, uIdx: number) => {
    const a = getActions(buttons[bIdx])[aIdx];
    updateAction(bIdx, aIdx, { updates: (a.updates || []).filter((_, i) => i !== uIdx) });
  };

  const addSelectModeBtn = (bIdx: number, aIdx: number) => {
    const a = getActions(buttons[bIdx])[aIdx];
    const smBtns = a.selectModeButtons || [];
    updateAction(bIdx, aIdx, { selectModeButtons: [...smBtns, { label: "", variant: "outline" as ButtonVariant, clickMode: "cancel-select" as const }] });
  };

  const updateSelectModeBtn = (bIdx: number, aIdx: number, smIdx: number, partial: Partial<SelectModeButtonConfig>) => {
    const a = getActions(buttons[bIdx])[aIdx];
    const smBtns = (a.selectModeButtons || []).map((s, i) => (i === smIdx ? { ...s, ...partial } : s));
    updateAction(bIdx, aIdx, { selectModeButtons: smBtns });
  };

  const removeSelectModeBtn = (bIdx: number, aIdx: number, smIdx: number) => {
    const a = getActions(buttons[bIdx])[aIdx];
    updateAction(bIdx, aIdx, { selectModeButtons: (a.selectModeButtons || []).filter((_, i) => i !== smIdx) });
  };

  const addSmBtnUpdate = (bIdx: number, aIdx: number, smIdx: number) => {
    const a = getActions(buttons[bIdx])[aIdx];
    const smBtns = [...(a.selectModeButtons || [])];
    smBtns[smIdx] = { ...smBtns[smIdx], updates: [...(smBtns[smIdx].updates || []), { column: "", value: "", valueType: "static" as const }] };
    updateAction(bIdx, aIdx, { selectModeButtons: smBtns });
  };

  const updateSmBtnUpdate = (bIdx: number, aIdx: number, smIdx: number, uIdx: number, partial: Partial<ActionButtonUpdate>) => {
    const a = getActions(buttons[bIdx])[aIdx];
    const smBtns = [...(a.selectModeButtons || [])];
    smBtns[smIdx] = { ...smBtns[smIdx], updates: (smBtns[smIdx].updates || []).map((u, i) => (i === uIdx ? { ...u, ...partial } : u)) };
    updateAction(bIdx, aIdx, { selectModeButtons: smBtns });
  };

  const removeSmBtnUpdate = (bIdx: number, aIdx: number, smIdx: number, uIdx: number) => {
    const a = getActions(buttons[bIdx])[aIdx];
    const smBtns = [...(a.selectModeButtons || [])];
    smBtns[smIdx] = { ...smBtns[smIdx], updates: (smBtns[smIdx].updates || []).filter((_, i) => i !== uIdx) };
    updateAction(bIdx, aIdx, { selectModeButtons: smBtns });
  };


  const storageKey = `action-btn-editor-${cell.row}-${cell.col}`;

  const [expandedBtns, setExpandedBtns] = useState<Set<number>>(() => {
    try {
      const saved = sessionStorage.getItem(`${storageKey}-btns`);
      return saved ? new Set(JSON.parse(saved) as number[]) : new Set();
    } catch { return new Set(); }
  });
  const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>(() => {
    try {
      const saved = sessionStorage.getItem(`${storageKey}-secs`);
      return saved ? JSON.parse(saved) : {};
    } catch { return {}; }
  });

  useEffect(() => {
    try { sessionStorage.setItem(`${storageKey}-btns`, JSON.stringify([...expandedBtns])); } catch {}
  }, [expandedBtns, storageKey]);

  useEffect(() => {
    try { sessionStorage.setItem(`${storageKey}-secs`, JSON.stringify(expandedSections)); } catch {}
  }, [expandedSections, storageKey]);

  const toggleBtn = (idx: number) => {
    setExpandedBtns((prev) => {
      const next = new Set(prev);
      if (next.has(idx)) next.delete(idx); else next.add(idx);
      return next;
    });
  };

  const toggleSection = (key: string) => {
    setExpandedSections((prev) => ({ ...prev, [key]: !prev[key] }));
  };

  const isSectionOpen = (key: string) => expandedSections[key] !== false;

  const ACTION_TYPE_LABELS: Record<string, string> = { immediate: "즉시 실행", "select-mode": "선택 후 실행", "modal-open": "모달 열기", "quantity-input": "수량 입력" };

  const getCondSummary = (btn: ActionButtonDef) => {
    const c = btn.showCondition;
    if (!c || c.type === "always") return "항상";
    if (c.type === "timeline-status") {
      const opt = statusOptions.find((o) => o.value === c.value);
      return opt ? opt.label : (c.value || "미설정");
    }
    if (c.type === "column-value") return `${c.column || "?"} = ${c.value || "?"}`;
    if (c.type === "owner-match") return `소유자(${c.column || "?"})`;
    return "항상";
  };

  const addButtonAndExpand = () => {
    addButton();
    setExpandedBtns((prev) => new Set([...prev, buttons.length]));
  };

  return (
    <div className="space-y-1.5">
      <div className="flex items-center justify-between">
        <span className="text-[9px] font-medium text-muted-foreground">버튼 규칙</span>
        <Button variant="ghost" size="sm" onClick={addButtonAndExpand} className="h-5 px-1.5 text-[9px]">
          <Plus className="mr-0.5 h-3 w-3" />추가
        </Button>
      </div>

      {buttons.length === 0 && (
        <p className="text-[8px] text-muted-foreground">버튼 규칙을 추가하세요. 상태별로 다른 버튼을 설정할 수 있습니다.</p>
      )}

      {buttons.map((btn, bi) => {
        const condType = btn.showCondition?.type || "always";
        const actions = getActions(btn);
        const isExpanded = expandedBtns.has(bi);
        const actionSummary = actions.map((a) => ACTION_TYPE_LABELS[a.type] || a.type).join(" -> ");

        return (
          <div key={bi} className="rounded border">
            {/* 접기/펼치기 헤더 */}
            <div
              className="flex cursor-pointer items-center gap-1 p-1.5 hover:bg-muted/40"
              onClick={() => toggleBtn(bi)}
            >
              {isExpanded
                ? <ChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
                : <ChevronRight className="h-3 w-3 shrink-0 text-muted-foreground" />}
              <span className="shrink-0 text-[9px] font-bold text-muted-foreground">#{bi + 1}</span>
              {isExpanded ? (
                <>
                  <Input
                    value={btn.label}
                    onChange={(e) => updateBtn(bi, { label: e.target.value })}
                    onClick={(e) => e.stopPropagation()}
                    placeholder="라벨"
                    className="h-6 flex-1 text-[10px]"
                  />
                  <Select value={btn.variant || "default"} onValueChange={(v) => updateBtn(bi, { variant: v as ButtonVariant })}>
                    <SelectTrigger className="h-6 w-16 text-[10px]" onClick={(e) => e.stopPropagation()}><SelectValue /></SelectTrigger>
                    <SelectContent>
                      <SelectItem value="default" className="text-[10px]">기본</SelectItem>
                      <SelectItem value="outline" className="text-[10px]">외곽</SelectItem>
                      <SelectItem value="destructive" className="text-[10px]">위험</SelectItem>
                    </SelectContent>
                  </Select>
                </>
              ) : (
                <span className="flex-1 truncate text-[9px] text-muted-foreground">
                  {btn.label || "(미입력)"} <span className="text-muted-foreground/60">|</span> {getCondSummary(btn)} <span className="text-muted-foreground/60">|</span> {actionSummary}
                </span>
              )}
              <Button
                variant="ghost" size="sm"
                onClick={(e) => { e.stopPropagation(); removeBtn(bi); }}
                className="h-5 w-5 shrink-0 p-0"
              >
                <Trash2 className="h-2.5 w-2.5 text-muted-foreground" />
              </Button>
            </div>

            {/* 펼쳐진 상세 */}
            {isExpanded && (
              <div className="space-y-0.5 border-t px-2 pb-2">
                {/* === 조건 섹션 === */}
                <div
                  className="flex cursor-pointer items-center gap-1 py-1 hover:bg-muted/30"
                  onClick={() => toggleSection(`${bi}-cond`)}
                >
                  {isSectionOpen(`${bi}-cond`)
                    ? <ChevronDown className="h-2.5 w-2.5 text-muted-foreground" />
                    : <ChevronRight className="h-2.5 w-2.5 text-muted-foreground" />}
                  <span className="text-[8px] font-medium text-muted-foreground">조건</span>
                  {!isSectionOpen(`${bi}-cond`) && (
                    <span className="truncate text-[8px] text-muted-foreground/70">{getCondSummary(btn)}</span>
                  )}
                </div>
                {isSectionOpen(`${bi}-cond`) && (
                  <div className="space-y-1 pl-3">
                    <div className="flex items-center gap-1">
                      <span className="w-10 shrink-0 text-[8px] text-muted-foreground">활성화</span>
                      <Select value={condType} onValueChange={(v) => updateCondition(bi, { type: v as ActionButtonShowCondition["type"] })}>
                        <SelectTrigger className="h-6 w-[80px] text-[10px]"><SelectValue /></SelectTrigger>
                        <SelectContent>
                          <SelectItem value="always" className="text-[10px]">항상</SelectItem>
                          <SelectItem value="timeline-status" className="text-[10px]">타임라인</SelectItem>
                          <SelectItem value="column-value" className="text-[10px]">카드 컬럼</SelectItem>
                          <SelectItem value="owner-match" className="text-[10px]">소유자 일치</SelectItem>
                        </SelectContent>
                      </Select>
                      {condType === "owner-match" && (
                        <Select
                          value={btn.showCondition?.column || "__none__"}
                          onValueChange={(v) => updateCondition(bi, { column: v === "__none__" ? "" : v })}
                        >
                          <SelectTrigger className="h-6 flex-1 text-[10px]"><SelectValue placeholder="컬럼" /></SelectTrigger>
                          <SelectContent>
                            <SelectItem value="__none__" className="text-[10px]">선택</SelectItem>
                            {renderColumnOptionGroups(columnOptionGroups)}
                          </SelectContent>
                        </Select>
                      )}
                      {condType === "timeline-status" && (
                        <Select
                          value={btn.showCondition?.value || "__none__"}
                          onValueChange={(v) => updateCondition(bi, { value: v === "__none__" ? "" : v })}
                        >
                          <SelectTrigger className="h-6 flex-1 text-[10px]"><SelectValue placeholder="상태 선택" /></SelectTrigger>
                          <SelectContent>
                            <SelectItem value="__none__" className="text-[10px]">선택</SelectItem>
                            {statusOptions.map((o) => (
                              <SelectItem key={o.value} value={o.value} className="text-[10px]">{o.label} ({o.value})</SelectItem>
                            ))}
                          </SelectContent>
                        </Select>
                      )}
                      {condType === "column-value" && (
                        <>
                          <Select
                            value={btn.showCondition?.column || "__none__"}
                            onValueChange={(v) => updateCondition(bi, { column: v === "__none__" ? "" : v })}
                          >
                            <SelectTrigger className="h-6 w-24 text-[10px]"><SelectValue placeholder="컬럼" /></SelectTrigger>
                            <SelectContent>
                              <SelectItem value="__none__" className="text-[10px]">선택</SelectItem>
                              {renderColumnOptionGroups(columnOptionGroups)}
                            </SelectContent>
                          </Select>
                          <Input
                            value={btn.showCondition?.value || ""}
                            onChange={(e) => updateCondition(bi, { value: e.target.value })}
                            placeholder="값"
                            className="h-6 w-20 text-[10px]"
                          />
                        </>
                      )}
                    </div>
                    {condType !== "always" && (
                      <div className="flex items-center gap-1">
                        <span className="w-10 shrink-0 text-[8px] text-muted-foreground">그 외</span>
                        <Select
                          value={btn.showCondition?.unmatchBehavior || "hidden"}
                          onValueChange={(v) => updateCondition(bi, { unmatchBehavior: v as "hidden" | "disabled" })}
                        >
                          <SelectTrigger className="h-6 w-[80px] text-[10px]"><SelectValue /></SelectTrigger>
                          <SelectContent>
                            <SelectItem value="hidden" className="text-[10px]">숨김</SelectItem>
                            <SelectItem value="disabled" className="text-[10px]">비활성화</SelectItem>
                          </SelectContent>
                        </Select>
                        <span className="text-[8px] text-muted-foreground">
                          {(btn.showCondition?.unmatchBehavior || "hidden") === "disabled"
                            ? "보이지만 클릭 불가"
                            : "버튼 안 보임"}
                        </span>
                      </div>
                    )}
                  </div>
                )}

                {/* === 실행 섹션 (다중 액션) === */}
                <div
                  className="flex cursor-pointer items-center gap-1 py-1 hover:bg-muted/30"
                  onClick={() => toggleSection(`${bi}-action`)}
                >
                  {isSectionOpen(`${bi}-action`)
                    ? <ChevronDown className="h-2.5 w-2.5 text-muted-foreground" />
                    : <ChevronRight className="h-2.5 w-2.5 text-muted-foreground" />}
                  <span className="text-[8px] font-medium text-muted-foreground">실행 ({actions.length})</span>
                  {!isSectionOpen(`${bi}-action`) && (
                    <span className="truncate text-[8px] text-muted-foreground/70">{actionSummary}</span>
                  )}
                </div>
                {isSectionOpen(`${bi}-action`) && (
                  <div className="space-y-1.5 pl-1">
                    {actions.map((action, ai) => {
                      const aType = action.type;
                      return (
                        <div key={ai} className="space-y-1 rounded border bg-muted/20 p-1.5">
                          <div className="flex items-center gap-1">
                            <span className="shrink-0 text-[8px] font-bold text-muted-foreground">#{ai + 1}</span>
                            <Select value={aType} onValueChange={(v) => updateAction(bi, ai, { type: v as ActionButtonClickAction["type"] })}>
                              <SelectTrigger className="h-6 flex-1 text-[10px]"><SelectValue /></SelectTrigger>
                              <SelectContent>
                                <SelectItem value="immediate" className="text-[10px]">즉시 실행</SelectItem>
                                <SelectItem value="quantity-input" className="text-[10px]">수량 입력</SelectItem>
                                <SelectItem value="select-mode" className="text-[10px]">선택 후 실행</SelectItem>
                                <SelectItem value="modal-open" className="text-[10px]">모달 열기</SelectItem>
                              </SelectContent>
                            </Select>
                            {actions.length > 1 && (
                              <Button variant="ghost" size="sm" onClick={() => removeAction(bi, ai)} className="h-5 w-5 shrink-0 p-0">
                                <Trash2 className="h-2.5 w-2.5 text-muted-foreground" />
                              </Button>
                            )}
                          </div>

                          {aType === "immediate" && (
                            <ImmediateActionEditor
                              action={action}
                              allColumnOptions={allColumnOptions}
                              availableTableOptions={availableTableOptions}
                              onAddUpdate={() => addActionUpdate(bi, ai)}
                              onUpdateUpdate={(ui, p) => updateActionUpdate(bi, ai, ui, p)}
                              onRemoveUpdate={(ui) => removeActionUpdate(bi, ai, ui)}
                              onUpdateAction={(p) => updateAction(bi, ai, p)}
                            />
                          )}

                          {aType === "quantity-input" && (
                            <div className="space-y-1.5">
                              <ImmediateActionEditor
                                action={action}
                                allColumnOptions={allColumnOptions}
                                availableTableOptions={availableTableOptions}
                                onAddUpdate={() => addActionUpdate(bi, ai)}
                                onUpdateUpdate={(ui, p) => updateActionUpdate(bi, ai, ui, p)}
                                onRemoveUpdate={(ui) => removeActionUpdate(bi, ai, ui)}
                                onUpdateAction={(p) => updateAction(bi, ai, p)}
                              />
                              <div className="rounded border bg-background/50 p-1.5 space-y-1">
                                <span className="text-[8px] font-medium text-muted-foreground">수량 모달 설정</span>
                                <div className="flex items-center gap-1">
                                  <span className="w-14 shrink-0 text-[8px] text-muted-foreground">최대값 컬럼</span>
                                  <Input
                                    value={action.quantityInput?.maxColumn || ""}
                                    onChange={(e) => updateAction(bi, ai, { quantityInput: { ...action.quantityInput, maxColumn: e.target.value } })}
                                    placeholder="예: qty"
                                    className="h-6 flex-1 text-[10px]"
                                  />
                                </div>
                                <div className="flex items-center gap-1">
                                  <span className="w-14 shrink-0 text-[8px] text-muted-foreground">현재값 컬럼</span>
                                  <Input
                                    value={action.quantityInput?.currentColumn || ""}
                                    onChange={(e) => updateAction(bi, ai, { quantityInput: { ...action.quantityInput, currentColumn: e.target.value } })}
                                    placeholder="예: input_qty"
                                    className="h-6 flex-1 text-[10px]"
                                  />
                                </div>
                                <div className="flex items-center gap-1">
                                  <span className="w-14 shrink-0 text-[8px] text-muted-foreground">단위</span>
                                  <Input
                                    value={action.quantityInput?.unit || ""}
                                    onChange={(e) => updateAction(bi, ai, { quantityInput: { ...action.quantityInput, unit: e.target.value } })}
                                    placeholder="예: EA"
                                    className="h-6 w-20 text-[10px]"
                                  />
                                </div>
                              </div>
                            </div>
                          )}

                          {aType === "select-mode" && (
                            <div className="space-y-1.5">
                              <div className="flex items-center justify-between">
                                <span className="text-[8px] font-medium text-muted-foreground">로직 순서</span>
                                <Button variant="ghost" size="sm" onClick={() => addSelectModeBtn(bi, ai)} className="h-4 px-1 text-[8px]">
                                  <Plus className="mr-0.5 h-2.5 w-2.5" />추가
                                </Button>
                              </div>
                              {(action.selectModeButtons || []).map((smBtn, si) => (
                                <div key={si} className="space-y-1 rounded border bg-muted/30 p-1.5">
                                  <div className="flex items-center gap-1">
                                    <Input value={smBtn.label} onChange={(e) => updateSelectModeBtn(bi, ai, si, { label: e.target.value })} placeholder="라벨" className="h-6 flex-1 text-[10px]" />
                                    <Select value={smBtn.variant || "outline"} onValueChange={(v) => updateSelectModeBtn(bi, ai, si, { variant: v as ButtonVariant })}>
                                      <SelectTrigger className="h-6 w-14 text-[10px]"><SelectValue /></SelectTrigger>
                                      <SelectContent>
                                        <SelectItem value="default" className="text-[10px]">기본</SelectItem>
                                        <SelectItem value="outline" className="text-[10px]">외곽</SelectItem>
                                        <SelectItem value="destructive" className="text-[10px]">위험</SelectItem>
                                      </SelectContent>
                                    </Select>
                                    <Button variant="ghost" size="sm" onClick={() => removeSelectModeBtn(bi, ai, si)} className="h-5 w-5 p-0">
                                      <Trash2 className="h-2.5 w-2.5 text-muted-foreground" />
                                    </Button>
                                  </div>
                                  <div className="flex items-center gap-1">
                                    <span className="w-8 shrink-0 text-[8px] text-muted-foreground">동작</span>
                                    <Select value={smBtn.clickMode} onValueChange={(v) => updateSelectModeBtn(bi, ai, si, { clickMode: v as SelectModeButtonConfig["clickMode"] })}>
                                      <SelectTrigger className="h-6 flex-1 text-[10px]"><SelectValue /></SelectTrigger>
                                      <SelectContent>
                                        <SelectItem value="cancel-select" className="text-[10px]">선택 취소</SelectItem>
                                        <SelectItem value="status-change" className="text-[10px]">상태 변경</SelectItem>
                                        <SelectItem value="modal-open" className="text-[10px]">모달 열기</SelectItem>
                                      </SelectContent>
                                    </Select>
                                  </div>
                                  {smBtn.clickMode === "status-change" && (
                                    <ImmediateActionEditor
                                      action={{ type: "immediate", targetTable: smBtn.targetTable, updates: smBtn.updates, confirmMessage: smBtn.confirmMessage }}
                                      allColumnOptions={allColumnOptions}
                                      availableTableOptions={availableTableOptions}
                                      onAddUpdate={() => addSmBtnUpdate(bi, ai, si)}
                                      onUpdateUpdate={(ui, p) => updateSmBtnUpdate(bi, ai, si, ui, p)}
                                      onRemoveUpdate={(ui) => removeSmBtnUpdate(bi, ai, si, ui)}
                                      onUpdateAction={(p) => updateSelectModeBtn(bi, ai, si, { targetTable: p.targetTable ?? smBtn.targetTable, confirmMessage: p.confirmMessage ?? smBtn.confirmMessage })}
                                    />
                                  )}
                                  {smBtn.clickMode === "modal-open" && (
                                    <div className="flex items-center gap-1">
                                      <span className="w-10 shrink-0 text-[8px] text-muted-foreground">POP 화면</span>
                                      <Input
                                        value={smBtn.modalScreenId || ""}
                                        onChange={(e) => updateSelectModeBtn(bi, ai, si, { modalScreenId: e.target.value })}
                                        placeholder="화면 ID (예: 4481)"
                                        className="h-6 flex-1 text-[10px]"
                                      />
                                    </div>
                                  )}
                                </div>
                              ))}
                              {(!action.selectModeButtons || action.selectModeButtons.length === 0) && (
                                <p className="text-[8px] text-muted-foreground">로직 순서를 추가하세요.</p>
                              )}
                            </div>
                          )}

                          {aType === "modal-open" && (
                            <div className="flex items-center gap-1">
                              <span className="w-10 shrink-0 text-[8px] text-muted-foreground">POP 화면</span>
                              <Input
                                value={action.modalScreenId || ""}
                                onChange={(e) => updateAction(bi, ai, { modalScreenId: e.target.value })}
                                placeholder="화면 ID (예: 4481)"
                                className="h-6 flex-1 text-[10px]"
                              />
                            </div>
                          )}
                        </div>
                      );
                    })}
                    <Button variant="ghost" size="sm" onClick={() => addAction(bi)} className="h-5 w-full text-[8px]">
                      <Plus className="mr-0.5 h-2.5 w-2.5" />액션 추가
                    </Button>
                  </div>
                )}
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

const SYSTEM_COLUMNS = new Set([
  "id", "company_code", "created_date", "updated_date", "writer",
]);

function ImmediateActionEditor({
  action,
  allColumnOptions,
  availableTableOptions,
  onAddUpdate,
  onUpdateUpdate,
  onRemoveUpdate,
  onUpdateAction,
}: {
  action: ActionButtonClickAction;
  allColumnOptions: { value: string; label: string }[];
  availableTableOptions: { value: string; label: string }[];
  onAddUpdate: () => void;
  onUpdateUpdate: (uIdx: number, partial: Partial<ActionButtonUpdate>) => void;
  onRemoveUpdate: (uIdx: number) => void;
  onUpdateAction: (partial: Partial<ActionButtonClickAction>) => void;
}) {
  const isExternalTable = action.targetTable && !availableTableOptions.some((t) => t.value === action.targetTable);
  const [dbSelectMode, setDbSelectMode] = useState(!!isExternalTable);
  const [allTables, setAllTables] = useState<TableInfo[]>([]);

  const [tableColumnGroups, setTableColumnGroups] = useState<
    { table: string; label: string; business: { value: string; label: string }[]; system: { value: string; label: string }[] }[]
  >([]);

  // 외부 DB 모드 시 전체 테이블 로드
  useEffect(() => {
    if (dbSelectMode && allTables.length === 0) {
      fetchTableList().then(setAllTables).catch(() => setAllTables([]));
    }
  }, [dbSelectMode, allTables.length]);

  // 선택된 테이블 컬럼 로드 (카드 소스 + 외부 공통)
  const effectiveTableOptions = useMemo(() => {
    if (dbSelectMode && action.targetTable) {
      const existing = availableTableOptions.find((t) => t.value === action.targetTable);
      if (!existing) return [...availableTableOptions, { value: action.targetTable, label: `${action.targetTable} (외부)` }];
    }
    return availableTableOptions;
  }, [availableTableOptions, dbSelectMode, action.targetTable]);

  useEffect(() => {
    let cancelled = false;
    const loadAll = async () => {
      const groups: typeof tableColumnGroups = [];
      for (const t of effectiveTableOptions) {
        try {
          const cols = await fetchTableColumns(t.value);
          const mapped = cols.map((c) => ({ value: c.name, label: c.name }));
          groups.push({
            table: t.value,
            label: t.label,
            business: mapped.filter((c) => !SYSTEM_COLUMNS.has(c.value)),
            system: mapped.filter((c) => SYSTEM_COLUMNS.has(c.value)),
          });
        } catch {
          groups.push({ table: t.value, label: t.label, business: [], system: [] });
        }
      }
      if (!cancelled) setTableColumnGroups(groups);
    };
    if (effectiveTableOptions.length > 0) loadAll();
    else setTableColumnGroups([]);
    return () => { cancelled = true; };
  }, [effectiveTableOptions]);

  const selectedGroup = tableColumnGroups.find((g) => g.table === action.targetTable);
  const businessCols = selectedGroup?.business || [];
  const systemCols = selectedGroup?.system || [];
  const tableName = action.targetTable?.trim() || "";

  // 메인 테이블 컬럼 (조인키 소스 컬럼 선택 용도)
  const mainTableGroup = tableColumnGroups.find((g) => availableTableOptions[0]?.value === g.table);
  const mainCols = mainTableGroup ? [...mainTableGroup.business, ...mainTableGroup.system] : [];

  return (
    <div className="space-y-1 pl-0.5">
      {/* 대상 테이블 */}
      <div className="flex items-center gap-1">
        <span className="w-14 shrink-0 text-[8px] text-muted-foreground">대상 테이블</span>
        {!dbSelectMode ? (
          <Select
            value={action.targetTable || "__none__"}
            onValueChange={(v) => {
              if (v === "__db_select__") {
                setDbSelectMode(true);
                onUpdateAction({ targetTable: "" });
              } else {
                onUpdateAction({ targetTable: v === "__none__" ? "" : v, joinConfig: undefined });
              }
            }}
          >
            <SelectTrigger className="h-6 flex-1 text-[10px]"><SelectValue placeholder="테이블 선택" /></SelectTrigger>
            <SelectContent>
              <SelectItem value="__none__" className="text-[10px]">선택</SelectItem>
              {availableTableOptions.map((t) => (
                <SelectItem key={t.value} value={t.value} className="text-[10px]">{t.label}</SelectItem>
              ))}
              <SelectItem value="__db_select__" className="text-[10px] font-medium text-primary">DB에서 직접 선택...</SelectItem>
            </SelectContent>
          </Select>
        ) : (
          <div className="flex flex-1 items-center gap-1">
            <DbTableCombobox
              value={action.targetTable || ""}
              tables={allTables}
              onSelect={(v) => onUpdateAction({ targetTable: v })}
            />
            <Button
              variant="ghost" size="sm"
              onClick={() => { setDbSelectMode(false); onUpdateAction({ targetTable: "", joinConfig: undefined }); }}
              className="h-5 shrink-0 px-1 text-[8px]"
            >
              취소
            </Button>
          </div>
        )}
      </div>

      {/* 외부 DB 선택 시 조인키 설정 */}
      {dbSelectMode && action.targetTable && (
        <>
          <div className="flex items-center gap-1">
            <span className="w-14 shrink-0 text-[8px] text-muted-foreground">기준 컬럼</span>
            <Select
              value={action.joinConfig?.sourceColumn || "__none__"}
              onValueChange={(v) => onUpdateAction({ joinConfig: { ...action.joinConfig || { sourceColumn: "", targetColumn: "" }, sourceColumn: v === "__none__" ? "" : v } })}
            >
              <SelectTrigger className="h-6 flex-1 text-[10px]"><SelectValue placeholder="메인 테이블 컬럼" /></SelectTrigger>
              <SelectContent>
                <SelectItem value="__none__" className="text-[10px]">선택</SelectItem>
                {mainCols.map((c) => (
                  <SelectItem key={c.value} value={c.value} className="text-[10px]">{c.label}</SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>
          <div className="flex items-center gap-1">
            <span className="w-14 shrink-0 text-[8px] text-muted-foreground">매칭 컬럼</span>
            <Select
              value={action.joinConfig?.targetColumn || "__none__"}
              onValueChange={(v) => onUpdateAction({ joinConfig: { ...action.joinConfig || { sourceColumn: "", targetColumn: "" }, targetColumn: v === "__none__" ? "" : v } })}
            >
              <SelectTrigger className="h-6 flex-1 text-[10px]"><SelectValue placeholder="외부 테이블 컬럼" /></SelectTrigger>
              <SelectContent>
                <SelectItem value="__none__" className="text-[10px]">선택</SelectItem>
                {[...businessCols, ...systemCols].map((c) => (
                  <SelectItem key={c.value} value={c.value} className="text-[10px]">{c.label}</SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>
          <p className="text-[7px] text-muted-foreground/70 pl-0.5">
            메인.기준컬럼 = 외부.매칭컬럼 으로 연결하여 업데이트
          </p>
        </>
      )}

      <div className="flex items-center gap-1">
        <span className="w-14 shrink-0 text-[8px] text-muted-foreground">확인 메시지</span>
        <Input
          value={action.confirmMessage || ""}
          onChange={(e) => onUpdateAction({ confirmMessage: e.target.value })}
          placeholder="처리하시겠습니까?"
          className="h-6 flex-1 text-[10px]"
        />
      </div>

      {/* 사전 조건 (중복 방지) */}
      <div className="rounded border bg-background/50 p-1.5 space-y-1">
        <div className="flex items-center justify-between">
          <span className="text-[8px] font-medium text-muted-foreground">사전 조건 (중복 방지)</span>
          <Switch
            checked={!!action.preCondition}
            onCheckedChange={(checked) => {
              if (checked) {
                onUpdateAction({ preCondition: { column: "", expectedValue: "", failMessage: "" } });
              } else {
                onUpdateAction({ preCondition: undefined });
              }
            }}
            className="h-3.5 w-7 [&>span]:h-2.5 [&>span]:w-2.5"
          />
        </div>
        {action.preCondition && (
          <div className="space-y-1">
            <div className="flex items-center gap-1">
              <span className="w-14 shrink-0 text-[8px] text-muted-foreground">검증 컬럼</span>
              <Select
                value={action.preCondition.column || "__none__"}
                onValueChange={(v) => onUpdateAction({ preCondition: { ...action.preCondition!, column: v === "__none__" ? "" : v } })}
              >
                <SelectTrigger className="h-6 flex-1 text-[10px]"><SelectValue placeholder="컬럼" /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__" className="text-[10px]">선택</SelectItem>
                  {businessCols.length > 0 && (
                    <SelectGroup>
                      <SelectLabel className="text-[8px] text-muted-foreground">{tableName}</SelectLabel>
                      {businessCols.map((o) => (
                        <SelectItem key={o.value} value={o.value} className="text-[10px]">{o.label}</SelectItem>
                      ))}
                    </SelectGroup>
                  )}
                </SelectContent>
              </Select>
            </div>
            <div className="flex items-center gap-1">
              <span className="w-14 shrink-0 text-[8px] text-muted-foreground">기대값</span>
              <Input
                value={action.preCondition.expectedValue || ""}
                onChange={(e) => onUpdateAction({ preCondition: { ...action.preCondition!, expectedValue: e.target.value } })}
                placeholder="예: waiting"
                className="h-6 flex-1 text-[10px]"
              />
            </div>
            <div className="flex items-center gap-1">
              <span className="w-14 shrink-0 text-[8px] text-muted-foreground">실패 메시지</span>
              <Input
                value={action.preCondition.failMessage || ""}
                onChange={(e) => onUpdateAction({ preCondition: { ...action.preCondition!, failMessage: e.target.value } })}
                placeholder="이미 다른 사용자가 처리했습니다"
                className="h-6 flex-1 text-[10px]"
              />
            </div>
            <p className="text-[7px] text-muted-foreground/70 pl-0.5">
              실행 시 해당 컬럼의 현재 DB 값이 기대값과 일치할 때만 처리됩니다
            </p>
          </div>
        )}
      </div>

      <div className="flex items-center justify-between">
        <span className="text-[8px] font-medium text-muted-foreground">
          변경할 컬럼{tableName ? ` (${tableName})` : ""}
        </span>
        <Button variant="ghost" size="sm" onClick={onAddUpdate} className="h-4 px-1 text-[8px]">
          <Plus className="mr-0.5 h-2.5 w-2.5" />추가
        </Button>
      </div>
      {(action.updates || []).map((u, ui) => (
        <div key={ui} className="flex items-center gap-1">
          <Select value={u.column || "__none__"} onValueChange={(v) => onUpdateUpdate(ui, { column: v === "__none__" ? "" : v })}>
            <SelectTrigger className="h-6 w-24 text-[10px]"><SelectValue placeholder="컬럼" /></SelectTrigger>
            <SelectContent>
              <SelectItem value="__none__" className="text-[10px]">선택</SelectItem>
              {businessCols.length > 0 && (
                <SelectGroup>
                  <SelectLabel className="text-[8px] text-muted-foreground">{tableName}</SelectLabel>
                  {businessCols.map((o) => (
                    <SelectItem key={o.value} value={o.value} className="text-[10px]">{o.label}</SelectItem>
                  ))}
                </SelectGroup>
              )}
              {systemCols.length > 0 && (
                <SelectGroup>
                  <SelectLabel className="text-[8px] text-muted-foreground">{tableName} - 시스템</SelectLabel>
                  {systemCols.map((o) => (
                    <SelectItem key={o.value} value={o.value} className="text-[10px]">{o.label}</SelectItem>
                  ))}
                </SelectGroup>
              )}
            </SelectContent>
          </Select>
          <Select value={u.valueType} onValueChange={(v) => onUpdateUpdate(ui, { valueType: v as ActionButtonUpdate["valueType"] })}>
            <SelectTrigger className="h-6 w-20 text-[10px]"><SelectValue /></SelectTrigger>
            <SelectContent>
              <SelectItem value="static" className="text-[10px]">직접입력</SelectItem>
              <SelectItem value="userInput" className="text-[10px]">사용자 입력</SelectItem>
              <SelectItem value="currentUser" className="text-[10px]">현재 사용자</SelectItem>
              <SelectItem value="currentTime" className="text-[10px]">현재 시간</SelectItem>
              <SelectItem value="columnRef" className="text-[10px]">컬럼 참조</SelectItem>
            </SelectContent>
          </Select>
          {u.valueType === "userInput" && (
            <Select value={u.operationType || "assign"} onValueChange={(v) => onUpdateUpdate(ui, { operationType: v as ActionButtonUpdate["operationType"] })}>
              <SelectTrigger className="h-6 w-16 text-[10px]"><SelectValue /></SelectTrigger>
              <SelectContent>
                <SelectItem value="assign" className="text-[10px]">대입</SelectItem>
                <SelectItem value="add" className="text-[10px]">합산</SelectItem>
                <SelectItem value="subtract" className="text-[10px]">차감</SelectItem>
              </SelectContent>
            </Select>
          )}
          {(u.valueType === "static" || u.valueType === "columnRef") && (
            <Input
              value={u.value || ""}
              onChange={(e) => onUpdateUpdate(ui, { value: e.target.value })}
              placeholder={u.valueType === "static" ? "값" : "컬럼명"}
              className="h-6 flex-1 text-[10px]"
            />
          )}
          <Button variant="ghost" size="sm" onClick={() => onRemoveUpdate(ui)} className="h-5 w-5 shrink-0 p-0">
            <Trash2 className="h-2.5 w-2.5 text-muted-foreground" />
          </Button>
        </div>
      ))}
      {(!action.updates || action.updates.length === 0) && (
        <p className="text-[8px] text-muted-foreground">변경 항목을 추가하면 클릭 시 DB가 변경됩니다.</p>
      )}
    </div>
  );
}


// ===== DB 테이블 검색 Combobox =====

function DbTableCombobox({
  value,
  tables,
  onSelect,
}: {
  value: string;
  tables: TableInfo[];
  onSelect: (tableName: string) => void;
}) {
  const [open, setOpen] = useState(false);
  const [search, setSearch] = useState("");

  const filtered = useMemo(() => {
    if (!search) return tables;
    const q = search.toLowerCase();
    return tables.filter(
      (t) =>
        t.tableName.toLowerCase().includes(q) ||
        (t.tableComment || "").toLowerCase().includes(q),
    );
  }, [tables, search]);

  const selectedLabel = useMemo(() => {
    if (!value) return "DB 테이블 검색...";
    const found = tables.find((t) => t.tableName === value);
    return found ? `${found.tableName}${found.tableComment ? ` (${found.tableComment})` : ""}` : value;
  }, [value, tables]);

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          className="h-6 flex-1 justify-between text-[10px] font-normal"
        >
          <span className="truncate">{selectedLabel}</span>
          <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)", minWidth: 240 }}
        align="start"
      >
        <Command shouldFilter={false}>
          <CommandInput
            placeholder="테이블명 또는 설명 검색..."
            className="text-[10px]"
            value={search}
            onValueChange={setSearch}
          />
          <CommandList className="max-h-[200px]">
            <CommandEmpty className="py-2 text-center text-[10px] text-muted-foreground">
              검색 결과가 없습니다.
            </CommandEmpty>
            <CommandGroup>
              {filtered.map((t) => (
                <CommandItem
                  key={t.tableName}
                  value={t.tableName}
                  onSelect={() => {
                    onSelect(t.tableName);
                    setOpen(false);
                    setSearch("");
                  }}
                  className="text-[10px]"
                >
                  <Check className={cn("mr-1.5 h-3 w-3", value === t.tableName ? "opacity-100" : "opacity-0")} />
                  <span className="font-medium">{t.tableName}</span>
                  {t.tableComment && (
                    <span className="ml-1 text-muted-foreground">({t.tableComment})</span>
                  )}
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  );
}

// ===== 하단 상태 에디터 =====

function FooterStatusEditor({
  cell,
  allColumnOptions,
  columnOptionGroups,
  onUpdate,
}: {
  cell: CardCellDefinitionV2;
  allColumnOptions: { value: string; label: string }[];
  columnOptionGroups: ColumnOptionGroup[];
  onUpdate: (partial: Partial<CardCellDefinitionV2>) => void;
}) {
  const footerStatusMap = cell.footerStatusMap || [];

  const addMapping = () => {
    onUpdate({ footerStatusMap: [...footerStatusMap, { value: "", label: "", color: "#6b7280" }] });
  };

  const updateMapping = (index: number, partial: Partial<{ value: string; label: string; color: string }>) => {
    onUpdate({ footerStatusMap: footerStatusMap.map((m, i) => (i === index ? { ...m, ...partial } : m)) });
  };

  const removeMapping = (index: number) => {
    onUpdate({ footerStatusMap: footerStatusMap.filter((_, i) => i !== index) });
  };

  return (
    <div className="space-y-1">
      <span className="text-[9px] font-medium text-muted-foreground">하단 상태 설정</span>
      <div className="flex gap-1">
        <Input
          value={cell.footerLabel || ""}
          onChange={(e) => onUpdate({ footerLabel: e.target.value })}
          placeholder="라벨 (예: 검사의뢰)"
          className="h-7 flex-1 text-[10px]"
        />
      </div>
      <div className="flex gap-1">
        <Select value={cell.footerStatusColumn || "__none__"} onValueChange={(v) => onUpdate({ footerStatusColumn: v === "__none__" ? undefined : v })}>
          <SelectTrigger className="h-7 flex-1 text-[10px]"><SelectValue placeholder="상태 컬럼" /></SelectTrigger>
          <SelectContent>
            <SelectItem value="__none__" className="text-[10px]">없음</SelectItem>
            {renderColumnOptionGroups(columnOptionGroups)}
          </SelectContent>
        </Select>
      </div>
      <div className="flex items-center gap-1">
        <Label className="text-[9px] text-muted-foreground">상단 구분선</Label>
        <Switch
          checked={cell.showTopBorder !== false}
          onCheckedChange={(v) => onUpdate({ showTopBorder: v })}
        />
      </div>
      <div className="flex items-center justify-between">
        <span className="text-[9px] font-medium text-muted-foreground">상태값-색상 매핑</span>
        <Button variant="ghost" size="sm" onClick={addMapping} className="h-5 px-1.5 text-[9px]">
          <Plus className="mr-0.5 h-3 w-3" />추가
        </Button>
      </div>
      {footerStatusMap.map((m, i) => (
        <div key={i} className="flex items-center gap-1">
          <Input value={m.value} onChange={(e) => updateMapping(i, { value: e.target.value })} placeholder="값" className="h-6 flex-1 text-[10px]" />
          <Input value={m.label} onChange={(e) => updateMapping(i, { label: e.target.value })} placeholder="라벨" className="h-6 flex-1 text-[10px]" />
          <input type="color" value={m.color} onChange={(e) => updateMapping(i, { color: e.target.value })} className="h-6 w-8 cursor-pointer rounded border" />
          <Button variant="ghost" size="sm" onClick={() => removeMapping(i)} className="h-5 w-5 p-0">
            <Trash2 className="h-3 w-3 text-destructive" />
          </Button>
        </div>
      ))}
    </div>
  );
}

// ===== 필드 설정 에디터 =====

function FieldConfigEditor({
  cell,
  allColumnOptions,
  columnOptionGroups,
  onUpdate,
}: {
  cell: CardCellDefinitionV2;
  allColumnOptions: { value: string; label: string }[];
  columnOptionGroups: ColumnOptionGroup[];
  onUpdate: (partial: Partial<CardCellDefinitionV2>) => void;
}) {
  const valueType = cell.valueType || "column";

  return (
    <div className="space-y-1">
      <span className="text-[9px] font-medium text-muted-foreground">필드 설정</span>
      <div className="flex gap-1">
        <Select value={valueType} onValueChange={(v) => onUpdate({ valueType: v as "column" | "formula" })}>
          <SelectTrigger className="h-7 flex-1 text-[10px]"><SelectValue /></SelectTrigger>
          <SelectContent>
            <SelectItem value="column" className="text-[10px]">컬럼 값</SelectItem>
            <SelectItem value="formula" className="text-[10px]">수식</SelectItem>
          </SelectContent>
        </Select>
        <Input value={cell.unit || ""} onChange={(e) => onUpdate({ unit: e.target.value })} placeholder="단위" className="h-7 w-16 text-[10px]" />
      </div>
      {valueType === "formula" && (
        <div className="flex gap-1">
          <Select value={cell.formulaLeft || ""} onValueChange={(v) => onUpdate({ formulaLeft: v })}>
            <SelectTrigger className="h-7 flex-1 text-[10px]"><SelectValue placeholder="좌항" /></SelectTrigger>
            <SelectContent>
              {renderColumnOptionGroups(columnOptionGroups)}
            </SelectContent>
          </Select>
          <Select value={cell.formulaOperator || "+"} onValueChange={(v) => onUpdate({ formulaOperator: v as "+" | "-" | "*" | "/" })}>
            <SelectTrigger className="h-7 w-12 text-[10px]"><SelectValue /></SelectTrigger>
            <SelectContent>
              {["+", "-", "*", "/"].map((op) => <SelectItem key={op} value={op} className="text-[10px]">{op}</SelectItem>)}
            </SelectContent>
          </Select>
          <Select value={cell.formulaRightType || "input"} onValueChange={(v) => onUpdate({ formulaRightType: v as "input" | "column" })}>
            <SelectTrigger className="h-7 w-16 text-[10px]"><SelectValue /></SelectTrigger>
            <SelectContent>
              <SelectItem value="input" className="text-[10px]">입력값</SelectItem>
              <SelectItem value="column" className="text-[10px]">컬럼</SelectItem>
            </SelectContent>
          </Select>
          {cell.formulaRightType === "column" && (
            <Select value={cell.formulaRight || ""} onValueChange={(v) => onUpdate({ formulaRight: v })}>
              <SelectTrigger className="h-7 flex-1 text-[10px]"><SelectValue placeholder="우항" /></SelectTrigger>
              <SelectContent>
                {renderColumnOptionGroups(columnOptionGroups)}
              </SelectContent>
            </Select>
          )}
        </div>
      )}
    </div>
  );
}

// ===== 탭 3: 동작 =====

function TabActions({
  cfg,
  onUpdate,
  columns,
}: {
  cfg: PopCardListV2Config;
  onUpdate: (partial: Partial<PopCardListV2Config>) => void;
  columns: ColumnInfo[];
}) {
  const overflow = cfg.overflow || { mode: "loadMore" as const, visibleCount: 6 };
  const clickAction = cfg.cardClickAction || "none";
  const modalConfig = cfg.cardClickModalConfig || { screenId: "" };

  const [processColumns, setProcessColumns] = useState<ColumnInfo[]>([]);
  const timelineCell = cfg.cardGrid?.cells?.find((c) => c.type === "timeline" && c.timelineSource?.processTable);
  const processTableName = timelineCell?.timelineSource?.processTable || "";
  useEffect(() => {
    if (!processTableName) { setProcessColumns([]); return; }
    fetchTableColumns(processTableName)
      .then(setProcessColumns)
      .catch(() => setProcessColumns([]));
  }, [processTableName]);

  const ownerColumnGroups: ColumnOptionGroup[] = useMemo(() => [
    {
      groupLabel: `메인 (${cfg.dataSource?.tableName || "테이블"})`,
      options: columns.map((c) => ({ value: c.name, label: c.name })),
    },
    ...(processColumns.length > 0
      ? [{
          groupLabel: `공정 (${processTableName})`,
          options: processColumns.map((c) => ({ value: `__process_${c.name}`, label: c.name })),
        }]
      : []),
  ], [columns, processColumns, processTableName, cfg.dataSource?.tableName]);

  return (
    <div className="space-y-3">
      {/* 소유자 우선 정렬 */}
      <div>
        <Label className="text-xs">소유자 우선 정렬</Label>
        <div className="mt-1 flex items-center gap-1">
          <Select
            value={cfg.ownerSortColumn || "__none__"}
            onValueChange={(v) => onUpdate({ ownerSortColumn: v === "__none__" ? undefined : v })}
          >
            <SelectTrigger className="h-7 flex-1 text-[10px]"><SelectValue placeholder="사용 안 함" /></SelectTrigger>
            <SelectContent>
              <SelectItem value="__none__" className="text-[10px]">사용 안 함</SelectItem>
              {renderColumnOptionGroups(ownerColumnGroups)}
            </SelectContent>
          </Select>
        </div>
        <p className="mt-0.5 text-[9px] text-muted-foreground">
          선택한 컬럼 값이 현재 로그인 사용자와 일치하는 카드가 맨 위에 표시됩니다
        </p>
      </div>

      {/* 카드 선택 시 */}
      <div>
        <Label className="text-xs">카드 선택 시 동작</Label>
        <div className="mt-1 space-y-1">
          {(["none", "publish", "navigate", "modal-open"] as V2CardClickAction[]).map((action) => (
            <label key={action} className="flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-muted/50">
              <input
                type="radio"
                name="cardClickAction"
                checked={clickAction === action}
                onChange={() => onUpdate({ cardClickAction: action })}
                className="h-3 w-3"
              />
              <span className="text-xs">
                {action === "none" && "없음"}
                {action === "publish" && "상세 데이터 전달 (다른 컴포넌트 연결)"}
                {action === "navigate" && "화면 이동"}
                {action === "modal-open" && "모달 열기"}
              </span>
            </label>
          ))}
        </div>
        {clickAction === "modal-open" && (
          <div className="mt-2 space-y-1.5 rounded border bg-muted/20 p-2">
            <div className="flex items-center gap-1">
              <span className="w-16 shrink-0 text-[9px] text-muted-foreground">POP 화면 ID</span>
              <Input
                value={modalConfig.screenId || ""}
                onChange={(e) => onUpdate({ cardClickModalConfig: { ...modalConfig, screenId: e.target.value } })}
                placeholder="화면 ID (예: 4481)"
                className="h-7 flex-1 text-[10px]"
              />
            </div>
            <div className="flex items-center gap-1">
              <span className="w-16 shrink-0 text-[9px] text-muted-foreground">모달 제목</span>
              <Input
                value={modalConfig.modalTitle || ""}
                onChange={(e) => onUpdate({ cardClickModalConfig: { ...modalConfig, modalTitle: e.target.value } })}
                placeholder="비우면 '상세 작업' 표시"
                className="h-7 flex-1 text-[10px]"
              />
            </div>
            <div className="flex items-center gap-1">
              <span className="w-16 shrink-0 text-[9px] text-muted-foreground">조건</span>
              <Select
                value={modalConfig.condition?.type || "always"}
                onValueChange={(v) => {
                  const cond = v === "always" ? undefined : { type: v as "timeline-status" | "column-value", value: "", column: "" };
                  onUpdate({ cardClickModalConfig: { ...modalConfig, condition: cond } });
                }}
              >
                <SelectTrigger className="h-7 flex-1 text-[10px]"><SelectValue /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="always" className="text-[10px]">항상</SelectItem>
                  <SelectItem value="timeline-status" className="text-[10px]">타임라인 상태 일치 시</SelectItem>
                  <SelectItem value="column-value" className="text-[10px]">컬럼 값 일치 시</SelectItem>
                </SelectContent>
              </Select>
            </div>
            {modalConfig.condition?.type === "timeline-status" && (
              <div className="flex items-center gap-1">
                <span className="w-16 shrink-0 text-[9px] text-muted-foreground">상태 값</span>
                <Input
                  value={modalConfig.condition?.value || ""}
                  onChange={(e) => onUpdate({ cardClickModalConfig: { ...modalConfig, condition: { ...modalConfig.condition!, value: e.target.value } } })}
                  placeholder="예: in_progress"
                  className="h-7 flex-1 text-[10px]"
                />
              </div>
            )}
            {modalConfig.condition?.type === "column-value" && (
              <>
                <div className="flex items-center gap-1">
                  <span className="w-16 shrink-0 text-[9px] text-muted-foreground">컬럼</span>
                  <Input
                    value={modalConfig.condition?.column || ""}
                    onChange={(e) => onUpdate({ cardClickModalConfig: { ...modalConfig, condition: { ...modalConfig.condition!, column: e.target.value } } })}
                    placeholder="컬럼명"
                    className="h-7 flex-1 text-[10px]"
                  />
                </div>
                <div className="flex items-center gap-1">
                  <span className="w-16 shrink-0 text-[9px] text-muted-foreground">값</span>
                  <Input
                    value={modalConfig.condition?.value || ""}
                    onChange={(e) => onUpdate({ cardClickModalConfig: { ...modalConfig, condition: { ...modalConfig.condition!, value: e.target.value } } })}
                    placeholder="값"
                    className="h-7 flex-1 text-[10px]"
                  />
                </div>
              </>
            )}
          </div>
        )}
      </div>

      {/* 필터 전 비표시 */}
      <div className="flex items-center justify-between">
        <Label className="text-xs">필터 전 데이터 숨김</Label>
        <Switch
          checked={!!cfg.hideUntilFiltered}
          onCheckedChange={(checked) => onUpdate({ hideUntilFiltered: checked })}
        />
      </div>
      {cfg.hideUntilFiltered && (
        <p className="text-[9px] text-muted-foreground -mt-2 pl-1">
          연결된 컴포넌트에서 필터 값이 전달되기 전까지 데이터를 표시하지 않습니다.
        </p>
      )}

      {/* 스크롤 방향 */}
      <div>
        <Label className="text-xs">스크롤 방향</Label>
        <div className="mt-1 flex gap-1">
          {(["vertical", "horizontal"] as const).map((dir) => (
            <button
              key={dir}
              type="button"
              onClick={() => onUpdate({ scrollDirection: dir })}
              className={cn(
                "flex-1 rounded border py-1 text-xs transition-colors",
                (cfg.scrollDirection || "vertical") === dir
                  ? "border-primary bg-primary/10 text-primary"
                  : "border-border hover:bg-muted"
              )}
            >
              {dir === "vertical" ? "세로" : "가로"}
            </button>
          ))}
        </div>
      </div>

      {/* 오버플로우 */}
      <div>
        <Label className="text-xs">오버플로우</Label>
        <div className="mt-1 flex gap-1">
          {(["loadMore", "pagination"] as const).map((mode) => (
            <button
              key={mode}
              type="button"
              onClick={() => onUpdate({ overflow: { ...overflow, mode } })}
              className={cn(
                "flex-1 rounded border py-1 text-xs transition-colors",
                overflow.mode === mode
                  ? "border-primary bg-primary/10 text-primary"
                  : "border-border hover:bg-muted"
              )}
            >
              {mode === "loadMore" ? "더보기" : "페이지네이션"}
            </button>
          ))}
        </div>
        <div className="mt-2 space-y-2">
          <div>
            <Label className="text-[10px]">기본 표시 수</Label>
            <Input
              type="number"
              min={1}
              max={50}
              value={overflow.visibleCount}
              onChange={(e) => onUpdate({ overflow: { ...overflow, visibleCount: Number(e.target.value) || 6 } })}
              className="mt-0.5 h-7 text-[10px]"
            />
          </div>
          {overflow.mode === "loadMore" && (
            <div>
              <Label className="text-[10px]">더보기 추가 수</Label>
              <Input
                type="number"
                min={1}
                max={50}
                value={overflow.loadMoreCount ?? 6}
                onChange={(e) => onUpdate({ overflow: { ...overflow, loadMoreCount: Number(e.target.value) || 6 } })}
                className="mt-0.5 h-7 text-[10px]"
              />
            </div>
          )}
          {overflow.mode === "pagination" && (
            <div>
              <Label className="text-[10px]">페이지당 수</Label>
              <Input
                type="number"
                min={1}
                max={100}
                value={overflow.pageSize ?? overflow.visibleCount}
                onChange={(e) => onUpdate({ overflow: { ...overflow, pageSize: Number(e.target.value) || 6 } })}
                className="mt-0.5 h-7 text-[10px]"
              />
            </div>
          )}
        </div>
      </div>

      {/* 장바구니 */}
      <div className="flex items-center justify-between">
        <Label className="text-xs">장바구니(카트) 사용</Label>
        <Switch
          checked={!!cfg.cartAction}
          onCheckedChange={(checked) => {
            if (checked) {
              onUpdate({ cartAction: { saveMode: "cart", label: "담기", cancelLabel: "취소" } });
            } else {
              onUpdate({ cartAction: undefined });
            }
          }}
        />
      </div>
    </div>
  );
}
