"use client"; /** * 금형정보 — Type E 카드리스트형 * * 좌측: 금형 카드 리스트 (이미지 + 코드 + 이름 + 유형뱃지 + 수명진행률) * 우측: 금형 상세 + 탭 3개 (일련번호 / 점검항목 / 부품) * 전용 API: /molds/* */ import React, { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { Plus, Trash2, Loader2, Pencil, Box, Inbox, Search, Wrench, ClipboardCheck, Package, LayoutGrid, List, Image as ImageIcon, Upload, X as XIcon, } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; import { ImageUpload } from "@/components/common/ImageUpload"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; // ─── API base ─── const API = "/mold"; // ─── 상태/유형 매핑 ─── const STATUS_MAP: Record = { ACTIVE: { label: "사용중", variant: "default" }, INSPECTION: { label: "점검중", variant: "secondary" }, REPAIR: { label: "수리중", variant: "outline" }, DISPOSED: { label: "폐기", variant: "destructive" }, }; const SERIAL_STATUS_MAP: Record = { IN_USE: { label: "사용중", variant: "default" }, STORED: { label: "보관중", variant: "secondary" }, REPAIR: { label: "수리중", variant: "outline" }, DISPOSED: { label: "폐기", variant: "destructive" }, }; // ─── 수명 진행률 색상 ─── function getProgressColor(pct: number) { if (pct < 60) return "bg-success"; if (pct < 85) return "bg-warning"; return "bg-destructive"; } function getProgressTextColor(pct: number) { if (pct < 60) return "text-success"; if (pct < 85) return "text-warning"; return "text-destructive"; } export default function MoldInfoPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent } = useConfirmDialog(); // ─── 좌측: 금형 목록 ─── const [molds, setMolds] = useState([]); const [loading, setLoading] = useState(false); const [selectedMoldCode, setSelectedMoldCode] = useState(null); const [viewMode, setViewMode] = useState<"list" | "grid">("list"); // ─── 검색 필터 ─── const [filterCode, setFilterCode] = useState(""); const [filterName, setFilterName] = useState(""); const [filterType, setFilterType] = useState(""); const [filterStatus, setFilterStatus] = useState(""); // ─── 우측: 상세 ─── const [detail, setDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [serialSummary, setSerialSummary] = useState(null); const [rightTab, setRightTab] = useState("serial"); // ─── 일련번호 ─── const [serials, setSerials] = useState([]); const [serialLoading, setSerialLoading] = useState(false); // ─── 점검항목 ─── const [inspections, setInspections] = useState([]); const [inspectionLoading, setInspectionLoading] = useState(false); // ─── 부품 ─── const [parts, setParts] = useState([]); const [partLoading, setPartLoading] = useState(false); // ─── 모달 ─── const [moldModalOpen, setMoldModalOpen] = useState(false); const [moldEditMode, setMoldEditMode] = useState(false); const [moldForm, setMoldForm] = useState>({}); const [numberingRuleId, setNumberingRuleId] = useState(null); const [previewCode, setPreviewCode] = useState(null); const [saving, setSaving] = useState(false); // moldImagePreview는 상세 표시용으로 유지 (ImageUpload는 모달에서만 사용) const [moldImagePreview, setMoldImagePreview] = useState(null); const [serialModalOpen, setSerialModalOpen] = useState(false); const [serialForm, setSerialForm] = useState>({}); const [inspectionModalOpen, setInspectionModalOpen] = useState(false); const [inspectionForm, setInspectionForm] = useState>({}); const [partModalOpen, setPartModalOpen] = useState(false); const [partForm, setPartForm] = useState>({}); // ─── 금형 목록 조회 ─── const fetchMolds = useCallback(async () => { setLoading(true); try { const params: Record = {}; if (filterCode) params.mold_code = filterCode; if (filterName) params.mold_name = filterName; if (filterType) params.mold_type = filterType; if (filterStatus) params.operation_status = filterStatus; const res = await apiClient.get(API, { params }); setMolds(res.data?.data || []); } catch { toast.error("금형 목록을 불러올 수 없어요."); } finally { setLoading(false); } }, [filterCode, filterName, filterType, filterStatus]); useEffect(() => { fetchMolds(); }, [fetchMolds]); // ─── 금형 상세 + 하위 데이터 ─── const fetchDetail = useCallback(async (code: string) => { setDetailLoading(true); try { const [detailRes, summaryRes] = await Promise.all([ apiClient.get(`${API}/${code}`), apiClient.get(`${API}/${code}/serial-summary`), ]); setDetail(detailRes.data?.data || null); setSerialSummary(summaryRes.data?.data || null); } catch { toast.error("금형 상세를 불러올 수 없어요."); } finally { setDetailLoading(false); } }, []); const fetchSerials = useCallback(async (code: string) => { setSerialLoading(true); try { const res = await apiClient.get(`${API}/${code}/serials`); setSerials(res.data?.data || []); } catch { toast.error("일련번호를 불러올 수 없어요."); } finally { setSerialLoading(false); } }, []); const fetchInspections = useCallback(async (code: string) => { setInspectionLoading(true); try { const res = await apiClient.get(`${API}/${code}/inspections`); setInspections(res.data?.data || []); } catch { toast.error("점검항목을 불러올 수 없어요."); } finally { setInspectionLoading(false); } }, []); const fetchParts = useCallback(async (code: string) => { setPartLoading(true); try { const res = await apiClient.get(`${API}/${code}/parts`); setParts(res.data?.data || []); } catch { toast.error("부품을 불러올 수 없어요."); } finally { setPartLoading(false); } }, []); // 금형 선택 const handleSelectMold = useCallback((code: string) => { setSelectedMoldCode(code); setRightTab("serial"); fetchDetail(code); fetchSerials(code); fetchInspections(code); fetchParts(code); }, [fetchDetail, fetchSerials, fetchInspections, fetchParts]); // 탭 변경 시 재조회 useEffect(() => { if (!selectedMoldCode) return; if (rightTab === "serial") fetchSerials(selectedMoldCode); else if (rightTab === "inspection") fetchInspections(selectedMoldCode); else if (rightTab === "part") fetchParts(selectedMoldCode); }, [rightTab, selectedMoldCode, fetchSerials, fetchInspections, fetchParts]); // ─── 금형 CRUD ─── const handleOpenRegister = async () => { setMoldEditMode(false); setMoldForm({}); setMoldImagePreview(null); setNumberingRuleId(null); setPreviewCode(null); setMoldModalOpen(true); // 채번 규칙 조회 (mold_mng.mold_code) try { const ruleRes = await apiClient.get(`/numbering-rules/by-column/mold_mng/mold_code`); const ruleData = ruleRes.data; if (ruleData?.success && ruleData?.data?.ruleId) { const ruleId = ruleData.data.ruleId; setNumberingRuleId(ruleId); const previewRes = await previewNumberingCode(ruleId); if (previewRes.success && previewRes.data?.generatedCode) { setPreviewCode(previewRes.data.generatedCode); } } } catch { // 채번 규칙 없으면 무시 — 수동 입력 모드 } }; const handleOpenEdit = () => { if (!detail) return; setMoldEditMode(true); setMoldForm({ ...detail }); setMoldImagePreview(detail.image_path || null); setMoldModalOpen(true); }; // 이미지 업로드는 ImageUpload 컴포넌트가 처리 → objid를 image_path에 저장 const handleSaveMold = async () => { // 등록 모드에서 채번이 없으면 수동 입력 필수 if (!moldEditMode && !numberingRuleId && !moldForm.mold_code) { toast.error("금형코드와 금형명은 필수예요."); return; } if (!moldForm.mold_name) { toast.error("금형명은 필수예요."); return; } if (moldEditMode && !moldForm.mold_code) { toast.error("금형코드가 없어요."); return; } setSaving(true); try { if (moldEditMode) { await apiClient.put(`${API}/${moldForm.mold_code}`, moldForm); toast.success("금형이 수정되었어요."); } else { // 채번 규칙이 있으면 allocate로 실제 코드 할당 let saveData = { ...moldForm }; if (numberingRuleId) { const allocRes = await allocateNumberingCode(numberingRuleId); if (allocRes.success && allocRes.data?.generatedCode) { saveData.mold_code = allocRes.data.generatedCode; } else { toast.error("채번 코드 할당에 실패했어요."); return; } } await apiClient.post(API, saveData); toast.success("금형이 등록되었어요."); } setMoldModalOpen(false); fetchMolds(); if (moldEditMode && selectedMoldCode === moldForm.mold_code) { fetchDetail(moldForm.mold_code); } } catch (err: any) { toast.error(err?.response?.data?.message || "저장에 실패했어요."); } finally { setSaving(false); } }; const handleDeleteMold = async () => { if (!selectedMoldCode || !detail) return; const ok = await confirm( "금형을 삭제하시겠어요?", { description: `"${detail.mold_name}" 금형과 하위 일련번호, 점검항목, 부품이 모두 삭제돼요.`, variant: "destructive" }, ); if (!ok) return; try { await apiClient.delete(`${API}/${selectedMoldCode}`); toast.success("금형이 삭제되었어요."); setSelectedMoldCode(null); setDetail(null); fetchMolds(); } catch { toast.error("삭제에 실패했어요."); } }; // ─── 일련번호 CRUD ─── const handleSaveSerial = async () => { if (!selectedMoldCode) return; setSaving(true); try { if (serialForm.id) { await apiClient.put(`${API}/serials/${serialForm.id}`, serialForm); toast.success("일련번호가 수정되었어요."); } else { await apiClient.post(`${API}/${selectedMoldCode}/serials`, serialForm); toast.success("일련번호가 등록되었어요."); } setSerialModalOpen(false); setSerialForm({}); fetchSerials(selectedMoldCode); fetchDetail(selectedMoldCode); } catch (err: any) { toast.error(err?.response?.data?.message || "저장에 실패했어요."); } finally { setSaving(false); } }; const handleDeleteSerial = async (id: number) => { const ok = await confirm("이 일련번호를 삭제하시겠어요?", { variant: "destructive" }); if (!ok) return; try { await apiClient.delete(`${API}/serials/${id}`); toast.success("삭제되었어요."); if (selectedMoldCode) { fetchSerials(selectedMoldCode); fetchDetail(selectedMoldCode); } } catch { toast.error("삭제에 실패했어요."); } }; // ─── 점검항목 CRUD ─── const handleAddInspection = async () => { if (!selectedMoldCode) return; if (!inspectionForm.inspection_item) { toast.error("점검항목명은 필수예요."); return; } setSaving(true); try { await apiClient.post(`${API}/${selectedMoldCode}/inspections`, inspectionForm); toast.success("점검항목이 등록되었어요."); setInspectionModalOpen(false); setInspectionForm({}); fetchInspections(selectedMoldCode); } catch { toast.error("등록에 실패했어요."); } finally { setSaving(false); } }; const handleDeleteInspection = async (id: number) => { const ok = await confirm("이 점검항목을 삭제하시겠어요?", { variant: "destructive" }); if (!ok) return; try { await apiClient.delete(`${API}/inspections/${id}`); toast.success("삭제되었어요."); if (selectedMoldCode) fetchInspections(selectedMoldCode); } catch { toast.error("삭제에 실패했어요."); } }; // ─── 부품 CRUD ─── const handleAddPart = async () => { if (!selectedMoldCode) return; if (!partForm.part_name) { toast.error("부품명은 필수예요."); return; } if (!partForm.replacement_cycle) { toast.error("교체주기는 필수예요."); return; } setSaving(true); try { await apiClient.post(`${API}/${selectedMoldCode}/parts`, partForm); toast.success("부품이 등록되었어요."); setPartModalOpen(false); setPartForm({}); fetchParts(selectedMoldCode); } catch { toast.error("등록에 실패했어요."); } finally { setSaving(false); } }; const handleDeletePart = async (id: number) => { const ok = await confirm("이 부품을 삭제하시겠어요?", { variant: "destructive" }); if (!ok) return; try { await apiClient.delete(`${API}/parts/${id}`); toast.success("삭제되었어요."); if (selectedMoldCode) fetchParts(selectedMoldCode); } catch { toast.error("삭제에 실패했어요."); } }; // ─── 수명 진행률 계산 ─── const calcLifePct = (mold: any) => { const max = Number(mold.shot_count) || 0; const current = Number(mold.current_shot_count) || 0; if (max <= 0) return 0; return Math.min(Math.round((current / max) * 100), 100); }; // ─── 카드 렌더링 ─── const renderCard = (mold: any) => { const pct = calcLifePct(mold); const st = STATUS_MAP[mold.operation_status] || { label: mold.operation_status || "-", variant: "secondary" as const }; const isSelected = selectedMoldCode === mold.mold_code; return (
handleSelectMold(mold.mold_code)} className={cn( "rounded-lg border cursor-pointer transition-all overflow-hidden", "hover:shadow-md hover:-translate-y-0.5", isSelected ? "border-primary ring-2 ring-primary/25" : "border-border hover:border-border", viewMode === "list" ? "flex flex-row" : "", )} > {/* 이미지 영역 */}
{mold.image_path ? ( {mold.mold_name} { (e.target as HTMLImageElement).style.display = "none"; }} /> ) : ( )}
{st.label}
{/* 본문 */}

