"use client";

import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
} from "@/components/ui/dialog";
import {
  Plus,
  Trash2,
  Save,
  Loader2,
  Pencil,
  ClipboardCheck,
  AlertTriangle,
  Wrench,
  Search,
  Inbox,
  Settings2,
  Upload,
  ArrowUpDown,
  ArrowUp,
  ArrowDown,
} from "lucide-react";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { ImageUpload } from "@/components/common/ImageUpload";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
import { SmartExcelUploadModal } from "@/components/common/SmartExcelUpload";
import type { SmartExcelUploadConfig, ParsedSheetData } from "@/components/common/SmartExcelUpload";

/* ───── 테이블명 ───── */
const INSPECTION_TABLE = "inspection_standard";

const INSPECTION_COLUMNS = [
  { key: "inspection_code", label: "검사코드" },
  { key: "inspection_type", label: "검사유형" },
  { key: "inspection_criteria", label: "검사기준" },
  { key: "criteria_detail", label: "기준상세" },
  { key: "inspection_item", label: "검사항목" },
  { key: "inspection_method", label: "검사방법" },
  { key: "judgment_criteria", label: "판단기준" },
  { key: "unit", label: "단위" },
  { key: "apply_type", label: "적용구분" },
  { key: "manager", label: "관리자" },
];
const DEFECT_TABLE = "defect_standard_mng";
const EQUIPMENT_TABLE = "inspection_equipment_mng";

/* ───── 카테고리 flatten ───── */
type CatOption = { code: string; label: string; depth: number; parentCode?: string };
const flattenCategories = (vals: any[], parentCode?: string): CatOption[] => {
  const result: CatOption[] = [];
  for (const v of vals) {
    result.push({ code: v.valueCode, label: v.valueLabel, depth: v.depth ?? 1, parentCode });
    if (v.children?.length) result.push(...flattenCategories(v.children, v.valueCode));
  }
  return result;
};

