"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Loader2, Settings, Plus, Pencil, Trash2, Search, RotateCcw, Wrench, } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { ResizablePanelGroup, ResizablePanel, ResizableHandle, } from "@/components/ui/resizable"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; import { getProcessList, createProcess, updateProcess, deleteProcesses, getProcessEquipments, addProcessEquipment, removeProcessEquipment, getEquipmentList, type ProcessMaster, type ProcessEquipment, type Equipment, } from "@/lib/api/processInfo"; import { getCategoryValues } from "@/lib/api/tableCategoryValue"; const ALL_VALUE = "__all__"; export function ProcessMasterTab() { const [processes, setProcesses] = useState([]); const [equipmentMaster, setEquipmentMaster] = useState([]); const [processTypeOptions, setProcessTypeOptions] = useState<{ valueCode: string; valueLabel: string }[]>([]); const [loadingInitial, setLoadingInitial] = useState(true); const [loadingList, setLoadingList] = useState(false); const [loadingEquipments, setLoadingEquipments] = useState(false); const [filterCode, setFilterCode] = useState(""); const [filterName, setFilterName] = useState(""); const [filterType, setFilterType] = useState(ALL_VALUE); const [filterUseYn, setFilterUseYn] = useState(ALL_VALUE); const [selectedProcess, setSelectedProcess] = useState(null); const [selectedIds, setSelectedIds] = useState>(() => new Set()); const [processEquipments, setProcessEquipments] = useState([]); const [equipmentPick, setEquipmentPick] = useState(""); const [addingEquipment, setAddingEquipment] = useState(false); const [formOpen, setFormOpen] = useState(false); const [formMode, setFormMode] = useState<"add" | "edit">("add"); const [savingForm, setSavingForm] = useState(false); const [formProcessCode, setFormProcessCode] = useState(""); const [formProcessName, setFormProcessName] = useState(""); const [formProcessType, setFormProcessType] = useState(""); const [formStandardTime, setFormStandardTime] = useState(""); const [formWorkerCount, setFormWorkerCount] = useState(""); const [formUseYn, setFormUseYn] = useState(""); const [editingId, setEditingId] = useState(null); const [deleteOpen, setDeleteOpen] = useState(false); const [deleting, setDeleting] = useState(false); const processTypeMap = useMemo(() => { const m = new Map(); processTypeOptions.forEach((o) => m.set(o.valueCode, o.valueLabel)); return m; }, [processTypeOptions]); const getProcessTypeLabel = useCallback( (code: string) => processTypeMap.get(code) ?? code, [processTypeMap] ); const loadProcesses = useCallback(async () => { setLoadingList(true); try { const res = await getProcessList({ processCode: filterCode.trim() || undefined, processName: filterName.trim() || undefined, processType: filterType === ALL_VALUE ? undefined : filterType, useYn: filterUseYn === ALL_VALUE ? undefined : filterUseYn, }); if (!res.success) { toast.error(res.message || "공정 목록을 불러오지 못했습니다."); return; } setProcesses(res.data ?? []); } finally { setLoadingList(false); } }, [filterCode, filterName, filterType, filterUseYn]); const loadInitial = useCallback(async () => { setLoadingInitial(true); try { const [procRes, eqRes] = await Promise.all([getProcessList(), getEquipmentList()]); if (!procRes.success) { toast.error(procRes.message || "공정 목록을 불러오지 못했습니다."); } else { setProcesses(procRes.data ?? []); } if (!eqRes.success) { toast.error(eqRes.message || "설비 목록을 불러오지 못했습니다."); } else { setEquipmentMaster(eqRes.data ?? []); } const ptRes = await getCategoryValues("process_mng", "process_type"); if (ptRes.success && "data" in ptRes && Array.isArray(ptRes.data)) { const activeValues = ptRes.data.filter((v: any) => v.isActive !== false); const seen = new Set(); const unique = activeValues.filter((v: any) => { if (seen.has(v.valueCode)) return false; seen.add(v.valueCode); return true; }); setProcessTypeOptions(unique.map((v: any) => ({ valueCode: v.valueCode, valueLabel: v.valueLabel }))); } } finally { setLoadingInitial(false); } }, []); useEffect(() => { void loadInitial(); }, [loadInitial]); useEffect(() => { setSelectedProcess((prev) => { if (!prev) return prev; if (!processes.some((p) => p.id === prev.id)) return null; return prev; }); }, [processes]); useEffect(() => { setEquipmentPick(""); }, [selectedProcess?.id]); useEffect(() => { if (!selectedProcess) { setProcessEquipments([]); return; } let cancelled = false; setLoadingEquipments(true); void (async () => { const res = await getProcessEquipments(selectedProcess.process_code); if (cancelled) return; if (!res.success) { toast.error(res.message || "공정 설비를 불러오지 못했습니다."); setProcessEquipments([]); } else { setProcessEquipments(res.data ?? []); } setLoadingEquipments(false); })(); return () => { cancelled = true; }; }, [selectedProcess?.process_code]); const allSelected = useMemo(() => { if (processes.length === 0) return false; return processes.every((p) => selectedIds.has(p.id)); }, [processes, selectedIds]); const toggleAll = (checked: boolean) => { if (checked) { setSelectedIds(new Set(processes.map((p) => p.id))); } else { setSelectedIds(new Set()); } }; const toggleOne = (id: string, checked: boolean) => { setSelectedIds((prev) => { const next = new Set(prev); if (checked) next.add(id); else next.delete(id); return next; }); }; const handleResetFilters = () => { setFilterCode(""); setFilterName(""); setFilterType(ALL_VALUE); setFilterUseYn(ALL_VALUE); }; const handleSearch = () => { void loadProcesses(); }; const openAdd = () => { setFormMode("add"); setEditingId(null); setFormProcessCode(""); setFormProcessName(""); setFormProcessType(processTypeOptions[0]?.valueCode ?? ""); setFormStandardTime(""); setFormWorkerCount(""); setFormUseYn("Y"); setFormOpen(true); }; const openEdit = () => { if (!selectedProcess) { toast.message("수정할 공정을 좌측 목록에서 선택하세요."); return; } setFormMode("edit"); setEditingId(selectedProcess.id); setFormProcessCode(selectedProcess.process_code); setFormProcessName(selectedProcess.process_name); setFormProcessType(selectedProcess.process_type); setFormStandardTime(selectedProcess.standard_time ?? ""); setFormWorkerCount(selectedProcess.worker_count ?? ""); setFormUseYn(selectedProcess.use_yn); setFormOpen(true); }; const submitForm = async () => { if (!formProcessName.trim()) { toast.error("공정명을 입력하세요."); return; } setSavingForm(true); try { if (formMode === "add") { const res = await createProcess({ process_name: formProcessName.trim(), process_type: formProcessType, standard_time: formStandardTime.trim() || "0", worker_count: formWorkerCount.trim() || "0", use_yn: formUseYn, }); if (!res.success || !res.data) { toast.error(res.message || "등록에 실패했습니다."); return; } toast.success("공정이 등록되었습니다."); setFormOpen(false); await loadProcesses(); setSelectedProcess(res.data); setSelectedIds(new Set()); } else if (editingId) { const res = await updateProcess(editingId, { process_name: formProcessName.trim(), process_type: formProcessType, standard_time: formStandardTime.trim() || "0", worker_count: formWorkerCount.trim() || "0", use_yn: formUseYn, }); if (!res.success || !res.data) { toast.error(res.message || "수정에 실패했습니다."); return; } toast.success("공정이 수정되었습니다."); setFormOpen(false); await loadProcesses(); setSelectedProcess(res.data); } } finally { setSavingForm(false); } }; const openDelete = () => { if (selectedIds.size === 0) { toast.message("삭제할 공정을 체크박스로 선택하세요."); return; } setDeleteOpen(true); }; const confirmDelete = async () => { const ids = Array.from(selectedIds); setDeleting(true); try { const res = await deleteProcesses(ids); if (!res.success) { toast.error(res.message || "삭제에 실패했습니다."); return; } toast.success(`${ids.length}건 삭제되었습니다.`); setDeleteOpen(false); setSelectedIds(new Set()); if (selectedProcess && ids.includes(selectedProcess.id)) { setSelectedProcess(null); } await loadProcesses(); } finally { setDeleting(false); } }; const availableEquipments = useMemo(() => { const used = new Set(processEquipments.map((e) => e.equipment_code)); return equipmentMaster.filter((e) => !used.has(e.equipment_code)); }, [equipmentMaster, processEquipments]); const handleAddEquipment = async () => { if (!selectedProcess) return; if (!equipmentPick) { toast.message("추가할 설비를 선택하세요."); return; } setAddingEquipment(true); try { const res = await addProcessEquipment({ process_code: selectedProcess.process_code, equipment_code: equipmentPick, }); if (!res.success) { toast.error(res.message || "설비 추가에 실패했습니다."); return; } toast.success("설비가 등록되었습니다."); setEquipmentPick(""); const listRes = await getProcessEquipments(selectedProcess.process_code); if (listRes.success && listRes.data) setProcessEquipments(listRes.data); } finally { setAddingEquipment(false); } }; const handleRemoveEquipment = async (row: ProcessEquipment) => { const res = await removeProcessEquipment(row.id); if (!res.success) { toast.error(res.message || "설비 제거에 실패했습니다."); return; } toast.success("설비가 제거되었습니다."); if (selectedProcess) { const listRes = await getProcessEquipments(selectedProcess.process_code); if (listRes.success && listRes.data) setProcessEquipments(listRes.data); } }; const listBusy = loadingInitial || loadingList; return (
공정 마스터
setFilterCode(e.target.value)} placeholder="코드" className="h-8 w-[120px] text-xs sm:h-10 sm:w-[140px] sm:text-sm" />
setFilterName(e.target.value)} placeholder="이름" className="h-8 w-[120px] text-xs sm:h-10 sm:w-[160px] sm:text-sm" />
{listBusy ? (

불러오는 중...

) : ( toggleAll(v === true)} aria-label="전체 선택" className="mx-auto" /> 공정코드 공정명 공정유형 표준시간(분) 작업인원 사용여부 {processes.length === 0 ? (

조회된 공정이 없습니다.

) : ( processes.map((row) => ( setSelectedProcess(row)} > e.stopPropagation()} > toggleOne(row.id, v === true)} aria-label={`${row.process_code} 선택`} className="mx-auto" /> {row.process_code} {row.process_name} {getProcessTypeLabel(row.process_type)} {row.standard_time ?? "-"} {row.worker_count ?? "-"} {row.use_yn === "Y" ? "사용" : "미사용"} )) )}
)}

