"use client";

import React, { useState, useCallback, useEffect, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogFooter,
  DialogDescription,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable";
import {
  Upload,
  Download,
  RefreshCw,
  ChevronRight,
  Save,
  Trash2,
  Zap,
  Package,
  Wrench,
  AlertTriangle,
  ClipboardList,
  Calendar,
  Scissors,
  Loader2,
  Maximize2,
  Minimize2,
  Merge,
  Settings2,
  ChevronsLeft,
  ChevronLeft,
  ChevronsRight,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import {
  getOrderSummary,
  getStockShortage,
  getPlans,
  updatePlan,
  deletePlan,
  generateSchedule,
  previewSchedule,
  mergeSchedules,
  generateSemiSchedule,
  splitSchedule,
  type OrderSummaryItem,
  type StockShortageItem,
  type ProductionPlan,
  type GenerateScheduleRequest,
  type GenerateScheduleResponse,
} from "@/lib/api/production";
import TimelineScheduler, {
  type TimelineResource,
  type TimelineEvent,
  type ZoomLevel,
  type StatusColor,
} from "@/components/common/TimelineScheduler";
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
import { ExcelUploadModal } from "@/components/common/ExcelUploadModal";
import { exportToExcel } from "@/lib/utils/excelExport";
import { apiClient } from "@/lib/api/client";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";

// ─── 상수 ───

const STATUS_COLORS: StatusColor[] = [
  { key: "planned", label: "계획", bgClass: "from-blue-500 to-blue-600" },
  { key: "work-order", label: "지시", bgClass: "from-amber-500 to-amber-600" },
  { key: "in-progress", label: "진행", bgClass: "from-emerald-500 to-emerald-600" },
  { key: "completed", label: "완료", bgClass: "from-gray-400 to-gray-500" },
];

const STATUS_LABEL: Record<string, string> = {
  planned: "계획",
  "work-order": "작업지시",
  "in-progress": "진행중",
  completed: "완료",
};

// 수주 테이블 컬럼 설정
const ORDER_COLUMNS = [
  { key: "total_order_qty", label: "총수주량" },
  { key: "total_ship_qty", label: "출고량" },
  { key: "total_balance_qty", label: "잔량" },
  { key: "current_stock", label: "현재고" },
  { key: "safety_stock", label: "안전재고" },
  { key: "existing_plan_qty", label: "기생산계획량" },
  { key: "in_progress_qty", label: "생산진행" },
  { key: "required_plan_qty", label: "필요생산계획" },
  { key: "lead_time", label: "리드타임(일)" },
];

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

export default function ProductionPlanManagementPage() {
  // 탭 상태
  const [leftTab, setLeftTab] = useState("order");
  const [rightTab, setRightTab] = useState("finished");

  // 전체화면 토글
  const [isFullscreen, setIsFullscreen] = useState(false);

  // 로딩 상태
  const [loadingOrders, setLoadingOrders] = useState(false);
  const [loadingStock, setLoadingStock] = useState(false);
  const [loadingPlans, setLoadingPlans] = useState(false);
  const [generating, setGenerating] = useState(false);
  const [saving, setSaving] = useState(false);

  // 데이터 상태
  const [orderItems, setOrderItems] = useState<OrderSummaryItem[]>([]);
  // 좌측 수주목록 페이지네이션 (서버 페이징)
  const [orderPage, setOrderPage] = useState(1);
  const [orderPageSize, setOrderPageSize] = useState(20);
  const [orderPageSizeInput, setOrderPageSizeInput] = useState("20");
  const [orderTotalCount, setOrderTotalCount] = useState(0);
  const [stockItems, setStockItems] = useState<StockShortageItem[]>([]);
  const [finishedPlans, setFinishedPlans] = useState<ProductionPlan[]>([]);
  const [semiPlans, setSemiPlans] = useState<ProductionPlan[]>([]);
  const [equipmentList, setEquipmentList] = useState<{ id: string; equipment_code: string; equipment_name: string }[]>([]);

  // 선택/토글 상태
  const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
  const [selectedItemGroups, setSelectedItemGroups] = useState<Set<string>>(new Set());
  const [filterUnplannedOrdersOnly, setFilterUnplannedOrdersOnly] = useState(false);
  const [selectedStockItems, setSelectedStockItems] = useState<Set<string>>(new Set());

  const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
  const [searchItemCode, setSearchItemCode] = useState("");
  const [searchStatus, setSearchStatus] = useState("all");
  const [searchStartDate, setSearchStartDate] = useState("");
  const [searchEndDate, setSearchEndDate] = useState("");

  // 테이블 설정
  const ts = useTableSettings("c16-plan-management", "sales_order_mng", ORDER_COLUMNS);

  // 타임라인 옵션
  const [recalculateUnstarted, setRecalculateUnstarted] = useState(true);
  const [finishedZoom, setFinishedZoom] = useState<ZoomLevel>("week");
  const [semiZoom, setSemiZoom] = useState<ZoomLevel>("week");

  // 반제품 옵션
  const [semiConsiderStock, setSemiConsiderStock] = useState(true);
  const [semiRecalculate, setSemiRecalculate] = useState(false);
  const [semiExcludeUsed, setSemiExcludeUsed] = useState(true);

  // 모달 상태
  const [scheduleModalOpen, setScheduleModalOpen] = useState(false);
  const [orderImportModalOpen, setOrderImportModalOpen] = useState(false);
  const [stockImportModalOpen, setStockImportModalOpen] = useState(false);
  const [changeConfirmModalOpen, setChangeConfirmModalOpen] = useState(false);
  const [excelUploadOpen, setExcelUploadOpen] = useState(false);

  // 모달 데이터
  const [selectedPlan, setSelectedPlan] = useState<ProductionPlan | null>(null);
  const [modalQuantity, setModalQuantity] = useState(0);
  const [modalStartDate, setModalStartDate] = useState("");
  const [modalEndDate, setModalEndDate] = useState("");
  const [modalEquipmentId, setModalEquipmentId] = useState("");

  // 미리보기 데이터
  const [previewData, setPreviewData] = useState<GenerateScheduleResponse | null>(null);

  // 불러오기 모드
  const [importMode, setImportMode] = useState<"add" | "new">("add");

  // 병합: 타임라인에서 선택된 계획 ID
  const [selectedPlanIds, setSelectedPlanIds] = useState<Set<number>>(new Set());

  // useConfirmDialog
  const { confirm, ConfirmDialogComponent, isConfirmOpenRef } = useConfirmDialog();

  // 수량 지정 분할 입력값
  const [customSplitQty, setCustomSplitQty] = useState<number | "">("");

  // ========== 데이터 로드 ==========

  const fetchOrderSummary = useCallback(async () => {
    setLoadingOrders(true);
    try {
      const res = await getOrderSummary({
        excludePlanned: filterUnplannedOrdersOnly,
        itemCode: searchItemCode || undefined,
        page: orderPage,
        size: orderPageSize,
      });
      if (res.success) {
        setOrderItems(res.data || []);
        setOrderTotalCount(res.total ?? res.data?.length ?? 0);
      }
    } catch (err: any) {
      toast.error("수주 데이터 조회 실패: " + (err.message || ""));
    } finally {
      setLoadingOrders(false);
    }
  }, [filterUnplannedOrdersOnly, searchItemCode, orderPage, orderPageSize]);

  // 수주목록 페이지네이션 계산
  const orderTotalPages = Math.max(1, Math.ceil(orderTotalCount / orderPageSize));
  const orderSafePage = Math.min(Math.max(1, orderPage), orderTotalPages);
  // 서버 페이징: 응답 자체가 페이지 데이터이므로 slice 불필요
  const paginatedOrderItems = orderItems;

  const applyOrderPageSize = () => {
    const n = parseInt(orderPageSizeInput, 10);
    if (!isNaN(n) && n >= 1) { setOrderPageSize(n); setOrderPage(1); }
    else setOrderPageSizeInput(String(orderPageSize));
  };

  const getOrderPageNumbers = (): (number | "...")[] => {
    const pages: (number | "...")[] = [];
    if (orderTotalPages <= 7) {
      for (let i = 1; i <= orderTotalPages; i++) pages.push(i);
    } else {
      pages.push(1);
      if (orderSafePage > 3) pages.push("...");
      for (let i = Math.max(2, orderSafePage - 1); i <= Math.min(orderTotalPages - 1, orderSafePage + 1); i++) pages.push(i);
      if (orderSafePage < orderTotalPages - 2) pages.push("...");
      pages.push(orderTotalPages);
    }
    return pages;
  };

  // 검색 필터 변경 시 1페이지로 리셋
  useEffect(() => { setOrderPage(1); }, [filterUnplannedOrdersOnly, searchItemCode]);

  const fetchStockShortage = useCallback(async () => {
    setLoadingStock(true);
    try {
      const res = await getStockShortage();
      if (res.success) setStockItems(res.data || []);
    } catch (err: any) {
      toast.error("안전재고 부족분 조회 실패: " + (err.message || ""));
    } finally {
      setLoadingStock(false);
    }
  }, []);

  const fetchPlans = useCallback(async () => {
    setLoadingPlans(true);
    try {
      // 타임라인 성능: 기간 필터 미입력 시 기본 오늘 ~ +60일 자동 적용
      // 이전 기록은 검색에서 시작일/종료일을 직접 지정하면 조회됨
      const today = new Date();
      const fmt = (d: Date) => d.toISOString().slice(0, 10);
      const defaultStart = fmt(today);
      const defaultEnd = fmt(new Date(today.getTime() + 60 * 86400000));
      const effectiveStart = searchStartDate || defaultStart;
      const effectiveEnd = searchEndDate || defaultEnd;

      const [finRes, semiRes] = await Promise.all([
        getPlans({
          productType: "완제품",
          status: searchStatus !== "all" ? searchStatus : undefined,
          startDate: effectiveStart,
          endDate: effectiveEnd,
          itemCode: searchItemCode || undefined,
        }),
        getPlans({
          productType: "반제품",
          status: searchStatus !== "all" ? searchStatus : undefined,
          startDate: effectiveStart,
          endDate: effectiveEnd,
        }),
      ]);
      if (finRes.success) setFinishedPlans(finRes.data || []);
      if (semiRes.success) setSemiPlans(semiRes.data || []);
    } catch (err: any) {
      toast.error("생산계획 조회 실패: " + (err.message || ""));
    } finally {
      setLoadingPlans(false);
    }
  }, [searchStatus, searchStartDate, searchEndDate, searchItemCode]);

  const fetchEquipmentList = useCallback(async () => {
    try {
      const res = await apiClient.get("/process-info/equipments");
      if (res.data?.success) setEquipmentList(res.data.data || []);
    } catch {
      // 설비 목록 없어도 정상 동작
    }
  }, []);

  useEffect(() => {
    fetchOrderSummary();
    fetchStockShortage();
    fetchPlans();
    fetchEquipmentList();
  }, []);

  // ========== DynamicSearchFilter 콜백 ==========

  const handleSearchFilterChange = useCallback((filters: FilterValue[]) => {
    setSearchFilters(filters);
    let itemCode = "";
    let status = "all";
    let startDate = "";
    let endDate = "";
    for (const f of filters) {
      if (f.columnName === "item_code" && f.value) itemCode = f.value;
      if (f.columnName === "status" && f.value) status = f.value;
      if (f.columnName === "start_date" && f.operator === "between" && f.value) {
        const [s, e] = f.value.split("|");
        if (s) startDate = s;
        if (e) endDate = e;
      }
    }
    setSearchItemCode(itemCode);
    setSearchStatus(status || "all");
    setSearchStartDate(startDate);
    setSearchEndDate(endDate);
  }, []);

  useEffect(() => {
    // 검색 state 변경 시 자동 재조회 (필터 비어있어도 default 기간으로 재조회)
    fetchOrderSummary();
    fetchPlans();
  }, [searchItemCode, searchStatus, searchStartDate, searchEndDate]); // eslint-disable-line react-hooks/exhaustive-deps

  // ========== 토글/선택 핸들러 ==========

  const toggleItemExpand = useCallback((itemCode: string) => {
    setExpandedItems((prev) => {
      const next = new Set(prev);
      if (next.has(itemCode)) next.delete(itemCode);
      else next.add(itemCode);
      return next;
    });
  }, []);

  const toggleItemGroupSelect = useCallback((itemCode: string) => {
    setSelectedItemGroups((prev) => {
      const next = new Set(prev);
      if (next.has(itemCode)) next.delete(itemCode);
      else next.add(itemCode);
      return next;
    });
  }, []);

  const toggleAllItemGroups = useCallback(
    (checked: boolean) => {
      setSelectedItemGroups(checked ? new Set(orderItems.map((i) => i.item_code)) : new Set());
    },
    [orderItems]
  );

  const toggleStockItem = useCallback((itemCode: string) => {
    setSelectedStockItems((prev) => {
      const next = new Set(prev);
      if (next.has(itemCode)) next.delete(itemCode);
      else next.add(itemCode);
      return next;
    });
  }, []);

  const toggleAllStockItems = useCallback(
    (checked: boolean) => {
      setSelectedStockItems(checked ? new Set(stockItems.map((i) => i.item_code)) : new Set());
    },
    [stockItems]
  );

  // ========== 타임라인 리소스 & 이벤트 ==========

  const finishedResources: TimelineResource[] = useMemo(() => {
    const map = new Map<string, TimelineResource>();
    for (const p of finishedPlans) {
      if (!map.has(p.item_code)) {
        map.set(p.item_code, {
          id: p.item_code,
          label: p.item_code,
          subLabel: p.item_name,
        });
      }
    }
    return Array.from(map.values());
  }, [finishedPlans]);

  const finishedEvents: TimelineEvent[] = useMemo(() => {
    return finishedPlans.map((p) => ({
      id: p.id,
      resourceId: p.item_code,
      startDate: p.start_date?.split("T")[0] || "",
      endDate: p.end_date?.split("T")[0] || "",
      label: `${Number(p.plan_qty).toLocaleString()}`,
      status: p.status,
      progress: Number(p.progress_rate) || 0,
      data: p,
    }));
  }, [finishedPlans]);

  const semiResources: TimelineResource[] = useMemo(() => {
    const map = new Map<string, TimelineResource>();
    for (const p of semiPlans) {
      if (!map.has(p.item_code)) {
        map.set(p.item_code, {
          id: p.item_code,
          label: p.item_code,
          subLabel: p.item_name,
        });
      }
    }
    return Array.from(map.values());
  }, [semiPlans]);

  const semiEvents: TimelineEvent[] = useMemo(() => {
    return semiPlans.map((p) => ({
      id: p.id,
      resourceId: p.item_code,
      startDate: p.start_date?.split("T")[0] || "",
      endDate: p.end_date?.split("T")[0] || "",
      label: `${Number(p.plan_qty).toLocaleString()}`,
      status: p.status,
      progress: Number(p.progress_rate) || 0,
      data: p,
    }));
  }, [semiPlans]);

  // ========== 액션 핸들러 ==========

  const handleSearch = useCallback(() => {
    fetchOrderSummary();
    fetchPlans();
  }, [fetchOrderSummary, fetchPlans]);

  // 자동 스케줄 생성 (preview → confirm → apply)
  const handleGenerateSchedule = useCallback(async () => {
    if (selectedItemGroups.size === 0) {
      toast.error("품목을 선택해주세요");
      return;
    }

    // 납기일별로 분리하여 각각 계획 생성
    const items: GenerateScheduleRequest["items"] = [];
    orderItems
      .filter((item) => selectedItemGroups.has(item.item_code))
      .forEach((item) => {
        const leadTime = Number(item.lead_time) || 0;
        // 재계산 모드: 기존 planned를 삭제 후 재생성 → 수주 잔량에서 진행중만 빼기
        const totalRequired = recalculateUnstarted
          ? Number(item.total_balance_qty || 0) - Number(item.in_progress_qty || 0)
          : Number(item.required_plan_qty);
        if (totalRequired <= 0) return;

        // 수주가 여러 건이고 납기일이 다르면 각각 분리
        if (item.orders && item.orders.length > 1) {
          const byDueDate = new Map<string, number>();
          for (const order of item.orders) {
            const dd = order.due_date || new Date().toISOString().split("T")[0];
            byDueDate.set(dd, (byDueDate.get(dd) || 0) + Number(order.balance_qty || 0));
          }
          if (byDueDate.size > 1) {
            // 납기일별 잔량 비율로 required_plan_qty 분배
            const totalBalance = Number(item.total_balance_qty) || 1;
            let distributed = 0;
            const entries = [...byDueDate.entries()];
            entries.forEach(([dueDate, balanceQty], idx) => {
              if (balanceQty <= 0) return;
              // 마지막 건은 나머지 할당 (반올림 오차 방지)
              const qty = idx === entries.length - 1
                ? totalRequired - distributed
                : Math.round(totalRequired * (balanceQty / totalBalance));
              if (qty <= 0) return;
              distributed += qty;
              items.push({
                item_code: item.item_code,
                item_name: item.item_name,
                required_qty: qty,
                earliest_due_date: dueDate,
                lead_time: leadTime,
              });
            });
          } else {
            items.push({
              item_code: item.item_code,
              item_name: item.item_name,
              required_qty: totalRequired,
              earliest_due_date: item.earliest_due_date || new Date().toISOString().split("T")[0],
              lead_time: leadTime,
            });
          }
        } else {
          items.push({
            item_code: item.item_code,
            item_name: item.item_name,
            required_qty: totalRequired,
            earliest_due_date: item.earliest_due_date || new Date().toISOString().split("T")[0],
            lead_time: leadTime,
          });
        }
      });

    // items가 비어있으면 사용자에게 알림


    if (items.length === 0) {
      toast.error("계획 수량이 있는 품목이 없습니다. 수주 잔량을 확인해주세요.");
      return;
    }

    setGenerating(true);
    try {
      const req: GenerateScheduleRequest = {
        items,
        options: {
          safety_lead_time: 0,
          recalculate_unstarted: recalculateUnstarted,
          product_type: "완제품",
        },
      };
      const previewRes = await previewSchedule(req);
      if (previewRes.success) {
        setPreviewData(previewRes.data);
        setChangeConfirmModalOpen(true);
      }
    } catch (err: any) {
      toast.error("스케줄 미리보기 실패: " + (err.message || ""));
    } finally {
      setGenerating(false);
    }
  }, [selectedItemGroups, orderItems, recalculateUnstarted]);

  const handleApplySchedule = useCallback(async () => {
    if (selectedItemGroups.size === 0) return;

    // 납기일별로 분리하여 각각 계획 생성
    const items: GenerateScheduleRequest["items"] = [];
    orderItems
      .filter((item) => selectedItemGroups.has(item.item_code))
      .forEach((item) => {
        const leadTime = Number(item.lead_time) || 0;
        const totalRequired = recalculateUnstarted
          ? Number(item.required_plan_qty) + Number(item.existing_plan_qty || 0)
          : Number(item.required_plan_qty);
        if (totalRequired <= 0) return;

        if (item.orders && item.orders.length > 1) {
          const byDueDate = new Map<string, number>();
          for (const order of item.orders) {
            const dd = order.due_date || new Date().toISOString().split("T")[0];
            byDueDate.set(dd, (byDueDate.get(dd) || 0) + Number(order.balance_qty || 0));
          }
          if (byDueDate.size > 1) {
            const totalBalance = Number(item.total_balance_qty) || 1;
            let distributed = 0;
            const entries = [...byDueDate.entries()];
            entries.forEach(([dueDate, balanceQty], idx) => {
              if (balanceQty <= 0) return;
              const qty = idx === entries.length - 1
                ? totalRequired - distributed
                : Math.round(totalRequired * (balanceQty / totalBalance));
              if (qty <= 0) return;
              distributed += qty;
              items.push({
                item_code: item.item_code,
                item_name: item.item_name,
                required_qty: qty,
                earliest_due_date: dueDate,
                lead_time: leadTime,
              });
            });
          } else {
            items.push({
              item_code: item.item_code,
              item_name: item.item_name,
              required_qty: totalRequired,
              earliest_due_date: item.earliest_due_date || new Date().toISOString().split("T")[0],
              lead_time: leadTime,
            });
          }
        } else {
          items.push({
            item_code: item.item_code,
            item_name: item.item_name,
            required_qty: totalRequired,
            earliest_due_date: item.earliest_due_date || new Date().toISOString().split("T")[0],
            lead_time: leadTime,
          });
        }
      });

    setGenerating(true);
    try {
      const res = await generateSchedule({
        items,
        options: {
          safety_lead_time: 0,
          recalculate_unstarted: recalculateUnstarted,
          product_type: "완제품",
        },
      });
      if (res.success) {
        toast.success(`스케줄이 생성되었습니다 (${res.data.summary.total}건)`);
        setChangeConfirmModalOpen(false);
        setPreviewData(null);
        fetchPlans();
        fetchOrderSummary();
      }
    } catch (err: any) {
      toast.error("스케줄 생성 실패: " + (err.message || ""));
    } finally {
      setGenerating(false);
    }
  }, [selectedItemGroups, orderItems, recalculateUnstarted, fetchPlans, fetchOrderSummary]);

  // 타임라인 초기화 (계획 상태만 삭제)
  const handleClearTimeline = useCallback(async () => {
    if (finishedPlans.length === 0) {
      toast.info("삭제할 계획이 없습니다");
      return;
    }
    const plannedIds = finishedPlans.filter((p) => p.status === "planned").map((p) => p.id);
    if (plannedIds.length === 0) {
      toast.info("삭제 가능한 계획이 없습니다 (계획 상태만 삭제 가능)");
      return;
    }
    const ok = await confirm(`${plannedIds.length}건의 계획을 삭제하시겠습니까?`, {
      description: "삭제된 계획은 복구할 수 없습니다.",
      variant: "destructive",
      confirmText: "삭제",
    });
    if (!ok) return;

    try {
      const results = await Promise.allSettled(plannedIds.map((id) => deletePlan(id)));
      const failedIds = plannedIds.filter((_, i) => results[i].status === "rejected");
      const succeededCount = plannedIds.length - failedIds.length;

      if (failedIds.length === plannedIds.length) {
        // 전부 삭제 실패
        toast.error(`${failedIds.length}건 모두 삭제에 실패했습니다. 다시 시도해주세요.`);
      } else if (failedIds.length > 0) {
        // 일부 삭제 실패
        toast.warning(
          `${succeededCount}건 삭제 완료, ${failedIds.length}건 삭제 실패. 실패 항목을 다시 시도해주세요.`
        );
      } else {
        // 전부 성공
        toast.success(`${plannedIds.length}건의 계획이 삭제되었습니다`);
      }
      fetchPlans();
    } catch (err: any) {
      toast.error("삭제 실패: " + (err.message || ""));
    }
  }, [finishedPlans, fetchPlans, confirm]);

  // 반제품 자동 생성
  const handleGenerateSemiSchedule = useCallback(async () => {
    const planIds = finishedPlans.map((p) => p.id);
    if (planIds.length === 0) {
      toast.error("완제품 생산계획이 없습니다");
      return;
    }
    setGenerating(true);
    try {
      const res = await generateSemiSchedule(planIds, {
        considerStock: semiConsiderStock,
        excludeUsed: semiExcludeUsed,
      });
      if (res.success) {
        toast.success(`반제품 계획 ${res.data.count}건이 생성되었습니다`);
        fetchPlans();
      }
    } catch (err: any) {
      toast.error("반제품 계획 생성 실패: " + (err.message || ""));
    } finally {
      setGenerating(false);
    }
  }, [finishedPlans, semiConsiderStock, semiExcludeUsed, fetchPlans]);

  // 스케줄 상세 모달 열기
  const openScheduleDetail = useCallback((event: TimelineEvent) => {
    const plan = event.data as ProductionPlan;
    if (!plan) return;
    setSelectedPlan(plan);
    setModalQuantity(Number(plan.plan_qty));
    setModalStartDate(plan.start_date?.split("T")[0] || "");
    setModalEndDate(plan.end_date?.split("T")[0] || "");
    setModalEquipmentId((plan as any).equipment_code || (plan.equipment_id ? String(plan.equipment_id) : ""));
    setCustomSplitQty("");
    setScheduleModalOpen(true);
  }, []);

  const handleSavePlan = useCallback(async () => {
    if (!selectedPlan) return;
    setSaving(true);
    try {
      const res = await updatePlan(selectedPlan.id, {
        plan_qty: modalQuantity,
        start_date: modalStartDate,
        end_date: modalEndDate,
        equipment_code: modalEquipmentId && modalEquipmentId !== "none" ? modalEquipmentId : null,
        equipment_name: modalEquipmentId && modalEquipmentId !== "none"
          ? equipmentList.find((eq) => (eq.equipment_code || eq.id) === modalEquipmentId)?.equipment_name || null
          : null,
      } as any);
      if (res.success) {
        toast.success("생산계획이 수정되었습니다");
        setScheduleModalOpen(false);
        fetchPlans();
        fetchOrderSummary();
      }
    } catch (err: any) {
      toast.error("수정 실패: " + (err?.response?.data?.message || err.message || ""));
    } finally {
      setSaving(false);
    }
  }, [selectedPlan, modalQuantity, modalStartDate, modalEndDate, modalEquipmentId, equipmentList, fetchPlans, fetchOrderSummary]);

  const handleDeletePlan = useCallback(async () => {
    if (!selectedPlan) return;
    const ok = await confirm("이 생산계획을 삭제하시겠습니까?", {
      variant: "destructive",
      confirmText: "삭제",
    });
    if (!ok) return;
    try {
      await deletePlan(selectedPlan.id);
      toast.success("삭제되었습니다");
      setScheduleModalOpen(false);
      fetchPlans();
      fetchOrderSummary();
    } catch (err: any) {
      toast.error("삭제 실패: " + (err?.response?.data?.message || err.message || ""));
    }
  }, [selectedPlan, fetchPlans, fetchOrderSummary, confirm]);

  // 에러 메시지 추출 헬퍼
  const extractErrMsg = (err: any): string => {
    return err?.response?.data?.message || err?.message || "";
  };

  // modalQuantity/일정/설비가 DB의 selectedPlan 값과 다른지 확인 (dirty 체크)
  const isModalDirty = useCallback((): boolean => {
    if (!selectedPlan) return false;
    const planQty = Number(selectedPlan.plan_qty) || 0;
    const planStart = selectedPlan.start_date?.split("T")[0] || "";
    const planEnd = selectedPlan.end_date?.split("T")[0] || "";
    const planEq = (selectedPlan as any).equipment_code || (selectedPlan.equipment_id ? String(selectedPlan.equipment_id) : "");
    return (
      planQty !== Number(modalQuantity) ||
      planStart !== modalStartDate ||
      planEnd !== modalEndDate ||
      planEq !== modalEquipmentId
    );
  }, [selectedPlan, modalQuantity, modalStartDate, modalEndDate, modalEquipmentId]);

  // dirty 상태면 자동 저장 후 selectedPlan 을 최신 값으로 갱신
  const ensureSavedBeforeSplit = useCallback(async (): Promise<boolean> => {
    if (!selectedPlan) return false;
    if (!isModalDirty()) return true;
    try {
      const res = await updatePlan(selectedPlan.id, {
        plan_qty: modalQuantity,
        start_date: modalStartDate,
        end_date: modalEndDate,
        equipment_code: modalEquipmentId && modalEquipmentId !== "none" ? modalEquipmentId : null,
        equipment_name: modalEquipmentId && modalEquipmentId !== "none"
          ? equipmentList.find((eq) => (eq.equipment_code || eq.id) === modalEquipmentId)?.equipment_name || null
          : null,
      } as any);
      if (!res.success) {
        toast.error("저장 실패로 분할이 중단되었습니다");
        return false;
      }
      // selectedPlan 을 최신 값으로 동기화 (이후 로직에서 plan_qty 를 참조)
      setSelectedPlan((prev) => prev ? ({
        ...prev,
        plan_qty: modalQuantity,
        start_date: modalStartDate,
        end_date: modalEndDate,
        equipment_code: modalEquipmentId && modalEquipmentId !== "none" ? modalEquipmentId : null,
      } as any) : prev);
      return true;
    } catch (err: any) {
      toast.error("저장 실패로 분할이 중단되었습니다: " + extractErrMsg(err));
      return false;
    }
  }, [selectedPlan, isModalDirty, modalQuantity, modalStartDate, modalEndDate, modalEquipmentId, equipmentList]);

  // 균등 분할 (2/3/4분할 버튼)
  const handleSplitSchedule = useCallback(async (splitCount: number) => {
    if (!selectedPlan || splitCount < 2) return;
    // 모달 입력값 기준 (이후 자동 저장되므로 modalQuantity 가 진실)
    const originalQty = Number(modalQuantity) || 0;
    if (originalQty < splitCount) {
      toast.error(`${splitCount}분할하려면 수량이 ${splitCount} 이상이어야 합니다`);
      return;
    }
    if (selectedPlan.status && selectedPlan.status !== "planned") {
      toast.error("계획 상태인 건만 분할할 수 있습니다");
      return;
    }
    const ok = await confirm(`이 계획을 ${splitCount}개로 균등 분할하시겠습니까?`, {
      description: `수량 ${originalQty}이(가) ${splitCount}개로 나뉩니다.`,
      confirmText: "분할",
    });
    if (!ok) return;

    // dirty 면 자동 저장
    const saved = await ensureSavedBeforeSplit();
    if (!saved) return;

    const eachQty = Math.floor(originalQty / splitCount);
    if (eachQty <= 0) {
      toast.error("분할 수량이 부족합니다");
      return;
    }

    let successCount = 0;
    try {
      // N-1회 호출: 매번 eachQty만큼 원본에서 떼어내 새 plan 생성
      for (let i = 0; i < splitCount - 1; i++) {
        const res = await splitSchedule(selectedPlan.id, eachQty);
        if (!res.success) throw new Error("분할 응답 실패");
        successCount++;
      }
      toast.success(`계획이 ${splitCount}개로 분할되었습니다`);
      setScheduleModalOpen(false);
      fetchPlans();
      fetchOrderSummary();
    } catch (err: any) {
      const msg = extractErrMsg(err);
      if (successCount > 0) {
        toast.error(`분할 일부 실패 (${successCount + 1}개 생성됨): ${msg}`);
      } else {
        toast.error("분할 실패: " + msg);
      }
      fetchPlans();
      fetchOrderSummary();
    }
  }, [selectedPlan, modalQuantity, fetchPlans, fetchOrderSummary, confirm, ensureSavedBeforeSplit]);

  // 수량 지정 분할 (원본에서 입력 수량만큼 떼어내기)
  const handleCustomSplit = useCallback(async () => {
    if (!selectedPlan) return;
    const splitQty = Number(customSplitQty);
    const originalQty = Number(modalQuantity) || 0;
    if (!splitQty || splitQty < 1) {
      toast.error("떼어낼 수량을 1 이상으로 입력하세요");
      return;
    }
    if (splitQty >= originalQty) {
      toast.error("떼어낼 수량은 원본 수량보다 작아야 합니다");
      return;
    }
    if (selectedPlan.status && selectedPlan.status !== "planned") {
      toast.error("계획 상태인 건만 분할할 수 있습니다");
      return;
    }
    const ok = await confirm(`이 계획에서 ${splitQty}만큼 떼어내시겠습니까?`, {
      description: `원본 ${originalQty} → 원본 ${originalQty - splitQty} + 신규 ${splitQty}`,
      confirmText: "분할",
    });
    if (!ok) return;

    const saved = await ensureSavedBeforeSplit();
    if (!saved) return;

    try {
      const res = await splitSchedule(selectedPlan.id, splitQty);
      if (!res.success) throw new Error("분할 응답 실패");
      toast.success(`${splitQty} 수량이 분리되었습니다`);
      setCustomSplitQty("");
      setScheduleModalOpen(false);
      fetchPlans();
      fetchOrderSummary();
    } catch (err: any) {
      toast.error("분할 실패: " + extractErrMsg(err));
      fetchPlans();
      fetchOrderSummary();
    }
  }, [selectedPlan, modalQuantity, customSplitQty, fetchPlans, fetchOrderSummary, confirm, ensureSavedBeforeSplit]);

  // 병합 핸들러
  const handleMergeSchedules = useCallback(async () => {
    if (selectedPlanIds.size < 2) {
      toast.error("2개 이상의 계획을 선택해주세요");
      return;
    }
    const ok = await confirm(`${selectedPlanIds.size}건의 계획을 병합하시겠습니까?`, {
      description: "동일 품목의 계획이 하나로 병합됩니다.",
      confirmText: "병합",
    });
    if (!ok) return;

    try {
      const ids = Array.from(selectedPlanIds);
      const productType = rightTab === "finished" ? "완제품" : "반제품";
      const res = await mergeSchedules(ids, productType);
      if (res.success) {
        toast.success("계획이 병합되었습니다");
        setSelectedPlanIds(new Set());
        fetchPlans();
        fetchOrderSummary();
      }
    } catch (err: any) {
      toast.error("병합 실패: " + (err?.response?.data?.message || err.message || ""));
    }
  }, [selectedPlanIds, rightTab, fetchPlans, fetchOrderSummary, confirm]);

  // 타임라인 이벤트 드래그 이동
  const handleEventMove = useCallback(async (eventId: string | number, newStart: string, newEnd: string) => {
    try {
      const res = await updatePlan(Number(eventId), {
        start_date: newStart,
        end_date: newEnd,
      } as any);
      if (res.success) {
        toast.success("일정이 변경되었습니다");
        fetchPlans();
        fetchOrderSummary();
      }
    } catch (err: any) {
      toast.error("일정 변경 실패: " + (err.message || ""));
    }
  }, [fetchPlans, fetchOrderSummary]);

  // 타임라인 이벤트 리사이즈
  const handleEventResize = useCallback(async (eventId: string | number, newStart: string, newEnd: string) => {
    try {
      const res = await updatePlan(Number(eventId), {
        start_date: newStart,
        end_date: newEnd,
      } as any);
      if (res.success) {
        toast.success("기간이 변경되었습니다");
        fetchPlans();
        fetchOrderSummary();
      }
    } catch (err: any) {
      toast.error("기간 변경 실패: " + (err.message || ""));
    }
  }, [fetchPlans, fetchOrderSummary]);

  // 불러오기 처리
  const handleImportOrderItems = useCallback(async () => {
    if (selectedItemGroups.size === 0) {
      toast.error("품목을 선택해주세요");
      return;
    }
    const items = orderItems
      .filter((item) => selectedItemGroups.has(item.item_code))
      .map((item) => ({
        item_code: item.item_code,
        item_name: item.item_name,
        required_qty: (importMode !== "new" && recalculateUnstarted)
          ? Number(item.total_balance_qty || 0) - Number(item.in_progress_qty || 0)
          : Number(item.required_plan_qty),
        earliest_due_date: item.earliest_due_date || new Date().toISOString().split("T")[0],
      }))
      .filter((item) => item.required_qty > 0);

    setGenerating(true);
    try {
      const res = await generateSchedule({
        items,
        options: {
          safety_lead_time: 0,
          recalculate_unstarted: importMode === "new" ? false : recalculateUnstarted,
          product_type: "완제품",
        },
      });
      if (res.success) {
        toast.success("품목을 불러왔습니다");
        setOrderImportModalOpen(false);
        fetchPlans();
        fetchOrderSummary();
      }
    } catch (err: any) {
      toast.error("불러오기 실패: " + (err.message || ""));
    } finally {
      setGenerating(false);
    }
  }, [selectedItemGroups, orderItems, importMode, recalculateUnstarted, fetchPlans, fetchOrderSummary]);

  const handleImportStockItems = useCallback(async () => {
    if (selectedStockItems.size === 0) {
      toast.error("품목을 선택해주세요");
      return;
    }
    const items = stockItems
      .filter((item) => selectedStockItems.has(item.item_code))
      .map((item) => ({
        item_code: item.item_code,
        item_name: item.item_name,
        required_qty: Number(item.recommended_qty),
        earliest_due_date: new Date().toISOString().split("T")[0],
      }));

    setGenerating(true);
    try {
      const res = await generateSchedule({
        items,
        options: {
          safety_lead_time: 0,
          recalculate_unstarted: importMode === "new" ? false : true,
          product_type: "완제품",
        },
      });
      if (res.success) {
        toast.success("안전재고 부족 품목을 불러왔습니다");
        setStockImportModalOpen(false);
        fetchPlans();
        fetchStockShortage();
      }
    } catch (err: any) {
      toast.error("불러오기 실패: " + (err.message || ""));
    } finally {
      setGenerating(false);
    }
  }, [selectedStockItems, stockItems, importMode, fetchPlans, fetchStockShortage]);

  // 엑셀 다운로드
  const handleExcelDownload = useCallback(async () => {
    const plans = rightTab === "finished" ? finishedPlans : semiPlans;
    if (plans.length === 0) {
      toast.error("다운로드할 데이터가 없습니다");
      return;
    }
    const data = plans.map((p) => ({
      계획번호: p.plan_no || "",
      품목코드: p.item_code,
      품목명: p.item_name,
      제품구분: p.product_type,
      수량: Number(p.plan_qty),
      완료수량: Number(p.completed_qty),
      진행률: `${p.progress_rate || 0}%`,
      시작일: p.start_date?.split("T")[0] || "",
      종료일: p.end_date?.split("T")[0] || "",
      납기일: p.due_date?.split("T")[0] || "",
      상태: STATUS_LABEL[p.status] || p.status,
      설비: p.equipment_name || "",
      비고: p.remarks || "",
    }));
    const type = rightTab === "finished" ? "완제품" : "반제품";
    await exportToExcel(data, `생산계획_${type}.xlsx`, `${type} 생산계획`);
    toast.success("엑셀 다운로드 완료");
  }, [rightTab, finishedPlans, semiPlans]);

  // 좌측 테이블 엑셀 다운로드
  const handleLeftExcelDownload = useCallback(async () => {
    if (leftTab === "order") {
      if (orderItems.length === 0) { toast.error("데이터가 없습니다"); return; }
      const data = orderItems.map((i) => ({
        품목코드: i.item_code,
        품목명: i.item_name,
        총수주량: Number(i.total_order_qty),
        출고량: Number(i.total_ship_qty),
        잔량: Number(i.total_balance_qty),
        현재고: Number(i.current_stock),
        안전재고: Number(i.safety_stock),
        기생산계획량: Number(i.existing_plan_qty),
        생산진행: Number(i.in_progress_qty),
        필요생산계획: Number(i.required_plan_qty),
        "리드타임(일)": Number(i.lead_time) || 0,
      }));
      await exportToExcel(data, "수주데이터.xlsx", "수주데이터");
    } else {
      if (stockItems.length === 0) { toast.error("데이터가 없습니다"); return; }
      const data = stockItems.map((s) => ({
        품목코드: s.item_code,
        품목명: s.item_name,
        현재고: Number(s.current_qty),
        안전재고: Number(s.safety_qty),
        부족수량: Number(s.shortage_qty),
        권장생산량: Number(s.recommended_qty),
        최종입고일: s.last_in_date || "",
      }));
      await exportToExcel(data, "안전재고부족분.xlsx", "안전재고 부족분");
    }
    toast.success("엑셀 다운로드 완료");
  }, [leftTab, orderItems, stockItems]);

  // 숫자 포맷
  const formatNumber = (num: number | string) => Number(num).toLocaleString();

  // (컬럼 표시는 ts.visibleColumns 순서를 따름)

  return (
    <div className={cn("flex flex-col gap-3", isFullscreen ? "fixed inset-0 z-50 bg-background p-4" : "h-full p-3")}>
      {/* 브레드크럼 */}
      <nav className="flex items-center gap-1.5 text-xs text-muted-foreground shrink-0">
        <span>생산관리</span>
        <span className="text-muted-foreground/50">/</span>
        <span className="font-medium text-foreground">생산계획</span>
      </nav>

      {/* 상단 바 */}
      <DynamicSearchFilter
        tableName="sales_order_mng"
        filterId="c16-plan-management"
        onFilterChange={handleSearchFilterChange}
        externalFilterConfig={ts.filterConfig}
        extraActions={
          <div className="flex items-center gap-2">
            <Button variant="outline" size="sm" className="text-success border-success/30 hover:bg-success/10" onClick={() => setExcelUploadOpen(true)}>
              <Upload className="mr-1.5 h-4 w-4" />
              엑셀업로드
            </Button>
            <Button variant="outline" size="sm" onClick={handleExcelDownload}>
              <Download className="mr-1.5 h-4 w-4" />
              엑셀다운로드
            </Button>
            <Button variant="ghost" size="sm" onClick={() => setIsFullscreen((p) => !p)}>
              {isFullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
            </Button>
          </div>
        }
      />

      {/* 데이터 섹션 */}
      <ResizablePanelGroup direction="horizontal" className="flex-1 overflow-hidden rounded-lg">
        {/* 왼쪽 패널 */}
        <ResizablePanel defaultSize={45} minSize={25}>
          <div className="flex h-full flex-col rounded-lg border bg-card">
            <Tabs value={leftTab} onValueChange={setLeftTab} className="flex h-full flex-col">
              <div className="shrink-0 border-b bg-muted/30 px-5">
                <TabsList className="h-11 bg-transparent gap-1">
                  <TabsTrigger value="order" className="data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none rounded-none px-4 text-sm">
                    <ClipboardList className="mr-1.5 h-4 w-4" />
                    수주데이터
                  </TabsTrigger>
                  <TabsTrigger value="stock" className="data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none rounded-none px-4 text-sm">
                    <AlertTriangle className="mr-1.5 h-4 w-4" />
                    안전재고 부족분
                  </TabsTrigger>
                </TabsList>
              </div>

              {/* 수주데이터 탭 */}
              <TabsContent value="order" className="flex-1 overflow-hidden mt-0 flex flex-col">
                <div className="flex items-center justify-between border-b bg-muted/20 px-5 py-3 shrink-0">
                  <span className="text-base font-semibold">수주 목록</span>
                  <div className="flex items-center gap-2">
                    <label className="flex items-center gap-1.5 rounded-md border border-primary/20 bg-primary/5 px-3 py-1.5 text-xs cursor-pointer">
                      <Checkbox
                        checked={filterUnplannedOrdersOnly}
                        onCheckedChange={(c) => {
                          setFilterUnplannedOrdersOnly(!!c);
                          setTimeout(fetchOrderSummary, 0);
                        }}
                        className="h-4 w-4"
                      />
                      <span className="font-medium text-foreground">계획에 없는 품목만</span>
                    </label>
                    <Button size="sm" variant="outline" className="text-success border-success/30 hover:bg-success/10" onClick={() => setOrderImportModalOpen(true)}>
                      <Download className="mr-1 h-3.5 w-3.5" />
                      불러오기
                    </Button>
                    <Button size="sm" variant="outline" onClick={handleLeftExcelDownload}>
                      <Download className="mr-1 h-3.5 w-3.5" />
                      엑셀
                    </Button>
                    <Button size="sm" variant="outline" onClick={fetchOrderSummary} disabled={loadingOrders}>
                      {loadingOrders ? <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="mr-1 h-3.5 w-3.5" />}
                      새로고침
                    </Button>
                    <Button size="sm" variant="ghost" onClick={() => ts.setOpen(true)}>
                      <Settings2 className="h-3.5 w-3.5" />
                    </Button>
                  </div>
                </div>
                <div className="flex-1 overflow-auto">
                  {loadingOrders ? (
                    <div className="flex items-center justify-center py-16">
                      <Loader2 className="h-8 w-8 animate-spin text-primary" />
                    </div>
                  ) : orderItems.length === 0 ? (
                    <div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
                      <ClipboardList className="h-12 w-12 mb-3 opacity-50" />
                      <p className="text-base font-medium mb-2">수주 데이터가 없습니다</p>
                    </div>
                  ) : (
                    <div>
                      {(() => {
                        // 디테일 행에서 개별 값을 표시하는 컬럼 매핑
                        const DETAIL_VALUE_MAP: Record<string, string> = {
                          total_order_qty: "order_qty",
                          total_ship_qty: "ship_qty",
                          total_balance_qty: "balance_qty",
                        };

                        // 그룹 행에서 특수 렌더링이 필요한 컬럼
                        const renderGroupCell = (col: { key: string }, item: any) => {
                          if (col.key === "required_plan_qty") {
                            return (
                              <TableCell key={col.key} style={ts.thStyle(col.key)} className={cn("text-[13px] text-right font-bold", Number(item.required_plan_qty) > 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}>
                                {formatNumber(item.required_plan_qty)}
                              </TableCell>
                            );
                          }
                          if (col.key === "lead_time") {
                            return (
                              <TableCell key={col.key} style={ts.thStyle(col.key)} className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>
                                {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"}
                              </TableCell>
                            );
                          }
                          return (
                            <TableCell key={col.key} style={ts.thStyle(col.key)} className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>
                              {formatNumber(item[col.key])}
                            </TableCell>
                          );
                        };

                        return (
                      <Table style={{ minWidth: "900px" }}>
                        <TableHeader className="sticky top-0 z-20">
                          <TableRow className="bg-muted hover:bg-muted">
                            <TableHead className="sticky left-0 z-20 bg-muted" style={{ width: "30px", minWidth: "30px" }}>
                              <Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
                            </TableHead>
                            <TableHead className="sticky left-[30px] z-20 bg-muted" style={{ width: "40px", minWidth: "40px" }} />
                            <TableHead className="sticky left-[70px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground" style={{ width: "140px", minWidth: "140px" }}>품목코드</TableHead>
                            <TableHead className="sticky left-[210px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground border-r" style={{ width: "140px", minWidth: "140px" }}>품목명</TableHead>
                            {ts.visibleColumns.map((col) => (
                              <TableHead key={col.key} style={{ ...ts.thStyle(col.key), minWidth: "80px" }} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
                                {col.label}
                              </TableHead>
                            ))}
                          </TableRow>
                        </TableHeader>
                        <TableBody>
                          {ts.groupData(paginatedOrderItems).map((item, rowIdx) => {
                            if (item._isGroupSummary) {
                              return (
                                <TableRow key={`summary-${rowIdx}`} className="bg-muted/60 font-semibold border-t border-primary/20">
                                  <TableCell className="sticky left-0 z-10 bg-muted/60" />
                                  <TableCell className="sticky left-[30px] z-10 bg-muted/60" />
                                  <TableCell colSpan={2} className="sticky left-[70px] z-10 bg-muted/60 border-r" />
                                  {ts.visibleColumns.map((col) => {
                                    const v = (item as any)[col.key];
                                    return (
                                    <TableCell key={col.key} style={ts.thStyle(col.key)} className={typeof v === "number" ? "text-right font-mono text-[13px]" : "text-[13px] text-primary"}>
                                      {typeof v === "number" ? Number(v).toLocaleString() : (v || "")}
                                    </TableCell>
                                    );
                                  })}
                                </TableRow>
                              );
                            }
                            return (
                            <React.Fragment key={item.item_code || rowIdx}>
                              <TableRow className={cn("cursor-pointer border-t-2 border-t-primary/30 bg-primary/5 font-semibold hover:bg-primary/10", selectedItemGroups.has(item.item_code) && "bg-primary/10")}>
                                <TableCell className="sticky left-0 z-10 bg-background text-center" onClick={(e) => e.stopPropagation()}>
                                  <Checkbox checked={selectedItemGroups.has(item.item_code)} onCheckedChange={() => toggleItemGroupSelect(item.item_code)} className="h-4 w-4" />
                                </TableCell>
                                <TableCell className="sticky left-[30px] z-10 bg-background text-center" onClick={() => toggleItemExpand(item.item_code)}>
                                  <ChevronRight className={cn("h-4 w-4 transition-transform duration-200", expandedItems.has(item.item_code) && "rotate-90")} />
                                </TableCell>
                                <TableCell className="sticky left-[70px] z-10 bg-background text-[13px] text-primary truncate" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
                                <TableCell className="sticky left-[210px] z-10 bg-background text-[13px] text-primary truncate border-r" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
                                {ts.visibleColumns.map((col) => renderGroupCell(col, item))}
                              </TableRow>
                              {expandedItems.has(item.item_code) && item.orders?.map((detail: any) => {
                                let remainColSpan = 0;
                                for (const col of ts.visibleColumns) {
                                  if (!DETAIL_VALUE_MAP[col.key]) remainColSpan++;
                                }
                                return (
                                <TableRow key={detail.id || detail.order_no} className="cursor-pointer hover:bg-muted/50">
                                  <TableCell className="sticky left-0 z-10 bg-card" />
                                  <TableCell className="sticky left-[30px] z-10 bg-card" />
                                  <TableCell colSpan={2} className="sticky left-[70px] z-10 bg-card pl-10 border-r">
                                    <div className="flex items-center gap-2 text-xs">
                                      <span className="text-muted-foreground">수주번호:</span>
                                      <span className="font-medium">{detail.order_no}</span>
                                      <span className="text-muted-foreground">거래처:</span>
                                      <span>{detail.customer_name || "-"}</span>
                                      <Badge variant="secondary" className="text-[10px] px-2 py-0">
                                        {detail.status || "일반"}
                                      </Badge>
                                    </div>
                                  </TableCell>
                                  {ts.visibleColumns.map((col) => {
                                    const detailKey = DETAIL_VALUE_MAP[col.key];
                                    if (detailKey) {
                                      return <TableCell key={col.key} style={ts.thStyle(col.key)} className="text-[13px] text-right">{formatNumber(detail[detailKey])}</TableCell>;
                                    }
                                    return null;
                                  })}
                                  {remainColSpan > 0 && (
                                    <TableCell colSpan={remainColSpan} className="text-[13px] text-muted-foreground">
                                      납기일: {detail.due_date || "-"}
                                    </TableCell>
                                  )}
                                </TableRow>
                                );
                              })}
                            </React.Fragment>
                            );
                          })}
                        </TableBody>
                      </Table>
                        );
                      })()}
                    </div>
                  )}
                </div>
                {/* 수주목록 페이지네이션 */}
                {orderItems.length > 0 && (
                <div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
                      <div className="flex items-center gap-3">
                        <div className="flex items-center gap-1">
                          <span>전체</span>
                          <span className="font-medium text-foreground">{orderItems.length.toLocaleString()}</span>
                          <span>건</span>
                        </div>
                        <div className="flex items-center gap-1.5">
                          <Input type="number" min={1} value={orderPageSizeInput}
                            onChange={(e) => setOrderPageSizeInput(e.target.value)}
                            onBlur={applyOrderPageSize}
                            onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyOrderPageSize(); } }}
                            className="h-7 w-16 text-center text-xs" />
                          <span>건씩 보기</span>
                        </div>
                      </div>
                      <div className="flex items-center gap-0.5">
                        <button onClick={() => setOrderPage(1)} disabled={orderSafePage === 1}
                          className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
                          <ChevronsLeft className="h-3.5 w-3.5" />
                        </button>
                        <button onClick={() => setOrderPage((p) => Math.max(1, p - 1))} disabled={orderSafePage === 1}
                          className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
                          <ChevronLeft className="h-3.5 w-3.5" />
                        </button>
                        {getOrderPageNumbers().map((page, idx) =>
                          page === "..." ? (
                            <span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
                          ) : (
                            <button key={page} onClick={() => setOrderPage(page as number)}
                              className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
                                page === orderSafePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
                              {page}
                            </button>
                          )
                        )}
                        <button onClick={() => setOrderPage((p) => Math.min(orderTotalPages, p + 1))} disabled={orderSafePage === orderTotalPages}
                          className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
                          <ChevronRight className="h-3.5 w-3.5" />
                        </button>
                        <button onClick={() => setOrderPage(orderTotalPages)} disabled={orderSafePage === orderTotalPages}
                          className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
                          <ChevronsRight className="h-3.5 w-3.5" />
                        </button>
                      </div>
                      <span>{orderSafePage} / {orderTotalPages} 페이지</span>
                </div>
                )}
              </TabsContent>

              {/* 안전재고 부족분 탭 */}
              <TabsContent value="stock" className="flex-1 overflow-hidden mt-0 flex flex-col">
                <div className="flex items-center justify-between border-b bg-muted/20 px-5 py-3">
                  <span className="text-base font-semibold">안전재고 부족 품목</span>
                  <div className="flex items-center gap-2">
                    <Button size="sm" variant="outline" className="text-success border-success/30 hover:bg-success/10" onClick={() => setStockImportModalOpen(true)}>
                      <Download className="mr-1 h-3.5 w-3.5" />
                      불러오기
                    </Button>
                    <Button size="sm" variant="outline" onClick={handleLeftExcelDownload}>
                      <Download className="mr-1 h-3.5 w-3.5" />
                      엑셀
                    </Button>
                    <Button size="sm" variant="outline" onClick={fetchStockShortage} disabled={loadingStock}>
                      {loadingStock ? <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="mr-1 h-3.5 w-3.5" />}
                      새로고침
                    </Button>
                  </div>
                </div>
                <div className="flex-1 overflow-auto p-4">
                  {loadingStock ? (
                    <div className="flex items-center justify-center py-16">
                      <Loader2 className="h-8 w-8 animate-spin text-primary" />
                    </div>
                  ) : stockItems.length === 0 ? (
                    <div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
                      <AlertTriangle className="h-12 w-12 mb-3 opacity-50" />
                      <p className="text-base font-medium mb-2">안전재고 부족 품목이 없습니다</p>
                    </div>
                  ) : (
                    <div className="overflow-x-auto rounded-md border">
                      <Table>
                        <TableHeader className="sticky top-0 z-10">
                          <TableRow className="bg-muted hover:bg-muted">
                            <TableHead className="w-[30px]">
                              <Checkbox checked={selectedStockItems.size === stockItems.length && stockItems.length > 0} onCheckedChange={(c) => toggleAllStockItems(!!c)} className="h-4 w-4" />
                            </TableHead>
                            <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
                            <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
                            <TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">현재고</TableHead>
                            <TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">안전재고</TableHead>
                            <TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">부족수량</TableHead>
                            <TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">권장생산량</TableHead>
                            <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">최종입고일</TableHead>
                          </TableRow>
                        </TableHeader>
                        <TableBody>
                          {stockItems.map((stock) => (
                            <TableRow key={stock.item_code} className="cursor-pointer hover:bg-muted/50">
                              <TableCell>
                                <Checkbox checked={selectedStockItems.has(stock.item_code)} onCheckedChange={() => toggleStockItem(stock.item_code)} className="h-4 w-4" />
                              </TableCell>
                              <TableCell className="text-[13px]">{stock.item_code}</TableCell>
                              <TableCell className="text-[13px]">{stock.item_name}</TableCell>
                              <TableCell className="text-[13px] text-right">{formatNumber(stock.current_qty)}</TableCell>
                              <TableCell className="text-[13px] text-right">{formatNumber(stock.safety_qty)}</TableCell>
                              <TableCell className="text-[13px] text-right font-semibold text-destructive">{formatNumber(stock.shortage_qty)}</TableCell>
                              <TableCell className="text-[13px] text-right font-semibold text-success">{formatNumber(stock.recommended_qty)}</TableCell>
                              <TableCell className="text-[13px]">{stock.last_in_date || "-"}</TableCell>
                            </TableRow>
                          ))}
                        </TableBody>
                      </Table>
                    </div>
                  )}
                </div>
              </TabsContent>
            </Tabs>
          </div>
        </ResizablePanel>

        <ResizableHandle withHandle />

        {/* 오른쪽 패널 — 타임라인 */}
        <ResizablePanel defaultSize={55} minSize={30}>
          <div className="flex h-full flex-col rounded-lg border bg-card">
            <Tabs value={rightTab} onValueChange={setRightTab} className="flex h-full flex-col">
              <div className="shrink-0 border-b bg-muted/30 px-5">
                <TabsList className="h-11 bg-transparent gap-1">
                  <TabsTrigger value="finished" className="data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none rounded-none px-4 text-sm">
                    <Package className="mr-1.5 h-4 w-4" />
                    완제품 생산계획
                  </TabsTrigger>
                  <TabsTrigger value="semi" className="data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none rounded-none px-4 text-sm">
                    <Wrench className="mr-1.5 h-4 w-4" />
                    반제품 생산계획
                  </TabsTrigger>
                </TabsList>
              </div>

              {/* 완제품 생산계획 */}
              <TabsContent value="finished" className="flex-1 overflow-hidden mt-0 flex flex-col">
                <div className="flex items-center justify-between border-b bg-muted/20 px-5 py-3">
                  <span className="text-base font-semibold flex items-center gap-1.5">
                    <Calendar className="h-4 w-4" />
                    완제품 생산 타임라인
                    <Badge variant="outline" className="ml-2">{finishedPlans.length}건</Badge>
                  </span>
                  <div className="flex items-center gap-2">
                    <Button size="sm" variant="outline" onClick={fetchPlans} disabled={loadingPlans}>
                      {loadingPlans ? <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="mr-1 h-3.5 w-3.5" />}
                      새로고침
                    </Button>
                    {selectedPlanIds.size >= 2 && (
                      <Button size="sm" variant="outline" onClick={handleMergeSchedules}>
                        <Merge className="mr-1 h-3.5 w-3.5" />
                        병합 ({selectedPlanIds.size})
                      </Button>
                    )}
                    <Button size="sm" onClick={handleGenerateSchedule} disabled={generating || selectedItemGroups.size === 0}>
                      {generating ? <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" /> : <Zap className="mr-1 h-3.5 w-3.5" />}
                      자동 스케줄 생성
                    </Button>
                    <Button size="sm" variant="ghost" className="text-destructive" onClick={handleClearTimeline}>
                      <Trash2 className="mr-1 h-3.5 w-3.5" />
                      초기화
                    </Button>
                  </div>
                </div>
                <div className="flex-1 overflow-auto p-4">
                  {/* 옵션 */}
                  <div className="rounded-lg border bg-muted/30 p-3 mb-4">
                    <div className="flex items-center gap-4 flex-wrap">
                      <div className="flex items-center gap-1.5">
                        <Checkbox checked={recalculateUnstarted} onCheckedChange={(c) => setRecalculateUnstarted(!!c)} className="h-4 w-4" />
                        <Label className="text-xs cursor-pointer">미진행 계획 재계산</Label>
                      </div>
                    </div>
                  </div>

                  {/* 타임라인 */}
                  <TimelineScheduler
                    resources={finishedResources}
                    events={finishedEvents}
                    zoomLevel={finishedZoom}
                    onZoomChange={setFinishedZoom}
                    statusColors={STATUS_COLORS}
                    showProgress
                    showMilestones
                    showTodayLine
                    showLegend
                    conflictDetection
                    loading={loadingPlans}
                    emptyMessage="생산 스케줄이 없습니다"
                    emptyIcon={<Package className="h-12 w-12 mb-3 opacity-50" />}
                    onEventClick={openScheduleDetail}
                    onEventMove={handleEventMove}
                    onEventResize={handleEventResize}
                    onRangeChange={(s, e) => { setSearchStartDate(s); setSearchEndDate(e); }}
                  />
                </div>
              </TabsContent>

              {/* 반제품 생산계획 */}
              <TabsContent value="semi" className="flex-1 overflow-hidden mt-0 flex flex-col">
                <div className="flex items-center justify-between border-b bg-muted/20 px-5 py-3">
                  <span className="text-base font-semibold flex items-center gap-1.5">
                    <Wrench className="h-4 w-4" />
                    반제품 생산 타임라인
                    <Badge variant="outline" className="ml-2">{semiPlans.length}건</Badge>
                  </span>
                  <div className="flex items-center gap-2">
                    <Button size="sm" variant="outline" onClick={fetchPlans} disabled={loadingPlans}>
                      {loadingPlans ? <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="mr-1 h-3.5 w-3.5" />}
                      새로고침
                    </Button>
                    <Button size="sm" onClick={handleGenerateSemiSchedule} disabled={generating}>
                      {generating ? <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" /> : <Zap className="mr-1 h-3.5 w-3.5" />}
                      반제품 계획 자동 생성
                    </Button>
                  </div>
                </div>
                <div className="flex-1 overflow-auto p-4">
                  <div className="flex gap-3 mb-4">
                    <div className="flex-1 rounded-lg border border-primary/20 bg-primary/5 p-4">
                      <p className="font-semibold text-foreground text-sm mb-3">반제품 계획 생성 옵션</p>
                      <div className="flex flex-col gap-2">
                        <label className="flex items-center gap-1.5 cursor-pointer text-xs">
                          <Checkbox checked={semiConsiderStock} onCheckedChange={(c) => setSemiConsiderStock(!!c)} className="h-4 w-4" />
                          <span className="font-medium">현재고 및 안전재고 감안</span>
                        </label>
                        <label className="flex items-center gap-1.5 cursor-pointer text-xs">
                          <Checkbox checked={semiRecalculate} onCheckedChange={(c) => setSemiRecalculate(!!c)} className="h-4 w-4" />
                          <span className="font-medium">진행중인 계획 유지하고 재계산</span>
                        </label>
                        <label className="flex items-center gap-1.5 cursor-pointer text-xs">
                          <Checkbox checked={semiExcludeUsed} onCheckedChange={(c) => setSemiExcludeUsed(!!c)} className="h-4 w-4" />
                          <span className="font-medium">투입 완료된 반제품 제외</span>
                        </label>
                      </div>
                    </div>
                    <div className="flex-1 rounded-lg border border-warning/20 bg-warning/5 p-4">
                      <p className="font-semibold text-foreground text-sm mb-2">반제품 계획 안내</p>
                      <ul className="text-xs text-foreground space-y-1 leading-relaxed">
                        <li>완제품 생산계획 기준으로 필요한 반제품 계획 자동 생성</li>
                        <li>모품목 생산 시작일 고려하여 납기일 설정</li>
                        <li>BOM(자재명세서) 정보 기반 필요 수량 계산</li>
                      </ul>
                    </div>
                  </div>

                  {/* 반제품 타임라인 */}
                  <TimelineScheduler
                    resources={semiResources}
                    events={semiEvents}
                    zoomLevel={semiZoom}
                    onZoomChange={setSemiZoom}
                    statusColors={STATUS_COLORS}
                    showProgress
                    showTodayLine
                    showLegend
                    conflictDetection
                    loading={loadingPlans}
                    emptyMessage="반제품 생산 스케줄이 없습니다"
                    emptyIcon={<Wrench className="h-12 w-12 mb-3 opacity-50" />}
                    onEventClick={openScheduleDetail}
                    onEventMove={handleEventMove}
                    onEventResize={handleEventResize}
                    onRangeChange={(s, e) => { setSearchStartDate(s); setSearchEndDate(e); }}
                  />
                </div>
              </TabsContent>
            </Tabs>
          </div>
        </ResizablePanel>
      </ResizablePanelGroup>

      {/* ========== 모달들 ========== */}

      {/* 스케줄 상세/편집 모달 */}
      <Dialog
        open={scheduleModalOpen}
        onOpenChange={(v) => {
          // confirm 다이얼로그가 열려 있는 동안 발생하는 닫힘 이벤트(포커스 이탈 등)는 무시
          if (!v && isConfirmOpenRef.current) return;
          setScheduleModalOpen(v);
        }}
      >
        <DialogContent
          className="max-w-[95vw] sm:max-w-[650px] max-h-[85vh] overflow-y-auto"
          onPointerDownOutside={(e) => {
            if (isConfirmOpenRef.current) e.preventDefault();
          }}
          onInteractOutside={(e) => {
            if (isConfirmOpenRef.current) e.preventDefault();
          }}
          onFocusOutside={(e) => {
            if (isConfirmOpenRef.current) e.preventDefault();
          }}
        >
          <DialogHeader>
            <DialogTitle className="text-base sm:text-lg flex items-center gap-2">
              <ClipboardList className="h-5 w-5" />
              생산 스케줄 상세
            </DialogTitle>
            <DialogDescription className="text-xs sm:text-sm">
              생산 스케줄의 상세 정보를 확인하고 수정할 수 있습니다
            </DialogDescription>
          </DialogHeader>

          {selectedPlan && (
            <div className="space-y-4">
              <div>
                <p className="text-sm font-semibold mb-3 pb-2 border-b">기본 정보</p>
                <div className="grid grid-cols-2 gap-3">
                  <div>
                    <Label className="text-xs">품목코드</Label>
                    <Input value={selectedPlan.item_code} readOnly className="h-9 text-xs bg-muted/50" />
                  </div>
                  <div>
                    <Label className="text-xs">품목명</Label>
                    <Input value={selectedPlan.item_name || ""} readOnly className="h-9 text-xs bg-muted/50" />
                  </div>
                  <div>
                    <Label className="text-xs">계획번호</Label>
                    <Input value={selectedPlan.plan_no || ""} readOnly className="h-9 text-xs bg-muted/50" />
                  </div>
                  <div>
                    <Label className="text-xs">상태</Label>
                    <Badge variant="secondary" className="mt-1">
                      {STATUS_LABEL[selectedPlan.status] || selectedPlan.status}
                    </Badge>
                  </div>
                </div>
              </div>

              <div>
                <p className="text-sm font-semibold mb-3 pb-2 border-b">생산 정보</p>
                <div className="grid grid-cols-2 gap-3">
                  <div>
                    <Label className="text-xs">총 생산수량</Label>
                    <Input type="number" value={modalQuantity} onChange={(e) => setModalQuantity(Number(e.target.value))} className="h-9 text-xs" min={0} />
                  </div>
                  <div>
                    <Label className="text-xs">설비</Label>
                    <Select value={modalEquipmentId} onValueChange={setModalEquipmentId}>
                      <SelectTrigger className="h-9 text-xs">
                        <SelectValue placeholder="설비 선택" />
                      </SelectTrigger>
                      <SelectContent>
                        <SelectItem value="none">미지정</SelectItem>
                        {equipmentList.map((eq) => (
                          <SelectItem key={eq.id} value={eq.equipment_code || eq.id}>
                            {eq.equipment_name} ({eq.equipment_code})
                          </SelectItem>
                        ))}
                      </SelectContent>
                    </Select>
                  </div>
                </div>
                <div className="mt-3 rounded-lg border border-success/20 bg-success/5 p-3">
                  <p className="text-xs font-semibold text-foreground mb-2">계획 기간 설정</p>
                  <div className="grid grid-cols-3 gap-3">
                    <div>
                      <Label className="text-[11px] text-muted-foreground">시작일</Label>
                      <Input type="date" value={modalStartDate} onChange={(e) => setModalStartDate(e.target.value)} className="h-9 text-xs" />
                    </div>
                    <div>
                      <Label className="text-[11px] text-muted-foreground">종료일</Label>
                      <Input type="date" value={modalEndDate} onChange={(e) => setModalEndDate(e.target.value)} className="h-9 text-xs" />
                    </div>
                    <div>
                      <Label className="text-[11px] text-muted-foreground">생산 기간</Label>
                      <div className="flex h-9 items-center justify-center rounded-md border bg-muted/50 text-xs font-semibold text-primary">
                        {modalStartDate && modalEndDate
                          ? `${Math.ceil((new Date(modalEndDate).getTime() - new Date(modalStartDate).getTime()) / (1000 * 60 * 60 * 24) + 1)}일`
                          : "-"}
                      </div>
                    </div>
                  </div>
                </div>
              </div>

              <div className="rounded-lg border border-warning/20 bg-warning/5 p-4">
                <div className="flex items-center justify-between mb-2">
                  <p className="text-sm font-semibold flex items-center gap-1.5">
                    <Scissors className="h-4 w-4" />
                    계획 분할
                  </p>
                  <div className="flex gap-1.5">
                    {[2, 3, 4].map((n) => {
                      const canSplit =
                        modalQuantity >= n &&
                        (selectedPlan?.status === "planned" || !selectedPlan?.status);
                      return (
                        <Button
                          key={n}
                          size="sm"
                          variant="warning"
                          className="h-7 text-xs"
                          disabled={!canSplit}
                          onClick={() => handleSplitSchedule(n)}
                        >
                          {n}분할
                        </Button>
                      );
                    })}
                  </div>
                </div>
                <p className="text-xs text-foreground mb-2">
                  하나의 생산계획을 선택한 개수만큼 균등 분할합니다. (수량 부족 또는 완료 상태는 불가)
                </p>
                {/* 수량 지정 분할 */}
                <div className="flex items-center gap-1.5 pt-2 border-t border-warning/20">
                  <Label className="text-xs text-muted-foreground shrink-0">수량 지정:</Label>
                  <Input
                    type="number"
                    value={customSplitQty}
                    onChange={(e) => {
                      const v = e.target.value;
                      if (v === "") setCustomSplitQty("");
                      else setCustomSplitQty(Math.max(0, Math.floor(Number(v) || 0)));
                    }}
                    className="h-7 w-28 text-xs"
                    placeholder="떼어낼 수량"
                    min={1}
                    max={Math.max(0, modalQuantity - 1)}
                    step={1}
                  />
                  <span className="text-xs text-muted-foreground">
                    / {modalQuantity}
                  </span>
                  <Button
                    size="sm"
                    variant="warning"
                    className="h-7 text-xs ml-auto"
                    disabled={
                      !customSplitQty ||
                      Number(customSplitQty) < 1 ||
                      Number(customSplitQty) >= modalQuantity ||
                      !(selectedPlan?.status === "planned" || !selectedPlan?.status)
                    }
                    onClick={handleCustomSplit}
                  >
                    분할
                  </Button>
                </div>
                <p className="text-[11px] text-muted-foreground mt-1.5">
                  입력한 수량만큼 떼어내 새 계획을 생성합니다. (1 이상, 원본 수량 미만)
                </p>
              </div>
            </div>
          )}

          <DialogFooter className="gap-2 sm:gap-0">
            <Button variant="outline" onClick={handleDeletePlan} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm text-destructive border-destructive/20 bg-destructive/5 hover:bg-destructive/10">
              <Trash2 className="mr-1 h-3.5 w-3.5" />
              삭제
            </Button>
            <Button variant="outline" onClick={() => setScheduleModalOpen(false)} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
              취소
            </Button>
            <Button onClick={handleSavePlan} disabled={saving} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
              {saving ? <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" /> : <Save className="mr-1 h-3.5 w-3.5" />}
              저장
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      {/* 수주 불러오기 모달 */}
      <Dialog open={orderImportModalOpen} onOpenChange={setOrderImportModalOpen}>
        <DialogContent className="max-w-[95vw] sm:max-w-[600px] max-h-[85vh] overflow-y-auto">
          <DialogHeader>
            <DialogTitle className="text-base sm:text-lg">신규 수주 품목 불러오기</DialogTitle>
            <DialogDescription className="text-xs sm:text-sm">선택한 수주 품목을 생산계획에 추가합니다</DialogDescription>
          </DialogHeader>
          <div className="space-y-4">
            <div>
              <p className="font-semibold text-sm mb-2 flex items-center gap-1.5">
                <ClipboardList className="h-4 w-4" />
                선택된 품목
              </p>
              <div className="space-y-2 max-h-[200px] overflow-y-auto">
                {selectedItemGroups.size > 0
                  ? Array.from(selectedItemGroups).map((code) => {
                      const item = orderItems.find((i) => i.item_code === code);
                      if (!item) return null;
                      const hasExistingPlan = Number(item.existing_plan_qty) > 0;
                      return (
                        <div key={code} className="rounded-lg border bg-muted/30 p-3">
                          <div className="flex items-center justify-between mb-1">
                            <span className="text-sm font-semibold">{item.item_code} - {item.item_name}</span>
                            {hasExistingPlan && <Badge variant="secondary" className="text-[10px]">가계획완료</Badge>}
                          </div>
                          <div className="flex items-center gap-3 text-xs text-muted-foreground">
                            <span>수주: {item.order_count}건</span>
                            <span className="text-border">|</span>
                            <span>잔량: {formatNumber(item.total_balance_qty)} EA</span>
                            <span className="text-border">|</span>
                            <span>재고: {formatNumber(item.current_stock)} EA</span>
                            <span className="text-border">|</span>
                            <span>필요수량: <span className={Number(item.required_plan_qty) > 0 ? "text-destructive font-semibold" : ""}>{formatNumber(item.required_plan_qty)} EA</span></span>
                          </div>
                        </div>
                      );
                    })
                  : <p className="text-xs text-muted-foreground p-3 text-center">선택된 품목이 없습니다.</p>}
              </div>
            </div>

            <div className="rounded-lg border border-warning/20 bg-warning/5 p-3">
              <p className="font-semibold text-sm mb-2 flex items-center gap-1.5">
                <AlertTriangle className="h-4 w-4 text-warning" />
                주의사항
              </p>
              <ul className="text-xs text-muted-foreground space-y-1 list-disc list-inside leading-relaxed">
                <li>기존 계획에 추가 시 새로운 수주의 납기를 기준으로 종료일이 조정됩니다.</li>
                <li>기존 계획이 없으면 새 계획이 자동 생성됩니다.</li>
                <li>수주정보(수주번호)가 있으면 관련 정보가 연결됩니다.</li>
                <li>수량이 0이거나 음수인 품목은 건너뜁니다.</li>
              </ul>
            </div>

            <div>
              <p className="font-semibold text-sm mb-2">계획 추가 방식을 선택하세요</p>
              <div className="grid gap-3">
                <label className={cn("flex items-center rounded-lg border-2 p-4 cursor-pointer transition-all", importMode === "add" ? "border-primary bg-primary/5" : "border-border hover:border-primary/50")} onClick={() => setImportMode("add")}>
                  <input type="radio" name="importMode" value="add" checked={importMode === "add"} onChange={() => setImportMode("add")} className="mr-3 h-4 w-4" />
                  <div>
                    <p className="font-semibold text-sm">+ 기존 계획에 추가</p>
                    <p className="text-xs text-muted-foreground">같은 품목의 기존 계획에 수주 건만을 추가하거나 수량을 합산합니다.</p>
                  </div>
                </label>
                <label className={cn("flex items-center rounded-lg border-2 p-4 cursor-pointer transition-all", importMode === "new" ? "border-primary bg-primary/5" : "border-border hover:border-primary/50")} onClick={() => setImportMode("new")}>
                  <input type="radio" name="importMode" value="new" checked={importMode === "new"} onChange={() => setImportMode("new")} className="mr-3 h-4 w-4" />
                  <div>
                    <p className="font-semibold text-sm">별도 계획으로 생성</p>
                    <p className="text-xs text-muted-foreground">이전 수량/일정 무시하고 생산계획을 생성합니다.</p>
                  </div>
                </label>
              </div>
            </div>
          </div>
          <DialogFooter className="gap-2 sm:gap-0">
            <Button variant="outline" onClick={() => setOrderImportModalOpen(false)} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">취소</Button>
            <Button onClick={handleImportOrderItems} disabled={generating || selectedItemGroups.size === 0} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
              {generating ? <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" /> : <Download className="mr-1 h-3.5 w-3.5" />}
              불러오기
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      {/* 안전재고 불러오기 모달 */}
      <Dialog open={stockImportModalOpen} onOpenChange={setStockImportModalOpen}>
        <DialogContent className="max-w-[95vw] sm:max-w-[600px] max-h-[85vh] overflow-y-auto">
          <DialogHeader>
            <DialogTitle className="text-base sm:text-lg">안전재고 부족 품목 불러오기</DialogTitle>
            <DialogDescription className="text-xs sm:text-sm">선택한 안전재고 부족 품목을 생산계획에 추가합니다</DialogDescription>
          </DialogHeader>
          <div className="space-y-4">
            <div>
              <p className="font-semibold text-sm mb-2 flex items-center gap-1.5">
                <AlertTriangle className="h-4 w-4" />
                선택된 품목
              </p>
              <div className="space-y-2 max-h-[200px] overflow-y-auto">
                {selectedStockItems.size > 0
                  ? Array.from(selectedStockItems).map((code) => {
                      const stock = stockItems.find((s) => s.item_code === code);
                      if (!stock) return null;
                      return (
                        <div key={code} className="rounded-lg border bg-muted/30 p-3">
                          <div className="flex items-center justify-between mb-1">
                            <span className="text-sm font-semibold">{stock.item_code} - {stock.item_name}</span>
                            <Badge variant="destructive" className="text-[10px]">재고부족</Badge>
                          </div>
                          <div className="flex items-center gap-3 text-xs text-muted-foreground">
                            <span>현재고: {formatNumber(stock.current_qty)} EA</span>
                            <span className="text-border">|</span>
                            <span>안전재고: {formatNumber(stock.safety_qty)} EA</span>
                            <span className="text-border">|</span>
                            <span>권장생산: <span className="text-destructive font-semibold">{formatNumber(stock.recommended_qty)} EA</span></span>
                          </div>
                        </div>
                      );
                    })
                  : <p className="text-xs text-muted-foreground p-3 text-center">선택된 품목이 없습니다.</p>}
              </div>
            </div>

            <div className="rounded-lg border border-warning/20 bg-warning/5 p-3">
              <p className="font-semibold text-sm mb-2 flex items-center gap-1.5">
                <AlertTriangle className="h-4 w-4 text-warning" />
                주의사항
              </p>
              <ul className="text-xs text-muted-foreground space-y-1 list-disc list-inside leading-relaxed">
                <li>안전재고 기준으로 부족분만큼 생산계획을 생성합니다.</li>
                <li>현재 재고와 안전재고 차이를 기반으로 수량이 산정됩니다.</li>
                <li>기존 계획이 있는 품목은 추가 방식에 따라 합산 또는 별도 생성됩니다.</li>
              </ul>
            </div>

            <div>
              <p className="font-semibold text-sm mb-2">계획 추가 방식을 선택하세요</p>
              <div className="grid gap-3">
                <label className={cn("flex items-center rounded-lg border-2 p-4 cursor-pointer transition-all", importMode === "add" ? "border-primary bg-primary/5" : "border-border hover:border-primary/50")} onClick={() => setImportMode("add")}>
                  <input type="radio" name="stockImportMode" value="add" checked={importMode === "add"} onChange={() => setImportMode("add")} className="mr-3 h-4 w-4" />
                  <div>
                    <p className="font-semibold text-sm">+ 기존 계획에 추가</p>
                    <p className="text-xs text-muted-foreground">같은 품목의 기존 계획에 수량을 합산합니다.</p>
                  </div>
                </label>
                <label className={cn("flex items-center rounded-lg border-2 p-4 cursor-pointer transition-all", importMode === "new" ? "border-primary bg-primary/5" : "border-border hover:border-primary/50")} onClick={() => setImportMode("new")}>
                  <input type="radio" name="stockImportMode" value="new" checked={importMode === "new"} onChange={() => setImportMode("new")} className="mr-3 h-4 w-4" />
                  <div>
                    <p className="font-semibold text-sm">별도 계획으로 생성</p>
                    <p className="text-xs text-muted-foreground">이전 수량/일정 무시하고 생산계획을 생성합니다.</p>
                  </div>
                </label>
              </div>
            </div>
          </div>
          <DialogFooter className="gap-2 sm:gap-0">
            <Button variant="outline" onClick={() => setStockImportModalOpen(false)} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">취소</Button>
            <Button onClick={handleImportStockItems} disabled={generating || selectedStockItems.size === 0} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
              {generating ? <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" /> : <Download className="mr-1 h-3.5 w-3.5" />}
              불러오기
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      {/* 변경사항 확인(미리보기) 모달 */}
      <Dialog open={changeConfirmModalOpen} onOpenChange={setChangeConfirmModalOpen}>
        <DialogContent className="max-w-[95vw] sm:max-w-[800px] max-h-[85vh] overflow-y-auto">
          <DialogHeader>
            <DialogTitle className="text-base sm:text-lg">생산계획 변경사항 확인</DialogTitle>
            <DialogDescription className="text-xs sm:text-sm">아래 변경사항을 검토하신 후 확인 버튼을 클릭하면 생산계획이 업데이트됩니다</DialogDescription>
          </DialogHeader>

          {previewData && (
            <div className="space-y-4">
              <div className="grid grid-cols-4 gap-3">
                <div className="rounded-lg border bg-primary/10 p-3 text-center">
                  <p className="text-2xl font-bold text-foreground">{previewData.summary?.total ?? 0}</p>
                  <p className="text-xs text-muted-foreground mt-1">총 계획</p>
                </div>
                <div className="rounded-lg border bg-success/10 p-3 text-center">
                  <p className="text-2xl font-bold text-foreground">{previewData.summary?.new_count ?? 0}</p>
                  <p className="text-xs text-muted-foreground mt-1">신규 생성</p>
                </div>
                <div className="rounded-lg border bg-warning/10 p-3 text-center">
                  <p className="text-2xl font-bold text-foreground">{previewData.summary?.kept_count ?? 0}</p>
                  <p className="text-xs text-muted-foreground mt-1">유지됨</p>
                </div>
                <div className="rounded-lg border bg-destructive/10 p-3 text-center">
                  <p className="text-2xl font-bold text-foreground">{previewData.summary?.deleted_count ?? 0}</p>
                  <p className="text-xs text-muted-foreground mt-1">삭제됨</p>
                </div>
              </div>

              {(previewData.schedules?.length || 0) > 0 && (
                <div>
                  <p className="text-sm font-semibold mb-2 text-success flex items-center gap-1.5">
                    <Zap className="h-4 w-4" />
                    신규 생성 ({previewData.schedules?.length || 0}건)
                  </p>
                  <div className="rounded-md border border-success/20 overflow-x-auto">
                    <Table>
                      <TableHeader className="sticky top-0 z-10">
                        <TableRow className="bg-muted hover:bg-muted">
                          <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
                          <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
                          <TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>
                          <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">시작일</TableHead>
                          <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">종료일</TableHead>
                        </TableRow>
                      </TableHeader>
                      <TableBody>
                        {(previewData.schedules || []).map((s: any, idx: number) => (
                          <TableRow key={`new-${idx}`}>
                            <TableCell className="text-[13px]">{s.item_code}</TableCell>
                            <TableCell className="text-[13px]">{s.item_name}</TableCell>
                            <TableCell className="text-[13px] text-right">{formatNumber(s.plan_qty || s.required_qty)}</TableCell>
                            <TableCell className="text-[13px]">{s.start_date?.split("T")[0]}</TableCell>
                            <TableCell className="text-[13px]">{s.end_date?.split("T")[0]}</TableCell>
                          </TableRow>
                        ))}
                      </TableBody>
                    </Table>
                  </div>
                </div>
              )}

              {(previewData.deletedSchedules?.length || 0) > 0 && (
                <div>
                  <p className="text-sm font-semibold mb-2 text-destructive flex items-center gap-1.5">
                    <Trash2 className="h-4 w-4" />
                    삭제 예정 ({previewData.deletedSchedules?.length || 0}건)
                  </p>
                  <div className="rounded-md border border-destructive/20 overflow-x-auto">
                    <Table>
                      <TableHeader className="sticky top-0 z-10">
                        <TableRow className="bg-muted hover:bg-muted">
                          <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">계획번호</TableHead>
                          <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
                          <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
                          <TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>
                          <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">시작일</TableHead>
                          <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">종료일</TableHead>
                        </TableRow>
                      </TableHeader>
                      <TableBody>
                        {(previewData.deletedSchedules || []).map((s: any, idx: number) => (
                          <TableRow key={`del-${idx}`} className="text-destructive/80">
                            <TableCell className="text-[13px]">{s.plan_no || "-"}</TableCell>
                            <TableCell className="text-[13px]">{s.item_code}</TableCell>
                            <TableCell className="text-[13px]">{s.item_name}</TableCell>
                            <TableCell className="text-[13px] text-right">{formatNumber(s.plan_qty)}</TableCell>
                            <TableCell className="text-[13px]">{s.start_date?.split("T")[0]}</TableCell>
                            <TableCell className="text-[13px]">{s.end_date?.split("T")[0]}</TableCell>
                          </TableRow>
                        ))}
                      </TableBody>
                    </Table>
                  </div>
                </div>
              )}

              {(previewData.keptSchedules?.length || 0) > 0 && (
                <div>
                  <p className="text-sm font-semibold mb-2 text-warning flex items-center gap-1.5">
                    <Package className="h-4 w-4" />
                    유지됨 ({previewData.keptSchedules?.length || 0}건)
                  </p>
                  <div className="rounded-md border border-warning/20 overflow-x-auto">
                    <Table>
                      <TableHeader className="sticky top-0 z-10">
                        <TableRow className="bg-muted hover:bg-muted">
                          <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">계획번호</TableHead>
                          <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
                          <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
                          <TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>
                          <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>
                        </TableRow>
                      </TableHeader>
                      <TableBody>
                        {(previewData.keptSchedules || []).map((s: any, idx: number) => (
                          <TableRow key={`kept-${idx}`}>
                            <TableCell className="text-[13px]">{s.plan_no || "-"}</TableCell>
                            <TableCell className="text-[13px]">{s.item_code}</TableCell>
                            <TableCell className="text-[13px]">{s.item_name}</TableCell>
                            <TableCell className="text-[13px] text-right">{formatNumber(s.plan_qty)}</TableCell>
                            <TableCell className="text-[13px]">
                              <Badge variant="outline" className="text-[10px]">{STATUS_LABEL[s.status] || s.status}</Badge>
                            </TableCell>
                          </TableRow>
                        ))}
                      </TableBody>
                    </Table>
                  </div>
                </div>
              )}
            </div>
          )}

          <DialogFooter className="gap-2 sm:gap-0">
            <Button variant="outline" onClick={() => setChangeConfirmModalOpen(false)} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">취소</Button>
            <Button onClick={handleApplySchedule} disabled={generating} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
              {generating ? <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" /> : null}
              확인 및 적용
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      {/* 엑셀 업로드 모달 */}
      <ExcelUploadModal
        open={excelUploadOpen}
        onOpenChange={setExcelUploadOpen}
        tableName="production_plan_mng"
        onSuccess={() => {
          fetchPlans();
          fetchOrderSummary();
        }}
      />

      {/* 테이블 설정 모달 */}
      <TableSettingsModal
        open={ts.open}
        onOpenChange={ts.setOpen}
        tableName={ts.tableName}
        settingsId={ts.settingsId}
        defaultVisibleKeys={ts.defaultVisibleKeys}
        onSave={ts.applySettings}
      />

      {/* ConfirmDialog 렌더 */}
      {ConfirmDialogComponent}
    </div>
  );
}
