"use client";

/**
 * V2 통합 필드 설정 패널 (INVYONE Studio 컨셉 적용)
 *
 *   분류 체계 (시안 panel-input-new 컨셉)
 *     kind = 입력 / 선택·태그  (type 의 group 으로 자동 결정)
 *     type
 *       입력 : 글자(text) · 숫자(number) · 여러 줄(textarea) · 채번(numbering)
 *       선택·태그 : 단일 선택(single) · 다중값(multi: 목록/코드/엔티티/태그)
 *     format = text 안에서만 자유/이메일/전화/URL/사업자번호/통화
 *
 *   화면 구조
 *     CPCrumb (입력|선택 ▸ type ▾)
 *     데이터 소속 (primary/reference/child) — 본 프로젝트 핵심 개념
 *     유형별 설정 (CPSection + 형식별 옵션)
 *     필터 (entity/category)
 *     고급 설정 (CPGroup)
 *
 *   데이터 모델은 기존 config 구조 100% 호환:
 *     fieldType, source, dataRole, sourceTable, sourceColumn,
 *     placeholder, format, mask, min/max/step, rows, options, defaultValue,
 *     entityTable, entityValueColumn, entityLabelColumn, categoryTable, categoryColumn,
 *     autoGeneration, mode, multiple, maxSelect, searchable, allowClear, filters
 */

import "./_shared/cp/cp.css";

import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Loader2 } from "lucide-react";
import { apiClient } from "@/lib/api/client";
import { AutoGenerationType } from "@/types/screen";
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
import { getAvailableNumberingRulesForScreen } from "@/lib/api/numberingRule";
import { NumberingRuleConfig } from "@/types/numbering-rule";
import { NumberingRuleCreateDialog } from "@/components/numbering-rule/NumberingRuleCreateDialog";
import type { OptionFilter } from "@/lib/registry/components/input/use-option-loader";

import {
  CP_ICONS,
  CPSection,
  CPRow,
  CPText,
  CPSelect,
  CPSwitch,
  CPNumber,
  CPSegment,
  CPColor,
  CPIconBtn,
  CPCrumb,
  CPFormatTrigger,
  CPGroup,
} from "./_shared/cp";
import type { CPCrumbType, CPFormatItem } from "./_shared/cp";

// ── 분류 체계 (HTML panel-input-new.jsx V4 와 1:1 매칭) ───────
type Kind = "input" | "choice" | "auto" | "attach";
type Type =
  | "text" | "number" | "money" | "date"           // 입력
  | "single" | "multi"                              // 선택/태그
  | "autonum" | "formula" | "audit"                 // 자동
  | "file";                                         // 첨부

const KINDS: { id: Kind; name: string; icon: React.ReactNode }[] = [
  { id: "input",  name: "입력", icon: <span style={{ fontSize: 11 }}>✎</span> },
  { id: "choice", name: "선택·태그", icon: <span style={{ fontSize: 11 }}>☷</span> },
  { id: "auto",   name: "자동", icon: <span style={{ fontSize: 11 }}>⚡</span> },
  { id: "attach", name: "첨부", icon: <span style={{ fontSize: 11 }}>📎</span> },
];

const TYPES_BY_KIND: Record<Kind, (CPCrumbType & { id: Type })[]> = {
  input: [
    { id: "text",   name: "글자", desc: "모든 텍스트", icon: "Aa", col: "VARCHAR" },
    { id: "number", name: "숫자", desc: "정수·소수",   icon: "#",  col: "NUMBER" },
    { id: "money",  name: "금액", desc: "통화·환율",   icon: "₩",  col: "NUMBER" },
    { id: "date",   name: "날짜", desc: "날짜·시간",   icon: "📅", col: "TIMESTAMP" },
  ],
  choice: [
    { id: "single", name: "단일 선택", desc: "목록에서 하나", icon: "◉", col: "VARCHAR" },
    { id: "multi",  name: "다중값", desc: "태그·여러 항목", icon: "☷", col: "JSON" },
  ],
  auto: [
    { id: "autonum", name: "채번", desc: "자동 일련번호",  icon: "№",  col: "VARCHAR" },
    { id: "formula", name: "계산", desc: "수식·다른 필드", icon: "ƒx", col: "VARIES" },
    { id: "audit",   name: "감사", desc: "작성자·작성일",  icon: "◉",  col: "TIMESTAMP" },
  ],
  attach: [
    { id: "file", name: "파일", desc: "업로드", icon: "📎", col: "VARCHAR" },
  ],
};

const FORMATS_BY_TYPE: Record<Type, CPFormatItem[]> = {
  text: [
    { id: "free",    name: "자유",         desc: "제한 없음 · 줄 수 조절",  icon: "Aa" },
    { id: "email",   name: "이메일",       desc: "@ 검증",                  icon: "@" },
    { id: "phone",   name: "전화번호",     desc: "국가·마스킹",             icon: "☎" },
    { id: "rrn",     name: "주민·사업자",  desc: "체크섬·암호화",           icon: "ID" },
    { id: "address", name: "주소",         desc: "우편번호·지도",           icon: "📍" },
    { id: "card",    name: "카드번호",     desc: "PCI 마스킹",              icon: "💳" },
    { id: "url",     name: "URL",          desc: "링크",                    icon: "🔗" },
    { id: "color",   name: "색상",         desc: "#RRGGBB",                 icon: "🎨" },
    { id: "mask",    name: "커스텀 마스킹", desc: "###-####",                icon: "⌨" },
  ],
  number: [
    { id: "int",     name: "정수",     desc: "0, 1, 2…",  icon: "#" },
    { id: "decimal", name: "소수",     desc: "1.23",      icon: "0.0" },
    { id: "percent", name: "퍼센트",   desc: "0~100%",    icon: "%" },
    { id: "slider",  name: "슬라이더", desc: "min~max",   icon: "⇄" },
  ],
  money: [
    { id: "krw",     name: "원",   desc: "KRW",        icon: "₩" },
    { id: "foreign", name: "외화", desc: "USD/EUR…",   icon: "$" },
  ],
  date: [
    { id: "date",     name: "날짜",       desc: "2025-01-31",       icon: "📅" },
    { id: "datetime", name: "날짜+시간",  desc: "2025-01-31 14:30", icon: "🕒" },
    { id: "time",     name: "시간",       desc: "14:30",            icon: "⏱" },
    { id: "range",    name: "기간",       desc: "시작 ~ 끝",         icon: "↔" },
  ],
  single: [
    { id: "list",    name: "고정 목록", desc: "여기서 정의",       icon: "LS" },
    { id: "code",    name: "공통코드",  desc: "코드 테이블",       icon: "CD" },
    { id: "entity",  name: "엔티티",    desc: "다른 테이블 (FK)",  icon: "FK" },
    { id: "status",  name: "상태",      desc: "색·라벨",           icon: "●" },
    { id: "boolean", name: "예/아니오", desc: "둘 중 하나",        icon: "◐" },
  ],
  multi: [
    { id: "tags",   name: "태그 입력", desc: "직접 입력",       icon: "TG" },
    { id: "list",   name: "고정 목록", desc: "여러 항목 선택",  icon: "CB" },
    { id: "code",   name: "공통코드",  desc: "여러 코드 선택",  icon: "CD" },
    { id: "entity", name: "엔티티",    desc: "여러 참조 선택",  icon: "FK" },
  ],
  autonum: [{ id: "autonum", name: "채번", desc: "ORD-2025-####", icon: "№" }],
  formula: [{ id: "formula", name: "계산", desc: "qty * price",    icon: "ƒx" }],
  audit:   [{ id: "audit",   name: "감사", desc: "created_at",     icon: "◉" }],
  file: [
    { id: "image", name: "이미지",  desc: "png, jpg…", icon: "🖼" },
    { id: "doc",   name: "문서",    desc: "pdf, docx…", icon: "📄" },
    { id: "any",   name: "모든파일", desc: "제한 없음", icon: "📎" },
  ],
};

const KIND_OF_TYPE: Record<Type, Kind> = {
  text: "input", number: "input", money: "input", date: "input",
  single: "choice", multi: "choice",
  autonum: "auto", formula: "auto", audit: "auto",
  file: "attach",
};

const defaultFormatFor = (type: Type): string => FORMATS_BY_TYPE[type][0].id;

const OPERATOR_OPTIONS = [
  { value: "=", label: "같음 (=)" },
  { value: "!=", label: "다름 (!=)" },
  { value: ">", label: "초과 (>)" },
  { value: "<", label: "미만 (<)" },
  { value: ">=", label: "이상 (>=)" },
  { value: "<=", label: "이하 (<=)" },
  { value: "in", label: "포함 (IN)" },
  { value: "notIn", label: "미포함 (NOT IN)" },
  { value: "like", label: "유사 (LIKE)" },
  { value: "isNull", label: "NULL" },
  { value: "isNotNull", label: "NOT NULL" },
] as const;

const VALUE_TYPE_OPTIONS = [
  { value: "static", label: "고정값" },
  { value: "field", label: "폼 필드 참조" },
  { value: "user", label: "로그인 사용자" },
] as const;

const USER_FIELD_OPTIONS = [
  { value: "companyCode", label: "회사코드" },
  { value: "userId", label: "사용자ID" },
  { value: "deptCode", label: "부서코드" },
  { value: "userName", label: "사용자명" },
] as const;

interface ColumnOption {
  columnName: string;
  columnLabel: string;
}

type TableSelectOption = {
  tableName?: string;
  table_name?: string;
  displayName?: string;
  display_name?: string;
  tableComment?: string;
  table_comment?: string;
  tableLabel?: string;
  table_label?: string;
  description?: string;
};

function normalizeTableSelectOptions(tables: TableSelectOption[] = []) {
  return tables
    .map((table) => {
      const tableName = table.tableName || table.table_name;
      if (!tableName) return null;
      const label =
        table.displayName ||
        table.display_name ||
        table.tableComment ||
        table.table_comment ||
        table.tableLabel ||
        table.table_label ||
        table.description;
      return {
        tableName,
        label: label ? `${label} (${tableName})` : tableName,
      };
    })
    .filter((table): table is { tableName: string; label: string } => table !== null);
}

interface CategoryValueOption {
  valueCode: string;
  valueLabel: string;
}

