From 4424071e472b3bef8fbc5eba49400ca989fda025 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 9 Apr 2026 15:46:22 +0900 Subject: [PATCH] feat: Enhance equipment monitoring page with process tracking - Added `wi_id` and `work_instruction_no` fields to the `WorkInstruction` interface for better tracking of work instructions. - Introduced a new `ProcessRow` interface to manage work order process statuses, including acceptable, in progress, and completed states. - Updated data fetching logic to include process data from the work order process API, improving the accuracy of equipment status determination. - Enhanced the inferred status logic to utilize process data for more accurate equipment status representation. - Refined summary statistics and filtering mechanisms to reflect the new process tracking capabilities. These changes aim to provide a more comprehensive and accurate monitoring experience for equipment operations across multiple companies. --- .../COMPANY_10/monitoring/equipment/page.tsx | 148 +++++++++++++----- .../COMPANY_16/monitoring/equipment/page.tsx | 148 +++++++++++++----- .../COMPANY_29/monitoring/equipment/page.tsx | 148 +++++++++++++----- .../COMPANY_30/monitoring/equipment/page.tsx | 148 +++++++++++++----- .../COMPANY_7/monitoring/equipment/page.tsx | 148 +++++++++++++----- .../COMPANY_8/monitoring/equipment/page.tsx | 148 +++++++++++++----- .../COMPANY_9/monitoring/equipment/page.tsx | 148 +++++++++++++----- 7 files changed, 784 insertions(+), 252 deletions(-) diff --git a/frontend/app/(main)/COMPANY_10/monitoring/equipment/page.tsx b/frontend/app/(main)/COMPANY_10/monitoring/equipment/page.tsx index 14adae20..b165ff65 100644 --- a/frontend/app/(main)/COMPANY_10/monitoring/equipment/page.tsx +++ b/frontend/app/(main)/COMPANY_10/monitoring/equipment/page.tsx @@ -118,11 +118,20 @@ interface Equipment { interface WorkInstruction { id: string; + wi_id?: string; instruction_number: string; + work_instruction_no?: string; item_name: string; equipment_id: string; worker_name: string; status: string; + progress_status?: string; +} + +interface ProcessRow { + wo_id: string; + status: string; // acceptable / in_progress / completed + parent_process_id?: string | null; } /* ───── 컴포넌트 ───── */ @@ -135,6 +144,7 @@ export default function EquipmentMonitoringPage() { const [equipments, setEquipments] = useState([]); const [workInstructions, setWorkInstructions] = useState([]); + const [processRows, setProcessRows] = useState([]); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); @@ -156,20 +166,28 @@ export default function EquipmentMonitoringPage() { const fetchData = useCallback(async () => { try { setLoading(true); - const [equipRes, wiRes] = await Promise.all([ + const [equipRes, wiRes, procRes] = await Promise.all([ apiClient.post("/table-management/tables/equipment_mng/data", { autoFilter: true, page: 1, size: 500, }), apiClient.get("/work-instruction/list").catch(() => ({ data: { data: [] } })), + apiClient.post("/table-management/tables/work_order_process/data", { + page: 1, + size: 2000, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), ]); - const eqRows: Equipment[] = equipRes.data?.data?.rows ?? equipRes.data?.rows ?? []; + const eqRows: Equipment[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? []; setEquipments(eqRows); const wiRows: WorkInstruction[] = wiRes.data?.data ?? wiRes.data?.rows ?? []; setWorkInstructions(wiRows); + + const pRows: ProcessRow[] = procRes.data?.data?.data ?? procRes.data?.data?.rows ?? []; + setProcessRows(pRows); } catch (err) { console.error("설비 모니터링 데이터 조회 실패:", err); } finally { @@ -189,49 +207,107 @@ export default function EquipmentMonitoringPage() { return () => clearInterval(interval); }, [fetchData, settings.refreshInterval]); - /* ── 요약 통계 ── */ - const stats = useMemo(() => { - const counts: Record = { - running: 0, - idle: 0, - maintenance: 0, - off: 0, - unknown: 0, - }; - equipments.forEach((eq) => { - const s = resolveStatus(eq.operation_status); - counts[s]++; - }); - return { total: equipments.length, ...counts }; - }, [equipments]); - - /* ── 필터된 설비 ── */ - const filteredEquipments = useMemo(() => { - if (filterStatus === "all") return equipments; - return equipments.filter((eq) => resolveStatus(eq.operation_status) === filterStatus); - }, [equipments, filterStatus]); /* ── 설비별 작업지시 맵 ── */ const wiMap = useMemo(() => { const map: Record = {}; workInstructions.forEach((wi) => { - if (wi.equipment_id) { - if (!map[wi.equipment_id]) map[wi.equipment_id] = []; - map[wi.equipment_id].push(wi); + const eqId = wi.equipment_id; + if (eqId) { + if (!map[eqId]) map[eqId] = []; + map[eqId].push(wi); } }); return map; }, [workInstructions]); - /* ── 가동률 (모킹 — 센서 미연동) ── */ - const getUtilization = (eq: Equipment): number | null => { - const s = resolveStatus(eq.operation_status); - if (s === "running") return 75 + Math.floor(Math.random() * 20); // 75~94 - if (s === "idle") return 20 + Math.floor(Math.random() * 30); // 20~49 - if (s === "maintenance") return 0; - if (s === "off") return 0; - return null; - }; + /* ── 설비 상태 자동 판단: 공정 데이터 기반 ── */ + const inferredStatus = useMemo(() => { + // 작업지시 ID → 설비 ID 매핑 + const wiToEquip: Record = {}; + workInstructions.forEach((wi) => { + const wiId = wi.wi_id || wi.id; + if (wi.equipment_id) wiToEquip[wiId] = wi.equipment_id; + }); + + // 설비별 공정 상태 집계 + const equipProcessStatus: Record> = {}; + processRows.forEach((p) => { + const eqId = wiToEquip[p.wo_id]; + if (!eqId) return; + if (!equipProcessStatus[eqId]) equipProcessStatus[eqId] = new Set(); + equipProcessStatus[eqId].add(p.status); + }); + + // 설비별 상태 판단 + const result: Record = {}; + equipments.forEach((eq) => { + const dbStatus = resolveStatus(eq.operation_status); + // DB에 점검/수리가 명시되어 있으면 그대로 사용 + if (dbStatus === "maintenance") { + result[eq.id] = "maintenance"; + return; + } + + const statuses = equipProcessStatus[eq.id]; + if (statuses) { + if (statuses.has("in_progress")) { + result[eq.id] = "running"; // 진행중 공정 있음 → 가동중 + } else if (statuses.has("acceptable")) { + result[eq.id] = "idle"; // 접수 대기 공정 있음 → 대기 + } else { + // 전부 completed → DB 상태 사용 + result[eq.id] = dbStatus !== "unknown" ? dbStatus : "idle"; + } + } else { + // 공정 데이터 없음 → 작업지시 여부로 판단 + const eqWIs = wiMap[eq.id]; + if (eqWIs && eqWIs.length > 0) { + result[eq.id] = "idle"; // 작업지시 배정됨 → 대기 + } else { + result[eq.id] = dbStatus !== "unknown" ? dbStatus : "off"; + } + } + }); + return result; + }, [equipments, workInstructions, processRows, wiMap]); + + /* ── 요약 통계 (추론 상태 기반) ── */ + const stats = useMemo(() => { + const counts: Record = { running: 0, idle: 0, maintenance: 0, off: 0, unknown: 0 }; + equipments.forEach((eq) => { + counts[inferredStatus[eq.id] ?? "unknown"]++; + }); + return { total: equipments.length, ...counts }; + }, [equipments, inferredStatus]); + + /* ── 필터된 설비 ── */ + const filteredEquipments = useMemo(() => { + if (filterStatus === "all") return equipments; + return equipments.filter((eq) => (inferredStatus[eq.id] ?? "unknown") === filterStatus); + }, [equipments, filterStatus, inferredStatus]); + + /* ── 가동률 (센서 미연동 — 상태 기반 고정값) ── */ + const utilizationMap = useMemo(() => { + const map: Record = {}; + // 설비 ID를 해시하여 상태별 고정 범위 내 값 생성 (리렌더링해도 안 변함) + const hash = (id: string) => { + let h = 0; + for (let i = 0; i < id.length; i++) h = ((h << 5) - h + id.charCodeAt(i)) | 0; + return Math.abs(h); + }; + equipments.forEach((eq) => { + const s = inferredStatus[eq.id] ?? "unknown"; + const h = hash(eq.id); + if (s === "running") map[eq.id] = 75 + (h % 20); // 75~94 고정 + else if (s === "idle") map[eq.id] = 20 + (h % 30); // 20~49 고정 + else if (s === "maintenance") map[eq.id] = 0; + else if (s === "off") map[eq.id] = 0; + else map[eq.id] = null; + }); + return map; + }, [equipments, inferredStatus]); + const getUtilization = (eq: Equipment): number | null => utilizationMap[eq.id] ?? null; /* ── 요약 카드 배열 ── */ const summaryCards: { @@ -430,7 +506,7 @@ export default function EquipmentMonitoringPage() { {filteredEquipments.length > 0 && (
{filteredEquipments.map((eq) => { - const status = resolveStatus(eq.operation_status); + const status = inferredStatus[eq.id] ?? "unknown"; const cfg = STATUS_MAP[status]; const utilization = getUtilization(eq); const eqWIs = wiMap[eq.id] ?? []; diff --git a/frontend/app/(main)/COMPANY_16/monitoring/equipment/page.tsx b/frontend/app/(main)/COMPANY_16/monitoring/equipment/page.tsx index 14adae20..b165ff65 100644 --- a/frontend/app/(main)/COMPANY_16/monitoring/equipment/page.tsx +++ b/frontend/app/(main)/COMPANY_16/monitoring/equipment/page.tsx @@ -118,11 +118,20 @@ interface Equipment { interface WorkInstruction { id: string; + wi_id?: string; instruction_number: string; + work_instruction_no?: string; item_name: string; equipment_id: string; worker_name: string; status: string; + progress_status?: string; +} + +interface ProcessRow { + wo_id: string; + status: string; // acceptable / in_progress / completed + parent_process_id?: string | null; } /* ───── 컴포넌트 ───── */ @@ -135,6 +144,7 @@ export default function EquipmentMonitoringPage() { const [equipments, setEquipments] = useState([]); const [workInstructions, setWorkInstructions] = useState([]); + const [processRows, setProcessRows] = useState([]); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); @@ -156,20 +166,28 @@ export default function EquipmentMonitoringPage() { const fetchData = useCallback(async () => { try { setLoading(true); - const [equipRes, wiRes] = await Promise.all([ + const [equipRes, wiRes, procRes] = await Promise.all([ apiClient.post("/table-management/tables/equipment_mng/data", { autoFilter: true, page: 1, size: 500, }), apiClient.get("/work-instruction/list").catch(() => ({ data: { data: [] } })), + apiClient.post("/table-management/tables/work_order_process/data", { + page: 1, + size: 2000, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), ]); - const eqRows: Equipment[] = equipRes.data?.data?.rows ?? equipRes.data?.rows ?? []; + const eqRows: Equipment[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? []; setEquipments(eqRows); const wiRows: WorkInstruction[] = wiRes.data?.data ?? wiRes.data?.rows ?? []; setWorkInstructions(wiRows); + + const pRows: ProcessRow[] = procRes.data?.data?.data ?? procRes.data?.data?.rows ?? []; + setProcessRows(pRows); } catch (err) { console.error("설비 모니터링 데이터 조회 실패:", err); } finally { @@ -189,49 +207,107 @@ export default function EquipmentMonitoringPage() { return () => clearInterval(interval); }, [fetchData, settings.refreshInterval]); - /* ── 요약 통계 ── */ - const stats = useMemo(() => { - const counts: Record = { - running: 0, - idle: 0, - maintenance: 0, - off: 0, - unknown: 0, - }; - equipments.forEach((eq) => { - const s = resolveStatus(eq.operation_status); - counts[s]++; - }); - return { total: equipments.length, ...counts }; - }, [equipments]); - - /* ── 필터된 설비 ── */ - const filteredEquipments = useMemo(() => { - if (filterStatus === "all") return equipments; - return equipments.filter((eq) => resolveStatus(eq.operation_status) === filterStatus); - }, [equipments, filterStatus]); /* ── 설비별 작업지시 맵 ── */ const wiMap = useMemo(() => { const map: Record = {}; workInstructions.forEach((wi) => { - if (wi.equipment_id) { - if (!map[wi.equipment_id]) map[wi.equipment_id] = []; - map[wi.equipment_id].push(wi); + const eqId = wi.equipment_id; + if (eqId) { + if (!map[eqId]) map[eqId] = []; + map[eqId].push(wi); } }); return map; }, [workInstructions]); - /* ── 가동률 (모킹 — 센서 미연동) ── */ - const getUtilization = (eq: Equipment): number | null => { - const s = resolveStatus(eq.operation_status); - if (s === "running") return 75 + Math.floor(Math.random() * 20); // 75~94 - if (s === "idle") return 20 + Math.floor(Math.random() * 30); // 20~49 - if (s === "maintenance") return 0; - if (s === "off") return 0; - return null; - }; + /* ── 설비 상태 자동 판단: 공정 데이터 기반 ── */ + const inferredStatus = useMemo(() => { + // 작업지시 ID → 설비 ID 매핑 + const wiToEquip: Record = {}; + workInstructions.forEach((wi) => { + const wiId = wi.wi_id || wi.id; + if (wi.equipment_id) wiToEquip[wiId] = wi.equipment_id; + }); + + // 설비별 공정 상태 집계 + const equipProcessStatus: Record> = {}; + processRows.forEach((p) => { + const eqId = wiToEquip[p.wo_id]; + if (!eqId) return; + if (!equipProcessStatus[eqId]) equipProcessStatus[eqId] = new Set(); + equipProcessStatus[eqId].add(p.status); + }); + + // 설비별 상태 판단 + const result: Record = {}; + equipments.forEach((eq) => { + const dbStatus = resolveStatus(eq.operation_status); + // DB에 점검/수리가 명시되어 있으면 그대로 사용 + if (dbStatus === "maintenance") { + result[eq.id] = "maintenance"; + return; + } + + const statuses = equipProcessStatus[eq.id]; + if (statuses) { + if (statuses.has("in_progress")) { + result[eq.id] = "running"; // 진행중 공정 있음 → 가동중 + } else if (statuses.has("acceptable")) { + result[eq.id] = "idle"; // 접수 대기 공정 있음 → 대기 + } else { + // 전부 completed → DB 상태 사용 + result[eq.id] = dbStatus !== "unknown" ? dbStatus : "idle"; + } + } else { + // 공정 데이터 없음 → 작업지시 여부로 판단 + const eqWIs = wiMap[eq.id]; + if (eqWIs && eqWIs.length > 0) { + result[eq.id] = "idle"; // 작업지시 배정됨 → 대기 + } else { + result[eq.id] = dbStatus !== "unknown" ? dbStatus : "off"; + } + } + }); + return result; + }, [equipments, workInstructions, processRows, wiMap]); + + /* ── 요약 통계 (추론 상태 기반) ── */ + const stats = useMemo(() => { + const counts: Record = { running: 0, idle: 0, maintenance: 0, off: 0, unknown: 0 }; + equipments.forEach((eq) => { + counts[inferredStatus[eq.id] ?? "unknown"]++; + }); + return { total: equipments.length, ...counts }; + }, [equipments, inferredStatus]); + + /* ── 필터된 설비 ── */ + const filteredEquipments = useMemo(() => { + if (filterStatus === "all") return equipments; + return equipments.filter((eq) => (inferredStatus[eq.id] ?? "unknown") === filterStatus); + }, [equipments, filterStatus, inferredStatus]); + + /* ── 가동률 (센서 미연동 — 상태 기반 고정값) ── */ + const utilizationMap = useMemo(() => { + const map: Record = {}; + // 설비 ID를 해시하여 상태별 고정 범위 내 값 생성 (리렌더링해도 안 변함) + const hash = (id: string) => { + let h = 0; + for (let i = 0; i < id.length; i++) h = ((h << 5) - h + id.charCodeAt(i)) | 0; + return Math.abs(h); + }; + equipments.forEach((eq) => { + const s = inferredStatus[eq.id] ?? "unknown"; + const h = hash(eq.id); + if (s === "running") map[eq.id] = 75 + (h % 20); // 75~94 고정 + else if (s === "idle") map[eq.id] = 20 + (h % 30); // 20~49 고정 + else if (s === "maintenance") map[eq.id] = 0; + else if (s === "off") map[eq.id] = 0; + else map[eq.id] = null; + }); + return map; + }, [equipments, inferredStatus]); + const getUtilization = (eq: Equipment): number | null => utilizationMap[eq.id] ?? null; /* ── 요약 카드 배열 ── */ const summaryCards: { @@ -430,7 +506,7 @@ export default function EquipmentMonitoringPage() { {filteredEquipments.length > 0 && (
{filteredEquipments.map((eq) => { - const status = resolveStatus(eq.operation_status); + const status = inferredStatus[eq.id] ?? "unknown"; const cfg = STATUS_MAP[status]; const utilization = getUtilization(eq); const eqWIs = wiMap[eq.id] ?? []; diff --git a/frontend/app/(main)/COMPANY_29/monitoring/equipment/page.tsx b/frontend/app/(main)/COMPANY_29/monitoring/equipment/page.tsx index 14adae20..b165ff65 100644 --- a/frontend/app/(main)/COMPANY_29/monitoring/equipment/page.tsx +++ b/frontend/app/(main)/COMPANY_29/monitoring/equipment/page.tsx @@ -118,11 +118,20 @@ interface Equipment { interface WorkInstruction { id: string; + wi_id?: string; instruction_number: string; + work_instruction_no?: string; item_name: string; equipment_id: string; worker_name: string; status: string; + progress_status?: string; +} + +interface ProcessRow { + wo_id: string; + status: string; // acceptable / in_progress / completed + parent_process_id?: string | null; } /* ───── 컴포넌트 ───── */ @@ -135,6 +144,7 @@ export default function EquipmentMonitoringPage() { const [equipments, setEquipments] = useState([]); const [workInstructions, setWorkInstructions] = useState([]); + const [processRows, setProcessRows] = useState([]); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); @@ -156,20 +166,28 @@ export default function EquipmentMonitoringPage() { const fetchData = useCallback(async () => { try { setLoading(true); - const [equipRes, wiRes] = await Promise.all([ + const [equipRes, wiRes, procRes] = await Promise.all([ apiClient.post("/table-management/tables/equipment_mng/data", { autoFilter: true, page: 1, size: 500, }), apiClient.get("/work-instruction/list").catch(() => ({ data: { data: [] } })), + apiClient.post("/table-management/tables/work_order_process/data", { + page: 1, + size: 2000, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), ]); - const eqRows: Equipment[] = equipRes.data?.data?.rows ?? equipRes.data?.rows ?? []; + const eqRows: Equipment[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? []; setEquipments(eqRows); const wiRows: WorkInstruction[] = wiRes.data?.data ?? wiRes.data?.rows ?? []; setWorkInstructions(wiRows); + + const pRows: ProcessRow[] = procRes.data?.data?.data ?? procRes.data?.data?.rows ?? []; + setProcessRows(pRows); } catch (err) { console.error("설비 모니터링 데이터 조회 실패:", err); } finally { @@ -189,49 +207,107 @@ export default function EquipmentMonitoringPage() { return () => clearInterval(interval); }, [fetchData, settings.refreshInterval]); - /* ── 요약 통계 ── */ - const stats = useMemo(() => { - const counts: Record = { - running: 0, - idle: 0, - maintenance: 0, - off: 0, - unknown: 0, - }; - equipments.forEach((eq) => { - const s = resolveStatus(eq.operation_status); - counts[s]++; - }); - return { total: equipments.length, ...counts }; - }, [equipments]); - - /* ── 필터된 설비 ── */ - const filteredEquipments = useMemo(() => { - if (filterStatus === "all") return equipments; - return equipments.filter((eq) => resolveStatus(eq.operation_status) === filterStatus); - }, [equipments, filterStatus]); /* ── 설비별 작업지시 맵 ── */ const wiMap = useMemo(() => { const map: Record = {}; workInstructions.forEach((wi) => { - if (wi.equipment_id) { - if (!map[wi.equipment_id]) map[wi.equipment_id] = []; - map[wi.equipment_id].push(wi); + const eqId = wi.equipment_id; + if (eqId) { + if (!map[eqId]) map[eqId] = []; + map[eqId].push(wi); } }); return map; }, [workInstructions]); - /* ── 가동률 (모킹 — 센서 미연동) ── */ - const getUtilization = (eq: Equipment): number | null => { - const s = resolveStatus(eq.operation_status); - if (s === "running") return 75 + Math.floor(Math.random() * 20); // 75~94 - if (s === "idle") return 20 + Math.floor(Math.random() * 30); // 20~49 - if (s === "maintenance") return 0; - if (s === "off") return 0; - return null; - }; + /* ── 설비 상태 자동 판단: 공정 데이터 기반 ── */ + const inferredStatus = useMemo(() => { + // 작업지시 ID → 설비 ID 매핑 + const wiToEquip: Record = {}; + workInstructions.forEach((wi) => { + const wiId = wi.wi_id || wi.id; + if (wi.equipment_id) wiToEquip[wiId] = wi.equipment_id; + }); + + // 설비별 공정 상태 집계 + const equipProcessStatus: Record> = {}; + processRows.forEach((p) => { + const eqId = wiToEquip[p.wo_id]; + if (!eqId) return; + if (!equipProcessStatus[eqId]) equipProcessStatus[eqId] = new Set(); + equipProcessStatus[eqId].add(p.status); + }); + + // 설비별 상태 판단 + const result: Record = {}; + equipments.forEach((eq) => { + const dbStatus = resolveStatus(eq.operation_status); + // DB에 점검/수리가 명시되어 있으면 그대로 사용 + if (dbStatus === "maintenance") { + result[eq.id] = "maintenance"; + return; + } + + const statuses = equipProcessStatus[eq.id]; + if (statuses) { + if (statuses.has("in_progress")) { + result[eq.id] = "running"; // 진행중 공정 있음 → 가동중 + } else if (statuses.has("acceptable")) { + result[eq.id] = "idle"; // 접수 대기 공정 있음 → 대기 + } else { + // 전부 completed → DB 상태 사용 + result[eq.id] = dbStatus !== "unknown" ? dbStatus : "idle"; + } + } else { + // 공정 데이터 없음 → 작업지시 여부로 판단 + const eqWIs = wiMap[eq.id]; + if (eqWIs && eqWIs.length > 0) { + result[eq.id] = "idle"; // 작업지시 배정됨 → 대기 + } else { + result[eq.id] = dbStatus !== "unknown" ? dbStatus : "off"; + } + } + }); + return result; + }, [equipments, workInstructions, processRows, wiMap]); + + /* ── 요약 통계 (추론 상태 기반) ── */ + const stats = useMemo(() => { + const counts: Record = { running: 0, idle: 0, maintenance: 0, off: 0, unknown: 0 }; + equipments.forEach((eq) => { + counts[inferredStatus[eq.id] ?? "unknown"]++; + }); + return { total: equipments.length, ...counts }; + }, [equipments, inferredStatus]); + + /* ── 필터된 설비 ── */ + const filteredEquipments = useMemo(() => { + if (filterStatus === "all") return equipments; + return equipments.filter((eq) => (inferredStatus[eq.id] ?? "unknown") === filterStatus); + }, [equipments, filterStatus, inferredStatus]); + + /* ── 가동률 (센서 미연동 — 상태 기반 고정값) ── */ + const utilizationMap = useMemo(() => { + const map: Record = {}; + // 설비 ID를 해시하여 상태별 고정 범위 내 값 생성 (리렌더링해도 안 변함) + const hash = (id: string) => { + let h = 0; + for (let i = 0; i < id.length; i++) h = ((h << 5) - h + id.charCodeAt(i)) | 0; + return Math.abs(h); + }; + equipments.forEach((eq) => { + const s = inferredStatus[eq.id] ?? "unknown"; + const h = hash(eq.id); + if (s === "running") map[eq.id] = 75 + (h % 20); // 75~94 고정 + else if (s === "idle") map[eq.id] = 20 + (h % 30); // 20~49 고정 + else if (s === "maintenance") map[eq.id] = 0; + else if (s === "off") map[eq.id] = 0; + else map[eq.id] = null; + }); + return map; + }, [equipments, inferredStatus]); + const getUtilization = (eq: Equipment): number | null => utilizationMap[eq.id] ?? null; /* ── 요약 카드 배열 ── */ const summaryCards: { @@ -430,7 +506,7 @@ export default function EquipmentMonitoringPage() { {filteredEquipments.length > 0 && (
{filteredEquipments.map((eq) => { - const status = resolveStatus(eq.operation_status); + const status = inferredStatus[eq.id] ?? "unknown"; const cfg = STATUS_MAP[status]; const utilization = getUtilization(eq); const eqWIs = wiMap[eq.id] ?? []; diff --git a/frontend/app/(main)/COMPANY_30/monitoring/equipment/page.tsx b/frontend/app/(main)/COMPANY_30/monitoring/equipment/page.tsx index 14adae20..b165ff65 100644 --- a/frontend/app/(main)/COMPANY_30/monitoring/equipment/page.tsx +++ b/frontend/app/(main)/COMPANY_30/monitoring/equipment/page.tsx @@ -118,11 +118,20 @@ interface Equipment { interface WorkInstruction { id: string; + wi_id?: string; instruction_number: string; + work_instruction_no?: string; item_name: string; equipment_id: string; worker_name: string; status: string; + progress_status?: string; +} + +interface ProcessRow { + wo_id: string; + status: string; // acceptable / in_progress / completed + parent_process_id?: string | null; } /* ───── 컴포넌트 ───── */ @@ -135,6 +144,7 @@ export default function EquipmentMonitoringPage() { const [equipments, setEquipments] = useState([]); const [workInstructions, setWorkInstructions] = useState([]); + const [processRows, setProcessRows] = useState([]); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); @@ -156,20 +166,28 @@ export default function EquipmentMonitoringPage() { const fetchData = useCallback(async () => { try { setLoading(true); - const [equipRes, wiRes] = await Promise.all([ + const [equipRes, wiRes, procRes] = await Promise.all([ apiClient.post("/table-management/tables/equipment_mng/data", { autoFilter: true, page: 1, size: 500, }), apiClient.get("/work-instruction/list").catch(() => ({ data: { data: [] } })), + apiClient.post("/table-management/tables/work_order_process/data", { + page: 1, + size: 2000, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), ]); - const eqRows: Equipment[] = equipRes.data?.data?.rows ?? equipRes.data?.rows ?? []; + const eqRows: Equipment[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? []; setEquipments(eqRows); const wiRows: WorkInstruction[] = wiRes.data?.data ?? wiRes.data?.rows ?? []; setWorkInstructions(wiRows); + + const pRows: ProcessRow[] = procRes.data?.data?.data ?? procRes.data?.data?.rows ?? []; + setProcessRows(pRows); } catch (err) { console.error("설비 모니터링 데이터 조회 실패:", err); } finally { @@ -189,49 +207,107 @@ export default function EquipmentMonitoringPage() { return () => clearInterval(interval); }, [fetchData, settings.refreshInterval]); - /* ── 요약 통계 ── */ - const stats = useMemo(() => { - const counts: Record = { - running: 0, - idle: 0, - maintenance: 0, - off: 0, - unknown: 0, - }; - equipments.forEach((eq) => { - const s = resolveStatus(eq.operation_status); - counts[s]++; - }); - return { total: equipments.length, ...counts }; - }, [equipments]); - - /* ── 필터된 설비 ── */ - const filteredEquipments = useMemo(() => { - if (filterStatus === "all") return equipments; - return equipments.filter((eq) => resolveStatus(eq.operation_status) === filterStatus); - }, [equipments, filterStatus]); /* ── 설비별 작업지시 맵 ── */ const wiMap = useMemo(() => { const map: Record = {}; workInstructions.forEach((wi) => { - if (wi.equipment_id) { - if (!map[wi.equipment_id]) map[wi.equipment_id] = []; - map[wi.equipment_id].push(wi); + const eqId = wi.equipment_id; + if (eqId) { + if (!map[eqId]) map[eqId] = []; + map[eqId].push(wi); } }); return map; }, [workInstructions]); - /* ── 가동률 (모킹 — 센서 미연동) ── */ - const getUtilization = (eq: Equipment): number | null => { - const s = resolveStatus(eq.operation_status); - if (s === "running") return 75 + Math.floor(Math.random() * 20); // 75~94 - if (s === "idle") return 20 + Math.floor(Math.random() * 30); // 20~49 - if (s === "maintenance") return 0; - if (s === "off") return 0; - return null; - }; + /* ── 설비 상태 자동 판단: 공정 데이터 기반 ── */ + const inferredStatus = useMemo(() => { + // 작업지시 ID → 설비 ID 매핑 + const wiToEquip: Record = {}; + workInstructions.forEach((wi) => { + const wiId = wi.wi_id || wi.id; + if (wi.equipment_id) wiToEquip[wiId] = wi.equipment_id; + }); + + // 설비별 공정 상태 집계 + const equipProcessStatus: Record> = {}; + processRows.forEach((p) => { + const eqId = wiToEquip[p.wo_id]; + if (!eqId) return; + if (!equipProcessStatus[eqId]) equipProcessStatus[eqId] = new Set(); + equipProcessStatus[eqId].add(p.status); + }); + + // 설비별 상태 판단 + const result: Record = {}; + equipments.forEach((eq) => { + const dbStatus = resolveStatus(eq.operation_status); + // DB에 점검/수리가 명시되어 있으면 그대로 사용 + if (dbStatus === "maintenance") { + result[eq.id] = "maintenance"; + return; + } + + const statuses = equipProcessStatus[eq.id]; + if (statuses) { + if (statuses.has("in_progress")) { + result[eq.id] = "running"; // 진행중 공정 있음 → 가동중 + } else if (statuses.has("acceptable")) { + result[eq.id] = "idle"; // 접수 대기 공정 있음 → 대기 + } else { + // 전부 completed → DB 상태 사용 + result[eq.id] = dbStatus !== "unknown" ? dbStatus : "idle"; + } + } else { + // 공정 데이터 없음 → 작업지시 여부로 판단 + const eqWIs = wiMap[eq.id]; + if (eqWIs && eqWIs.length > 0) { + result[eq.id] = "idle"; // 작업지시 배정됨 → 대기 + } else { + result[eq.id] = dbStatus !== "unknown" ? dbStatus : "off"; + } + } + }); + return result; + }, [equipments, workInstructions, processRows, wiMap]); + + /* ── 요약 통계 (추론 상태 기반) ── */ + const stats = useMemo(() => { + const counts: Record = { running: 0, idle: 0, maintenance: 0, off: 0, unknown: 0 }; + equipments.forEach((eq) => { + counts[inferredStatus[eq.id] ?? "unknown"]++; + }); + return { total: equipments.length, ...counts }; + }, [equipments, inferredStatus]); + + /* ── 필터된 설비 ── */ + const filteredEquipments = useMemo(() => { + if (filterStatus === "all") return equipments; + return equipments.filter((eq) => (inferredStatus[eq.id] ?? "unknown") === filterStatus); + }, [equipments, filterStatus, inferredStatus]); + + /* ── 가동률 (센서 미연동 — 상태 기반 고정값) ── */ + const utilizationMap = useMemo(() => { + const map: Record = {}; + // 설비 ID를 해시하여 상태별 고정 범위 내 값 생성 (리렌더링해도 안 변함) + const hash = (id: string) => { + let h = 0; + for (let i = 0; i < id.length; i++) h = ((h << 5) - h + id.charCodeAt(i)) | 0; + return Math.abs(h); + }; + equipments.forEach((eq) => { + const s = inferredStatus[eq.id] ?? "unknown"; + const h = hash(eq.id); + if (s === "running") map[eq.id] = 75 + (h % 20); // 75~94 고정 + else if (s === "idle") map[eq.id] = 20 + (h % 30); // 20~49 고정 + else if (s === "maintenance") map[eq.id] = 0; + else if (s === "off") map[eq.id] = 0; + else map[eq.id] = null; + }); + return map; + }, [equipments, inferredStatus]); + const getUtilization = (eq: Equipment): number | null => utilizationMap[eq.id] ?? null; /* ── 요약 카드 배열 ── */ const summaryCards: { @@ -430,7 +506,7 @@ export default function EquipmentMonitoringPage() { {filteredEquipments.length > 0 && (
{filteredEquipments.map((eq) => { - const status = resolveStatus(eq.operation_status); + const status = inferredStatus[eq.id] ?? "unknown"; const cfg = STATUS_MAP[status]; const utilization = getUtilization(eq); const eqWIs = wiMap[eq.id] ?? []; diff --git a/frontend/app/(main)/COMPANY_7/monitoring/equipment/page.tsx b/frontend/app/(main)/COMPANY_7/monitoring/equipment/page.tsx index 14adae20..b165ff65 100644 --- a/frontend/app/(main)/COMPANY_7/monitoring/equipment/page.tsx +++ b/frontend/app/(main)/COMPANY_7/monitoring/equipment/page.tsx @@ -118,11 +118,20 @@ interface Equipment { interface WorkInstruction { id: string; + wi_id?: string; instruction_number: string; + work_instruction_no?: string; item_name: string; equipment_id: string; worker_name: string; status: string; + progress_status?: string; +} + +interface ProcessRow { + wo_id: string; + status: string; // acceptable / in_progress / completed + parent_process_id?: string | null; } /* ───── 컴포넌트 ───── */ @@ -135,6 +144,7 @@ export default function EquipmentMonitoringPage() { const [equipments, setEquipments] = useState([]); const [workInstructions, setWorkInstructions] = useState([]); + const [processRows, setProcessRows] = useState([]); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); @@ -156,20 +166,28 @@ export default function EquipmentMonitoringPage() { const fetchData = useCallback(async () => { try { setLoading(true); - const [equipRes, wiRes] = await Promise.all([ + const [equipRes, wiRes, procRes] = await Promise.all([ apiClient.post("/table-management/tables/equipment_mng/data", { autoFilter: true, page: 1, size: 500, }), apiClient.get("/work-instruction/list").catch(() => ({ data: { data: [] } })), + apiClient.post("/table-management/tables/work_order_process/data", { + page: 1, + size: 2000, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), ]); - const eqRows: Equipment[] = equipRes.data?.data?.rows ?? equipRes.data?.rows ?? []; + const eqRows: Equipment[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? []; setEquipments(eqRows); const wiRows: WorkInstruction[] = wiRes.data?.data ?? wiRes.data?.rows ?? []; setWorkInstructions(wiRows); + + const pRows: ProcessRow[] = procRes.data?.data?.data ?? procRes.data?.data?.rows ?? []; + setProcessRows(pRows); } catch (err) { console.error("설비 모니터링 데이터 조회 실패:", err); } finally { @@ -189,49 +207,107 @@ export default function EquipmentMonitoringPage() { return () => clearInterval(interval); }, [fetchData, settings.refreshInterval]); - /* ── 요약 통계 ── */ - const stats = useMemo(() => { - const counts: Record = { - running: 0, - idle: 0, - maintenance: 0, - off: 0, - unknown: 0, - }; - equipments.forEach((eq) => { - const s = resolveStatus(eq.operation_status); - counts[s]++; - }); - return { total: equipments.length, ...counts }; - }, [equipments]); - - /* ── 필터된 설비 ── */ - const filteredEquipments = useMemo(() => { - if (filterStatus === "all") return equipments; - return equipments.filter((eq) => resolveStatus(eq.operation_status) === filterStatus); - }, [equipments, filterStatus]); /* ── 설비별 작업지시 맵 ── */ const wiMap = useMemo(() => { const map: Record = {}; workInstructions.forEach((wi) => { - if (wi.equipment_id) { - if (!map[wi.equipment_id]) map[wi.equipment_id] = []; - map[wi.equipment_id].push(wi); + const eqId = wi.equipment_id; + if (eqId) { + if (!map[eqId]) map[eqId] = []; + map[eqId].push(wi); } }); return map; }, [workInstructions]); - /* ── 가동률 (모킹 — 센서 미연동) ── */ - const getUtilization = (eq: Equipment): number | null => { - const s = resolveStatus(eq.operation_status); - if (s === "running") return 75 + Math.floor(Math.random() * 20); // 75~94 - if (s === "idle") return 20 + Math.floor(Math.random() * 30); // 20~49 - if (s === "maintenance") return 0; - if (s === "off") return 0; - return null; - }; + /* ── 설비 상태 자동 판단: 공정 데이터 기반 ── */ + const inferredStatus = useMemo(() => { + // 작업지시 ID → 설비 ID 매핑 + const wiToEquip: Record = {}; + workInstructions.forEach((wi) => { + const wiId = wi.wi_id || wi.id; + if (wi.equipment_id) wiToEquip[wiId] = wi.equipment_id; + }); + + // 설비별 공정 상태 집계 + const equipProcessStatus: Record> = {}; + processRows.forEach((p) => { + const eqId = wiToEquip[p.wo_id]; + if (!eqId) return; + if (!equipProcessStatus[eqId]) equipProcessStatus[eqId] = new Set(); + equipProcessStatus[eqId].add(p.status); + }); + + // 설비별 상태 판단 + const result: Record = {}; + equipments.forEach((eq) => { + const dbStatus = resolveStatus(eq.operation_status); + // DB에 점검/수리가 명시되어 있으면 그대로 사용 + if (dbStatus === "maintenance") { + result[eq.id] = "maintenance"; + return; + } + + const statuses = equipProcessStatus[eq.id]; + if (statuses) { + if (statuses.has("in_progress")) { + result[eq.id] = "running"; // 진행중 공정 있음 → 가동중 + } else if (statuses.has("acceptable")) { + result[eq.id] = "idle"; // 접수 대기 공정 있음 → 대기 + } else { + // 전부 completed → DB 상태 사용 + result[eq.id] = dbStatus !== "unknown" ? dbStatus : "idle"; + } + } else { + // 공정 데이터 없음 → 작업지시 여부로 판단 + const eqWIs = wiMap[eq.id]; + if (eqWIs && eqWIs.length > 0) { + result[eq.id] = "idle"; // 작업지시 배정됨 → 대기 + } else { + result[eq.id] = dbStatus !== "unknown" ? dbStatus : "off"; + } + } + }); + return result; + }, [equipments, workInstructions, processRows, wiMap]); + + /* ── 요약 통계 (추론 상태 기반) ── */ + const stats = useMemo(() => { + const counts: Record = { running: 0, idle: 0, maintenance: 0, off: 0, unknown: 0 }; + equipments.forEach((eq) => { + counts[inferredStatus[eq.id] ?? "unknown"]++; + }); + return { total: equipments.length, ...counts }; + }, [equipments, inferredStatus]); + + /* ── 필터된 설비 ── */ + const filteredEquipments = useMemo(() => { + if (filterStatus === "all") return equipments; + return equipments.filter((eq) => (inferredStatus[eq.id] ?? "unknown") === filterStatus); + }, [equipments, filterStatus, inferredStatus]); + + /* ── 가동률 (센서 미연동 — 상태 기반 고정값) ── */ + const utilizationMap = useMemo(() => { + const map: Record = {}; + // 설비 ID를 해시하여 상태별 고정 범위 내 값 생성 (리렌더링해도 안 변함) + const hash = (id: string) => { + let h = 0; + for (let i = 0; i < id.length; i++) h = ((h << 5) - h + id.charCodeAt(i)) | 0; + return Math.abs(h); + }; + equipments.forEach((eq) => { + const s = inferredStatus[eq.id] ?? "unknown"; + const h = hash(eq.id); + if (s === "running") map[eq.id] = 75 + (h % 20); // 75~94 고정 + else if (s === "idle") map[eq.id] = 20 + (h % 30); // 20~49 고정 + else if (s === "maintenance") map[eq.id] = 0; + else if (s === "off") map[eq.id] = 0; + else map[eq.id] = null; + }); + return map; + }, [equipments, inferredStatus]); + const getUtilization = (eq: Equipment): number | null => utilizationMap[eq.id] ?? null; /* ── 요약 카드 배열 ── */ const summaryCards: { @@ -430,7 +506,7 @@ export default function EquipmentMonitoringPage() { {filteredEquipments.length > 0 && (
{filteredEquipments.map((eq) => { - const status = resolveStatus(eq.operation_status); + const status = inferredStatus[eq.id] ?? "unknown"; const cfg = STATUS_MAP[status]; const utilization = getUtilization(eq); const eqWIs = wiMap[eq.id] ?? []; diff --git a/frontend/app/(main)/COMPANY_8/monitoring/equipment/page.tsx b/frontend/app/(main)/COMPANY_8/monitoring/equipment/page.tsx index 14adae20..b165ff65 100644 --- a/frontend/app/(main)/COMPANY_8/monitoring/equipment/page.tsx +++ b/frontend/app/(main)/COMPANY_8/monitoring/equipment/page.tsx @@ -118,11 +118,20 @@ interface Equipment { interface WorkInstruction { id: string; + wi_id?: string; instruction_number: string; + work_instruction_no?: string; item_name: string; equipment_id: string; worker_name: string; status: string; + progress_status?: string; +} + +interface ProcessRow { + wo_id: string; + status: string; // acceptable / in_progress / completed + parent_process_id?: string | null; } /* ───── 컴포넌트 ───── */ @@ -135,6 +144,7 @@ export default function EquipmentMonitoringPage() { const [equipments, setEquipments] = useState([]); const [workInstructions, setWorkInstructions] = useState([]); + const [processRows, setProcessRows] = useState([]); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); @@ -156,20 +166,28 @@ export default function EquipmentMonitoringPage() { const fetchData = useCallback(async () => { try { setLoading(true); - const [equipRes, wiRes] = await Promise.all([ + const [equipRes, wiRes, procRes] = await Promise.all([ apiClient.post("/table-management/tables/equipment_mng/data", { autoFilter: true, page: 1, size: 500, }), apiClient.get("/work-instruction/list").catch(() => ({ data: { data: [] } })), + apiClient.post("/table-management/tables/work_order_process/data", { + page: 1, + size: 2000, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), ]); - const eqRows: Equipment[] = equipRes.data?.data?.rows ?? equipRes.data?.rows ?? []; + const eqRows: Equipment[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? []; setEquipments(eqRows); const wiRows: WorkInstruction[] = wiRes.data?.data ?? wiRes.data?.rows ?? []; setWorkInstructions(wiRows); + + const pRows: ProcessRow[] = procRes.data?.data?.data ?? procRes.data?.data?.rows ?? []; + setProcessRows(pRows); } catch (err) { console.error("설비 모니터링 데이터 조회 실패:", err); } finally { @@ -189,49 +207,107 @@ export default function EquipmentMonitoringPage() { return () => clearInterval(interval); }, [fetchData, settings.refreshInterval]); - /* ── 요약 통계 ── */ - const stats = useMemo(() => { - const counts: Record = { - running: 0, - idle: 0, - maintenance: 0, - off: 0, - unknown: 0, - }; - equipments.forEach((eq) => { - const s = resolveStatus(eq.operation_status); - counts[s]++; - }); - return { total: equipments.length, ...counts }; - }, [equipments]); - - /* ── 필터된 설비 ── */ - const filteredEquipments = useMemo(() => { - if (filterStatus === "all") return equipments; - return equipments.filter((eq) => resolveStatus(eq.operation_status) === filterStatus); - }, [equipments, filterStatus]); /* ── 설비별 작업지시 맵 ── */ const wiMap = useMemo(() => { const map: Record = {}; workInstructions.forEach((wi) => { - if (wi.equipment_id) { - if (!map[wi.equipment_id]) map[wi.equipment_id] = []; - map[wi.equipment_id].push(wi); + const eqId = wi.equipment_id; + if (eqId) { + if (!map[eqId]) map[eqId] = []; + map[eqId].push(wi); } }); return map; }, [workInstructions]); - /* ── 가동률 (모킹 — 센서 미연동) ── */ - const getUtilization = (eq: Equipment): number | null => { - const s = resolveStatus(eq.operation_status); - if (s === "running") return 75 + Math.floor(Math.random() * 20); // 75~94 - if (s === "idle") return 20 + Math.floor(Math.random() * 30); // 20~49 - if (s === "maintenance") return 0; - if (s === "off") return 0; - return null; - }; + /* ── 설비 상태 자동 판단: 공정 데이터 기반 ── */ + const inferredStatus = useMemo(() => { + // 작업지시 ID → 설비 ID 매핑 + const wiToEquip: Record = {}; + workInstructions.forEach((wi) => { + const wiId = wi.wi_id || wi.id; + if (wi.equipment_id) wiToEquip[wiId] = wi.equipment_id; + }); + + // 설비별 공정 상태 집계 + const equipProcessStatus: Record> = {}; + processRows.forEach((p) => { + const eqId = wiToEquip[p.wo_id]; + if (!eqId) return; + if (!equipProcessStatus[eqId]) equipProcessStatus[eqId] = new Set(); + equipProcessStatus[eqId].add(p.status); + }); + + // 설비별 상태 판단 + const result: Record = {}; + equipments.forEach((eq) => { + const dbStatus = resolveStatus(eq.operation_status); + // DB에 점검/수리가 명시되어 있으면 그대로 사용 + if (dbStatus === "maintenance") { + result[eq.id] = "maintenance"; + return; + } + + const statuses = equipProcessStatus[eq.id]; + if (statuses) { + if (statuses.has("in_progress")) { + result[eq.id] = "running"; // 진행중 공정 있음 → 가동중 + } else if (statuses.has("acceptable")) { + result[eq.id] = "idle"; // 접수 대기 공정 있음 → 대기 + } else { + // 전부 completed → DB 상태 사용 + result[eq.id] = dbStatus !== "unknown" ? dbStatus : "idle"; + } + } else { + // 공정 데이터 없음 → 작업지시 여부로 판단 + const eqWIs = wiMap[eq.id]; + if (eqWIs && eqWIs.length > 0) { + result[eq.id] = "idle"; // 작업지시 배정됨 → 대기 + } else { + result[eq.id] = dbStatus !== "unknown" ? dbStatus : "off"; + } + } + }); + return result; + }, [equipments, workInstructions, processRows, wiMap]); + + /* ── 요약 통계 (추론 상태 기반) ── */ + const stats = useMemo(() => { + const counts: Record = { running: 0, idle: 0, maintenance: 0, off: 0, unknown: 0 }; + equipments.forEach((eq) => { + counts[inferredStatus[eq.id] ?? "unknown"]++; + }); + return { total: equipments.length, ...counts }; + }, [equipments, inferredStatus]); + + /* ── 필터된 설비 ── */ + const filteredEquipments = useMemo(() => { + if (filterStatus === "all") return equipments; + return equipments.filter((eq) => (inferredStatus[eq.id] ?? "unknown") === filterStatus); + }, [equipments, filterStatus, inferredStatus]); + + /* ── 가동률 (센서 미연동 — 상태 기반 고정값) ── */ + const utilizationMap = useMemo(() => { + const map: Record = {}; + // 설비 ID를 해시하여 상태별 고정 범위 내 값 생성 (리렌더링해도 안 변함) + const hash = (id: string) => { + let h = 0; + for (let i = 0; i < id.length; i++) h = ((h << 5) - h + id.charCodeAt(i)) | 0; + return Math.abs(h); + }; + equipments.forEach((eq) => { + const s = inferredStatus[eq.id] ?? "unknown"; + const h = hash(eq.id); + if (s === "running") map[eq.id] = 75 + (h % 20); // 75~94 고정 + else if (s === "idle") map[eq.id] = 20 + (h % 30); // 20~49 고정 + else if (s === "maintenance") map[eq.id] = 0; + else if (s === "off") map[eq.id] = 0; + else map[eq.id] = null; + }); + return map; + }, [equipments, inferredStatus]); + const getUtilization = (eq: Equipment): number | null => utilizationMap[eq.id] ?? null; /* ── 요약 카드 배열 ── */ const summaryCards: { @@ -430,7 +506,7 @@ export default function EquipmentMonitoringPage() { {filteredEquipments.length > 0 && (
{filteredEquipments.map((eq) => { - const status = resolveStatus(eq.operation_status); + const status = inferredStatus[eq.id] ?? "unknown"; const cfg = STATUS_MAP[status]; const utilization = getUtilization(eq); const eqWIs = wiMap[eq.id] ?? []; diff --git a/frontend/app/(main)/COMPANY_9/monitoring/equipment/page.tsx b/frontend/app/(main)/COMPANY_9/monitoring/equipment/page.tsx index 14adae20..b165ff65 100644 --- a/frontend/app/(main)/COMPANY_9/monitoring/equipment/page.tsx +++ b/frontend/app/(main)/COMPANY_9/monitoring/equipment/page.tsx @@ -118,11 +118,20 @@ interface Equipment { interface WorkInstruction { id: string; + wi_id?: string; instruction_number: string; + work_instruction_no?: string; item_name: string; equipment_id: string; worker_name: string; status: string; + progress_status?: string; +} + +interface ProcessRow { + wo_id: string; + status: string; // acceptable / in_progress / completed + parent_process_id?: string | null; } /* ───── 컴포넌트 ───── */ @@ -135,6 +144,7 @@ export default function EquipmentMonitoringPage() { const [equipments, setEquipments] = useState([]); const [workInstructions, setWorkInstructions] = useState([]); + const [processRows, setProcessRows] = useState([]); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); @@ -156,20 +166,28 @@ export default function EquipmentMonitoringPage() { const fetchData = useCallback(async () => { try { setLoading(true); - const [equipRes, wiRes] = await Promise.all([ + const [equipRes, wiRes, procRes] = await Promise.all([ apiClient.post("/table-management/tables/equipment_mng/data", { autoFilter: true, page: 1, size: 500, }), apiClient.get("/work-instruction/list").catch(() => ({ data: { data: [] } })), + apiClient.post("/table-management/tables/work_order_process/data", { + page: 1, + size: 2000, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), ]); - const eqRows: Equipment[] = equipRes.data?.data?.rows ?? equipRes.data?.rows ?? []; + const eqRows: Equipment[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? []; setEquipments(eqRows); const wiRows: WorkInstruction[] = wiRes.data?.data ?? wiRes.data?.rows ?? []; setWorkInstructions(wiRows); + + const pRows: ProcessRow[] = procRes.data?.data?.data ?? procRes.data?.data?.rows ?? []; + setProcessRows(pRows); } catch (err) { console.error("설비 모니터링 데이터 조회 실패:", err); } finally { @@ -189,49 +207,107 @@ export default function EquipmentMonitoringPage() { return () => clearInterval(interval); }, [fetchData, settings.refreshInterval]); - /* ── 요약 통계 ── */ - const stats = useMemo(() => { - const counts: Record = { - running: 0, - idle: 0, - maintenance: 0, - off: 0, - unknown: 0, - }; - equipments.forEach((eq) => { - const s = resolveStatus(eq.operation_status); - counts[s]++; - }); - return { total: equipments.length, ...counts }; - }, [equipments]); - - /* ── 필터된 설비 ── */ - const filteredEquipments = useMemo(() => { - if (filterStatus === "all") return equipments; - return equipments.filter((eq) => resolveStatus(eq.operation_status) === filterStatus); - }, [equipments, filterStatus]); /* ── 설비별 작업지시 맵 ── */ const wiMap = useMemo(() => { const map: Record = {}; workInstructions.forEach((wi) => { - if (wi.equipment_id) { - if (!map[wi.equipment_id]) map[wi.equipment_id] = []; - map[wi.equipment_id].push(wi); + const eqId = wi.equipment_id; + if (eqId) { + if (!map[eqId]) map[eqId] = []; + map[eqId].push(wi); } }); return map; }, [workInstructions]); - /* ── 가동률 (모킹 — 센서 미연동) ── */ - const getUtilization = (eq: Equipment): number | null => { - const s = resolveStatus(eq.operation_status); - if (s === "running") return 75 + Math.floor(Math.random() * 20); // 75~94 - if (s === "idle") return 20 + Math.floor(Math.random() * 30); // 20~49 - if (s === "maintenance") return 0; - if (s === "off") return 0; - return null; - }; + /* ── 설비 상태 자동 판단: 공정 데이터 기반 ── */ + const inferredStatus = useMemo(() => { + // 작업지시 ID → 설비 ID 매핑 + const wiToEquip: Record = {}; + workInstructions.forEach((wi) => { + const wiId = wi.wi_id || wi.id; + if (wi.equipment_id) wiToEquip[wiId] = wi.equipment_id; + }); + + // 설비별 공정 상태 집계 + const equipProcessStatus: Record> = {}; + processRows.forEach((p) => { + const eqId = wiToEquip[p.wo_id]; + if (!eqId) return; + if (!equipProcessStatus[eqId]) equipProcessStatus[eqId] = new Set(); + equipProcessStatus[eqId].add(p.status); + }); + + // 설비별 상태 판단 + const result: Record = {}; + equipments.forEach((eq) => { + const dbStatus = resolveStatus(eq.operation_status); + // DB에 점검/수리가 명시되어 있으면 그대로 사용 + if (dbStatus === "maintenance") { + result[eq.id] = "maintenance"; + return; + } + + const statuses = equipProcessStatus[eq.id]; + if (statuses) { + if (statuses.has("in_progress")) { + result[eq.id] = "running"; // 진행중 공정 있음 → 가동중 + } else if (statuses.has("acceptable")) { + result[eq.id] = "idle"; // 접수 대기 공정 있음 → 대기 + } else { + // 전부 completed → DB 상태 사용 + result[eq.id] = dbStatus !== "unknown" ? dbStatus : "idle"; + } + } else { + // 공정 데이터 없음 → 작업지시 여부로 판단 + const eqWIs = wiMap[eq.id]; + if (eqWIs && eqWIs.length > 0) { + result[eq.id] = "idle"; // 작업지시 배정됨 → 대기 + } else { + result[eq.id] = dbStatus !== "unknown" ? dbStatus : "off"; + } + } + }); + return result; + }, [equipments, workInstructions, processRows, wiMap]); + + /* ── 요약 통계 (추론 상태 기반) ── */ + const stats = useMemo(() => { + const counts: Record = { running: 0, idle: 0, maintenance: 0, off: 0, unknown: 0 }; + equipments.forEach((eq) => { + counts[inferredStatus[eq.id] ?? "unknown"]++; + }); + return { total: equipments.length, ...counts }; + }, [equipments, inferredStatus]); + + /* ── 필터된 설비 ── */ + const filteredEquipments = useMemo(() => { + if (filterStatus === "all") return equipments; + return equipments.filter((eq) => (inferredStatus[eq.id] ?? "unknown") === filterStatus); + }, [equipments, filterStatus, inferredStatus]); + + /* ── 가동률 (센서 미연동 — 상태 기반 고정값) ── */ + const utilizationMap = useMemo(() => { + const map: Record = {}; + // 설비 ID를 해시하여 상태별 고정 범위 내 값 생성 (리렌더링해도 안 변함) + const hash = (id: string) => { + let h = 0; + for (let i = 0; i < id.length; i++) h = ((h << 5) - h + id.charCodeAt(i)) | 0; + return Math.abs(h); + }; + equipments.forEach((eq) => { + const s = inferredStatus[eq.id] ?? "unknown"; + const h = hash(eq.id); + if (s === "running") map[eq.id] = 75 + (h % 20); // 75~94 고정 + else if (s === "idle") map[eq.id] = 20 + (h % 30); // 20~49 고정 + else if (s === "maintenance") map[eq.id] = 0; + else if (s === "off") map[eq.id] = 0; + else map[eq.id] = null; + }); + return map; + }, [equipments, inferredStatus]); + const getUtilization = (eq: Equipment): number | null => utilizationMap[eq.id] ?? null; /* ── 요약 카드 배열 ── */ const summaryCards: { @@ -430,7 +506,7 @@ export default function EquipmentMonitoringPage() { {filteredEquipments.length > 0 && (
{filteredEquipments.map((eq) => { - const status = resolveStatus(eq.operation_status); + const status = inferredStatus[eq.id] ?? "unknown"; const cfg = STATUS_MAP[status]; const utilization = getUtilization(eq); const eqWIs = wiMap[eq.id] ?? [];