"use client"; import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { cn } from "@/lib/utils"; import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; import { getMonitoringTheme } from "@/lib/monitoringTheme"; import { useTabStore } from "@/stores/tabStore"; import { RefreshCw, Clock, Loader2, Inbox, Search, ClipboardCheck, Settings2 } from "lucide-react"; /* ───── 타입 ───── */ interface ProcessRow { id: number; wo_id: number; process_code: string; process_name: string; status: string; plan_qty: number; input_qty: number; good_qty: number; defect_qty: number; started_at: string | null; completed_at: string | null; worker_name: string; } interface InspectionRow { no: number; inspectionNo: string; inspectionType: string; itemName: string; spec: string; inspectionQty: number; goodQty: number; defectQty: number; defectRate: number; result: "합격" | "불합격" | "대기"; inspectorName: string; inspectedAt: string; remark: string; } /* ───── 탭 정의 ───── */ const TABS = [ { key: "all", label: "전체" }, { key: "process", label: "공정검사" }, { key: "incoming", label: "입고검사" }, { key: "shipping", label: "출하검사" }, ] as const; type TabKey = (typeof TABS)[number]["key"]; /* ───── 유틸 ───── */ const fmt = (n: number) => n.toLocaleString("ko-KR"); const pct = (n: number) => `${n.toFixed(1)}%`; const badgeVariant = (type: "result" | "type" | "defectRate", value: string | number) => { if (type === "result") { if (value === "합격") return "bg-emerald-100 text-emerald-700 border-emerald-200"; if (value === "불합격") return "bg-red-100 text-red-700 border-red-200"; return "bg-amber-100 text-amber-700 border-amber-200"; } if (type === "type") { if (value === "공정검사") return "bg-purple-100 text-purple-700 border-purple-200"; if (value === "입고검사") return "bg-blue-100 text-blue-700 border-blue-200"; return "bg-emerald-100 text-emerald-700 border-emerald-200"; } // defectRate const rate = typeof value === "number" ? value : parseFloat(String(value)); if (rate > 3) return "text-red-600 font-semibold"; if (rate >= 1) return "text-amber-600 font-semibold"; return "text-emerald-600"; }; /* ───── 컴포넌트 ───── */ export default function QualityMonitoringPage() { const { settings } = useMonitoringSettings("quality"); const theme = getMonitoringTheme(settings.theme); const openTab = useTabStore((s) => s.openTab); const tc = settings.tableColumns; const [processData, setProcessData] = useState([]); const [loading, setLoading] = useState(false); const [currentTime, setCurrentTime] = useState(new Date()); const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [activeTab, setActiveTab] = useState("all"); const intervalRef = useRef | null>(null); /* ───── 시계 ───── */ useEffect(() => { const timer = setInterval(() => setCurrentTime(new Date()), 1000); return () => clearInterval(timer); }, []); /* ───── 데이터 조회 ───── */ const fetchData = useCallback(async () => { setLoading(true); try { const res = await apiClient.post("/table-management/tables/work_order_process/data", { autoFilter: true }); const rows: ProcessRow[] = res.data?.data?.rows ?? res.data?.rows ?? []; setProcessData(rows); } catch (err) { console.error("품질점검현황 데이터 조회 실패:", err); } finally { setLoading(false); } }, []); useEffect(() => { fetchData(); }, [fetchData]); /* ───── 자동 갱신 ───── */ useEffect(() => { if (autoRefresh) { intervalRef.current = setInterval(fetchData, settings.refreshInterval * 1000); } return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [autoRefresh, fetchData, settings.refreshInterval]); /* ───── 검사 행 변환 ───── */ const inspectionRows: InspectionRow[] = useMemo(() => { const today = new Date().toISOString().slice(0, 10); return processData .filter((r) => { // 금일 데이터만 const dt = r.completed_at || r.started_at || ""; return dt.slice(0, 10) === today; }) .map((r, idx) => { const inspQty = r.input_qty || r.plan_qty || 0; const goodQty = r.good_qty ?? 0; const defectQty = r.defect_qty ?? 0; const defectRate = inspQty > 0 ? (defectQty / inspQty) * 100 : 0; const result: InspectionRow["result"] = r.status !== "completed" ? "대기" : defectQty > 0 ? "불합격" : "합격"; return { no: idx + 1, inspectionNo: `QC-${String(r.id).padStart(8, "0").slice(0, 8)}`, inspectionType: "공정검사", itemName: r.process_name || "-", spec: r.process_code || "-", inspectionQty: inspQty, goodQty, defectQty, defectRate, result, inspectorName: r.worker_name || "-", inspectedAt: r.completed_at || r.started_at || "-", remark: "", }; }); }, [processData]); /* ───── 탭 필터링 ───── */ const filteredRows = useMemo(() => { if (activeTab === "all" || activeTab === "process") return inspectionRows; // 입고/출하는 데이터 없음 return []; }, [activeTab, inspectionRows]); /* ───── 요약 통계 ───── */ const summary = useMemo(() => { const total = inspectionRows.length; const passed = inspectionRows.filter((r) => r.result === "합격").length; const failed = inspectionRows.filter((r) => r.result === "불합격").length; const pending = inspectionRows.filter((r) => r.result === "대기").length; const passRate = total > 0 ? (passed / total) * 100 : 0; return { total, passed, failed, pending, passRate }; }, [inspectionRows]); /* ───── 요약 카드 정의 ───── */ const summaryCards = [ { label: "금일 검사건수", value: fmt(summary.total), sub: "건", color: "from-slate-500 to-slate-600", textColor: "text-white", }, { label: "합격", value: fmt(summary.passed), sub: "건", color: "from-emerald-500 to-emerald-600", textColor: "text-white", }, { label: "불합격", value: fmt(summary.failed), sub: "건", color: "from-red-500 to-red-600", textColor: "text-white", }, { label: "검사대기", value: fmt(summary.pending), sub: "건", color: "from-amber-500 to-amber-600", textColor: "text-white", }, { label: "합격률", value: pct(summary.passRate), sub: "", color: "from-purple-500 to-purple-600", textColor: "text-white", }, ]; /* ───── 렌더링 ───── */ return (
{/* ── 헤더 ── */}

품질점검현황 모니터링

{currentTime.toLocaleString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, })}
{/* ── 본문 ── */}
{/* 요약 카드 */}
{summaryCards.map((card) => (

{card.label}

{card.value} {card.sub && {card.sub}}

))}
{/* 검사유형 탭 */}
{TABS.map((tab) => ( ))}
{/* 테이블 영역 */}
{/* 입고/출하 준비중 */} {activeTab === "incoming" || activeTab === "shipping" ? (

준비중

{activeTab === "incoming" ? "입고검사" : "출하검사"} 데이터는 아직 지원되지 않습니다.

) : loading && filteredRows.length === 0 ? (

데이터를 불러오는 중...

) : filteredRows.length === 0 ? (

금일 검사 데이터가 없습니다

) : (
No {tc.inspectionNo && 검사번호} {tc.inspectionType && 검사유형} {tc.itemName && 품목명} {tc.spec && 규격} {tc.inspectionQty && 검사수량} {tc.passFailQty && 합격수량} {tc.passFailQty && 불합격수량} {tc.defectRate && 불량율} {tc.resultBar && 검사결과} {tc.judgment && 판정} {tc.inspector && 검사자} {tc.inspectedAt && 검사일시} {tc.inspectionCriteria && 검사기준} {filteredRows.map((row) => { const goodPct = row.inspectionQty > 0 ? (row.goodQty / row.inspectionQty) * 100 : 0; const defectPct = row.inspectionQty > 0 ? (row.defectQty / row.inspectionQty) * 100 : 0; return ( {row.no} {tc.inspectionNo && {row.inspectionNo}} {tc.inspectionType && ( {row.inspectionType} )} {tc.itemName && {row.itemName}} {tc.spec && {row.spec}} {tc.inspectionQty && ( {fmt(row.inspectionQty)} )} {tc.passFailQty && ( {fmt(row.goodQty)} )} {tc.passFailQty && ( {fmt(row.defectQty)} )} {tc.defectRate && ( {pct(row.defectRate)} )} {tc.resultBar && (
{pct(goodPct)}
)} {tc.judgment && ( {row.result} )} {tc.inspector && {row.inspectorName}} {tc.inspectedAt && ( {row.inspectedAt !== "-" ? new Date(row.inspectedAt).toLocaleString("ko-KR", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, }) : "-"} )} {tc.inspectionCriteria && -} ); })}
)}
); }