// ── 하위 호환: 기존 config → (kind, type, format) 추론 ──────────
function resolveTriple(
  config: Record<string, any>,
  componentType?: string,
  metaInputType?: string,
): { kind: Kind; type: Type; format: string } {
  // 0. config 에 새 형식이 명시돼 있으면 그대로
  if (config.kind && config.type) {
    return {
      kind: config.kind as Kind,
      type: config.type as Type,
      format: config.format || defaultFormatFor(config.type as Type),
    };
  }

  // 1. autoGeneration 으로 자동 kind 판정
  const ag = config.autoGeneration;
  if (config.fieldType === "numbering" || ag?.type === "numbering_rule") {
    return { kind: "auto", type: "autonum", format: "autonum" };
  }
  if (ag?.enabled && (ag?.type === "current_time" || ag?.type === "current_user")) {
    return { kind: "auto", type: "audit", format: "audit" };
  }
  if (ag?.enabled && ag?.type === "formula") {
    return { kind: "auto", type: "formula", format: "formula" };
  }

  // 2. 컴포넌트 ID 가 명시적으로 별도면 그쪽으로
  if (config.fieldType === "file") {
    const a = config.accept || "";
    if (a.startsWith("image/")) return { kind: "attach", type: "file", format: "image" };
    if (a.includes("pdf") || a.includes("docx") || a.includes(".doc"))
      return { kind: "attach", type: "file", format: "doc" };
    return { kind: "attach", type: "file", format: "any" };
  }

  // 2.5 옛 입력 6종 (text-input/number-input/date-input/select-basic/checkbox-basic/
  //     textarea-basic) 은 Phase E 에서 canonical input 으로 흡수 — 레거시 매핑 제거.

  // 3. 선택 (source / multiple)
  const isMulti = !!config.multiple;
  const src = config.source || (config.fieldType === "select" ? "static" : config.fieldType);
  if (src === "static" || src === "select") {
    return { kind: "choice", type: isMulti ? "multi" : "single", format: isMulti ? "list" : "list" };
  }
  if (src === "category" || src === "code") {
    return { kind: "choice", type: isMulti ? "multi" : "single", format: "code" };
  }
  if (src === "entity") {
    return { kind: "choice", type: isMulti ? "multi" : "single", format: "entity" };
  }
  if (config.fieldType === "checkbox" || (config.fieldType as any) === "boolean") {
    return { kind: "choice", type: "single", format: "boolean" };
  }
  if (config.fieldType === "tags") {
    return { kind: "choice", type: "multi", format: "tags" };
  }
  if (config.fieldType === "status") {
    return { kind: "choice", type: "single", format: "status" };
  }

  // 4. 입력 - money / number / date / text
  const it = (config.inputType || config.fieldType || metaInputType || "").toString();
  if (it === "currency" || config.format === "currency") {
    return { kind: "input", type: "money", format: "krw" };
  }
  if (it === "decimal") return { kind: "input", type: "number", format: "decimal" };
  if (it === "percentage" || it === "percent") return { kind: "input", type: "number", format: "percent" };
  if (it === "number") return { kind: "input", type: "number", format: "int" };
  if (it === "date") return { kind: "input", type: "date", format: "date" };
  if (it === "datetime") return { kind: "input", type: "date", format: "datetime" };
  if (it === "time") return { kind: "input", type: "date", format: "time" };

  // 5. text (textarea 는 text + free + rows>1 로 흡수)
  if (it === "textarea") return { kind: "input", type: "text", format: "free" };

  // 6. text formats (config.format 우선)
  const fmt = config.format;
  if (fmt) {
    const validTextFormats = FORMATS_BY_TYPE.text.map((f) => f.id);
    // 기존 호환: tel → phone, biz_no → rrn, none → free
    const remap: Record<string, string> = { tel: "phone", biz_no: "rrn", none: "free", currency: "free" };
    const mapped = remap[fmt] || fmt;
    if (validTextFormats.includes(mapped)) {
      return { kind: "input", type: "text", format: mapped };
    }
  }

  // 7. webType 기반
  if (it === "email") return { kind: "input", type: "text", format: "email" };
  if (it === "tel") return { kind: "input", type: "text", format: "phone" };
  if (it === "url") return { kind: "input", type: "text", format: "url" };
  if (it === "password") return { kind: "input", type: "text", format: "free" };

  // default
  return { kind: "input", type: "text", format: "free" };
}

// 모든 type 분기 사이 공유될 수 있는 잔재 필드.
// 분기 진입 시 일괄 reset 후 자기 분기에 필요한 필드만 set → 분기 간 잔재 0.
const TYPE_VOLATILE_FIELDS = [
  "fieldType", "inputType", "source", "multiple",
  "step", "min", "max", "rows", "tags", "accept",
  "unit", "thousands", "mode", "mask",
  "boolStyle", "trueLabel", "falseLabel",
  "autoGeneration", "computed", "readonly",
  // type 별 옵션 (select options / text length / date 옵션)
  "options", "minLength", "maxLength",
  "dateFormat", "minDate", "maxDate", "showToday", "maxRangeDays",
  // multi 의 maxSelect (mode 는 이미 위 라인에 포함)
  "maxSelect",
  // defaultValue — type 마다 의미 다르므로 잔재 방지 (예: text "저는 김민호" → entity 로 변경 시 부적합)
  "defaultValue",
] as const;

function clearVolatileFields(next: Record<string, any>) {
  // spread merge ({...currentConfig, ...newConfig}) 에서 키 제거 (delete) 만 하면 currentConfig 의
  // 같은 키가 살아남음. = undefined 로 명시해야 spread 가 덮어씀 → 잔재 완전 제거
  for (const f of TYPE_VOLATILE_FIELDS) next[f] = undefined;
}

// 새 (kind, type, format) 으로 config 재작성. 사용자 입력 필드 (label/placeholder/helperText/
// required/editable/disabled 등) 는 보존, defaultValue 와 type 별 잔재 필드는 일괄 reset.
function applyTriple(
  prev: Record<string, any>,
  kind: Kind,
  type: Type,
  format: string,
  ctx: { numberingTableName?: string },
): Record<string, any> {
  const next: Record<string, any> = { ...prev, kind, type, format };
  clearVolatileFields(next); // ★ 분기 진입 전 잔재 일괄 정리

  if (kind === "input") {
    if (type === "text") {
      next.fieldType = "text";
      next.inputType = format === "free" ? "text" : format;
      // free 는 줄 수 옵션, 나머지는 단일 라인 (clearVolatileFields 로 이미 reset 됨)
      if (format === "email") next.inputType = "email";
      if (format === "phone") next.inputType = "tel";
      if (format === "url") next.inputType = "url";
      return next;
    }
    if (type === "number") {
      next.fieldType = "number";
      next.step = format === "decimal" ? 0.01 : 1;
      next.unit = format === "percent" ? "%" : undefined;
      next.inputType = format === "decimal" ? "decimal" : format === "percent" ? "percentage" : "number";
      return next;
    }
    if (type === "money") {
      next.fieldType = "number";
      next.thousands = true;
      next.unit = format === "krw" ? "원" : (prev.currency || "USD");
      next.inputType = "currency";
      return next;
    }
    if (type === "date") {
      next.fieldType = "date";
      next.inputType = format === "range" ? "daterange" : format;
      return next;
    }
  }

  if (kind === "choice") {
    const isMulti = type === "multi";
    next.multiple = isMulti;
    if (format === "list" || format === "tags") {
      next.fieldType = "select";
      next.source = "static";
      if (format === "list") next.mode = isMulti ? "check" : "dropdown";
      if (format === "tags") {
        next.tags = true;
        next.mode = "tag";
      }
    } else if (format === "code") {
      next.fieldType = "category";
      next.source = "category";
    } else if (format === "entity") {
      next.fieldType = "entity";
      next.source = "entity";
    } else if (format === "status") {
      next.fieldType = "status";
      next.source = "category";
    } else if (format === "boolean") {
      next.fieldType = "checkbox";
      next.multiple = false;
      next.mode = "toggle";
      next.boolStyle = "switch";
    }
    return next;
  }

  if (kind === "auto") {
    if (type === "autonum") {
      next.fieldType = "numbering";
      next.inputType = "numbering";
      next.autoGeneration = {
        ...prev.autoGeneration,
        enabled: true,
        type: "numbering_rule",
        tableName: ctx.numberingTableName,
      };
      next.readonly = prev.readonly ?? true;
      return next;
    }
    if (type === "formula") {
      next.fieldType = "text";
      next.computed = prev.computed || "";
      next.autoGeneration = { enabled: true, type: "formula" };
      next.readonly = true;
      return next;
    }
    if (type === "audit") {
      next.fieldType = "datetime";
      next.autoGeneration = { enabled: true, type: "current_time" };
      next.readonly = true;
      return next;
    }
  }

  if (kind === "attach" && type === "file") {
    next.fieldType = "file";
    next.accept =
      format === "image" ? "image/*" : format === "doc" ? ".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx" : "*/*";
    return next;
  }

  return next;
}

// ─────────────────────────────────────────────────────────
interface InvFieldConfigPanelProps {
  config: Record<string, any>;
  onChange: (config: Record<string, any>) => void;
  tableName?: string;
  columnName?: string;
  tables?: TableSelectOption[];
  /** 전체 테이블 목록 (entity 참조 테이블 dropdown 용). 없으면 tables 로 폴백. */
  allTables?: TableSelectOption[];
  menuObjid?: number;
  screenTableName?: string;
  inputType?: string;
  componentType?: string;
}