공정별 사용설비

{selectedProcess ? (

{selectedProcess.process_name}{" "} ({selectedProcess.process_code})

) : (

공정 미선택

)}
{!selectedProcess ? (

좌측에서 공정을 선택하세요

목록 행을 클릭하면 이 공정에 연결된 설비를 관리할 수 있습니다.

) : (
{loadingEquipments ? (

설비 목록 불러오는 중...

) : processEquipments.length === 0 ? (

등록된 설비가 없습니다. 상단에서 설비를 추가하세요.

) : (
    {processEquipments.map((pe) => (
  • {pe.equipment_code}

    {pe.equipment_name || "설비명 없음"}

  • ))}
)}
)}
{formMode === "add" ? "공정 추가" : "공정 수정"} 공정 마스터 정보를 입력합니다. 표준시간과 작업인원은 숫자로 입력하세요.
setFormProcessName(e.target.value)} placeholder="공정명" className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" />
setFormStandardTime(e.target.value)} placeholder="0" inputMode="numeric" className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" />
setFormWorkerCount(e.target.value)} placeholder="0" inputMode="numeric" className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" />
공정 삭제 선택한 {selectedIds.size}건의 공정을 삭제합니다. 연결된 공정-설비 매핑도 함께 삭제됩니다. 이 작업은 되돌릴 수 없습니다.
); }