{mold.mold_code}

{mold.mold_name}

{mold.mold_type && ( {mold.mold_type} )}
{/* 수명 진행률 */}
수명 {pct}%
); }; // ─── 선택 금형 상세 ─── const selectedMold = detail; const lifePct = selectedMold ? calcLifePct(selectedMold) : 0; return (
{ConfirmDialogComponent} {/* 검색 필터 */}
setFilterCode(e.target.value)} />
setFilterName(e.target.value)} />
{/* 메인 분할 패널 */} {/* ─── 좌측: 카드 리스트 ─── */}
{/* 패널 헤더 */}

금형 목록

{molds.length}
{/* 뷰 토글 */}
{/* 카드 스크롤 영역 */}
{loading ? (
) : molds.length === 0 ? (

등록된 금형이 없어요.

) : (
{molds.map(renderCard)}
)}
{/* ─── 우측: 상세 ─── */}
{!selectedMoldCode ? (

좌측에서 금형을 선택해주세요

금형을 선택하면 상세 정보를 확인할 수 있어요

) : detailLoading ? (
) : selectedMold ? (
{/* 상세 헤더 */}
{/* 이미지 */}
{selectedMold.image_path ? ( {selectedMold.mold_name} { (e.target as HTMLImageElement).style.display = "none"; }} /> ) : ( )}
{/* 메타 정보 */}

{selectedMold.mold_code}

{selectedMold.mold_name}

{selectedMold.mold_type && ( {selectedMold.mold_type} )} {selectedMold.category && ( {selectedMold.category} )} {STATUS_MAP[selectedMold.operation_status]?.label || selectedMold.operation_status || "-"}
제조사

{selectedMold.manufacturer || "-"}

제작일

{selectedMold.manufacturing_date || "-"}

캐비티수

{selectedMold.cavity_count ?? "-"}

수량

{selectedMold.mold_quantity ?? "-"}

최대수명

{Number(selectedMold.shot_count || 0).toLocaleString()}회

보증수명

{Number(selectedMold.warranty_shot_count || 0).toLocaleString()}회

{/* 수명 진행률 바 */}
수명 사용률 {lifePct}%
{/* 액션 버튼 */}
{/* 탭 3개 */} 일련번호 {serials.length} 점검항목 {inspections.length} 부품 {parts.length} {/* ─── 탭1: 일련번호 ─── */}
{serialSummary && ( <> 사용중 {serialSummary.in_use || 0} · 보관중 {serialSummary.stored || 0} · 수리중 {serialSummary.repair || 0} · 폐기 {serialSummary.disposed || 0} )}
{serialLoading ? (
) : serials.length === 0 ? (

등록된 일련번호가 없어요.

) : (
일련번호 상태 샷수 현황 보관위치 비고 작업 {serials.map((s: any) => { const ss = SERIAL_STATUS_MAP[s.status] || { label: s.status || "-", variant: "secondary" as const }; const maxShot = detail?.shot_count || 0; const curShot = s.current_shot_count || 0; const pct = maxShot > 0 ? Math.min(Math.round((curShot / maxShot) * 100), 100) : 0; return ( {s.serial_number} {ss.label} {maxShot > 0 ? (
{pct}%

{curShot.toLocaleString()} / {maxShot.toLocaleString()}

) : ( - )} {s.storage_location || "-"} {s.remarks || "-"}
); })}
)}
{/* ─── 탭2: 점검항목 ─── */}
점검항목 목록
{inspectionLoading ? (
) : inspections.length === 0 ? (

등록된 점검항목이 없어요.

) : (
점검항목 점검주기 점검방법 하한치 상한치 단위 {inspections.map((ins: any) => ( {ins.inspection_item} {ins.inspection_cycle || "-"} {ins.inspection_method || "-"} {ins.lower_limit || "-"} {ins.upper_limit || "-"} {ins.unit || "-"} ))}
)}
{/* ─── 탭3: 부품 ─── */}
부품 목록
{partLoading ? (
) : parts.length === 0 ? (

등록된 부품이 없어요.

) : (
부품명 교체주기 단위 규격 제조사 비고 {parts.map((p: any) => ( {p.part_name} {p.replacement_cycle || "-"} {p.unit || "-"} {p.specification || "-"} {p.manufacturer || "-"} {p.remarks || "-"} ))}
)}
) : null}
{/* ========== 금형 등록/수정 모달 ========== */} {moldEditMode ? "금형 수정" : "금형 등록"} 금형 기본 정보를 입력해주세요.
{ if (!moldEditMode && !numberingRuleId) { setMoldForm({ ...moldForm, mold_code: e.target.value }); } }} readOnly={moldEditMode || !!numberingRuleId} placeholder={moldEditMode ? "" : (numberingRuleId ? (previewCode ? "" : "자동 생성 중...") : "금형코드를 입력해주세요")} />
setMoldForm({ ...moldForm, mold_name: e.target.value })} placeholder="금형명을 입력해주세요" />
setMoldForm({ ...moldForm, category: e.target.value })} placeholder="카테고리" />
setMoldForm({ ...moldForm, manufacturer: e.target.value })} placeholder="제조사" />
setMoldForm({ ...moldForm, manufacturing_date: e.target.value })} />
setMoldForm({ ...moldForm, cavity_count: e.target.value })} min={0} />
setMoldForm({ ...moldForm, shot_count: e.target.value })} min={0} />
setMoldForm({ ...moldForm, mold_quantity: e.target.value })} min={1} />
setMoldForm({ ...moldForm, warranty_shot_count: e.target.value })} min={0} />
setMoldForm((prev) => ({ ...prev, image_path: v }))} tableName="mold_mng" recordId={moldForm.id || ""} columnName="image_path" height="h-32" />
{/* ========== 일련번호 등록/수정 모달 ========== */} {serialForm.id ? "일련번호 수정" : "일련번호 등록"} {serialForm.id ? `${serialForm.serial_number} 정보를 수정해요.` : "일련번호는 자동으로 채번돼요."}
setSerialForm({ ...serialForm, current_shot_count: e.target.value ? Number(e.target.value) : null })} placeholder="0" min={0} />
setSerialForm({ ...serialForm, storage_location: e.target.value })} placeholder="보관위치" />
setSerialForm({ ...serialForm, remarks: e.target.value })} placeholder="비고" />
{/* ========== 점검항목 등록 모달 ========== */} 점검항목 등록 금형 점검항목을 등록해주세요.
setInspectionForm({ ...inspectionForm, inspection_item: e.target.value })} placeholder="점검항목명" />
setInspectionForm({ ...inspectionForm, inspection_cycle: e.target.value })} placeholder="예: 월 1회" />
{inspectionForm.inspection_method === "숫자" && ( <>
setInspectionForm({ ...inspectionForm, lower_limit: e.target.value })} placeholder="기준값" />
setInspectionForm({ ...inspectionForm, upper_limit: e.target.value })} placeholder="허용 오차" />
setInspectionForm({ ...inspectionForm, unit: e.target.value })} placeholder="mm, ℃ 등" />
)}