export default function InspectionManagementPage() {
  const { user } = useAuth();
  const { confirm, ConfirmDialogComponent } = useConfirmDialog();
  const ts = useTableSettings("c16-inspection", INSPECTION_TABLE, INSPECTION_COLUMNS);

  const [activeTab, setActiveTab] = useState("inspection");

  /* ───── 검사기준 ───── */
  const [inspections, setInspections] = useState<any[]>([]);
  const [inspLoading, setInspLoading] = useState(false);
  const [inspCount, setInspCount] = useState(0);
  const [inspChecked, setInspChecked] = useState<string[]>([]);
  const [inspModalOpen, setInspModalOpen] = useState(false);
  const [inspEditMode, setInspEditMode] = useState(false);
  const [inspForm, setInspForm] = useState<Record<string, any>>({});
  const [inspSaving, setInspSaving] = useState(false);
  const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
  const [inspPage, setInspPage] = useState(1);
  const [inspPageSize, setInspPageSize] = useState(20);
  const [inspTotal, setInspTotal] = useState(0);

  /* ───── 불량관리 ───── */
  const [defects, setDefects] = useState<any[]>([]);
  const [defLoading, setDefLoading] = useState(false);
  const [defCount, setDefCount] = useState(0);
  const [defChecked, setDefChecked] = useState<string[]>([]);
  const [defModalOpen, setDefModalOpen] = useState(false);
  const [defEditMode, setDefEditMode] = useState(false);
  const [defForm, setDefForm] = useState<Record<string, any>>({});
  const [defSaving, setDefSaving] = useState(false);
  const [defKeyword, setDefKeyword] = useState("");
  const [defPage, setDefPage] = useState(1);
  const [defPageSize, setDefPageSize] = useState(20);
  const [defTotal, setDefTotal] = useState(0);

  /* ───── 검사장비 ───── */
  const [equipments, setEquipments] = useState<any[]>([]);
  const [eqLoading, setEqLoading] = useState(false);
  const [eqCount, setEqCount] = useState(0);
  const [eqChecked, setEqChecked] = useState<string[]>([]);
  const [eqModalOpen, setEqModalOpen] = useState(false);
  const [eqEditMode, setEqEditMode] = useState(false);
  const [eqForm, setEqForm] = useState<Record<string, any>>({});
  const [eqSaving, setEqSaving] = useState(false);
  const [eqKeyword, setEqKeyword] = useState("");
  const [eqSortKey, setEqSortKey] = useState<string | null>(null);
  const [eqSortDir, setEqSortDir] = useState<"asc" | "desc">("asc");
  const [eqPage, setEqPage] = useState(1);
  const [eqPageSize, setEqPageSize] = useState(20);
  const [eqTotal, setEqTotal] = useState(0);

  /* ───── 채번 ───── */
  const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
  const [previewCode, setPreviewCode] = useState<string | null>(null);

  /* ───── 카테고리 옵션 ───── */
  const [catOptions, setCatOptions] = useState<Record<string, CatOption[]>>({});
  const [userOptions, setUserOptions] = useState<{ code: string; label: string }[]>([]);

  /* ───── 엑셀업로드 모달 오픈 상태 ───── */
  const [inspExcelOpen, setInspExcelOpen] = useState(false);
  const [defExcelOpen, setDefExcelOpen] = useState(false);
  const [eqExcelOpen, setEqExcelOpen] = useState(false);

  /* ═══════════════════ 카테고리 로드 ═══════════════════ */
  useEffect(() => {
    const load = async () => {
      const optMap: Record<string, CatOption[]> = {};
      const catList = [
        { table: INSPECTION_TABLE, col: "inspection_type" },
        { table: INSPECTION_TABLE, col: "apply_type" },
        { table: INSPECTION_TABLE, col: "inspection_method" },
        { table: INSPECTION_TABLE, col: "judgment_criteria" },
        { table: INSPECTION_TABLE, col: "unit" },
        { table: INSPECTION_TABLE, col: "is_active" },
        { table: DEFECT_TABLE, col: "defect_type" },
        { table: DEFECT_TABLE, col: "severity" },
        { table: DEFECT_TABLE, col: "inspection_type" },
        { table: DEFECT_TABLE, col: "is_active" },
        { table: EQUIPMENT_TABLE, col: "equipment_type" },
        { table: EQUIPMENT_TABLE, col: "equipment_status" },
      ];
      await Promise.all(
        catList.map(async ({ table, col }) => {
          try {
            const res = await apiClient.get(`/table-categories/${table}/${col}/values?filterCompanyCode=COMPANY_16`);
            if (res.data?.data?.length > 0) {
              optMap[`${table}.${col}`] = flattenCategories(res.data.data);
            }
          } catch {
            /* skip */
          }
        }),
      );
      setCatOptions(optMap);
      // 사용자 목록 로드
      try {
        const userRes = await apiClient.post(`/table-management/tables/user_info/data`, {
          page: 1,
          size: 0,
          autoFilter: true,
        });
        const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
        setUserOptions(
          users.map((u: any) => ({
            code: u.user_id || u.id,
            label: `${u.user_name || u.name || u.user_id}${u.dept_name ? ` (${u.dept_name})` : ""}`,
          })),
        );
      } catch {
        /* skip */
      }
    };
    load();
  }, []);

  const getCatLabel = (table: string, col: string, code: string) => {
    if (!code) return "";
    const opts = catOptions[`${table}.${col}`];
    if (!opts) return code;
    // 쉼표 구분 다중 코드 지원
    if (code.includes(",")) {
      return code
        .split(",")
        .filter(Boolean)
        .map((c) => opts.find((o) => o.code === c)?.label || c)
        .join(", ");
    }
    return opts.find((o) => o.code === code)?.label || code;
  };

  const inspTableColumns = useMemo<EDataTableColumn[]>(() => {
    return ts.visibleColumns.map((col) => {
      const base: EDataTableColumn = { key: col.key, label: col.label };
      if (["inspection_type", "inspection_method", "judgment_criteria", "unit", "apply_type"].includes(col.key)) {
        base.render = (v: any, row: any) => getCatLabel(INSPECTION_TABLE, col.key, row[col.key]);
      }
      if (col.key === "manager") {
        base.render = (v: any, row: any) => userOptions.find((u) => u.code === row.manager)?.label || row.manager || "-";
      }
      return base;
    });
  }, [ts.visibleColumns, catOptions, userOptions]); // eslint-disable-line react-hooks/exhaustive-deps

  const defTableColumns = useMemo<EDataTableColumn[]>(() => [
    { key: "defect_code", label: "불량코드", render: (_v: any, row: any) => (
      <span className="font-semibold">{row.defect_code || "-"}</span>
    )},
    { key: "defect_type", label: "불량유형", render: (_v: any, row: any) => (
      <Badge variant="secondary" className="text-[10px]">
        {getCatLabel(DEFECT_TABLE, "defect_type", row.defect_type)}
      </Badge>
    )},
    { key: "defect_name", label: "불량명", render: (_v: any, row: any) => row.defect_name || "-" },
    { key: "defect_content", label: "불량내용", render: (_v: any, row: any) => (
      <span className="text-muted-foreground block max-w-[200px] truncate" title={row.defect_content || ""}>
        {row.defect_content || "-"}
      </span>
    )},
    { key: "severity", label: "심각도", render: (_v: any, row: any) => {
      const label = getCatLabel(DEFECT_TABLE, "severity", row.severity);
      const color = label === "치명적" || label === "심각" ? "destructive" : label === "보통" ? "secondary" : "outline";
      return <Badge variant={color as any} className="text-[10px]">{label || "-"}</Badge>;
    }},
    { key: "inspection_type", label: "검사유형", render: (_v: any, row: any) => (
      <div className="flex flex-wrap gap-1">
        {row.inspection_type
          ? row.inspection_type.split(",").filter(Boolean).map((c: string) => (
              <Badge key={c} variant="outline" className="text-[10px]">
                {getCatLabel(DEFECT_TABLE, "inspection_type", c)}
              </Badge>
            ))
          : "-"}
      </div>
    )},
    { key: "apply_target", label: "적용대상", render: (_v: any, row: any) => (
      <div className="flex flex-wrap gap-1">
        {row.apply_target
          ? row.apply_target.split(",").filter(Boolean).map((t: string) => (
              <Badge key={t} variant="outline" className="text-[10px]">
                {getCatLabel(DEFECT_TABLE, "inspection_type", t)}
              </Badge>
            ))
          : "-"}
      </div>
    )},
    { key: "is_active", label: "사용여부", render: (_v: any, row: any) => {
      const label = getCatLabel(DEFECT_TABLE, "is_active", row.is_active) || row.is_active || "-";
      const isOn = row.is_active === "CAT_DA_01" || label === "사용";
      return <Badge variant={isOn ? "default" : "secondary"} className="text-[10px]">{label}</Badge>;
    }},
    { key: "reg_date", label: "등록일", render: (_v: any, row: any) => (
      <span className="text-muted-foreground">
        {row.reg_date || (row.created_date ? String(row.created_date).slice(0, 10) : "-")}
      </span>
    )},
    { key: "manager_id", label: "관리자", render: (_v: any, row: any) => (
      userOptions.find((u) => u.code === row.manager_id)?.label || row.manager_id || "-"
    )},
    { key: "remarks", label: "비고", render: (_v: any, row: any) => (
      <span className="text-muted-foreground">{row.remarks || "-"}</span>
    )},
  ], [catOptions, userOptions]); // eslint-disable-line react-hooks/exhaustive-deps

  const eqTableColumns = useMemo<EDataTableColumn[]>(() => [
    { key: "image_path", label: "이미지", render: (_v: any, row: any) => (
      row.image_path ? (
        <img
          src={String(row.image_path).startsWith("http") || String(row.image_path).startsWith("/")
            ? row.image_path
            : `/api/files/preview/${row.image_path}`}
          alt=""
          className="border-border h-8 w-8 rounded border object-cover"
          onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
        />
      ) : (
        <div className="bg-muted h-8 w-8 rounded" />
      )
    )},
    { key: "equipment_code", label: "장비코드", render: (_v: any, row: any) => (
      <span className="text-primary font-semibold">{row.equipment_code || "-"}</span>
    )},
    { key: "equipment_name", label: "장비명", render: (_v: any, row: any) => row.equipment_name || "-" },
    { key: "equipment_type", label: "장비유형", render: (_v: any, row: any) => (
      <Badge variant="secondary" className="text-[10px]">
        {getCatLabel(EQUIPMENT_TABLE, "equipment_type", row.equipment_type) || "-"}
      </Badge>
    )},
    { key: "model_name", label: "모델명", render: (_v: any, row: any) => row.model_name || "-" },
    { key: "manufacturer", label: "제조사", render: (_v: any, row: any) => row.manufacturer || "-" },
    { key: "installation_location", label: "설치장소", render: (_v: any, row: any) => row.installation_location || "-" },
    { key: "last_calibration_date", label: "최근교정일", render: (_v: any, row: any) => row.last_calibration_date || "-" },
    { key: "calibration_period", label: "교정주기(개월)", render: (_v: any, row: any) => (
      row.calibration_period ? `${row.calibration_period}개월` : "-"
    )},
    { key: "equipment_status", label: "장비상태", render: (_v: any, row: any) => {
      const label = getCatLabel(EQUIPMENT_TABLE, "equipment_status", row.equipment_status);
      const color = label === "정상" ? "default" : label === "폐기" ? "destructive" : "secondary";
      return <Badge variant={color as any} className="text-[10px]">{label || "-"}</Badge>;
    }},
    { key: "manager_id", label: "담당자", render: (_v: any, row: any) => (
      userOptions.find((u) => u.code === row.manager_id)?.label || row.manager_id || "-"
    )},
  ], [catOptions, userOptions]); // eslint-disable-line react-hooks/exhaustive-deps

  /* ═══════════════════ 데이터 조회 ═══════════════════ */
  // 다중값 컬럼 (쉼표 구분 저장) — 서버 equals 대신 contains 사용
  const MULTI_VALUE_COLUMNS = ["inspection_type"];

  const fetchInspections = useCallback(async () => {
    setInspLoading(true);
    try {
      const filters = searchFilters.map((f) => ({
        columnName: f.columnName,
        operator: MULTI_VALUE_COLUMNS.includes(f.columnName) ? "contains" : f.operator,
        value: f.value,
      }));
      const res = await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/data`, {
        page: inspPage,
        size: inspPageSize,
        dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
        autoFilter: true,
      });
      const rows = res.data?.data?.data || res.data?.data?.rows || [];
      const total = res.data?.data?.total ?? res.data?.data?.totalCount ?? rows.length;
      setInspections(rows);
      setInspCount(total);
      setInspTotal(total);
    } catch {
      toast.error("검사기준 조회에 실패했어요");
    } finally {
      setInspLoading(false);
    }
  }, [searchFilters, inspPage, inspPageSize]);

  const fetchDefects = useCallback(async () => {
    setDefLoading(true);
    try {
      const kw = defKeyword.trim();
      const filters = kw
        ? [{ columnName: "defect_name", operator: "contains", value: kw }]
        : [];
      const res = await apiClient.post(`/table-management/tables/${DEFECT_TABLE}/data`, {
        page: defPage,
        size: defPageSize,
        dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
        autoFilter: true,
      });
      const rows = res.data?.data?.data || res.data?.data?.rows || [];
      const total = res.data?.data?.total ?? res.data?.data?.totalCount ?? rows.length;
      setDefects(rows);
      setDefCount(total);
      setDefTotal(total);
    } catch {
      toast.error("불량관리 조회에 실패했어요");
    } finally {
      setDefLoading(false);
    }
  }, [defPage, defPageSize, defKeyword]);

  const fetchEquipments = useCallback(async () => {
    setEqLoading(true);
    try {
      const kw = eqKeyword.trim();
      const filters = kw
        ? [{ columnName: "equipment_name", operator: "contains", value: kw }]
        : [];
      const res = await apiClient.post(`/table-management/tables/${EQUIPMENT_TABLE}/data`, {
        page: eqPage,
        size: eqPageSize,
        dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
        autoFilter: true,
      });
      const rows = res.data?.data?.data || res.data?.data?.rows || [];
      const total = res.data?.data?.total ?? res.data?.data?.totalCount ?? rows.length;
      setEquipments(rows);
      setEqCount(total);
      setEqTotal(total);
    } catch {
      toast.error("검사장비 조회에 실패했어요");
    } finally {
      setEqLoading(false);
    }
  }, [eqPage, eqPageSize, eqKeyword]);

  useEffect(() => {
    fetchInspections();
  }, [fetchInspections]);
  useEffect(() => {
    fetchDefects();
  }, [fetchDefects]);
  useEffect(() => {
    fetchEquipments();
  }, [fetchEquipments]);

  // 키워드/필터 변경 시 1페이지로 리셋
  useEffect(() => { setInspPage(1); }, [searchFilters]);
  useEffect(() => { setDefPage(1); }, [defKeyword]);
  useEffect(() => { setEqPage(1); }, [eqKeyword]);

  // 서버 페이징 전환: defects/equipments는 서버에서 필터·페이징된 결과 그대로 사용

  const handleEqSort = useCallback((key: string) => {
    setEqSortKey((prev) => {
      if (prev === key) {
        setEqSortDir((d) => (d === "asc" ? "desc" : "asc"));
        return prev;
      }
      setEqSortDir("asc");
      return key;
    });
  }, []);

  const renderEqSortIcon = (key: string) => {
    if (eqSortKey !== key)
      return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-40" />;
    return eqSortDir === "asc" ? (
      <ArrowUp className="ml-1 inline h-3 w-3" />
    ) : (
      <ArrowDown className="ml-1 inline h-3 w-3" />
    );
  };

  /* ═══════════════════ 검사기준 CRUD ═══════════════════ */
  const openInspCreate = async () => {
    setInspForm({});
    setInspEditMode(false);
    setNumberingRuleId(null);
    setPreviewCode(null);
    setInspModalOpen(true);
    try {
      const ruleRes = await apiClient.get(`/numbering-rules/by-column/${INSPECTION_TABLE}/inspection_code`);
      const ruleData = ruleRes.data;
      if (ruleData?.success && ruleData?.data?.ruleId) {
        const ruleId = ruleData.data.ruleId;
        setNumberingRuleId(ruleId);
        const prev = await previewNumberingCode(ruleId);
        if (prev.success && prev.data?.generatedCode) {
          setPreviewCode(prev.data.generatedCode);
        }
      }
    } catch { /* 채번 규칙 없으면 무시 */ }
  };
  const openInspEdit = (row: any) => {
    setInspForm({ ...row });
    setInspEditMode(true);
    setInspModalOpen(true);
  };
  const saveInspection = async () => {
    if (!numberingRuleId && !inspForm.inspection_code) {
      toast.error("검사코드는 필수예요");
      return;
    }
    if (!inspForm.inspection_type) {
      toast.error("유형을 1개 이상 선택해주세요");
      return;
    }
    if (!inspForm.inspection_criteria) {
      toast.error("검사기준은 필수예요");
      return;
    }
    if (!inspForm.inspection_item) {
      toast.error("검사항목은 필수예요");
      return;
    }
    if (!inspForm.judgment_criteria) {
      toast.error("판단기준은 필수예요");
      return;
    }
    const judgmentLabel = (catOptions[`${INSPECTION_TABLE}.judgment_criteria`] || []).find((o) => o.code === inspForm.judgment_criteria)?.label;
    if (judgmentLabel === "선택형" && !inspForm.selection_options?.trim()) {
      toast.error("선택형은 옵션을 1개 이상 추가해주세요");
      return;
    }
    setInspSaving(true);
    try {
      let finalCode = inspForm.inspection_code || "";
      if (!inspEditMode && numberingRuleId) {
        const allocRes = await allocateNumberingCode(numberingRuleId);
        if (allocRes.success && allocRes.data?.generatedCode) {
          finalCode = allocRes.data.generatedCode;
        } else {
          toast.error("채번 코드 할당에 실패했습니다.");
          setInspSaving(false);
          return;
        }
      }
      if (inspEditMode) {
        await apiClient.put(`/table-management/tables/${INSPECTION_TABLE}/edit`, {
          originalData: { id: inspForm.id },
          updatedData: inspForm,
        });
        toast.success("검사기준을 수정했어요");
      } else {
        await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/add`, {
          id: crypto.randomUUID(),
          ...inspForm,
          inspection_code: finalCode,
        });
        toast.success("검사기준을 등록했어요");
      }
      setInspModalOpen(false);
      fetchInspections();
    } catch {
      toast.error("저장에 실패했어요");
    } finally {
      setInspSaving(false);
    }
  };
  const deleteInspections = async () => {
    if (inspChecked.length === 0) {
      toast.error("삭제할 항목을 선택해주세요");
      return;
    }
    const ok = await confirm("검사기준 삭제", {
      description: `선택한 ${inspChecked.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.`,
    });
    if (!ok) return;
    try {
      await apiClient.delete(`/table-management/tables/${INSPECTION_TABLE}/delete`, {
        data: inspChecked.map((id) => ({ id })),
      });
      toast.success(`${inspChecked.length}건을 삭제했어요`);
      setInspChecked([]);
      fetchInspections();
    } catch {
      toast.error("삭제에 실패했어요");
    }
  };

  /* ═══════════════════ 불량관리 CRUD ═══════════════════ */
  const openDefCreate = async () => {
    setDefForm({ is_active: "CAT_DA_01" });
    setDefEditMode(false);
    setNumberingRuleId(null);
    setPreviewCode(null);
    setDefModalOpen(true);
    try {
      const ruleRes = await apiClient.get(`/numbering-rules/by-column/${DEFECT_TABLE}/defect_code`);
      const ruleData = ruleRes.data;
      if (ruleData?.success && ruleData?.data?.ruleId) {
        const ruleId = ruleData.data.ruleId;
        setNumberingRuleId(ruleId);
        const prev = await previewNumberingCode(ruleId);
        if (prev.success && prev.data?.generatedCode) {
          setPreviewCode(prev.data.generatedCode);
        }
      }
    } catch { /* 채번 규칙 없으면 무시 */ }
  };
  const openDefEdit = (row: any) => {
    setDefForm({ ...row });
    setDefEditMode(true);
    setDefModalOpen(true);
  };
  const saveDefect = async () => {
    if (!numberingRuleId && !defForm.defect_code) {
      toast.error("불량코드는 필수예요");
      return;
    }
    if (!defForm.defect_type) {
      toast.error("불량유형은 필수예요");
      return;
    }
    if (!defForm.defect_name) {
      toast.error("불량명은 필수예요");
      return;
    }
    if (!defForm.severity) {
      toast.error("심각도는 필수예요");
      return;
    }
    if (!defForm.defect_content) {
      toast.error("불량내용은 필수예요");
      return;
    }
    if (!defForm.inspection_type) {
      toast.error("검사유형을 1개 이상 선택해주세요");
      return;
    }
    setDefSaving(true);
    try {
      let finalCode = defForm.defect_code || "";
      if (!defEditMode && numberingRuleId) {
        const allocRes = await allocateNumberingCode(numberingRuleId);
        if (allocRes.success && allocRes.data?.generatedCode) {
          finalCode = allocRes.data.generatedCode;
        } else {
          toast.error("채번 코드 할당에 실패했습니다.");
          setDefSaving(false);
          return;
        }
      }
      if (defEditMode) {
        await apiClient.put(`/table-management/tables/${DEFECT_TABLE}/edit`, {
          originalData: { id: defForm.id },
          updatedData: defForm,
        });
        toast.success("불량유형을 수정했어요");
      } else {
        await apiClient.post(`/table-management/tables/${DEFECT_TABLE}/add`, { id: crypto.randomUUID(), ...defForm, defect_code: finalCode });
        toast.success("불량유형을 등록했어요");
      }
      setDefModalOpen(false);
      fetchDefects();
    } catch {
      toast.error("저장에 실패했어요");
    } finally {
      setDefSaving(false);
    }
  };
  const deleteDefects = async () => {
    if (defChecked.length === 0) {
      toast.error("삭제할 항목을 선택해주세요");
      return;
    }
    const ok = await confirm("불량유형 삭제", {
      description: `선택한 ${defChecked.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.`,
    });
    if (!ok) return;
    try {
      await apiClient.delete(`/table-management/tables/${DEFECT_TABLE}/delete`, {
        data: defChecked.map((id) => ({ id })),
      });
      toast.success(`${defChecked.length}건을 삭제했어요`);
      setDefChecked([]);
      fetchDefects();
    } catch {
      toast.error("삭제에 실패했어요");
    }
  };

  /* ═══════════════════ 검사장비 CRUD ═══════════════════ */
  const openEqCreate = async () => {
    setEqForm({
      calibration_period: "12",
      equipment_status: "NORMAL",
    });
    setEqEditMode(false);
    setNumberingRuleId(null);
    setPreviewCode(null);
    setEqModalOpen(true);
    try {
      const ruleRes = await apiClient.get(`/numbering-rules/by-column/${EQUIPMENT_TABLE}/equipment_code`);
      const ruleData = ruleRes.data;
      if (ruleData?.success && ruleData?.data?.ruleId) {
        const ruleId = ruleData.data.ruleId;
        setNumberingRuleId(ruleId);
        const prev = await previewNumberingCode(ruleId);
        if (prev.success && prev.data?.generatedCode) {
          setPreviewCode(prev.data.generatedCode);
        }
      } else {
        // 채번 규칙 없으면 기존 수동 채번 fallback
        const maxNum =
          equipments
            .map((e: any) => e.equipment_code || "")
            .filter((c: string) => /^EQP-\d+$/.test(c))
            .map((c: string) => parseInt(c.replace("EQP-", ""), 10))
            .sort((a: number, b: number) => b - a)[0] || 0;
        setEqForm((p) => ({ ...p, equipment_code: `EQP-${String(maxNum + 1).padStart(3, "0")}` }));
      }
    } catch {
      // 채번 규칙 조회 실패 시 기존 수동 채번 fallback
      const maxNum =
        equipments
          .map((e: any) => e.equipment_code || "")
          .filter((c: string) => /^EQP-\d+$/.test(c))
          .map((c: string) => parseInt(c.replace("EQP-", ""), 10))
          .sort((a: number, b: number) => b - a)[0] || 0;
      setEqForm((p) => ({ ...p, equipment_code: `EQP-${String(maxNum + 1).padStart(3, "0")}` }));
    }
  };
  const openEqEdit = (row: any) => {
    setEqForm({ ...row });
    setEqEditMode(true);
    setEqModalOpen(true);
  };
  const saveEquipment = async () => {
    if (!numberingRuleId && !eqForm.equipment_code) {
      toast.error("장비코드는 필수예요");
      return;
    }
    if (!eqForm.equipment_name) {
      toast.error("장비명은 필수예요");
      return;
    }
    if (!eqForm.equipment_type) {
      toast.error("장비유형은 필수예요");
      return;
    }
    setEqSaving(true);
    try {
      let finalCode = eqForm.equipment_code || "";
      if (!eqEditMode && numberingRuleId) {
        const allocRes = await allocateNumberingCode(numberingRuleId);
        if (allocRes.success && allocRes.data?.generatedCode) {
          finalCode = allocRes.data.generatedCode;
        } else {
          toast.error("채번 코드 할당에 실패했습니다.");
          setEqSaving(false);
          return;
        }
      }
      if (eqEditMode) {
        await apiClient.put(`/table-management/tables/${EQUIPMENT_TABLE}/edit`, {
          originalData: { id: eqForm.id },
          updatedData: eqForm,
        });
        toast.success("검사장비를 수정했어요");
      } else {
        await apiClient.post(`/table-management/tables/${EQUIPMENT_TABLE}/add`, { id: crypto.randomUUID(), ...eqForm, equipment_code: finalCode });
        toast.success("검사장비를 등록했어요");
      }
      setEqModalOpen(false);
      fetchEquipments();
    } catch {
      toast.error("저장에 실패했어요");
    } finally {
      setEqSaving(false);
    }
  };
  const deleteEquipments = async () => {
    if (eqChecked.length === 0) {
      toast.error("삭제할 항목을 선택해주세요");
      return;
    }
    const ok = await confirm("검사장비 삭제", {
      description: `선택한 ${eqChecked.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.`,
    });
    if (!ok) return;
    try {
      await apiClient.delete(`/table-management/tables/${EQUIPMENT_TABLE}/delete`, {
        data: eqChecked.map((id) => ({ id })),
      });
      toast.success(`${eqChecked.length}건을 삭제했어요`);
      setEqChecked([]);
      fetchEquipments();
    } catch {
      toast.error("삭제에 실패했어요");
    }
  };

  /* ═══════════════════ 엑셀 업로드 공통 헬퍼 ═══════════════════ */
  // 라벨 배열 추출 (비어있는 카테고리는 빈 배열 반환)
  const catLabels = useCallback(
    (table: string, col: string): string[] => {
      return (catOptions[`${table}.${col}`] || []).map((o) => o.label);
    },
    [catOptions],
  );
  // 라벨→코드 단일 맵 생성
  const catLabelToCode = useCallback(
    (table: string, col: string): Record<string, string> => {
      const map: Record<string, string> = {};
      (catOptions[`${table}.${col}`] || []).forEach((o) => {
        map[o.label] = o.code;
      });
      return map;
    },
    [catOptions],
  );

  /* ═══════════════════ 탭 1: 검사기준 엑셀업로드 ═══════════════════ */
  const inspExcelConfig = useMemo<SmartExcelUploadConfig>(() => {
    return {
      templateName: "검사기준",
      sheets: [
        {
          name: "검사기준",
          typeKey: "inspection_standard",
          columns: [
            { key: "inspection_code", label: "검사코드", type: "text", width: 14 },
            { key: "inspection_type", label: "검사유형", required: true, type: "text", width: 22 },
            { key: "inspection_criteria", label: "검사기준", required: true, type: "text", width: 20 },
            { key: "criteria_detail", label: "기준상세", type: "text", width: 20 },
            { key: "inspection_item", label: "검사항목", required: true, type: "text", width: 18 },
            {
              key: "inspection_method",
              label: "검사방법",
              type: "dropdown",
              dropdown: { source: "custom", values: catLabels(INSPECTION_TABLE, "inspection_method") },
              width: 14,
            },
            {
              key: "judgment_criteria",
              label: "판단기준",
              required: true,
              type: "dropdown",
              dropdown: { source: "custom", values: catLabels(INSPECTION_TABLE, "judgment_criteria") },
              width: 14,
            },
            {
              key: "unit",
              label: "단위",
              type: "dropdown",
              dropdown: { source: "custom", values: catLabels(INSPECTION_TABLE, "unit") },
              width: 10,
            },
            {
              key: "apply_type",
              label: "적용구분",
              type: "dropdown",
              dropdown: { source: "custom", values: catLabels(INSPECTION_TABLE, "apply_type") },
              width: 12,
            },
            {
              key: "is_active",
              label: "사용여부",
              type: "dropdown",
              dropdown: { source: "custom", values: catLabels(INSPECTION_TABLE, "is_active") },
              width: 10,
            },
            { key: "selection_options", label: "선택옵션", type: "text", width: 22 },
            {
              key: "manager",
              label: "관리자",
              type: "dropdown",
              dropdown: { source: "custom", values: userOptions.map((u) => u.label) },
              width: 18,
            },
            { key: "remark", label: "비고", type: "text", width: 20 },
          ],
        },
      ],
      conditionalRules: [
        { when: { column: "judgment_criteria", equals: "선택형" }, require: ["selection_options"], ignore: [] },
      ],
    };
  }, [catLabels, userOptions]);

  const inspDropdownOptions = useMemo<Record<string, string[]>>(() => {
    return {
      inspection_method: catLabels(INSPECTION_TABLE, "inspection_method"),
      judgment_criteria: catLabels(INSPECTION_TABLE, "judgment_criteria"),
      unit: catLabels(INSPECTION_TABLE, "unit"),
      apply_type: catLabels(INSPECTION_TABLE, "apply_type"),
      is_active: catLabels(INSPECTION_TABLE, "is_active"),
      manager: userOptions.map((u) => u.label),
    };
  }, [catLabels, userOptions]);

  const inspLabelToCodeMap = useMemo<Record<string, Record<string, string>>>(() => {
    const userMap: Record<string, string> = {};
    userOptions.forEach((u) => {
      userMap[u.label] = u.code;
    });
    return {
      inspection_method: catLabelToCode(INSPECTION_TABLE, "inspection_method"),
      judgment_criteria: catLabelToCode(INSPECTION_TABLE, "judgment_criteria"),
      unit: catLabelToCode(INSPECTION_TABLE, "unit"),
      apply_type: catLabelToCode(INSPECTION_TABLE, "apply_type"),
      is_active: catLabelToCode(INSPECTION_TABLE, "is_active"),
      manager: userMap,
    };
  }, [catLabelToCode, userOptions]);

  const handleInspExcelUpload = async (data: ParsedSheetData[]) => {
    const rows = data[0]?.rows ?? [];
    if (rows.length === 0) {
      toast.error("업로드할 데이터가 없어요");
      return;
    }

    // 채번 규칙 사전 조회 (코드 미입력 건에만 사용)
    let ruleId: string | null = null;
    try {
      const ruleRes = await apiClient.get(`/numbering-rules/by-column/${INSPECTION_TABLE}/inspection_code`);
      if (ruleRes.data?.success && ruleRes.data?.data?.ruleId) {
        ruleId = ruleRes.data.data.ruleId;
      }
    } catch {
      /* 채번 규칙 없으면 기존 코드로만 처리 */
    }

    // templateParser가 dropdown 컬럼은 labelToCodeMap으로 이미 label→code 자동변환 완료.
    // row.{key}는 코드, row.{key}_label은 원본 라벨.
    // inspection_type은 text 타입이라 자동변환 제외 → 수동 처리 + 카테고리 라벨 검증.
    const inspTypeMap = catLabelToCode(INSPECTION_TABLE, "inspection_type");
    const inspTypeLabels = catLabels(INSPECTION_TABLE, "inspection_type");

    let okCount = 0;
    const failList: string[] = [];

    for (let i = 0; i < rows.length; i++) {
      const row = rows[i];
      try {
        // inspection_type 다중값: ,로 split → 각 라벨이 카테고리에 있는지 검증 → 코드로 변환
        const rawTypes = String(row.inspection_type || "").trim();
        const typeLabelsArr = rawTypes
          ? rawTypes.split(",").map((s) => s.trim()).filter(Boolean)
          : [];
        const invalidTypes = typeLabelsArr.filter((label) => !inspTypeLabels.includes(label));
        if (invalidTypes.length > 0) {
          failList.push(`${i + 2}행: 검사유형 미등록 값(${invalidTypes.join(", ")})`);
          continue;
        }
        const typeCodes = typeLabelsArr.map((label) => inspTypeMap[label] || label).join(",");

        // selection_options 조건부 검증 (판단기준 라벨이 "선택형"일 때 필수)
        const judgmentOrigLabel = String(row.judgment_criteria_label || row.judgment_criteria || "").trim();
        if (judgmentOrigLabel === "선택형" && !String(row.selection_options || "").trim()) {
          failList.push(`${i + 2}행: 선택형은 옵션을 입력해주세요`);
          continue;
        }

        // row.{key}는 이미 코드 (dropdown 컬럼, templateParser가 labelToCodeMap 자동변환).
        const payload: Record<string, any> = {
          inspection_type: typeCodes,
          inspection_criteria: row.inspection_criteria || "",
          criteria_detail: row.criteria_detail || "",
          inspection_item: row.inspection_item || "",
          inspection_method: row.inspection_method || "",
          judgment_criteria: row.judgment_criteria || "",
          unit: row.unit || "",
          apply_type: row.apply_type || "",
          is_active: row.is_active || "CAT_IS_01",
          selection_options: row.selection_options || "",
          manager: row.manager || "",
          remark: row.remark || "",
        };

        // 코드 있으면 update, 없으면 insert (채번 할당)
        const inspectionCode = String(row.inspection_code || "").trim();
        if (inspectionCode) {
          // 기존 행 조회 후 upsert
          const existRes = await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/data`, {
            page: 1,
            size: 0,
            dataFilter: {
              enabled: true,
              filters: [{ columnName: "inspection_code", operator: "equals", value: inspectionCode }],
            },
            autoFilter: true,
          });
          const existing = existRes.data?.data?.data || existRes.data?.data?.rows || [];
          if (existing.length > 0) {
            await apiClient.put(`/table-management/tables/${INSPECTION_TABLE}/edit`, {
              originalData: { id: existing[0].id },
              updatedData: { ...payload, inspection_code: inspectionCode },
            });
          } else {
            await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/add`, {
              id: crypto.randomUUID(),
              inspection_code: inspectionCode,
              ...payload,
            });
          }
        } else {
          // 코드 자동 채번
          let finalCode = "";
          if (ruleId) {
            const allocRes = await allocateNumberingCode(ruleId);
            if (allocRes.success && allocRes.data?.generatedCode) {
              finalCode = allocRes.data.generatedCode;
            }
          }
          await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/add`, {
            id: crypto.randomUUID(),
            inspection_code: finalCode,
            ...payload,
          });
        }
        okCount++;
      } catch (e) {
        failList.push(`${i + 2}행: 저장 실패`);
      }
    }

    if (okCount > 0) toast.success(`${okCount}건을 업로드했어요`);
    if (failList.length > 0) {
      toast.error(`실패 ${failList.length}건: ${failList.slice(0, 3).join(" / ")}${failList.length > 3 ? " …" : ""}`);
    }
    fetchInspections();
  };

  /* ═══════════════════ 탭 2: 불량관리 엑셀업로드 ═══════════════════ */
  const defExcelConfig = useMemo<SmartExcelUploadConfig>(() => {
    return {
      templateName: "불량관리",
      sheets: [
        {
          name: "불량관리",
          typeKey: "defect_standard_mng",
          columns: [
            { key: "defect_code", label: "불량코드", type: "text", width: 14 },
            {
              key: "defect_type",
              label: "불량유형",
              required: true,
              type: "dropdown",
              dropdown: { source: "custom", values: catLabels(DEFECT_TABLE, "defect_type") },
              width: 14,
            },
            { key: "defect_name", label: "불량명", required: true, type: "text", width: 20 },
            { key: "defect_content", label: "불량내용", required: true, type: "text", width: 24 },
            {
              key: "severity",
              label: "심각도",
              required: true,
              type: "dropdown",
              dropdown: { source: "custom", values: catLabels(DEFECT_TABLE, "severity") },
              width: 12,
            },
            { key: "inspection_type", label: "검사유형", required: true, type: "text", width: 22 },
            { key: "apply_target", label: "적용대상", type: "text", width: 18 },
            {
              key: "is_active",
              label: "사용여부",
              type: "dropdown",
              dropdown: { source: "custom", values: catLabels(DEFECT_TABLE, "is_active") },
              width: 10,
            },
            {
              key: "manager_id",
              label: "관리자",
              type: "dropdown",
              dropdown: { source: "custom", values: userOptions.map((u) => u.label) },
              width: 18,
            },
            { key: "remarks", label: "비고", type: "text", width: 20 },
          ],
        },
      ],
    };
  }, [catLabels, userOptions]);

  const defDropdownOptions = useMemo<Record<string, string[]>>(() => {
    return {
      defect_type: catLabels(DEFECT_TABLE, "defect_type"),
      severity: catLabels(DEFECT_TABLE, "severity"),
      is_active: catLabels(DEFECT_TABLE, "is_active"),
      manager_id: userOptions.map((u) => u.label),
    };
  }, [catLabels, userOptions]);

  const defLabelToCodeMap = useMemo<Record<string, Record<string, string>>>(() => {
    const userMap: Record<string, string> = {};
    userOptions.forEach((u) => {
      userMap[u.label] = u.code;
    });
    return {
      defect_type: catLabelToCode(DEFECT_TABLE, "defect_type"),
      severity: catLabelToCode(DEFECT_TABLE, "severity"),
      is_active: catLabelToCode(DEFECT_TABLE, "is_active"),
      manager_id: userMap,
    };
  }, [catLabelToCode, userOptions]);

  const handleDefExcelUpload = async (data: ParsedSheetData[]) => {
    const rows = data[0]?.rows ?? [];
    if (rows.length === 0) {
      toast.error("업로드할 데이터가 없어요");
      return;
    }

    let ruleId: string | null = null;
    try {
      const ruleRes = await apiClient.get(`/numbering-rules/by-column/${DEFECT_TABLE}/defect_code`);
      if (ruleRes.data?.success && ruleRes.data?.data?.ruleId) {
        ruleId = ruleRes.data.data.ruleId;
      }
    } catch {
      /* skip */
    }

    // templateParser가 dropdown 컬럼은 이미 label→code 변환 완료. inspection_type/apply_target만 text라 수동 처리 + 계층 검증.
    const inspTypeMap = catLabelToCode(DEFECT_TABLE, "inspection_type");
    const defInspOpts = catOptions[`${DEFECT_TABLE}.inspection_type`] || [];
    // depth=1 부모(검사유형), depth=2 자식(적용대상)
    const parentLabels = defInspOpts.filter((o) => o.depth === 1).map((o) => o.label);

    let okCount = 0;
    const failList: string[] = [];

    for (let i = 0; i < rows.length; i++) {
      const row = rows[i];
      try {
        // inspection_type 다중값: ,로 split → depth=1 라벨 검증 → 코드로 변환
        const rawTypes = String(row.inspection_type || "").trim();
        const typeLabelsArr = rawTypes
          ? rawTypes.split(",").map((s) => s.trim()).filter(Boolean)
          : [];
        const invalidTypes = typeLabelsArr.filter((label) => !parentLabels.includes(label));
        if (invalidTypes.length > 0) {
          failList.push(`${i + 2}행: 검사유형 미등록 값(${invalidTypes.join(", ")})`);
          continue;
        }
        const typeCodeList = typeLabelsArr.map((label) => inspTypeMap[label] || label);
        const typeCodes = typeCodeList.join(",");

        // apply_target 다중값: 선택한 검사유형의 자식(depth=2)만 허용 → 자식 코드로 변환
        const rawTargets = String(row.apply_target || "").trim();
        const targetLabelsArr = rawTargets
          ? rawTargets.split(",").map((s) => s.trim()).filter(Boolean)
          : [];
        const targetCodeList: string[] = [];
        const invalidTargets: string[] = [];
        for (const label of targetLabelsArr) {
          const child = defInspOpts.find(
            (o) =>
              o.depth === 2 &&
              o.label === label &&
              o.parentCode &&
              typeCodeList.includes(o.parentCode),
          );
          if (child) targetCodeList.push(child.code);
          else invalidTargets.push(label);
        }
        if (invalidTargets.length > 0) {
          failList.push(
            `${i + 2}행: 적용대상이 선택한 검사유형의 하위가 아니거나 미등록(${invalidTargets.join(", ")})`,
          );
          continue;
        }
        const targetCodes = targetCodeList.join(",");

        const payload: Record<string, any> = {
          defect_type: row.defect_type || "",
          defect_name: row.defect_name || "",
          defect_content: row.defect_content || "",
          severity: row.severity || "",
          inspection_type: typeCodes,
          apply_target: targetCodes,
          is_active: row.is_active || "",
          manager_id: row.manager_id || "",
          remarks: row.remarks || "",
        };

        const defectCode = String(row.defect_code || "").trim();
        if (defectCode) {
          const existRes = await apiClient.post(`/table-management/tables/${DEFECT_TABLE}/data`, {
            page: 1,
            size: 0,
            dataFilter: {
              enabled: true,
              filters: [{ columnName: "defect_code", operator: "equals", value: defectCode }],
            },
            autoFilter: true,
          });
          const existing = existRes.data?.data?.data || existRes.data?.data?.rows || [];
          if (existing.length > 0) {
            await apiClient.put(`/table-management/tables/${DEFECT_TABLE}/edit`, {
              originalData: { id: existing[0].id },
              updatedData: { ...payload, defect_code: defectCode },
            });
          } else {
            await apiClient.post(`/table-management/tables/${DEFECT_TABLE}/add`, {
              id: crypto.randomUUID(),
              defect_code: defectCode,
              ...payload,
            });
          }
        } else {
          let finalCode = "";
          if (ruleId) {
            const allocRes = await allocateNumberingCode(ruleId);
            if (allocRes.success && allocRes.data?.generatedCode) {
              finalCode = allocRes.data.generatedCode;
            }
          }
          await apiClient.post(`/table-management/tables/${DEFECT_TABLE}/add`, {
            id: crypto.randomUUID(),
            defect_code: finalCode,
            ...payload,
          });
        }
        okCount++;
      } catch {
        failList.push(`${i + 2}행: 저장 실패`);
      }
    }

    if (okCount > 0) toast.success(`${okCount}건을 업로드했어요`);
    if (failList.length > 0) {
      toast.error(`실패 ${failList.length}건: ${failList.slice(0, 3).join(" / ")}${failList.length > 3 ? " …" : ""}`);
    }
    fetchDefects();
  };

  /* ═══════════════════ 탭 3: 검사장비 엑셀업로드 ═══════════════════ */
  const eqExcelConfig = useMemo<SmartExcelUploadConfig>(() => {
    return {
      templateName: "검사장비",
      sheets: [
        {
          name: "검사장비",
          typeKey: "inspection_equipment_mng",
          columns: [
            { key: "equipment_code", label: "장비코드", type: "text", width: 14 },
            { key: "equipment_name", label: "장비명", required: true, type: "text", width: 20 },
            {
              key: "equipment_type",
              label: "장비유형",
              required: true,
              type: "dropdown",
              dropdown: { source: "custom", values: catLabels(EQUIPMENT_TABLE, "equipment_type") },
              width: 14,
            },
            { key: "model_name", label: "모델명", type: "text", width: 16 },
            { key: "manufacturer", label: "제조사", type: "text", width: 16 },
            { key: "serial_number", label: "시리얼번호", type: "text", width: 16 },
            { key: "installation_location", label: "설치위치", type: "text", width: 18 },
            { key: "last_calibration_date", label: "최종교정일", type: "date", width: 14 },
            { key: "calibration_period", label: "교정주기(개월)", type: "number", width: 14 },
            {
              key: "equipment_status",
              label: "장비상태",
              type: "dropdown",
              dropdown: { source: "custom", values: catLabels(EQUIPMENT_TABLE, "equipment_status") },
              width: 12,
            },
            {
              key: "manager_id",
              label: "관리자",
              type: "dropdown",
              dropdown: { source: "custom", values: userOptions.map((u) => u.label) },
              width: 18,
            },
            { key: "remarks", label: "비고", type: "text", width: 20 },
          ],
        },
      ],
    };
  }, [catLabels, userOptions]);

  const eqDropdownOptions = useMemo<Record<string, string[]>>(() => {
    return {
      equipment_type: catLabels(EQUIPMENT_TABLE, "equipment_type"),
      equipment_status: catLabels(EQUIPMENT_TABLE, "equipment_status"),
      manager_id: userOptions.map((u) => u.label),
    };
  }, [catLabels, userOptions]);

  const eqLabelToCodeMap = useMemo<Record<string, Record<string, string>>>(() => {
    const userMap: Record<string, string> = {};
    userOptions.forEach((u) => {
      userMap[u.label] = u.code;
    });
    return {
      equipment_type: catLabelToCode(EQUIPMENT_TABLE, "equipment_type"),
      equipment_status: catLabelToCode(EQUIPMENT_TABLE, "equipment_status"),
      manager_id: userMap,
    };
  }, [catLabelToCode, userOptions]);

  const handleEqExcelUpload = async (data: ParsedSheetData[]) => {
    const rows = data[0]?.rows ?? [];
    if (rows.length === 0) {
      toast.error("업로드할 데이터가 없어요");
      return;
    }

    let ruleId: string | null = null;
    try {
      const ruleRes = await apiClient.get(`/numbering-rules/by-column/${EQUIPMENT_TABLE}/equipment_code`);
      if (ruleRes.data?.success && ruleRes.data?.data?.ruleId) {
        ruleId = ruleRes.data.data.ruleId;
      }
    } catch {
      /* skip */
    }

    let okCount = 0;
    const failList: string[] = [];

    for (let i = 0; i < rows.length; i++) {
      const row = rows[i];
      try {
        // calibration_period 숫자 검증
        const periodRaw = String(row.calibration_period ?? "").trim();
        if (periodRaw && isNaN(Number(periodRaw))) {
          failList.push(`${i + 2}행: 교정주기는 숫자만 입력해주세요`);
          continue;
        }

        // last_calibration_date 포맷 검증 (YYYY-MM-DD 기대)
        let calibDate = String(row.last_calibration_date ?? "").trim();
        if (calibDate) {
          // Date 객체로 변환되어 왔을 수도 있어 ISO 추출
          const d = new Date(calibDate);
          if (isNaN(d.getTime())) {
            failList.push(`${i + 2}행: 최종교정일 포맷 오류(${calibDate})`);
            continue;
          }
          calibDate = d.toISOString().slice(0, 10);
        }

        // row.{key}는 이미 코드 (templateParser가 labelToCodeMap으로 자동변환 완료).
        const payload: Record<string, any> = {
          equipment_name: row.equipment_name || "",
          equipment_type: row.equipment_type || "",
          model_name: row.model_name || "",
          manufacturer: row.manufacturer || "",
          serial_number: row.serial_number || "",
          installation_location: row.installation_location || "",
          last_calibration_date: calibDate,
          calibration_period: periodRaw,
          equipment_status: row.equipment_status || "",
          manager_id: row.manager_id || "",
          remarks: row.remarks || "",
        };

        const equipmentCode = String(row.equipment_code || "").trim();
        if (equipmentCode) {
          const existRes = await apiClient.post(`/table-management/tables/${EQUIPMENT_TABLE}/data`, {
            page: 1,
            size: 0,
            dataFilter: {
              enabled: true,
              filters: [{ columnName: "equipment_code", operator: "equals", value: equipmentCode }],
            },
            autoFilter: true,
          });
          const existing = existRes.data?.data?.data || existRes.data?.data?.rows || [];
          if (existing.length > 0) {
            await apiClient.put(`/table-management/tables/${EQUIPMENT_TABLE}/edit`, {
              originalData: { id: existing[0].id },
              updatedData: { ...payload, equipment_code: equipmentCode },
            });
          } else {
            await apiClient.post(`/table-management/tables/${EQUIPMENT_TABLE}/add`, {
              id: crypto.randomUUID(),
              equipment_code: equipmentCode,
              ...payload,
            });
          }
        } else {
          let finalCode = "";
          if (ruleId) {
            const allocRes = await allocateNumberingCode(ruleId);
            if (allocRes.success && allocRes.data?.generatedCode) {
              finalCode = allocRes.data.generatedCode;
            }
          }
          await apiClient.post(`/table-management/tables/${EQUIPMENT_TABLE}/add`, {
            id: crypto.randomUUID(),
            equipment_code: finalCode,
            ...payload,
          });
        }
        okCount++;
      } catch {
        failList.push(`${i + 2}행: 저장 실패`);
      }
    }

    if (okCount > 0) toast.success(`${okCount}건을 업로드했어요`);
    if (failList.length > 0) {
      toast.error(`실패 ${failList.length}건: ${failList.slice(0, 3).join(" / ")}${failList.length > 3 ? " …" : ""}`);
    }
    fetchEquipments();
  };

  /* ═══════════════════ JSX ═══════════════════ */
  return (
    <div className="flex flex-col gap-3 p-3 h-[calc(100vh-4rem)] overflow-hidden">
      {ConfirmDialogComponent}

      <div className="bg-card flex flex-1 min-h-0 flex-col overflow-hidden rounded-lg border">
        <Tabs value={activeTab} onValueChange={setActiveTab} className="flex flex-1 min-h-0 flex-col">
          <div className="border-b px-3">
            <TabsList className="h-auto gap-0 bg-transparent p-0">
              <TabsTrigger
                value="inspection"
                className="data-[state=active]:border-primary rounded-none border-b-2 border-transparent px-4 py-3 text-sm font-medium data-[state=active]:bg-transparent data-[state=active]:shadow-none"
              >
                <ClipboardCheck className="mr-2 h-4 w-4" />
                검사기준
                <Badge variant="secondary" className="bg-primary/10 text-primary ml-2 text-xs">
                  {inspCount}
                </Badge>
              </TabsTrigger>
              <TabsTrigger
                value="defect"
                className="data-[state=active]:border-primary rounded-none border-b-2 border-transparent px-4 py-3 text-sm font-medium data-[state=active]:bg-transparent data-[state=active]:shadow-none"
              >
                <AlertTriangle className="mr-2 h-4 w-4" />
                불량관리
                <Badge variant="secondary" className="bg-primary/10 text-primary ml-2 text-xs">
                  {defCount}
                </Badge>
              </TabsTrigger>
              <TabsTrigger
                value="equipment"
                className="data-[state=active]:border-primary rounded-none border-b-2 border-transparent px-4 py-3 text-sm font-medium data-[state=active]:bg-transparent data-[state=active]:shadow-none"
              >
                <Wrench className="mr-2 h-4 w-4" />
                검사장비
                <Badge variant="secondary" className="bg-primary/10 text-primary ml-2 text-xs">
                  {eqCount}
                </Badge>
              </TabsTrigger>
            </TabsList>
          </div>

          {/* ──── 검사기준 탭 ──── */}
          <TabsContent value="inspection" className="mt-0 flex flex-1 min-h-0 flex-col p-3">
            <div className="mb-3">
              <DynamicSearchFilter
                tableName={INSPECTION_TABLE}
                filterId="c16-inspection"
                onFilterChange={setSearchFilters}
                externalFilterConfig={ts.filterConfig}
                dataCount={inspCount}
                extraActions={
                  <div className="flex items-center gap-2">
                    <Button size="sm" onClick={openInspCreate}>
                      <Plus className="mr-1 h-4 w-4" />
                      등록
                    </Button>
                    <Button size="sm" variant="outline" onClick={() => setInspExcelOpen(true)}>
                      <Upload className="mr-1 h-4 w-4" />
                      엑셀업로드
                    </Button>
                    <Button
                      size="sm"
                      variant="outline"
                      onClick={() => {
                        const sel = inspections.find((r) => inspChecked.includes(r.id));
                        if (sel) openInspEdit(sel);
                        else toast.error("수정할 항목을 선택해주세요");
                      }}
                    >
                      <Pencil className="mr-1 h-4 w-4" />
                      수정
                    </Button>
                    <Button size="sm" variant="destructive" onClick={deleteInspections}>
                      <Trash2 className="mr-1 h-4 w-4" />
                      삭제
                    </Button>
                    <Button variant="ghost" size="sm" onClick={() => ts.setOpen(true)} title="테이블 설정">
                      <Settings2 className="h-4 w-4" />
                    </Button>
                  </div>
                }
              />
            </div>
            <div className="flex flex-1 min-h-0 flex-col overflow-hidden rounded-lg border">
              <EDataTable
                columns={inspTableColumns}
                data={ts.groupData(inspections)}
                loading={inspLoading}
                emptyMessage="등록된 검사기준이 없어요"
                showCheckbox={true}
                checkedIds={inspChecked}
                onCheckedChange={setInspChecked}
                onRowDoubleClick={(row) => openInspEdit(row)}
                showPagination={true}
                draggableColumns={false}
                columnOrderKey="c16-inspection-main"
                serverPagination
                serverCurrentPage={inspPage}
                serverPageSize={inspPageSize}
                serverTotalCount={inspTotal}
                onServerPageChange={setInspPage}
                onServerPageSizeChange={(s) => { setInspPageSize(s); setInspPage(1); }}
              />
            </div>
          </TabsContent>

          {/* ──── 불량관리 탭 ──── */}
          <TabsContent value="defect" className="mt-0 flex flex-1 min-h-0 flex-col p-3">
            <div className="mb-3 flex items-center justify-between">
              <div className="flex items-center gap-2">
                <div className="relative">
                  <Search className="text-muted-foreground absolute top-2 left-2.5 h-3.5 w-3.5" />
                  <Input
                    className="h-8 w-56 pl-8 text-sm"
                    placeholder="불량명 검색..."
                    value={defKeyword}
                    onChange={(e) => setDefKeyword(e.target.value)}
                  />
                </div>
                <Badge variant="secondary" className="bg-primary/10 text-primary">
                  {defTotal}건
                </Badge>
              </div>
              <div className="flex items-center gap-2">
                <Button size="sm" onClick={openDefCreate}>
                  <Plus className="mr-1 h-4 w-4" />
                  등록
                </Button>
                <Button size="sm" variant="outline" onClick={() => setDefExcelOpen(true)}>
                  <Upload className="mr-1 h-4 w-4" />
                  엑셀업로드
                </Button>
                <Button
                  size="sm"
                  variant="outline"
                  onClick={() => {
                    const sel = defects.find((r) => defChecked.includes(r.id));
                    if (sel) openDefEdit(sel);
                    else toast.error("수정할 항목을 선택해주세요");
                  }}
                >
                  <Pencil className="mr-1 h-4 w-4" />
                  수정
                </Button>
                <Button size="sm" variant="destructive" onClick={deleteDefects}>
                  <Trash2 className="mr-1 h-4 w-4" />
                  삭제
                </Button>
              </div>
            </div>
            <div className="flex flex-1 min-h-0 flex-col overflow-hidden rounded-lg border">
              <EDataTable
                columns={defTableColumns}
                data={defects}
                loading={defLoading}
                emptyMessage="등록된 불량유형이 없어요"
                showCheckbox={true}
                checkedIds={defChecked}
                onCheckedChange={setDefChecked}
                onRowDoubleClick={(row) => openDefEdit(row)}
                showPagination={true}
                draggableColumns={false}
                columnOrderKey="c16-inspection-defect"
                serverPagination
                serverCurrentPage={defPage}
                serverPageSize={defPageSize}
                serverTotalCount={defTotal}
                onServerPageChange={setDefPage}
                onServerPageSizeChange={(s) => { setDefPageSize(s); setDefPage(1); }}
              />
            </div>
          </TabsContent>

          {/* ──── 검사장비 탭 ──── */}
          <TabsContent value="equipment" className="mt-0 flex flex-1 min-h-0 flex-col p-3">
            <div className="mb-3 flex items-center justify-between">
              <div className="flex items-center gap-2">
                <div className="relative">
                  <Search className="text-muted-foreground absolute top-2 left-2.5 h-3.5 w-3.5" />
                  <Input
                    className="h-8 w-56 pl-8 text-sm"
                    placeholder="장비명 검색..."
                    value={eqKeyword}
                    onChange={(e) => setEqKeyword(e.target.value)}
                  />
                </div>
                <Badge variant="secondary" className="bg-primary/10 text-primary">
                  {eqTotal}건
                </Badge>
              </div>
              <div className="flex items-center gap-2">
                <Button size="sm" onClick={openEqCreate}>
                  <Plus className="mr-1 h-4 w-4" />
                  등록
                </Button>
                <Button size="sm" variant="outline" onClick={() => setEqExcelOpen(true)}>
                  <Upload className="mr-1 h-4 w-4" />
                  엑셀업로드
                </Button>
                <Button
                  size="sm"
                  variant="outline"
                  onClick={() => {
                    const sel = equipments.find((r) => eqChecked.includes(r.id));
                    if (sel) openEqEdit(sel);
                    else toast.error("수정할 항목을 선택해주세요");
                  }}
                >
                  <Pencil className="mr-1 h-4 w-4" />
                  수정
                </Button>
                <Button size="sm" variant="destructive" onClick={deleteEquipments}>
                  <Trash2 className="mr-1 h-4 w-4" />
                  삭제
                </Button>
              </div>
            </div>
            <div className="flex flex-1 min-h-0 flex-col overflow-hidden rounded-lg border">
              <EDataTable
                columns={eqTableColumns}
                data={equipments}
                loading={eqLoading}
                emptyMessage="등록된 검사장비가 없어요"
                showCheckbox={true}
                checkedIds={eqChecked}
                onCheckedChange={setEqChecked}
                onRowDoubleClick={(row) => openEqEdit(row)}
                showPagination={true}
                draggableColumns={false}
                columnOrderKey="c16-inspection-equipment"
                serverPagination
                serverCurrentPage={eqPage}
                serverPageSize={eqPageSize}
                serverTotalCount={eqTotal}
                onServerPageChange={setEqPage}
                onServerPageSizeChange={(s) => { setEqPageSize(s); setEqPage(1); }}
              />
            </div>
          </TabsContent>
        </Tabs>
      </div>

      {/* ═══════════════════ 검사기준 모달 ═══════════════════ */}
      <Dialog open={inspModalOpen} onOpenChange={setInspModalOpen}>
        <DialogContent className="max-h-[85vh] max-w-[95vw] overflow-y-auto sm:max-w-[640px]">
          <DialogHeader>
            <DialogTitle>{inspEditMode ? "검사기준 수정" : "검사기준 등록"}</DialogTitle>
            <DialogDescription>검사기준 정보를 입력해주세요</DialogDescription>
          </DialogHeader>
          <div className="grid grid-cols-2 gap-4">
            {/* 검사코드 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">
                검사코드 <span className="text-destructive">*</span>
              </Label>
              {!inspEditMode && numberingRuleId ? (
                <Input
                  className="h-9 bg-muted"
                  value={previewCode || ""}
                  readOnly
                  placeholder="채번 조회 중..."
                />
              ) : inspEditMode ? (
                <Input
                  className="h-9 bg-muted"
                  value={inspForm.inspection_code || ""}
                  disabled
                />
              ) : (
                <Input
                  className="h-9"
                  value={inspForm.inspection_code || ""}
                  onChange={(e) => setInspForm((p) => ({ ...p, inspection_code: e.target.value }))}
                  placeholder="검사코드 입력"
                />
              )}
            </div>
            {/* 유형 (다중선택) */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">
                유형 <span className="text-destructive">*</span> (다중선택)
              </Label>
              <div className="flex flex-wrap gap-3 pt-1">
                {(catOptions[`${INSPECTION_TABLE}.inspection_type`] || []).map((o) => {
                  const types: string[] = inspForm.inspection_type
                    ? inspForm.inspection_type.split(",").filter(Boolean)
                    : [];
                  const checked = types.includes(o.code);
                  return (
                    <div key={o.code} className="flex items-center gap-1.5">
                      <Checkbox
                        checked={checked}
                        onCheckedChange={(v) => {
                          const next = v ? [...types, o.code] : types.filter((t) => t !== o.code);
                          setInspForm((p) => ({ ...p, inspection_type: next.join(",") }));
                        }}
                      />
                      <Label className="cursor-pointer text-sm">{o.label}</Label>
                    </div>
                  );
                })}
              </div>
            </div>
            {/* 검사기준 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">
                검사기준 <span className="text-destructive">*</span>
              </Label>
              <Input
                className="h-9"
                value={inspForm.inspection_criteria || ""}
                onChange={(e) => setInspForm((p) => ({ ...p, inspection_criteria: e.target.value }))}
                placeholder="검사기준 입력"
              />
            </div>
            {/* 기준상세 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">기준상세</Label>
              <Input
                className="h-9"
                value={inspForm.criteria_detail || ""}
                onChange={(e) => setInspForm((p) => ({ ...p, criteria_detail: e.target.value }))}
                placeholder="기준상세 입력"
              />
            </div>
            {/* 검사항목 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">
                검사항목 <span className="text-destructive">*</span>
              </Label>
              <Input
                className="h-9"
                value={inspForm.inspection_item || ""}
                onChange={(e) => setInspForm((p) => ({ ...p, inspection_item: e.target.value }))}
                placeholder="검사항목 입력"
              />
            </div>
            {/* 검사방법 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">검사방법</Label>
              <Select
                value={inspForm.inspection_method || "__none__"}
                onValueChange={(v) => setInspForm((p) => ({ ...p, inspection_method: v === "__none__" ? "" : v }))}
              >
                <SelectTrigger className="h-9">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__">선택 안함</SelectItem>
                  {(catOptions[`${INSPECTION_TABLE}.inspection_method`] || []).map((o) => (
                    <SelectItem key={o.code} value={o.code}>
                      {o.label}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
            {/* 판단기준 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">
                판단기준 <span className="text-destructive">*</span>
              </Label>
              <Select
                value={inspForm.judgment_criteria || "__none__"}
                onValueChange={(v) => setInspForm((p) => ({ ...p, judgment_criteria: v === "__none__" ? "" : v }))}
              >
                <SelectTrigger className="h-9">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__">선택 안함</SelectItem>
                  {(catOptions[`${INSPECTION_TABLE}.judgment_criteria`] || []).map((o) => (
                    <SelectItem key={o.code} value={o.code}>
                      {o.label}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
            {/* 선택형 옵션 입력 (판단기준이 선택형일 때만) */}
            {(catOptions[`${INSPECTION_TABLE}.judgment_criteria`] || []).find((o) => o.code === inspForm.judgment_criteria)?.label === "선택형" && (
              <div className="space-y-1.5 col-span-2">
                <Label className="text-xs font-semibold">선택 옵션 설정</Label>
                <div className="rounded-lg border p-3 space-y-2">
                  <div className="flex flex-wrap gap-1.5">
                    {(inspForm.selection_options ? inspForm.selection_options.split(",").filter(Boolean) : []).map((opt: string, idx: number) => (
                      <span key={idx} className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary">
                        {opt}
                        <button
                          type="button"
                          className="ml-0.5 rounded-full hover:bg-primary/20 p-0.5"
                          onClick={() => {
                            const opts = inspForm.selection_options.split(",").filter(Boolean);
                            opts.splice(idx, 1);
                            setInspForm((p: any) => ({ ...p, selection_options: opts.join(",") }));
                          }}
                        >
                          <span className="text-[10px]">✕</span>
                        </button>
                      </span>
                    ))}
                  </div>
                  <div className="flex gap-2">
                    <Input
                      placeholder="옵션명 입력 후 Enter"
                      className="h-8 text-xs flex-1"
                      onKeyDown={(e) => {
                        if (e.key === "Enter" && !e.nativeEvent.isComposing) {
                          e.preventDefault();
                          const v = (e.target as HTMLInputElement).value.trim();
                          if (!v) return;
                          const existing = inspForm.selection_options ? inspForm.selection_options.split(",").filter(Boolean) : [];
                          if (existing.includes(v)) { toast.error("이미 존재하는 옵션입니다."); return; }
                          setInspForm((p: any) => ({ ...p, selection_options: [...existing, v].join(",") }));
                          (e.target as HTMLInputElement).value = "";
                        }
                      }}
                    />
                  </div>
                  <p className="text-[10px] text-muted-foreground">옵션명을 입력하고 Enter를 눌러 추가하세요. POP에서 이 옵션 중 선택하여 검사 결과를 입력합니다.</p>
                </div>
              </div>
            )}
            {/* 단위 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">단위</Label>
              <Select
                value={inspForm.unit || "__none__"}
                onValueChange={(v) => setInspForm((p) => ({ ...p, unit: v === "__none__" ? "" : v }))}
              >
                <SelectTrigger className="h-9">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__">선택 안함</SelectItem>
                  {(catOptions[`${INSPECTION_TABLE}.unit`] || []).map((o) => (
                    <SelectItem key={o.code} value={o.code}>
                      {o.label}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
            {/* 적용구분 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">적용구분</Label>
              <Select
                value={inspForm.apply_type || "__none__"}
                onValueChange={(v) => setInspForm((p) => ({ ...p, apply_type: v === "__none__" ? "" : v }))}
              >
                <SelectTrigger className="h-9">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__">선택 안함</SelectItem>
                  {(catOptions[`${INSPECTION_TABLE}.apply_type`] || []).map((o) => (
                    <SelectItem key={o.code} value={o.code}>
                      {o.label}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
            {/* 관리자 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">관리자</Label>
              <Select
                value={inspForm.manager || "__none__"}
                onValueChange={(v) => setInspForm((p) => ({ ...p, manager: v === "__none__" ? "" : v }))}
              >
                <SelectTrigger className="h-9">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__">선택 안함</SelectItem>
                  {userOptions.map((u) => (
                    <SelectItem key={u.code} value={u.code}>
                      {u.label}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
            {/* 비고 */}
            <div className="col-span-2 space-y-1.5">
              <Label className="text-xs font-semibold">비고</Label>
              <textarea
                className="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 flex min-h-[80px] w-full resize-y rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs focus-visible:ring-[3px] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
                value={inspForm.remark || ""}
                onChange={(e) => setInspForm((p) => ({ ...p, remark: e.target.value }))}
                placeholder="비고 입력"
                rows={3}
              />
            </div>
          </div>
          <DialogFooter>
            <Button variant="outline" onClick={() => setInspModalOpen(false)}>
              취소
            </Button>
            <Button onClick={saveInspection} disabled={inspSaving}>
              {inspSaving ? <Loader2 className="mr-1 h-4 w-4 animate-spin" /> : <Save className="mr-1 h-4 w-4" />}
              저장
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      {/* ═══════════════════ 불량관리 모달 ═══════════════════ */}
      <Dialog open={defModalOpen} onOpenChange={setDefModalOpen}>
        <DialogContent className="max-h-[85vh] max-w-[95vw] overflow-y-auto sm:max-w-[640px]">
          <DialogHeader>
            <DialogTitle>{defEditMode ? "불량유형 수정" : "불량유형 등록"}</DialogTitle>
            <DialogDescription>불량유형 정보를 입력해주세요</DialogDescription>
          </DialogHeader>
          <div className="grid grid-cols-2 gap-4">
            {/* 불량코드 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">
                불량코드 <span className="text-destructive">*</span>
              </Label>
              {!defEditMode && numberingRuleId ? (
                <Input
                  className="h-9 bg-muted"
                  value={previewCode || ""}
                  readOnly
                  placeholder="채번 조회 중..."
                />
              ) : defEditMode ? (
                <Input
                  className="h-9 bg-muted"
                  value={defForm.defect_code || ""}
                  disabled
                />
              ) : (
                <Input
                  className="h-9"
                  value={defForm.defect_code || ""}
                  onChange={(e) => setDefForm((p) => ({ ...p, defect_code: e.target.value }))}
                  placeholder="불량코드 입력"
                />
              )}
            </div>
            {/* 불량유형 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">
                불량유형 <span className="text-destructive">*</span>
              </Label>
              <Select
                value={defForm.defect_type || "__none__"}
                onValueChange={(v) => setDefForm((p) => ({ ...p, defect_type: v === "__none__" ? "" : v }))}
              >
                <SelectTrigger className="h-9">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__">선택 안함</SelectItem>
                  {(catOptions[`${DEFECT_TABLE}.defect_type`] || []).map((o) => (
                    <SelectItem key={o.code} value={o.code}>
                      {o.label}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
            {/* 불량명 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">
                불량명 <span className="text-destructive">*</span>
              </Label>
              <Input
                className="h-9"
                value={defForm.defect_name || ""}
                onChange={(e) => setDefForm((p) => ({ ...p, defect_name: e.target.value }))}
                placeholder="불량명 입력"
              />
            </div>
            {/* 심각도 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">
                심각도 <span className="text-destructive">*</span>
              </Label>
              <Select
                value={defForm.severity || "__none__"}
                onValueChange={(v) => setDefForm((p) => ({ ...p, severity: v === "__none__" ? "" : v }))}
              >
                <SelectTrigger className="h-9">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__">선택 안함</SelectItem>
                  {(catOptions[`${DEFECT_TABLE}.severity`] || []).map((o) => (
                    <SelectItem key={o.code} value={o.code}>
                      {o.label}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
            {/* 불량내용 */}
            <div className="col-span-2 space-y-1.5">
              <Label className="text-xs font-semibold">
                불량내용 <span className="text-destructive">*</span>
              </Label>
              <textarea
                className="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 flex min-h-[80px] w-full resize-y rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs focus-visible:ring-[3px] focus-visible:outline-none"
                value={defForm.defect_content || ""}
                onChange={(e) => setDefForm((p) => ({ ...p, defect_content: e.target.value }))}
                placeholder="불량 상세 내용 및 정의를 입력하세요"
                rows={3}
              />
            </div>
            {/* 검사유형 (다중선택) */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">
                검사유형 <span className="text-destructive">*</span> (다중선택)
              </Label>
              <div className="flex flex-wrap gap-3 rounded-md border p-3">
                {(catOptions[`${DEFECT_TABLE}.inspection_type`] || [])
                  .filter((o) => o.depth === 1)
                  .map((o) => {
                    const types: string[] = defForm.inspection_type
                      ? defForm.inspection_type.split(",").filter(Boolean)
                      : [];
                    const checked = types.includes(o.code);
                    return (
                      <div key={o.code} className="flex items-center gap-1.5">
                        <Checkbox
                          checked={checked}
                          onCheckedChange={(v) => {
                            const next = v ? [...types, o.code] : types.filter((t) => t !== o.code);
                            setDefForm((p) => ({ ...p, inspection_type: next.join(","), apply_target: "" }));
                          }}
                        />
                        <Label className="cursor-pointer text-sm">{o.label}</Label>
                      </div>
                    );
                  })}
              </div>
            </div>
            {/* 적용대상 (다중선택, 검사유형별 동적) */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">적용대상 (다중선택)</Label>
              <div className="min-h-[60px] rounded-md border p-3">
                {(() => {
                  const selectedTypes = defForm.inspection_type
                    ? defForm.inspection_type.split(",").filter(Boolean)
                    : [];
                  if (selectedTypes.length === 0)
                    return <p className="text-muted-foreground text-xs">검사유형을 먼저 선택하세요</p>;
                  const defInspOpts = catOptions[`${DEFECT_TABLE}.inspection_type`] || [];
                  const targets: string[] = defForm.apply_target ? defForm.apply_target.split(",").filter(Boolean) : [];
                  return selectedTypes.map((parentCode: string) => {
                    const parentLabel = defInspOpts.find((o) => o.code === parentCode)?.label || parentCode;
                    const children = defInspOpts.filter((o) => o.parentCode === parentCode);
                    return (
                      <div key={parentCode} className="mb-2 last:mb-0">
                        <p className="mb-2 border-b pb-1 text-xs font-semibold">{parentLabel}</p>
                        {children.length === 0 ? (
                          <p className="text-muted-foreground text-xs">
                            하위분류가 없습니다. 옵션설정에서 &quot;{parentLabel}&quot;의 하위분류를 등록해주세요.
                          </p>
                        ) : (
                          <div className="flex flex-wrap gap-3">
                            {children.map((t) => (
                              <div key={t.code} className="flex items-center gap-1.5">
                                <Checkbox
                                  checked={targets.includes(t.code)}
                                  onCheckedChange={(v) => {
                                    const next = v ? [...targets, t.code] : targets.filter((x) => x !== t.code);
                                    setDefForm((p) => ({ ...p, apply_target: next.join(",") }));
                                  }}
                                />
                                <Label className="cursor-pointer text-sm">{t.label}</Label>
                              </div>
                            ))}
                          </div>
                        )}
                      </div>
                    );
                  });
                })()}
              </div>
            </div>
            {/* 사용여부 (카테고리 코드 저장) */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">사용여부</Label>
              <Select
                value={defForm.is_active || "CAT_DA_01"}
                onValueChange={(v) => setDefForm((p) => ({ ...p, is_active: v }))}
              >
                <SelectTrigger className="h-9">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  {(catOptions[`${DEFECT_TABLE}.is_active`] || []).map((o) => (
                    <SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
            {/* 관리자 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">관리자</Label>
              <Select
                value={defForm.manager_id || "__none__"}
                onValueChange={(v) => setDefForm((p) => ({ ...p, manager_id: v === "__none__" ? "" : v }))}
              >
                <SelectTrigger className="h-9">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__">선택 안함</SelectItem>
                  {userOptions.map((u) => (
                    <SelectItem key={u.code} value={u.code}>
                      {u.label}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
            {/* 비고 */}
            <div className="col-span-2 space-y-1.5">
              <Label className="text-xs font-semibold">비고</Label>
              <textarea
                className="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 flex min-h-[80px] w-full resize-y rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs focus-visible:ring-[3px] focus-visible:outline-none"
                value={defForm.remarks || ""}
                onChange={(e) => setDefForm((p) => ({ ...p, remarks: e.target.value }))}
                placeholder="비고 입력"
                rows={3}
              />
            </div>
          </div>
          <DialogFooter>
            <Button variant="outline" onClick={() => setDefModalOpen(false)}>
              취소
            </Button>
            <Button onClick={saveDefect} disabled={defSaving}>
              {defSaving ? <Loader2 className="mr-1 h-4 w-4 animate-spin" /> : <Save className="mr-1 h-4 w-4" />}
              저장
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      {/* ═══════════════════ 검사장비 모달 ═══════════════════ */}
      <Dialog open={eqModalOpen} onOpenChange={setEqModalOpen}>
        <DialogContent className="max-h-[85vh] max-w-[95vw] overflow-y-auto sm:max-w-[720px]">
          <DialogHeader>
            <DialogTitle>{eqEditMode ? "장비 수정" : "장비 등록"}</DialogTitle>
            <DialogDescription>검사장비 정보를 입력해주세요</DialogDescription>
          </DialogHeader>
          <div className="grid grid-cols-3 gap-4">
            {/* Row 1: 장비코드, 장비명, 장비유형 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">
                장비코드 <span className="text-destructive">*</span>
              </Label>
              {!eqEditMode && numberingRuleId ? (
                <Input
                  className="h-9 bg-muted"
                  value={previewCode || ""}
                  readOnly
                  placeholder="채번 조회 중..."
                />
              ) : eqEditMode ? (
                <Input
                  className="h-9 bg-muted"
                  value={eqForm.equipment_code || ""}
                  disabled
                />
              ) : (
                <Input
                  className="h-9"
                  value={eqForm.equipment_code || ""}
                  onChange={(e) => setEqForm((p) => ({ ...p, equipment_code: e.target.value }))}
                  placeholder="장비코드"
                />
              )}
            </div>
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">
                장비명 <span className="text-destructive">*</span>
              </Label>
              <Input
                className="h-9"
                value={eqForm.equipment_name || ""}
                onChange={(e) => setEqForm((p) => ({ ...p, equipment_name: e.target.value }))}
                placeholder="장비명 입력"
              />
            </div>
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">
                장비유형 <span className="text-destructive">*</span>
              </Label>
              <Select
                value={eqForm.equipment_type || "__none__"}
                onValueChange={(v) => setEqForm((p) => ({ ...p, equipment_type: v === "__none__" ? "" : v }))}
              >
                <SelectTrigger className="h-9">
                  <SelectValue placeholder="장비유형 선택" />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__">장비유형 선택</SelectItem>
                  {(catOptions[`${EQUIPMENT_TABLE}.equipment_type`] || []).map((o) => (
                    <SelectItem key={o.code} value={o.code}>
                      {o.label}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
            {/* Row 2: 모델명, 제조사, 시리얼번호 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">모델명</Label>
              <Input
                className="h-9"
                value={eqForm.model_name || ""}
                onChange={(e) => setEqForm((p) => ({ ...p, model_name: e.target.value }))}
                placeholder="모델명 입력"
              />
            </div>
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">제조사</Label>
              <Input
                className="h-9"
                value={eqForm.manufacturer || ""}
                onChange={(e) => setEqForm((p) => ({ ...p, manufacturer: e.target.value }))}
                placeholder="제조사 입력"
              />
            </div>
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">시리얼번호</Label>
              <Input
                className="h-9"
                value={eqForm.serial_number || ""}
                onChange={(e) => setEqForm((p) => ({ ...p, serial_number: e.target.value }))}
                placeholder="시리얼번호 입력"
              />
            </div>
            {/* Row 3: 설치장소, 최근교정일, 교정주기 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">설치장소</Label>
              <Input
                className="h-9"
                value={eqForm.installation_location || ""}
                onChange={(e) => setEqForm((p) => ({ ...p, installation_location: e.target.value }))}
                placeholder="설치장소 입력"
              />
            </div>
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">최근교정일</Label>
              <Input
                type="date"
                className="h-9"
                value={eqForm.last_calibration_date || ""}
                onChange={(e) => setEqForm((p) => ({ ...p, last_calibration_date: e.target.value }))}
              />
            </div>
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">교정주기 (개월)</Label>
              <Input
                type="number"
                className="h-9"
                value={eqForm.calibration_period || ""}
                onChange={(e) => setEqForm((p) => ({ ...p, calibration_period: e.target.value }))}
                placeholder="12"
              />
            </div>
            {/* Row 4: 장비상태, 담당자 */}
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">장비상태</Label>
              <Select
                value={eqForm.equipment_status || "__none__"}
                onValueChange={(v) => setEqForm((p) => ({ ...p, equipment_status: v === "__none__" ? "" : v }))}
              >
                <SelectTrigger className="h-9">
                  <SelectValue placeholder="장비상태 선택" />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__">장비상태 선택</SelectItem>
                  {(catOptions[`${EQUIPMENT_TABLE}.equipment_status`] || []).map((o) => (
                    <SelectItem key={o.code} value={o.code}>
                      {o.label}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
            <div className="space-y-1.5">
              <Label className="text-xs font-semibold">담당자</Label>
              <Select
                value={eqForm.manager_id || "__none__"}
                onValueChange={(v) => setEqForm((p) => ({ ...p, manager_id: v === "__none__" ? "" : v }))}
              >
                <SelectTrigger className="h-9">
                  <SelectValue placeholder="담당자 선택" />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="__none__">담당자 선택</SelectItem>
                  {userOptions.map((u) => (
                    <SelectItem key={u.code} value={u.code}>
                      {u.label}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
            <div />
            {/* Row 5: 이미지 (full width) */}
            <div className="col-span-3 space-y-1.5">
              <Label className="text-xs font-semibold">이미지</Label>
              <ImageUpload
                value={eqForm.image_path}
                onChange={(v) => setEqForm((p) => ({ ...p, image_path: v }))}
                tableName={EQUIPMENT_TABLE}
                recordId={eqForm.id}
                columnName="image_path"
              />
            </div>
            {/* Row 6: 비고 (full width) */}
            <div className="col-span-3 space-y-1.5">
              <Label className="text-xs font-semibold">비고</Label>
              <textarea
                className="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 flex min-h-[80px] w-full resize-y rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs focus-visible:ring-[3px] focus-visible:outline-none"
                value={eqForm.remarks || ""}
                onChange={(e) => setEqForm((p) => ({ ...p, remarks: e.target.value }))}
                placeholder="비고 입력"
                rows={3}
              />
            </div>
          </div>
          <DialogFooter>
            <Button variant="outline" onClick={() => setEqModalOpen(false)}>
              취소
            </Button>
            <Button onClick={saveEquipment} disabled={eqSaving}>
              {eqSaving ? <Loader2 className="mr-1 h-4 w-4 animate-spin" /> : <Save className="mr-1 h-4 w-4" />}
              저장
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      <TableSettingsModal
        open={ts.open}
        onOpenChange={ts.setOpen}
        tableName={ts.tableName}
        settingsId={ts.settingsId}
        defaultVisibleKeys={ts.defaultVisibleKeys}
        onSave={ts.applySettings}
      />

      {/* ═══════════════════ 엑셀업로드 모달 ═══════════════════ */}
      <SmartExcelUploadModal
        open={inspExcelOpen}
        onOpenChange={setInspExcelOpen}
        config={inspExcelConfig}
        dropdownOptions={inspDropdownOptions}
        labelToCodeMap={inspLabelToCodeMap}
        onUpload={handleInspExcelUpload}
        customValidator={(data) => {
          // inspection_type 다중값(text) 카테고리 라벨 검증
          const errors: any[] = [];
          const validLabels = catLabels(INSPECTION_TABLE, "inspection_type");
          for (const sheet of data) {
            sheet.rows.forEach((row, idx) => {
              const raw = String(row.inspection_type || "").trim();
              if (!raw) return;
              const labels = raw.split(",").map((s) => s.trim()).filter(Boolean);
              const invalid = labels.filter((l) => !validLabels.includes(l));
              if (invalid.length > 0) {
                errors.push({
                  sheet: sheet.sheetName,
                  row: idx + 2,
                  column: "검사유형",
                  message: `등록되지 않은 값: ${invalid.join(", ")}`,
                });
              }
            });
          }
          return errors;
        }}
      />
      <SmartExcelUploadModal
        open={defExcelOpen}
        onOpenChange={setDefExcelOpen}
        config={defExcelConfig}
        dropdownOptions={defDropdownOptions}
        labelToCodeMap={defLabelToCodeMap}
        onUpload={handleDefExcelUpload}
        customValidator={(data) => {
          const errors: any[] = [];
          const defInspOpts = catOptions[`${DEFECT_TABLE}.inspection_type`] || [];
          const parentByLabel = new Map(defInspOpts.filter((o) => o.depth === 1).map((o) => [o.label, o.code]));
          for (const sheet of data) {
            sheet.rows.forEach((row, idx) => {
              // 검사유형(depth=1) 검증
              const rawType = String(row.inspection_type || "").trim();
              const typeLabels = rawType ? rawType.split(",").map((s) => s.trim()).filter(Boolean) : [];
              const invalidTypes = typeLabels.filter((l) => !parentByLabel.has(l));
              if (invalidTypes.length > 0) {
                errors.push({
                  sheet: sheet.sheetName,
                  row: idx + 2,
                  column: "검사유형",
                  message: `등록되지 않은 값: ${invalidTypes.join(", ")}`,
                });
              }
              // 적용대상(depth=2) 계층 검증 — 선택한 검사유형의 자식만 허용
              const rawTarget = String(row.apply_target || "").trim();
              if (!rawTarget) return;
              const targetLabels = rawTarget.split(",").map((s) => s.trim()).filter(Boolean);
              const selectedParentCodes = typeLabels
                .map((l) => parentByLabel.get(l))
                .filter(Boolean) as string[];
              const invalidTargets = targetLabels.filter((label) => {
                const child = defInspOpts.find(
                  (o) =>
                    o.depth === 2 &&
                    o.label === label &&
                    o.parentCode &&
                    selectedParentCodes.includes(o.parentCode),
                );
                return !child;
              });
              if (invalidTargets.length > 0) {
                errors.push({
                  sheet: sheet.sheetName,
                  row: idx + 2,
                  column: "적용대상",
                  message: `선택한 검사유형의 하위가 아니거나 미등록: ${invalidTargets.join(", ")}`,
                });
              }
            });
          }
          return errors;
        }}
      />
      <SmartExcelUploadModal
        open={eqExcelOpen}
        onOpenChange={setEqExcelOpen}
        config={eqExcelConfig}
        dropdownOptions={eqDropdownOptions}
        labelToCodeMap={eqLabelToCodeMap}
        onUpload={handleEqExcelUpload}
      />
    </div>
  );
}