export const InvFieldConfigPanel: React.FC<InvFieldConfigPanelProps> = ({
  config,
  onChange,
  tableName,
  columnName,
  tables = [],
  allTables,
  screenTableName,
  inputType: metaInputType,
  componentType,
}) => {
  const triple = resolveTriple(config, componentType, metaInputType);
  const { kind: currentKind, type: fieldType, format: currentFormat } = triple;
  const isSelectGroup = currentKind === "choice";
  const primaryTableName = screenTableName || tableName || "";
  const dataRole = (config.dataRole || "primary") as "primary" | "reference" | "child";
  const selectedSourceTable = dataRole === "primary" ? primaryTableName : config.sourceTable || "";
  const hasReferenceSource = dataRole !== "primary";

  const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
  const [loadingRules, setLoadingRules] = useState(false);
  const [rulesRefreshKey, setRulesRefreshKey] = useState(0);
  const numberingTableName = primaryTableName;

  const [entityColumns, setEntityColumns] = useState<ColumnOption[]>([]);
  const [loadingColumns, setLoadingColumns] = useState(false);
  const [sourceColumns, setSourceColumns] = useState<ColumnOption[]>([]);
  const [loadingSourceColumns, setLoadingSourceColumns] = useState(false);
  const [categoryValues, setCategoryValues] = useState<CategoryValueOption[]>([]);
  const [loadingCategoryValues, setLoadingCategoryValues] = useState(false);
  const [filterColumns, setFilterColumns] = useState<ColumnOption[]>([]);
  const [loadingFilterColumns, setLoadingFilterColumns] = useState(false);

  const updateConfig = (field: string, value: any) => onChange({ ...config, [field]: value });

  const loadColumnsForTable = useCallback(async (tblName: string): Promise<ColumnOption[]> => {
    if (!tblName) return [];
    const resp = await apiClient.get(`/table-management/tables/${tblName}/columns?size=500`);
    const data = resp.data.data || resp.data;
    const cols = data.columns || data || [];
    return cols.map((col: any) => ({
      columnName: col.columnName || col.column_name || col.name,
      columnLabel:
        col.displayName ||
        col.display_name ||
        col.columnLabel ||
        col.column_label ||
        col.columnName ||
        col.column_name ||
        col.name,
    }));
  }, []);

  const setTriple = (nextKind: Kind, nextType: Type, nextFormat: string) => {
    const next = applyTriple(config, nextKind, nextType, nextFormat, { numberingTableName });
    onChange(next);

    const syncTableName = screenTableName || tableName;
    const syncColumnName = columnName || config.columnName || config.fieldName;
    if (syncTableName && syncColumnName) {
      apiClient
        .put(`/table-management/tables/${syncTableName}/columns/${syncColumnName}/input-type`, {
          inputType: next.inputType || next.fieldType,
        })
        .then(() => {
          window.dispatchEvent(new CustomEvent("table-columns-refresh"));
        })
        .catch(() => {});
    }
  };

  const handleKindChange = (nextKind: Kind) => {
    const firstType = TYPES_BY_KIND[nextKind][0]?.id;
    if (!firstType) return;
    setTriple(nextKind, firstType, defaultFormatFor(firstType));
  };

  const handleTypeChange = (nextType: Type) => {
    const nextKind = KIND_OF_TYPE[nextType];
    setTriple(nextKind, nextType, defaultFormatFor(nextType));
  };

  const handleFormatChange = (nextFormat: string) => {
    setTriple(currentKind, fieldType, nextFormat);
  };

  const handleDataRoleChange = (nextRole: "primary" | "reference" | "child") => {
    const nextConfig: Record<string, any> = { ...config, dataRole: nextRole };
    if (nextRole === "primary") {
      nextConfig.sourceTable = primaryTableName || "";
      nextConfig.sourceColumn = config.sourceColumn || columnName || config.columnName || "";
    } else {
      nextConfig.sourceTable = config.sourceTable || "";
      nextConfig.sourceColumn = config.sourceColumn || "";
    }
    onChange(nextConfig);
  };

  // 채번 규칙 로드
  useEffect(() => {
    if (!(currentKind === "auto" && fieldType === "autonum")) return;
    if (!numberingTableName) {
      setNumberingRules([]);
      return;
    }
    let cancelled = false;
    setLoadingRules(true);
    (async () => {
      try {
        const resp = await getAvailableNumberingRulesForScreen(numberingTableName);
        if (!cancelled) setNumberingRules(resp.success && resp.data ? resp.data : []);
      } catch {
        if (!cancelled) setNumberingRules([]);
      } finally {
        if (!cancelled) setLoadingRules(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [numberingTableName, currentKind, fieldType, rulesRefreshKey]);

  // 엔티티 컬럼 로드
  const loadEntityColumns = useCallback(
    async (tblName: string) => {
      if (!tblName) {
        setEntityColumns([]);
        return;
      }
      setLoadingColumns(true);
      try {
        setEntityColumns(await loadColumnsForTable(tblName));
      } catch {
        setEntityColumns([]);
      } finally {
        setLoadingColumns(false);
      }
    },
    [loadColumnsForTable],
  );
  useEffect(() => {
    if (currentKind === "choice" && currentFormat === "entity" && config.entityTable) {
      loadEntityColumns(config.entityTable);
    }
  }, [currentKind, currentFormat, config.entityTable, loadEntityColumns]);

  // 데이터 컬럼 로드 (참조/하위 일 때)
  useEffect(() => {
    if (!selectedSourceTable) {
      setSourceColumns([]);
      return;
    }
    let cancelled = false;
    setLoadingSourceColumns(true);
    (async () => {
      try {
        if (!cancelled) setSourceColumns(await loadColumnsForTable(selectedSourceTable));
      } catch {
        if (!cancelled) setSourceColumns([]);
      } finally {
        if (!cancelled) setLoadingSourceColumns(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [selectedSourceTable, loadColumnsForTable]);

  // 카테고리 값 로드
  const loadCategoryValues = useCallback(async (catTable: string, catColumn: string) => {
    if (!catTable || !catColumn) {
      setCategoryValues([]);
      return;
    }
    setLoadingCategoryValues(true);
    try {
      const resp = await apiClient.get(`/table-categories/${catTable}/${catColumn}/values`);
      if (resp.data.success && resp.data.data) {
        const flattenTree = (items: any[], depth = 0): CategoryValueOption[] => {
          const result: CategoryValueOption[] = [];
          for (const item of items) {
            result.push({
              valueCode: item.valueCode,
              valueLabel: depth > 0 ? `${"  ".repeat(depth)}${item.valueLabel}` : item.valueLabel,
            });
            if (item.children?.length) result.push(...flattenTree(item.children, depth + 1));
          }
          return result;
        };
        setCategoryValues(flattenTree(resp.data.data));
      }
    } catch {
      setCategoryValues([]);
    } finally {
      setLoadingCategoryValues(false);
    }
  }, []);
  useEffect(() => {
    if (currentKind === "choice" && currentFormat === "code") {
      const catTable = config.categoryTable || tableName;
      const catColumn = config.categoryColumn || columnName;
      if (catTable && catColumn) loadCategoryValues(catTable, catColumn);
    }
  }, [currentKind, currentFormat, config.categoryTable, config.categoryColumn, tableName, columnName, loadCategoryValues]);

  // 필터 컬럼 로드 (entity / code 일 때만)
  const filterTargetTable = useMemo(() => {
    if (currentKind !== "choice") return null;
    if (currentFormat === "entity") return config.entityTable;
    if (currentFormat === "code" || currentFormat === "status") return config.categoryTable || tableName;
    return null;
  }, [currentKind, currentFormat, config.entityTable, config.categoryTable, tableName]);

  useEffect(() => {
    if (!filterTargetTable) {
      setFilterColumns([]);
      return;
    }
    let cancelled = false;
    setLoadingFilterColumns(true);
    (async () => {
      try {
        if (!cancelled) setFilterColumns(await loadColumnsForTable(filterTargetTable));
      } catch {
        if (!cancelled) setFilterColumns([]);
      } finally {
        if (!cancelled) setLoadingFilterColumns(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [filterTargetTable, loadColumnsForTable]);

  // ── 렌더 ────────────────────────────────────────────
  const visibleTypes = TYPES_BY_KIND[currentKind] || [];
  const visibleFormats = FORMATS_BY_TYPE[fieldType] || [];
  const showFormatTrigger = visibleFormats.length > 1;
  const tableOptions = normalizeTableSelectOptions(
    allTables && allTables.length > 0 ? allTables : tables,
  );

  return (
    <div
      style={{
        fontFamily: "var(--v5-font-sans)",
        color: "var(--cp-text)",
      }}
    >
      <CPCrumb
        kinds={KINDS}
        currentKind={currentKind}
        onChangeKind={(k) => handleKindChange(k as Kind)}
        types={visibleTypes}
        value={fieldType}
        onChange={(v) => handleTypeChange(v as Type)}
      />

      <div style={{ padding: "12px" }}>
        {/* ── 데이터 소속 ─────────────────────────── */}
        <CPSection title="① 데이터 소속" desc="이 필드의 값이 어디 컬럼에 저장되는가">
          <CPRow label="역할">
            <CPSegment
              value={dataRole}
              onChange={(v) => handleDataRoleChange(v as "primary" | "reference" | "child")}
              options={[
                { value: "primary", label: "주" },
                { value: "reference", label: "참조" },
                { value: "child", label: "하위" },
              ]}
            />
          </CPRow>
          <CPRow label="테이블" required={hasReferenceSource}>
            {hasReferenceSource ? (
              <CPSelect
                value={config.sourceTable || ""}
                onChange={(v) => onChange({ ...config, dataRole, sourceTable: v, sourceColumn: "" })}
              >
                <option value="">참조할 테이블 선택</option>
                {tableOptions.map((table) => (
                  <option key={table.tableName} value={table.tableName}>
                    {table.label}
                  </option>
                ))}
              </CPSelect>
            ) : (
              <DimText value={primaryTableName || "메인 테이블 미연결"} muted={!primaryTableName} />
            )}
          </CPRow>
          <CPRow label="컬럼" required={hasReferenceSource}>
            {hasReferenceSource ? (
              loadingSourceColumns ? (
                <InlineLoader text="컬럼 로딩 중..." />
              ) : (
                <CPSelect
                  value={config.sourceColumn || ""}
                  onChange={(v) => updateConfig("sourceColumn", v)}
                  disabled={!selectedSourceTable}
                >
                  <option value="">{!selectedSourceTable ? "먼저 테이블 선택" : "컬럼 선택"}</option>
                  {sourceColumns.map((column) => (
                    <option key={column.columnName} value={column.columnName}>
                      {column.columnLabel}
                    </option>
                  ))}
                </CPSelect>
              )
            ) : (
              <DimText value={columnName || config.columnName || config.fieldKey || "-"} mono />
            )}
          </CPRow>
          {hasReferenceSource && selectedSourceTable && !loadingSourceColumns && sourceColumns.length === 0 && (
            <Hint tone="warn">선택한 테이블의 컬럼 정보를 불러올 수 없어요.</Hint>
          )}
        </CPSection>

        {/* ── 유형별 설정 (format trigger + 본문) ──────────────── */}
        <CPSection title="② 유형별 설정">
          {showFormatTrigger && (
            <CPFormatTrigger
              formats={visibleFormats}
              value={currentFormat}
              onChange={handleFormatChange}
            />
          )}
          <FormatBody
            kind={currentKind}
            type={fieldType}
            format={currentFormat}
            config={config}
            updateConfig={updateConfig}
            onChange={onChange}
            tableName={tableName}
            columnName={columnName}
            tables={tables}
            allTables={allTables}
            loadingColumns={loadingColumns}
            entityColumns={entityColumns}
            categoryValues={categoryValues}
            loadingCategoryValues={loadingCategoryValues}
            numberingRules={numberingRules}
            loadingRules={loadingRules}
            numberingTableName={numberingTableName}
            onRulesRefresh={() => setRulesRefreshKey((k) => k + 1)}
          />
        </CPSection>

        {/* ── 데이터 필터 (code/entity/status 일 때만) ── */}
        {filterTargetTable && (
          <CPSection title="③ 데이터 필터" desc={`${filterTargetTable} 테이블에서 옵션을 불러올 때 적용`}>
            <FilterConditionsSection
              filters={(config.filters as OptionFilter[]) || []}
              columns={filterColumns}
              loadingColumns={loadingFilterColumns}
              onFiltersChange={(filters) => updateConfig("filters", filters)}
            />
          </CPSection>
        )}

        {/* ── 고급 설정 ─────────────────────────── */}
        {currentKind !== "auto" && (
          <CPGroup title="고급 설정" defaultOpen={false}>
            {isSelectGroup ? (
              <SelectAdvancedOptions config={config} updateConfig={updateConfig} multi={fieldType === "multi"} />
            ) : (
              <InputAdvancedOptions config={config} updateConfig={updateConfig} />
            )}
          </CPGroup>
        )}
      </div>
    </div>
  );
};

InvFieldConfigPanel.displayName = "InvFieldConfigPanel";

// ───────────────────────────────────────────────────────
// 유틸 컴포넌트
// ───────────────────────────────────────────────────────
function DimText({ value, mono, muted }: { value: React.ReactNode; mono?: boolean; muted?: boolean }) {
  return (
    <div
      style={{
        height: 28,
        padding: "0 8px",
        display: "flex",
        alignItems: "center",
        fontSize: 12,
        fontFamily: mono ? "var(--v5-font-mono)" : "var(--v5-font-sans)",
        background: "var(--cp-bg-subtle)",
        border: "1px solid var(--cp-border)",
        borderRadius: 6,
        color: muted ? "var(--cp-text-muted)" : "var(--cp-text)",
      }}
    >
      {value}
    </div>
  );
}

function InlineLoader({ text }: { text: string }) {
  return (
    <span
      style={{
        fontSize: 11,
        color: "var(--cp-text-muted)",
        display: "inline-flex",
        alignItems: "center",
        gap: 5,
      }}
    >
      <Loader2 size={11} className="animate-spin" /> {text}
    </span>
  );
}

function Hint({ children, tone = "default" }: { children: React.ReactNode; tone?: "default" | "warn" }) {
  return (
    <div
      style={{
        fontSize: 10.5,
        color: tone === "warn" ? "var(--v5-amber)" : "var(--cp-text-muted)",
        marginTop: 4,
        lineHeight: 1.4,
      }}
    >
      {children}
    </div>
  );
}

// ───────────────────────────────────────────────────────
// 유형별 본문 디스패처
// ───────────────────────────────────────────────────────
type FormatBodyProps = {
  kind: Kind;
  type: Type;
  format: string;
  config: Record<string, any>;
  updateConfig: (k: string, v: any) => void;
  onChange: (config: Record<string, any>) => void;
  tableName?: string;
  columnName?: string;
  tables: TableSelectOption[];
  /** 전체 테이블 목록 (entity 참조 테이블 dropdown 용). 없으면 tables 폴백. */
  allTables?: TableSelectOption[];
  loadingColumns: boolean;
  entityColumns: ColumnOption[];
  categoryValues: CategoryValueOption[];
  loadingCategoryValues: boolean;
  numberingRules: NumberingRuleConfig[];
  loadingRules: boolean;
  numberingTableName: string;
  onRulesRefresh: () => void;
};

function FormatBody(p: FormatBodyProps) {
  const { kind, type, format, config, updateConfig, onChange } = p;

  // 입력
  if (kind === "input" && type === "text") return <TextOptions format={format} config={config} updateConfig={updateConfig} />;
  if (kind === "input" && type === "number") return <NumberOptions format={format} config={config} updateConfig={updateConfig} />;
  if (kind === "input" && type === "money") return <MoneyOptions format={format} config={config} updateConfig={updateConfig} />;
  if (kind === "input" && type === "date") return <DateOptions format={format} config={config} updateConfig={updateConfig} />;

  // 선택
  if (kind === "choice") {
    const isMulti = type === "multi";
    if (format === "list") return <SelectOptions multi={isMulti} config={config} updateConfig={updateConfig} />;
    if (format === "code") return (
      <CategoryOptions
        config={config}
        tableName={p.tableName}
        columnName={p.columnName}
        loading={p.loadingCategoryValues}
        values={p.categoryValues}
        updateConfig={updateConfig}
      />
    );
    if (format === "entity") return (
      <EntityOptions
        config={config}
        tables={(p.allTables && p.allTables.length > 0) ? p.allTables : p.tables}
        loadingColumns={p.loadingColumns}
        entityColumns={p.entityColumns}
        onChange={onChange}
        updateConfig={updateConfig}
        multi={isMulti}
      />
    );
    if (format === "status") return <StatusOptions config={config} updateConfig={updateConfig} />;
    if (format === "boolean") return <BooleanOptions config={config} updateConfig={updateConfig} />;
    if (format === "tags") return <TagsOptions config={config} updateConfig={updateConfig} />;
  }

  // 자동
  if (kind === "auto" && type === "autonum") return (
    <NumberingOptions
      config={config}
      numberingTableName={p.numberingTableName}
      columnName={p.columnName}
      loading={p.loadingRules}
      rules={p.numberingRules}
      onChange={onChange}
      updateConfig={updateConfig}
      onRulesRefresh={p.onRulesRefresh}
    />
  );
  if (kind === "auto" && type === "formula") return <FormulaOptions config={config} updateConfig={updateConfig} />;
  if (kind === "auto" && type === "audit") return <AuditOptions config={config} updateConfig={updateConfig} />;

  // 첨부
  if (kind === "attach" && type === "file") return <FileOptions format={format} config={config} updateConfig={updateConfig} />;

  return <Hint>준비 중인 유형입니다.</Hint>;
}

// ───────────────────────────────────────────────────────
// 입력 — 글자
// ───────────────────────────────────────────────────────
function TextOptions({
  format,
  config,
  updateConfig,
}: {
  format: string;
  config: Record<string, any>;
  updateConfig: (k: string, v: any) => void;
}) {
  return (
    <>
      <CPRow label="안내 텍스트">
        <CPText value={config.placeholder ?? ""} onChange={(v) => updateConfig("placeholder", v)} placeholder="입력 안내" />
      </CPRow>
      <CPRow label="기본값">
        <CPText value={config.defaultValue ?? ""} onChange={(v) => updateConfig("defaultValue", v)} placeholder="비움" />
      </CPRow>

      {format === "free" && (
        <>
          <CPRow label="줄 수">
            <CPSegment
              value={String(config.rows ?? 1)}
              onChange={(v) => updateConfig("rows", Number(v))}
              options={[{ value: "1", label: "한 줄" }, { value: "4", label: "여러 줄" }]}
            />
          </CPRow>
          <CPRow label="최소/최대">
            <div style={{ display: "flex", gap: 5 }}>
              <CPNumber value={config.minLength ?? ""} onChange={(v) => updateConfig("minLength", v)} placeholder="최소" suffix="자" />
              <CPNumber value={config.maxLength ?? ""} onChange={(v) => updateConfig("maxLength", v)} placeholder="제한 없음" suffix="자" />
            </div>
          </CPRow>
          <CPRow label="허용 문자">
            <CPSelect value={config.allow || "any"} onChange={(v) => updateConfig("allow", v)}>
              <option value="any">제한 없음</option>
              <option value="alphanum">영문+숫자</option>
              <option value="kor">한글만</option>
              <option value="num">숫자만</option>
            </CPSelect>
          </CPRow>
          <CPRow label="글자수 카운터">
            <CPSwitch value={!!config.showCounter} onChange={(v) => updateConfig("showCounter", v)} />
          </CPRow>
        </>
      )}

      {format === "email" && (
        <>
          <CPRow label="도메인 제한">
            <CPText mono value={config.domain ?? ""} onChange={(v) => updateConfig("domain", v)} placeholder="예: @company.com" />
          </CPRow>
          <CPRow label="중복 검사">
            <CPSwitch value={config.uniqueCheck ?? false} onChange={(v) => updateConfig("uniqueCheck", v)} />
          </CPRow>
          <CPRow label="대소문자">
            <CPSegment
              value={config.caseRule || "lower"}
              onChange={(v) => updateConfig("caseRule", v)}
              options={[{ value: "keep", label: "유지" }, { value: "lower", label: "소문자" }]}
            />
          </CPRow>
        </>
      )}

      {format === "phone" && (
        <>
          <CPRow label="국가">
            <CPSegment
              value={config.country || "kr"}
              onChange={(v) => updateConfig("country", v)}
              options={[{ value: "kr", label: "국내" }, { value: "intl", label: "국제" }]}
            />
          </CPRow>
          <CPRow label="형식 마스크">
            <CPText mono value={config.mask ?? "010-####-####"} onChange={(v) => updateConfig("mask", v)} />
          </CPRow>
          <CPRow label="휴대폰만">
            <CPSwitch value={!!config.mobileOnly} onChange={(v) => updateConfig("mobileOnly", v)} />
          </CPRow>
          <CPRow label="SMS 인증">
            <CPSwitch value={!!config.smsVerify} onChange={(v) => updateConfig("smsVerify", v)} />
          </CPRow>
        </>
      )}

      {format === "rrn" && (
        <>
          <CPRow label="종류">
            <CPSegment
              value={config.rrnKind || "biz"}
              onChange={(v) => updateConfig("rrnKind", v)}
              options={[
                { value: "rrn", label: "주민" },
                { value: "biz", label: "사업자" },
                { value: "foreign", label: "외국인" },
              ]}
            />
          </CPRow>
          <CPRow label="형식 마스크">
            <CPText
              mono
              value={config.mask ?? (config.rrnKind === "biz" ? "###-##-#####" : "######-#######")}
              onChange={(v) => updateConfig("mask", v)}
            />
          </CPRow>
          <CPRow label="마스킹 표시">
            <CPSwitch value={config.maskDisplay ?? true} onChange={(v) => updateConfig("maskDisplay", v)} />
          </CPRow>
          <CPRow label="암호화 저장">
            <CPSwitch value={config.encrypt ?? true} onChange={(v) => updateConfig("encrypt", v)} />
          </CPRow>
          <CPRow label="체크섬 검증">
            <CPSwitch value={config.checksum ?? true} onChange={(v) => updateConfig("checksum", v)} />
          </CPRow>
        </>
      )}

      {format === "address" && (
        <>
          <Hint>주소 검색 API 연동 (juso.go.kr / 카카오) — 백엔드 미구현. 현재는 일반 텍스트로 저장됩니다.</Hint>
          <CPRow label="저장 형태">
            <CPSegment
              value={config.addressShape || "single"}
              onChange={(v) => updateConfig("addressShape", v)}
              options={[
                { value: "single", label: "한 덩어리" },
                { value: "split", label: "분리" },
                { value: "json", label: "JSON" },
              ]}
            />
          </CPRow>
          <CPRow label="검색 API">
            <CPSelect value={config.addressApi || "juso"} onChange={(v) => updateConfig("addressApi", v)}>
              <option value="juso">도로명주소 API</option>
              <option value="kakao">카카오</option>
              <option value="none">사용 안 함</option>
            </CPSelect>
          </CPRow>
          <CPRow label="국내만">
            <CPSwitch value={config.koreaOnly ?? true} onChange={(v) => updateConfig("koreaOnly", v)} />
          </CPRow>
        </>
      )}

      {format === "card" && (
        <>
          <Hint>카드번호 — Luhn 검증 / PCI 마스킹은 백엔드 별도 처리 필요.</Hint>
          <CPRow label="형식 마스크">
            <CPText mono value={config.mask ?? "####-****-****-####"} onChange={(v) => updateConfig("mask", v)} />
          </CPRow>
          <CPRow label="암호화 저장">
            <CPSwitch value={config.encrypt ?? true} onChange={(v) => updateConfig("encrypt", v)} />
          </CPRow>
          <CPRow label="Luhn 검증">
            <CPSwitch value={config.luhn ?? true} onChange={(v) => updateConfig("luhn", v)} />
          </CPRow>
        </>
      )}

      {format === "url" && (
        <>
          <CPRow label="프로토콜">
            <CPSegment
              value={config.urlProto || "any"}
              onChange={(v) => updateConfig("urlProto", v)}
              options={[{ value: "any", label: "모두" }, { value: "https", label: "HTTPS만" }]}
            />
          </CPRow>
          <CPRow label="도메인 화이트리스트">
            <CPText mono value={config.urlWhitelist ?? ""} onChange={(v) => updateConfig("urlWhitelist", v)} placeholder="invyone.com, *.co.kr" />
          </CPRow>
        </>
      )}

      {format === "mask" && (
        <>
          <CPRow label="마스킹 패턴" required>
            <CPText mono value={config.mask ?? ""} onChange={(v) => updateConfig("mask", v)} placeholder="예: ###-####" />
          </CPRow>
          <CPRow label="허용 문자">
            <CPSelect value={config.allow || "any"} onChange={(v) => updateConfig("allow", v)}>
              <option value="any">제한 없음</option>
              <option value="num">숫자만</option>
              <option value="alphanum">영문+숫자</option>
            </CPSelect>
          </CPRow>
        </>
      )}
    </>
  );
}

// ───────────────────────────────────────────────────────
// 입력 — 숫자
// ───────────────────────────────────────────────────────
function NumberOptions({
  format,
  config,
  updateConfig,
}: {
  format: string;
  config: Record<string, any>;
  updateConfig: (k: string, v: any) => void;
}) {
  return (
    <>
      <CPRow label="안내 텍스트">
        <CPText value={config.placeholder ?? ""} onChange={(v) => updateConfig("placeholder", v)} placeholder="입력 안내" />
      </CPRow>
      <CPRow label="값 범위">
        <div style={{ display: "flex", gap: 5 }}>
          <CPNumber value={config.min ?? ""} onChange={(v) => updateConfig("min", v)} placeholder={format === "percent" ? "0" : "최소"} />
          <CPNumber value={config.max ?? ""} onChange={(v) => updateConfig("max", v)} placeholder={format === "percent" ? "100" : "최대"} />
        </div>
      </CPRow>
      <CPRow label="단계">
        <CPNumber
          value={config.step ?? ""}
          onChange={(v) => updateConfig("step", v)}
          placeholder={format === "decimal" ? "0.01" : "1"}
        />
      </CPRow>
      {format === "decimal" && (
        <CPRow label="소수 자리">
          <CPNumber value={config.decimalPlaces ?? 2} onChange={(v) => updateConfig("decimalPlaces", v ?? 2)} min={0} max={10} />
        </CPRow>
      )}
      {format === "percent" && (
        <CPRow label="기호 표시">
          <CPSwitch value={config.showPercent ?? true} onChange={(v) => updateConfig("showPercent", v)} />
        </CPRow>
      )}
      <CPRow label="천 단위 구분">
        <CPSwitch value={config.thousands ?? false} onChange={(v) => updateConfig("thousands", v)} />
      </CPRow>
      <CPRow label="단위">
        <CPText value={config.unit ?? (format === "percent" ? "%" : "")} onChange={(v) => updateConfig("unit", v)} placeholder="예: 개, kg" />
      </CPRow>
    </>
  );
}

// ───────────────────────────────────────────────────────
// 입력 — 금액
// ───────────────────────────────────────────────────────
function MoneyOptions({
  format,
  config,
  updateConfig,
}: {
  format: string;
  config: Record<string, any>;
  updateConfig: (k: string, v: any) => void;
}) {
  return (
    <>
      <CPRow label="안내 텍스트">
        <CPText value={config.placeholder ?? ""} onChange={(v) => updateConfig("placeholder", v)} placeholder="0" />
      </CPRow>
      {format === "krw" ? (
        <>
          <CPRow label="기호">
            <CPSegment
              value={config.symbolPos || "prefix"}
              onChange={(v) => updateConfig("symbolPos", v)}
              options={[{ value: "prefix", label: "₩ 앞" }, { value: "suffix", label: "원 뒤" }, { value: "none", label: "없음" }]}
            />
          </CPRow>
          <CPRow label="천 단위 구분">
            <CPSwitch value={config.thousands ?? true} onChange={(v) => updateConfig("thousands", v)} />
          </CPRow>
          <CPRow label="음수 허용">
            <CPSwitch value={config.allowNegative ?? false} onChange={(v) => updateConfig("allowNegative", v)} />
          </CPRow>
        </>
      ) : (
        <>
          <Hint>외화 — 백엔드 다중 통화 컬럼 미구현. 현재는 통화 코드만 함께 저장.</Hint>
          <CPRow label="통화">
            <CPSelect value={config.currency || "USD"} onChange={(v) => updateConfig("currency", v)}>
              <option value="USD">USD (달러)</option>
              <option value="EUR">EUR (유로)</option>
              <option value="JPY">JPY (엔)</option>
              <option value="CNY">CNY (위안)</option>
              <option value="GBP">GBP (파운드)</option>
            </CPSelect>
          </CPRow>
          <CPRow label="환율 자동 조회">
            <CPSwitch value={!!config.autoFx} onChange={(v) => updateConfig("autoFx", v)} />
          </CPRow>
          <CPRow label="기준 통화">
            <CPText mono value={config.baseCurrency ?? "KRW"} onChange={(v) => updateConfig("baseCurrency", v)} />
          </CPRow>
        </>
      )}
    </>
  );
}

// ───────────────────────────────────────────────────────
// 입력 — 날짜
// ───────────────────────────────────────────────────────
function DateOptions({
  format,
  config,
  updateConfig,
}: {
  format: string;
  config: Record<string, any>;
  updateConfig: (k: string, v: any) => void;
}) {
  const dateFormatFallback =
    format === "datetime" ? "YYYY-MM-DD HH:mm" : format === "time" ? "HH:mm" : "YYYY-MM-DD";
  return (
    <>
      <CPRow label="기본값">
        <CPSegment
          value={config.dateDefault || "none"}
          onChange={(v) => updateConfig("dateDefault", v)}
          options={[
            { value: "none", label: "비움" },
            { value: "today", label: "오늘" },
            { value: "now", label: "지금" },
          ]}
        />
      </CPRow>
      {format === "range" ? (
        <>
          <CPRow label="범위 프리셋">
            <CPSwitch value={config.rangePresets ?? true} onChange={(v) => updateConfig("rangePresets", v)} />
          </CPRow>
          <CPRow label="최대 기간(일)">
            <CPNumber value={config.maxRangeDays ?? ""} onChange={(v) => updateConfig("maxRangeDays", v)} placeholder="제한 없음" />
          </CPRow>
        </>
      ) : (
        <CPRow label="형식 표시">
          <CPText
            mono
            value={config.dateFormat ?? dateFormatFallback}
            onChange={(v) => updateConfig("dateFormat", v)}
          />
        </CPRow>
      )}
      <CPRow label="제한">
        <div style={{ display: "flex", gap: 5 }}>
          <CPText mono value={config.minDate ?? ""} onChange={(v) => updateConfig("minDate", v)} placeholder="최소 (YYYY-MM-DD)" />
          <CPText mono value={config.maxDate ?? ""} onChange={(v) => updateConfig("maxDate", v)} placeholder="최대" />
        </div>
      </CPRow>
      <CPRow label="오늘 버튼">
        <CPSwitch value={config.showToday ?? false} onChange={(v) => updateConfig("showToday", v)} />
      </CPRow>
      <CPRow label="달력 시작 요일">
        <CPSegment
          value={config.weekStart || "sun"}
          onChange={(v) => updateConfig("weekStart", v)}
          options={[{ value: "sun", label: "일" }, { value: "mon", label: "월" }]}
        />
      </CPRow>
    </>
  );
}

// ───────────────────────────────────────────────────────
// 선택형 옵션
// ───────────────────────────────────────────────────────
function SelectOptions({
  config,
  updateConfig,
  multi = false,
}: {
  config: Record<string, any>;
  updateConfig: (k: string, v: any) => void;
  multi?: boolean;
}) {
  const options = (config.options as { value: string; label?: string }[]) || [];
  const addOption = () => updateConfig("options", [...options, { value: "", label: "" }]);
  const updateOptionValue = (index: number, value: string) => {
    const next = [...options];
    next[index] = { ...next[index], value, label: value };
    updateConfig("options", next);
  };
  const removeOption = (index: number) => updateConfig("options", options.filter((_, i) => i !== index));

  return (
    <>
      <div
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
          paddingBottom: 6,
          marginBottom: 4,
        }}
      >
        <span style={{ fontSize: 11, fontWeight: 600, color: "var(--cp-text-sec)" }}>
          {multi ? "선택 가능 옵션" : "옵션 목록"} ({options.length})
        </span>
        <button
          type="button"
          onClick={addOption}
          style={{
            fontSize: 11,
            fontWeight: 600,
            padding: "4px 9px",
            border: "1px solid var(--cp-border)",
            background: "var(--cp-surface)",
            color: "var(--cp-text-sec)",
            borderRadius: 5,
            cursor: "pointer",
            display: "inline-flex",
            alignItems: "center",
            gap: 4,
          }}
        >
          {CP_ICONS.plus} 추가
        </button>
      </div>
      {options.length === 0 ? (
        <div
          style={{
            fontSize: 11,
            color: "var(--cp-text-muted)",
            padding: "16px 8px",
            textAlign: "center",
            border: "1px dashed var(--cp-border)",
            borderRadius: 6,
          }}
        >
          아직 옵션이 없어요. 위 추가 버튼으로 만들어보세요.
        </div>
      ) : (
        <div
          style={{
            display: "flex",
            flexDirection: "column",
            gap: 4,
            maxHeight: 220,
            overflowY: "auto",
          }}
        >
          {options.map((option, index) => (
            <div
              key={index}
              style={{
                display: "grid",
                gridTemplateColumns: "10px 1fr 24px",
                gap: 6,
                alignItems: "center",
                padding: 4,
                background: "var(--cp-bg-subtle)",
                border: "1px solid var(--cp-border)",
                borderRadius: 4,
              }}
            >
              <span style={{ color: "var(--cp-text-muted)" }}>{CP_ICONS.grip}</span>
              <CPText value={option.value || ""} onChange={(v) => updateOptionValue(index, v)} placeholder={`옵션 ${index + 1}`} />
              <CPIconBtn tone="danger" size={20} onClick={() => removeOption(index)}>
                {CP_ICONS.trash}
              </CPIconBtn>
            </div>
          ))}
        </div>
      )}
      {options.length > 0 && (
        <CPRow label="기본 선택값">
          <CPSelect value={config.defaultValue || ""} onChange={(v) => updateConfig("defaultValue", v)}>
            <option value="">선택 안함</option>
            {options.map((opt, i) => (
              <option key={`d-${i}`} value={opt.value || `_idx_${i}`}>
                {opt.label || opt.value || `옵션 ${i + 1}`}
              </option>
            ))}
          </CPSelect>
        </CPRow>
      )}
    </>
  );
}

function CategoryOptions({
  config,
  tableName,
  columnName,
  loading,
  values,
  updateConfig,
}: {
  config: Record<string, any>;
  tableName?: string;
  columnName?: string;
  loading: boolean;
  values: CategoryValueOption[];
  updateConfig: (k: string, v: any) => void;
}) {
  return (
    <>
      <CPRow label="테이블">
        <DimText value={config.categoryTable || tableName || "-"} mono />
      </CPRow>
      <CPRow label="컬럼">
        <DimText value={config.categoryColumn || columnName || "-"} mono />
      </CPRow>
      {config.source === "code" && config.codeGroup && (
        <CPRow label="코드 그룹">
          <DimText value={config.codeGroup} mono />
        </CPRow>
      )}

      {loading ? (
        <div style={{ padding: "8px 0" }}>
          <InlineLoader text="카테고리 값 로딩 중..." />
        </div>
      ) : values.length > 0 ? (
        <>
          <div
            style={{
              fontSize: 10.5,
              color: "var(--cp-text-muted)",
              padding: "8px 0 4px",
            }}
          >
            등록된 값 {values.length}개
          </div>
          <div
            style={{
              maxHeight: 120,
              overflowY: "auto",
              border: "1px solid var(--cp-border)",
              borderRadius: 6,
              background: "var(--cp-surface)",
            }}
          >
            {values.map((cv) => (
              <div
                key={cv.valueCode}
                style={{
                  display: "flex",
                  alignItems: "center",
                  gap: 6,
                  padding: "3px 8px",
                  borderBottom: "1px solid var(--cp-border-subtle)",
                  fontSize: 11,
                }}
              >
                <span
                  style={{
                    fontFamily: "var(--v5-font-mono)",
                    fontSize: 10,
                    color: "var(--cp-text-muted)",
                    flexShrink: 0,
                    minWidth: 36,
                  }}
                >
                  {cv.valueCode}
                </span>
                <span style={{ color: "var(--cp-text)" }}>{cv.valueLabel}</span>
              </div>
            ))}
          </div>
          <CPRow label="기본 선택값">
            <CPSelect value={config.defaultValue || ""} onChange={(v) => updateConfig("defaultValue", v)}>
              <option value="">선택 안함</option>
              {values.map((cv) => (
                <option key={cv.valueCode} value={cv.valueCode}>
                  {cv.valueLabel}
                </option>
              ))}
            </CPSelect>
          </CPRow>
        </>
      ) : (
        <Hint tone="warn">카테고리 값이 없습니다. 테이블 카테고리 관리에서 값을 추가해주세요.</Hint>
      )}
    </>
  );
}

function EntityOptions({
  config,
  tables,
  loadingColumns,
  entityColumns,
  onChange,
  updateConfig,
  multi = false,
}: {
  config: Record<string, any>;
  tables: TableSelectOption[];
  loadingColumns: boolean;
  entityColumns: ColumnOption[];
  onChange: (config: Record<string, any>) => void;
  updateConfig: (k: string, v: any) => void;
  multi?: boolean;
}) {
  const tableOptions = normalizeTableSelectOptions(tables);

  return (
    <>
      <CPRow label="참조 테이블" required>
        <CPSelect
          value={config.entityTable || ""}
          onChange={(v) =>
            onChange({ ...config, entityTable: v, entityValueColumn: "", entityLabelColumn: "" })
          }
        >
          <option value="">테이블 선택</option>
          {tableOptions.map((table) => (
            <option key={table.tableName} value={table.tableName}>
              {table.label}
            </option>
          ))}
        </CPSelect>
      </CPRow>

      {loadingColumns ? (
        <div style={{ padding: "4px 0" }}>
          <InlineLoader text="컬럼 목록 로딩 중..." />
        </div>
      ) : entityColumns.length > 0 ? (
        <>
          <CPRow label="저장 값" required help="DB 에 실제 저장되는 컬럼">
            <CPSelect value={config.entityValueColumn || ""} onChange={(v) => updateConfig("entityValueColumn", v)}>
              <option value="">컬럼 선택</option>
              {entityColumns.map((col) => (
                <option key={col.columnName} value={col.columnName}>
                  {col.columnLabel}
                </option>
              ))}
            </CPSelect>
          </CPRow>
          <CPRow label="표시 라벨" required help="사용자 화면에 보이는 컬럼">
            <CPSelect value={config.entityLabelColumn || ""} onChange={(v) => updateConfig("entityLabelColumn", v)}>
              <option value="">컬럼 선택</option>
              {entityColumns.map((col) => (
                <option key={col.columnName} value={col.columnName}>
                  {col.columnLabel}
                </option>
              ))}
            </CPSelect>
          </CPRow>
        </>
      ) : config.entityTable ? (
        <Hint tone="warn">선택한 테이블의 컬럼 정보를 불러올 수 없어요.</Hint>
      ) : null}
      {multi && <Hint>다중 엔티티(다대다) — 백엔드 매핑 테이블 미구현. 현재는 다중값 컬럼에 JSON 으로 저장.</Hint>}
    </>
  );
}

// ───────────────────────────────────────────────────────
// 선택 — 상태 (color label)
// ───────────────────────────────────────────────────────
function StatusOptions({
  config,
  updateConfig,
}: {
  config: Record<string, any>;
  updateConfig: (k: string, v: any) => void;
}) {
  const states: Array<{ value: string; label: string; color?: string }> = config.statusList || [];
  const update = (next: any[]) => updateConfig("statusList", next);
  return (
    <>
      <Hint>각 상태에 색을 지정해 라벨로 표시합니다.</Hint>
      <div style={{ display: "flex", flexDirection: "column", gap: 4, marginTop: 4 }}>
        {states.map((s, i) => (
          <div
            key={i}
            style={{
              display: "grid",
              gridTemplateColumns: "1fr 1fr 36px 24px",
              gap: 4,
              alignItems: "center",
              padding: 4,
              border: "1px solid var(--cp-border)",
              borderRadius: 4,
              background: "var(--cp-bg-subtle)",
            }}
          >
            <CPText value={s.value} placeholder="값" onChange={(v) => update(states.map((x, j) => j === i ? { ...x, value: v } : x))} />
            <CPText value={s.label} placeholder="라벨" onChange={(v) => update(states.map((x, j) => j === i ? { ...x, label: v } : x))} />
            <CPColor value={s.color || "#9998ad"} onChange={(v) => update(states.map((x, j) => j === i ? { ...x, color: v } : x))} />
            <CPIconBtn tone="danger" size={20} onClick={() => update(states.filter((_, j) => j !== i))}>{CP_ICONS.trash}</CPIconBtn>
          </div>
        ))}
      </div>
      <button
        type="button"
        onClick={() => update([...states, { value: "", label: "", color: "#9998ad" }])}
        style={{
          marginTop: 6,
          fontSize: 11, fontWeight: 600,
          padding: "4px 9px",
          border: "1px solid var(--cp-border)",
          background: "var(--cp-surface)",
          color: "var(--cp-text-sec)",
          borderRadius: 5,
          cursor: "pointer",
          display: "inline-flex", alignItems: "center", gap: 4,
        }}
      >
        {CP_ICONS.plus} 상태 추가
      </button>
    </>
  );
}

// ───────────────────────────────────────────────────────
// 선택 — 예/아니오
// ───────────────────────────────────────────────────────
function BooleanOptions({
  config,
  updateConfig,
}: {
  config: Record<string, any>;
  updateConfig: (k: string, v: any) => void;
}) {
  return (
    <>
      <Hint>canonical input 이 직접 처리합니다. 표시 형태와 라벨만 설정합니다.</Hint>
      <CPRow label="표시 형태">
        <CPSegment
          value={config.boolStyle || "switch"}
          onChange={(v) => updateConfig("boolStyle", v)}
          options={[
            { value: "checkbox", label: "체크박스" },
            { value: "switch", label: "스위치" },
            { value: "yesno", label: "버튼 Y/N" },
          ]}
        />
      </CPRow>
      <CPRow label="True 라벨">
        <CPText value={config.trueLabel ?? "예"} onChange={(v) => updateConfig("trueLabel", v)} placeholder="예" />
      </CPRow>
      <CPRow label="False 라벨">
        <CPText value={config.falseLabel ?? "아니오"} onChange={(v) => updateConfig("falseLabel", v)} placeholder="아니오" />
      </CPRow>
      <CPRow label="기본값">
        <CPSegment
          value={String(config.defaultValue ?? "false")}
          onChange={(v) => updateConfig("defaultValue", v === "true")}
          options={[
            { value: "true", label: "예" },
            { value: "false", label: "아니오" },
          ]}
        />
      </CPRow>
    </>
  );
}

// ───────────────────────────────────────────────────────
// 선택 — 태그 (자유 다중)
// ───────────────────────────────────────────────────────
function TagsOptions({
  config,
  updateConfig,
}: {
  config: Record<string, any>;
  updateConfig: (k: string, v: any) => void;
}) {
  return (
    <>
      <CPRow label="자유 입력">
        <CPSwitch value={config.allowFreeTags ?? true} onChange={(v) => updateConfig("allowFreeTags", v)} />
      </CPRow>
      <CPRow label="구분자">
        <CPSegment
          value={config.tagSeparator || ","}
          onChange={(v) => updateConfig("tagSeparator", v)}
          options={[{ value: ",", label: "," }, { value: " ", label: "공백" }, { value: "enter", label: "엔터" }]}
        />
      </CPRow>
      <CPRow label="최대 개수">
        <CPNumber value={config.maxTags ?? ""} onChange={(v) => updateConfig("maxTags", v)} placeholder="제한 없음" />
      </CPRow>
      <CPRow label="추천 목록">
        <CPText value={config.tagSuggestions ?? ""} onChange={(v) => updateConfig("tagSuggestions", v)} placeholder="태그1, 태그2 ..." />
      </CPRow>
    </>
  );
}

// ───────────────────────────────────────────────────────
// 자동 — 계산 (formula)
// ───────────────────────────────────────────────────────
function FormulaOptions({
  config,
  updateConfig,
}: {
  config: Record<string, any>;
  updateConfig: (k: string, v: any) => void;
}) {
  return (
    <>
      <Hint tone="warn">계산 — 안전 수식 파서(`lib/formula-parser`) 미구현. UI 에 작성한 식은 저장은 되지만 실행은 미적용.</Hint>
      <CPRow label="수식" required>
        <CPText
          mono
          value={config.computed ?? ""}
          onChange={(v) => updateConfig("computed", v)}
          placeholder="예: qty * price"
        />
      </CPRow>
      <CPRow label="결과 형식">
        <CPSegment
          value={config.formulaResult || "number"}
          onChange={(v) => updateConfig("formulaResult", v)}
          options={[
            { value: "number", label: "숫자" },
            { value: "currency", label: "금액" },
            { value: "text", label: "글자" },
          ]}
        />
      </CPRow>
      <CPRow label="재계산 시점">
        <CPSegment
          value={config.recalcOn || "change"}
          onChange={(v) => updateConfig("recalcOn", v)}
          options={[
            { value: "change", label: "변경 시" },
            { value: "save", label: "저장 시" },
            { value: "load", label: "조회 시" },
          ]}
        />
      </CPRow>
    </>
  );
}

// ───────────────────────────────────────────────────────
// 자동 — 감사 (audit)
// ───────────────────────────────────────────────────────
function AuditOptions({
  config,
  updateConfig,
}: {
  config: Record<string, any>;
  updateConfig: (k: string, v: any) => void;
}) {
  const ag = config.autoGeneration || {};
  const auditEvent = config.auditEvent || (ag.type === "current_user" ? "user" : "time");
  return (
    <>
      <CPRow label="이벤트">
        <CPSelect value={auditEvent} onChange={(v) => updateConfig("auditEvent", v)}>
          <option value="created_at">작성일 (created_at)</option>
          <option value="updated_at">수정일 (updated_at)</option>
          <option value="created_by">작성자 (created_by)</option>
          <option value="updated_by">수정자 (updated_by)</option>
        </CPSelect>
      </CPRow>
      <CPRow label="값 채우기">
        <CPSelect
          value={ag.type || "current_time"}
          onChange={(v) => updateConfig("autoGeneration", { ...ag, enabled: true, type: v })}
        >
          <option value="current_time">현재 시간</option>
          <option value="current_user">현재 사용자</option>
        </CPSelect>
      </CPRow>
      <CPRow label="읽기 전용">
        <CPSwitch value={config.readonly !== false} onChange={(v) => updateConfig("readonly", v)} />
      </CPRow>
    </>
  );
}

// ───────────────────────────────────────────────────────
// 첨부 — 파일
// ───────────────────────────────────────────────────────
function FileOptions({
  format,
  config,
  updateConfig,
}: {
  format: string;
  config: Record<string, any>;
  updateConfig: (k: string, v: any) => void;
}) {
  return (
    <>
      <Hint>canonical input 의 파일 필드로 업로드합니다. 허용 형식과 제한만 설정합니다.</Hint>
      <CPRow label="허용 형식">
        <CPText
          mono
          value={config.accept ?? (format === "image" ? "image/*" : format === "doc" ? ".pdf,.doc,.docx,.xls,.xlsx" : "*/*")}
          onChange={(v) => updateConfig("accept", v)}
          placeholder="image/* 또는 .pdf,.docx"
        />
      </CPRow>
      <CPRow label="최대 크기">
        <CPNumber value={config.maxFileSize ?? ""} onChange={(v) => updateConfig("maxFileSize", v)} placeholder="제한 없음" suffix="MB" />
      </CPRow>
      <CPRow label="복수 파일">
        <CPSwitch value={!!config.multipleFiles} onChange={(v) => updateConfig("multipleFiles", v)} />
      </CPRow>
      {config.multipleFiles && (
        <CPRow label="최대 개수">
          <CPNumber value={config.maxFiles ?? ""} onChange={(v) => updateConfig("maxFiles", v)} placeholder="제한 없음" />
        </CPRow>
      )}
      {format === "image" && (
        <>
          <CPRow label="썸네일 생성">
            <CPSwitch value={config.thumbnail ?? true} onChange={(v) => updateConfig("thumbnail", v)} />
          </CPRow>
          <CPRow label="최대 해상도">
            <div style={{ display: "flex", gap: 5 }}>
              <CPNumber value={config.maxWidth ?? ""} onChange={(v) => updateConfig("maxWidth", v)} placeholder="가로 px" />
              <CPNumber value={config.maxHeight ?? ""} onChange={(v) => updateConfig("maxHeight", v)} placeholder="세로 px" />
            </div>
          </CPRow>
        </>
      )}
    </>
  );
}

function NumberingOptions({
  config,
  numberingTableName,
  columnName,
  loading,
  rules,
  onChange,
  updateConfig,
  onRulesRefresh,
}: {
  config: Record<string, any>;
  numberingTableName: string;
  columnName?: string;
  loading: boolean;
  rules: NumberingRuleConfig[];
  onChange: (config: Record<string, any>) => void;
  updateConfig: (k: string, v: any) => void;
  onRulesRefresh: () => void;
}) {
  const [createOpen, setCreateOpen] = useState(false);

  const applyRuleId = (ruleId: string) => {
    // canonical 저장 위치: autoGeneration.options.numberingRuleId
    // (autoGeneration.ts.generateValue 의 numbering_rule case + InputComponent 의 NumberingPicker prop 과 일치)
    onChange({
      ...config,
      autoGeneration: {
        ...config.autoGeneration,
        enabled: true,
        type: "numbering_rule" as AutoGenerationType,
        tableName: numberingTableName,
        options: {
          ...(config.autoGeneration?.options ?? {}),
          numberingRuleId: ruleId,
        },
      },
    });
  };

  return (
    <>
      <CPRow label="대상 테이블">
        {numberingTableName ? (
          <DimText value={numberingTableName} mono />
        ) : (
          <Hint tone="warn">화면에 연결된 테이블이 없어서 채번 규칙을 불러올 수 없어요.</Hint>
        )}
      </CPRow>
      {numberingTableName && (
        <>
          <CPRow
            label="채번 규칙"
            required
            help={
              !loading && rules.length === 0
                ? "이 테이블에 등록된 채번 규칙이 없어요. 아래 버튼으로 새로 만드세요."
                : undefined
            }
          >
            {loading ? (
              <InlineLoader text="채번 규칙 로딩 중..." />
            ) : (
              <CPSelect
                value={config.autoGeneration?.options?.numberingRuleId || ""}
                onChange={(v) => applyRuleId(v)}
                disabled={rules.length === 0}
              >
                <option value="">{rules.length === 0 ? "등록된 규칙 없음" : "채번 규칙 선택"}</option>
                {rules.map((rule) => (
                  <option key={rule.rule_id} value={String(rule.rule_id)}>
                    {rule.rule_name} ({rule.separator || "-"}
                    {"{번호}"})
                  </option>
                ))}
              </CPSelect>
            )}
          </CPRow>
          <CPRow label="">
            <button
              type="button"
              onClick={() => setCreateOpen(true)}
              style={{
                alignSelf: "flex-start",
                padding: "4px 10px",
                fontSize: 11,
                fontWeight: 600,
                color: "hsl(var(--primary))",
                background: "hsl(var(--primary) / 0.08)",
                border: "1px solid hsl(var(--primary) / 0.3)",
                borderRadius: 4,
                cursor: "pointer",
              }}
            >
              + 새 규칙 만들기
            </button>
          </CPRow>
        </>
      )}
      <CPRow label="읽기 전용" help="채번 필드는 자동 생성되므로 읽기전용 권장">
        <div style={{ display: "flex", alignItems: "center", minHeight: 20 }}>
          <CPSwitch value={config.readonly !== false} onChange={(v) => updateConfig("readonly", v)} />
        </div>
      </CPRow>
      {numberingTableName && (
        <NumberingRuleCreateDialog
          open={createOpen}
          onOpenChange={setCreateOpen}
          tableName={numberingTableName}
          columnName={columnName}
          onCreated={(rule) => {
            // 저장 직후: 새 ruleId 자동 선택 + 목록 refresh
            if (rule.rule_id) applyRuleId(String(rule.rule_id));
            onRulesRefresh();
          }}
        />
      )}
    </>
  );
}

// ───────────────────────────────────────────────────────
// 고급 설정
// ───────────────────────────────────────────────────────
function SelectAdvancedOptions({
  config,
  updateConfig,
  multi = false,
}: {
  config: Record<string, any>;
  updateConfig: (k: string, v: any) => void;
  multi?: boolean;
}) {
  const isTagsFormat = multi && config.format === "tags";

  return (
    <>
      {!isTagsFormat && (
        <CPRow label="선택 방식">
          <CPSelect value={config.mode || "dropdown"} onChange={(v) => updateConfig("mode", v)}>
            <option value="dropdown">드롭다운</option>
            <option value="combobox">검색 가능 드롭다운</option>
            {!multi && <option value="radio">라디오 버튼</option>}
            {multi && <option value="check">체크박스</option>}
            {multi && <option value="swap">스왑(좌우 이동)</option>}
            {!multi && <option value="toggle">토글</option>}
          </CPSelect>
        </CPRow>
      )}
      {multi && (
        <CPRow label={isTagsFormat ? "최대 태그 개수" : "최대 개수"}>
          <CPNumber
            value={config.maxSelect ?? ""}
            onChange={(v) => updateConfig("maxSelect", v)}
            placeholder="제한 없음"
            min={1}
          />
        </CPRow>
      )}
      {!isTagsFormat && (
        <>
          <CPRow label="검색 기능">
            <CPSwitch value={!!config.searchable} onChange={(v) => updateConfig("searchable", v)} />
          </CPRow>
          <CPRow label="선택 초기화">
            <CPSwitch value={config.allowClear !== false} onChange={(v) => updateConfig("allowClear", v)} />
          </CPRow>
        </>
      )}
    </>
  );
}

function InputAdvancedOptions({
  config,
  updateConfig,
}: {
  config: Record<string, any>;
  updateConfig: (k: string, v: any) => void;
}) {
  const ag = config.autoGeneration ?? {};
  return (
    <>
      <CPRow label="자동 생성" help="값이 자동으로 채워져요">
        <CPSwitch
          value={!!ag.enabled}
          onChange={(v) =>
            updateConfig("autoGeneration", {
              ...ag,
              enabled: v,
              type: ag.type ?? "none",
            })
          }
        />
      </CPRow>
      {ag.enabled && (
        <>
          <CPRow label="생성 방식">
            <CPSelect
              value={ag.type ?? "none"}
              onChange={(v) => updateConfig("autoGeneration", { ...ag, type: v as AutoGenerationType })}
            >
              <option value="none">자동생성 없음</option>
              <option value="uuid">UUID 생성</option>
              <option value="current_user">현재 사용자 ID</option>
              <option value="current_time">현재 시간</option>
              <option value="sequence">순차 번호</option>
              <option value="company_code">회사 코드</option>
              <option value="department">부서 코드</option>
            </CPSelect>
          </CPRow>
          {ag.type && ag.type !== "none" && (
            <Hint>{AutoGenerationUtils.getTypeDescription(ag.type as AutoGenerationType)}</Hint>
          )}
        </>
      )}
      <CPRow label="입력 마스크" help="# = 숫자, A = 문자, * = 모두">
        <CPText
          mono
          value={config.mask ?? ""}
          onChange={(v) => updateConfig("mask", v)}
          placeholder="###-####-####"
        />
      </CPRow>
    </>
  );
}

// ───────────────────────────────────────────────────────
// 필터 조건 (선택형)
// ───────────────────────────────────────────────────────
const FilterConditionsSection: React.FC<{
  filters: OptionFilter[];
  columns: ColumnOption[];
  loadingColumns: boolean;
  onFiltersChange: (filters: OptionFilter[]) => void;
}> = ({ filters, columns, loadingColumns, onFiltersChange }) => {
  const addFilter = () =>
    onFiltersChange([...filters, { column: "", operator: "=", value_type: "static", value: "" }]);
  const updateFilter = (index: number, patch: Partial<OptionFilter>) => {
    const updated = [...filters];
    updated[index] = { ...updated[index], ...patch };
    if (patch.value_type) {
      if (patch.value_type === "static") {
        updated[index].field_ref = undefined;
        updated[index].user_field = undefined;
      } else if (patch.value_type === "field") {
        updated[index].value = undefined;
        updated[index].user_field = undefined;
      } else if (patch.value_type === "user") {
        updated[index].value = undefined;
        updated[index].field_ref = undefined;
      }
    }
    if (patch.operator === "isNull" || patch.operator === "isNotNull") {
      updated[index].value = undefined;
      updated[index].field_ref = undefined;
      updated[index].user_field = undefined;
      updated[index].value_type = "static";
    }
    onFiltersChange(updated);
  };
  const removeFilter = (index: number) => onFiltersChange(filters.filter((_, i) => i !== index));
  const needsValue = (op: string) => op !== "isNull" && op !== "isNotNull";

  return (
    <div>
      <div
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
          marginBottom: 6,
        }}
      >
        <span style={{ fontSize: 11, color: "var(--cp-text-muted)" }}>{filters.length}개 조건</span>
        <button
          type="button"
          onClick={addFilter}
          style={{
            fontSize: 11,
            fontWeight: 600,
            padding: "3px 8px",
            border: "1px solid var(--cp-border)",
            background: "var(--cp-surface)",
            color: "var(--cp-text-sec)",
            borderRadius: 5,
            cursor: "pointer",
            display: "inline-flex",
            alignItems: "center",
            gap: 4,
          }}
        >
          {CP_ICONS.plus} 조건 추가
        </button>
      </div>

      {loadingColumns && <InlineLoader text="컬럼 목록 로딩 중..." />}
      {filters.length === 0 && !loadingColumns && (
        <div
          style={{
            fontSize: 11,
            color: "var(--cp-text-muted)",
            padding: "12px 8px",
            textAlign: "center",
            border: "1px dashed var(--cp-border)",
            borderRadius: 6,
          }}
        >
          필터 조건이 없습니다
        </div>
      )}

      <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
        {filters.map((filter, index) => (
          <div
            key={index}
            style={{
              border: "1px solid var(--cp-border)",
              borderRadius: 6,
              padding: 7,
              background: "var(--cp-bg-subtle)",
              display: "flex",
              flexDirection: "column",
              gap: 5,
            }}
          >
            <div style={{ display: "flex", alignItems: "center", gap: 5 }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <CPSelect value={filter.column || ""} onChange={(v) => updateFilter(index, { column: v })}>
                  <option value="">컬럼</option>
                  {columns.map((col) => (
                    <option key={col.columnName} value={col.columnName}>
                      {col.columnLabel}
                    </option>
                  ))}
                </CPSelect>
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <CPSelect
                  value={filter.operator || "="}
                  onChange={(v) => updateFilter(index, { operator: v as OptionFilter["operator"] })}
                >
                  {OPERATOR_OPTIONS.map((op) => (
                    <option key={op.value} value={op.value}>
                      {op.label}
                    </option>
                  ))}
                </CPSelect>
              </div>
              <CPIconBtn tone="danger" size={24} onClick={() => removeFilter(index)}>
                {CP_ICONS.trash}
              </CPIconBtn>
            </div>
            {needsValue(filter.operator) && (
              <div style={{ display: "flex", alignItems: "center", gap: 5 }}>
                <div style={{ width: 100, flexShrink: 0 }}>
                  <CPSelect
                    value={filter.value_type || "static"}
                    onChange={(v) => updateFilter(index, { value_type: v as OptionFilter["value_type"] })}
                  >
                    {VALUE_TYPE_OPTIONS.map((vt) => (
                      <option key={vt.value} value={vt.value}>
                        {vt.label}
                      </option>
                    ))}
                  </CPSelect>
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  {(filter.value_type || "static") === "static" && (
                    <CPText
                      value={String(filter.value ?? "")}
                      onChange={(v) => updateFilter(index, { value: v })}
                      placeholder={
                        filter.operator === "in" || filter.operator === "notIn" ? "값1, 값2, ..." : "값 입력"
                      }
                    />
                  )}
                  {filter.value_type === "field" && (
                    <CPText
                      value={filter.field_ref || ""}
                      onChange={(v) => updateFilter(index, { field_ref: v })}
                      placeholder="참조 필드명"
                    />
                  )}
                  {filter.value_type === "user" && (
                    <CPSelect
                      value={filter.user_field || ""}
                      onChange={(v) => updateFilter(index, { user_field: v as OptionFilter["user_field"] })}
                    >
                      <option value="">사용자 필드</option>
                      {USER_FIELD_OPTIONS.map((uf) => (
                        <option key={uf.value} value={uf.value}>
                          {uf.label}
                        </option>
                      ))}
                    </CPSelect>
                  )}
                </div>
              </div>
            )}
          </div>
        ))}
      </div>
    </div>
  );
};

export default InvFieldConfigPanel;
