"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { GitBranch, Loader2, PackagePlus, Pencil, Plus, Save, Search, Star, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { createRoutingVersion, getProcessList, getRegisteredItems, getRoutingDetails, getRoutingVersions, registerItemsBatch, saveRoutingDetails, searchAllItems, unregisterItem, type ItemForRouting, type ProcessMaster, type RegisteredItem, type RoutingDetail, type RoutingVersion, } from "@/lib/api/processInfo"; import { cn } from "@/lib/utils"; function normalizeDefaultFlag(v: RoutingVersion): boolean { const raw = v.is_default as unknown; if (typeof raw === "boolean") return raw; if (raw === "t" || raw === true || raw === "Y" || raw === "true") return true; return false; } function sortDetailsBySeq(rows: RoutingDetail[]): RoutingDetail[] { return [...rows].sort((a, b) => { const na = parseInt(String(a.seq_no), 10) || 0; const nb = parseInt(String(b.seq_no), 10) || 0; return na - nb; }); } export function ItemRoutingTab() { const [searchInput, setSearchInput] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); const [items, setItems] = useState([]); const [itemsLoading, setItemsLoading] = useState(false); const [selectedItem, setSelectedItem] = useState(null); const [versions, setVersions] = useState([]); const [versionsLoading, setVersionsLoading] = useState(false); const [selectedVersionId, setSelectedVersionId] = useState(null); const [details, setDetails] = useState([]); const [detailsLoading, setDetailsLoading] = useState(false); const [saving, setSaving] = useState(false); const [selectedDetailIds, setSelectedDetailIds] = useState>(new Set()); const [processes, setProcesses] = useState([]); const [processesLoading, setProcessesLoading] = useState(false); const [versionDialogOpen, setVersionDialogOpen] = useState(false); const [versionName, setVersionName] = useState(""); const [versionDescription, setVersionDescription] = useState(""); const [versionIsDefault, setVersionIsDefault] = useState(false); const [versionSubmitting, setVersionSubmitting] = useState(false); const [detailDialogOpen, setDetailDialogOpen] = useState(false); const [detailDialogMode, setDetailDialogMode] = useState<"add" | "edit">("add"); const [editingDetailId, setEditingDetailId] = useState(null); const [formProcessCode, setFormProcessCode] = useState(""); const [formSeqNo, setFormSeqNo] = useState(""); const [formRequired, setFormRequired] = useState("Y"); const [formFixedOrder, setFormFixedOrder] = useState("Y"); const [formWorkType, setFormWorkType] = useState("내부"); const [formStandardTime, setFormStandardTime] = useState(""); const [formOutsource, setFormOutsource] = useState(""); const [detailSubmitting, setDetailSubmitting] = useState(false); // 품목 등록 모달 state const [registerDialogOpen, setRegisterDialogOpen] = useState(false); const [registerSearch, setRegisterSearch] = useState(""); const [registerSearchDebounced, setRegisterSearchDebounced] = useState(""); const [registerItems, setRegisterItems] = useState([]); const [registerLoading, setRegisterLoading] = useState(false); const [registerSelectedIds, setRegisterSelectedIds] = useState>(new Set()); const [registerSubmitting, setRegisterSubmitting] = useState(false); useEffect(() => { const t = window.setTimeout(() => setDebouncedSearch(searchInput.trim()), 300); return () => window.clearTimeout(t); }, [searchInput]); useEffect(() => { const t = window.setTimeout(() => setRegisterSearchDebounced(registerSearch.trim()), 300); return () => window.clearTimeout(t); }, [registerSearch]); const loadItems = useCallback(async (search: string) => { setItemsLoading(true); try { const res = await getRegisteredItems(search || undefined); if (res.success && res.data) { setItems(res.data); } else { toast.error(res.message || "품목 목록을 불러오지 못했습니다."); setItems([]); } } finally { setItemsLoading(false); } }, []); useEffect(() => { void loadItems(debouncedSearch); }, [debouncedSearch, loadItems]); const loadProcesses = useCallback(async () => { setProcessesLoading(true); try { const procRes = await getProcessList({ useYn: "Y" }); if (!procRes.success || !procRes.data) { toast.error(procRes.message || "공정 목록을 불러오지 못했습니다."); setProcesses([]); return; } setProcesses(procRes.data); } finally { setProcessesLoading(false); } }, []); useEffect(() => { void loadProcesses(); }, [loadProcesses]); // 등록 모달 품목 검색 const loadRegisterItems = useCallback(async (search: string) => { setRegisterLoading(true); try { const res = await searchAllItems(search || undefined); if (res.success && res.data) { setRegisterItems(res.data); } else { setRegisterItems([]); } } finally { setRegisterLoading(false); } }, []); useEffect(() => { if (!registerDialogOpen) return; void loadRegisterItems(registerSearchDebounced); }, [registerSearchDebounced, registerDialogOpen, loadRegisterItems]); // 이미 등록된 품목인지 판별 (item_code 기준) const registeredItemCodes = useMemo(() => new Set(items.map((i) => i.item_code)), [items]); const handleRegisterItems = async () => { if (registerSelectedIds.size === 0) { toast.error("등록할 품목을 선택하세요."); return; } setRegisterSubmitting(true); try { const selected = registerItems.filter((ri) => registerSelectedIds.has(ri.id)); const batchItems = selected.map((item) => ({ itemId: item.id, itemCode: item.item_number, })); const res = await registerItemsBatch(batchItems); if (res.success) { toast.success(`${batchItems.length}건 품목이 등록되었습니다.`); setRegisterDialogOpen(false); setRegisterSearch(""); setRegisterSelectedIds(new Set()); void loadItems(debouncedSearch); } else { toast.error(res.message || "품목 등록에 실패했습니다."); } } finally { setRegisterSubmitting(false); } }; const loadVersions = useCallback(async (item: RegisteredItem, preferVersionId?: string) => { setVersionsLoading(true); setVersions([]); setSelectedVersionId(null); setDetails([]); setSelectedDetailIds(new Set()); try { const res = await getRoutingVersions(item.item_code); if (!res.success || !res.data) { toast.error(res.message || "라우팅 버전을 불러오지 못했습니다."); return; } const list = res.data.map((v) => ({ ...v, is_default: normalizeDefaultFlag(v) })); setVersions(list); const preferred = preferVersionId ? list.find((v) => v.id === preferVersionId) : undefined; const def = list.find((v) => v.is_default); const pick = preferred ?? def ?? list[0]; if (pick) setSelectedVersionId(pick.id); } finally { setVersionsLoading(false); } }, []); useEffect(() => { if (!selectedItem) return; void loadVersions(selectedItem); }, [selectedItem, loadVersions]); const loadDetails = useCallback(async (versionId: string) => { setDetailsLoading(true); setSelectedDetailIds(new Set()); try { const res = await getRoutingDetails(versionId); if (res.success && res.data) { setDetails(sortDetailsBySeq(res.data)); } else { toast.error(res.message || "라우팅 공정을 불러오지 못했습니다."); setDetails([]); } } finally { setDetailsLoading(false); } }, []); useEffect(() => { if (!selectedVersionId) { setDetails([]); return; } void loadDetails(selectedVersionId); }, [selectedVersionId, loadDetails]); const showOutsourceField = formWorkType === "외주" || formWorkType === "선택가능"; const openAddDetailDialog = () => { if (!selectedVersionId) { toast.error("먼저 라우팅 버전을 선택하세요."); return; } setDetailDialogMode("add"); setEditingDetailId(null); setFormProcessCode(""); const nextSeq = details.length === 0 ? 1 : Math.max(...details.map((d) => parseInt(String(d.seq_no), 10) || 0)) + 1; setFormSeqNo(String(nextSeq)); setFormRequired("Y"); setFormFixedOrder("Y"); setFormWorkType("내부"); setFormStandardTime(""); setFormOutsource(""); setDetailDialogOpen(true); }; const openEditDetailDialog = () => { if (!selectedVersionId) { toast.error("먼저 라우팅 버전을 선택하세요."); return; } if (selectedDetailIds.size !== 1) { toast.error("수정할 공정 한 건만 선택하세요."); return; } const id = [...selectedDetailIds][0]; const row = details.find((d) => d.id === id); if (!row) { toast.error("선택한 공정을 찾을 수 없습니다."); return; } setDetailDialogMode("edit"); setEditingDetailId(row.id); setFormProcessCode(row.process_code); setFormSeqNo(String(row.seq_no)); setFormRequired(row.is_required === "N" ? "N" : "Y"); setFormFixedOrder(row.is_fixed_order === "N" ? "N" : "Y"); setFormWorkType(row.work_type || "내부"); setFormStandardTime(row.standard_time || ""); setFormOutsource(row.outsource_supplier || ""); setDetailDialogOpen(true); }; const submitDetailForm = () => { if (!selectedVersionId) return; if (!formProcessCode) { toast.error("공정을 선택하세요."); return; } const seq = parseInt(formSeqNo, 10); if (Number.isNaN(seq) || seq < 1) { toast.error("올바른 순번을 입력하세요."); return; } const st = formStandardTime.trim(); if (st !== "" && Number.isNaN(Number(st))) { toast.error("표준작업시간은 숫자로 입력하세요."); return; } const proc = processes.find((p) => p.process_code === formProcessCode); const outsource = showOutsourceField ? formOutsource.trim() : ""; setDetailSubmitting(true); try { if (detailDialogMode === "add") { const newRow: RoutingDetail = { id: `new-${crypto.randomUUID()}`, routing_version_id: selectedVersionId, seq_no: String(seq), process_code: formProcessCode, process_name: proc?.process_name, is_required: formRequired, is_fixed_order: formFixedOrder, work_type: formWorkType, standard_time: st || "0", outsource_supplier: outsource, }; setDetails((prev) => sortDetailsBySeq([...prev, newRow])); toast.success("공정이 추가되었습니다. 저장을 눌러 반영하세요."); } else if (editingDetailId) { setDetails((prev) => sortDetailsBySeq( prev.map((d) => d.id === editingDetailId ? { ...d, seq_no: String(seq), process_code: formProcessCode, process_name: proc?.process_name ?? d.process_name, is_required: formRequired, is_fixed_order: formFixedOrder, work_type: formWorkType, standard_time: st || "0", outsource_supplier: outsource, } : d, ), ), ); toast.success("공정이 수정되었습니다. 저장을 눌러 반영하세요."); } setDetailDialogOpen(false); } finally { setDetailSubmitting(false); } }; const deleteSelectedDetails = () => { if (selectedDetailIds.size === 0) { toast.error("삭제할 공정을 선택하세요."); return; } setDetails((prev) => prev.filter((d) => !selectedDetailIds.has(d.id))); setSelectedDetailIds(new Set()); toast.success("선택한 공정이 목록에서 제거되었습니다. 저장을 눌러 반영하세요."); }; const toggleDetailSelected = (id: string, checked: boolean) => { setSelectedDetailIds((prev) => { const next = new Set(prev); if (checked) next.add(id); else next.delete(id); return next; }); }; const toggleAllDetails = (checked: boolean) => { if (!checked) { setSelectedDetailIds(new Set()); return; } setSelectedDetailIds(new Set(details.map((d) => d.id))); }; const allDetailsSelected = details.length > 0 && details.every((d) => selectedDetailIds.has(d.id)); const persistDetails = async () => { if (!selectedVersionId) { toast.error("저장할 버전이 없습니다."); return; } const payload = details.map((d) => ({ seq_no: String(d.seq_no), process_code: d.process_code, is_required: d.is_required || "Y", is_fixed_order: d.is_fixed_order || "Y", work_type: d.work_type || "내부", standard_time: String(d.standard_time ?? "0"), outsource_supplier: d.outsource_supplier || "", })); setSaving(true); try { const res = await saveRoutingDetails(selectedVersionId, payload); if (res.success) { toast.success("저장되었습니다."); await loadDetails(selectedVersionId); } else { toast.error(res.message || "저장에 실패했습니다."); } } finally { setSaving(false); } }; const submitNewVersion = async () => { if (!selectedItem) return; const name = versionName.trim(); if (!name) { toast.error("버전명을 입력하세요."); return; } setVersionSubmitting(true); try { const res = await createRoutingVersion({ item_code: selectedItem.item_code, version_name: name, description: versionDescription.trim() || undefined, is_default: versionIsDefault || undefined, }); if (res.success && res.data) { toast.success("버전이 추가되었습니다."); setVersionDialogOpen(false); setVersionName(""); setVersionDescription(""); setVersionIsDefault(false); const created = res.data as RoutingVersion; await loadVersions(selectedItem, created?.id); } else { toast.error(res.message || "버전 추가에 실패했습니다."); } } finally { setVersionSubmitting(false); } }; return (
setSearchInput(e.target.value)} placeholder="품목코드 / 품목명 검색" className="focus-visible:ring-ring h-8 pl-8 text-xs focus-visible:ring-2 focus-visible:ring-offset-2 sm:h-10 sm:pl-9 sm:text-sm" aria-label="품목 검색" />
{itemsLoading ? (
불러오는 중...
) : items.length === 0 ? (

등록된 품목이 없습니다.

) : (
{items.map((item) => { const active = selectedItem?.id === item.id; return ( ); })}
)}
{!selectedItem ? (
좌측에서 품목을 선택하면 라우팅을 관리할 수 있습니다.
) : ( <>

{selectedItem.item_name}

품목코드 {selectedItem.item_code}

{versionsLoading ? (
버전 불러오는 중...
) : versions.length === 0 ? (

등록된 버전이 없습니다. 버전을 추가하세요.

) : ( versions.map((v) => { const selected = v.id === selectedVersionId; const def = normalizeDefaultFlag(v); return ( ); }) )}
{detailsLoading ? (
공정 순서 불러오는 중...
) : !selectedVersionId ? (

버전을 선택하세요.

) : (
toggleAllDetails(c === true)} aria-label="전체 선택" className="focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2" /> 순번 공정명 필수 순서고정 작업구분 표준시간 외주업체 {details.length === 0 ? ( 등록된 공정이 없습니다. ) : ( details.map((row) => ( toggleDetailSelected(row.id, c === true)} aria-label={`${row.process_name || row.process_code} 선택`} className="focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2" /> {row.seq_no} {row.process_name || row.process_code} {row.is_required === "N" ? "N" : "Y"} {row.is_fixed_order === "N" ? "N" : "Y"} {row.work_type} {row.standard_time} {row.outsource_supplier || "—"} )) )}
)}
)}
라우팅 버전 추가 선택한 품목에 새 라우팅 버전을 추가합니다.
setVersionName(e.target.value)} placeholder="예: Rev.A" className="focus-visible:ring-ring mt-1.5 h-8 text-xs focus-visible:ring-2 focus-visible:ring-offset-2 sm:h-10 sm:text-sm" />
setVersionDescription(e.target.value)} placeholder="선택 입력" className="focus-visible:ring-ring mt-1.5 h-8 text-xs focus-visible:ring-2 focus-visible:ring-offset-2 sm:h-10 sm:text-sm" />
setVersionIsDefault(c === true)} className="focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2" />
{detailDialogMode === "add" ? "공정 추가" : "공정 수정"} 라우팅 공정 순서에 반영할 내용을 입력합니다. 적용 후 저장을 눌러주세요.
setFormSeqNo(e.target.value)} className="focus-visible:ring-ring mt-1.5 h-8 text-xs focus-visible:ring-2 focus-visible:ring-offset-2 sm:h-10 sm:text-sm" />
setFormStandardTime(e.target.value)} placeholder="0" className="focus-visible:ring-ring mt-1.5 h-8 text-xs focus-visible:ring-2 focus-visible:ring-offset-2 sm:h-10 sm:text-sm" />
{showOutsourceField && (
setFormOutsource(e.target.value)} placeholder="외주 업체명" className="focus-visible:ring-ring mt-1.5 h-8 text-xs focus-visible:ring-2 focus-visible:ring-offset-2 sm:h-10 sm:text-sm" />
)}
{/* 품목 등록 모달 */} 품목 등록 라우팅에 추가할 품목을 검색하고 선택하세요.
setRegisterSearch(e.target.value)} placeholder="품목코드 / 품목명으로 검색" className="h-8 pl-8 text-xs sm:h-10 sm:pl-9 sm:text-sm" aria-label="품목 검색" />
{registerLoading ? (
검색 중...
) : registerItems.length === 0 ? (

{registerSearchDebounced ? "검색 결과가 없습니다." : "품목을 검색하세요."}

) : ( !registeredItemCodes.has(ri.item_number)).length > 0 && registerItems .filter((ri) => !registeredItemCodes.has(ri.item_number)) .every((ri) => registerSelectedIds.has(ri.id)) } onCheckedChange={(checked) => { const next = new Set(registerSelectedIds); const available = registerItems.filter((ri) => !registeredItemCodes.has(ri.item_number)); if (checked) { available.forEach((ri) => next.add(ri.id)); } else { available.forEach((ri) => next.delete(ri.id)); } setRegisterSelectedIds(next); }} /> 품목코드 품목명 상태 {registerItems.map((ri) => { const alreadyRegistered = registeredItemCodes.has(ri.item_number); return ( { const next = new Set(registerSelectedIds); if (checked) next.add(ri.id); else next.delete(ri.id); setRegisterSelectedIds(next); }} /> {ri.item_number} {ri.item_name} {alreadyRegistered ? ( 등록됨 ) : ( 미등록 )} ); })}
)}
{registerSelectedIds.size > 0 && (

{registerSelectedIds.size}건 선택됨

)}
); }