"use client"; /** * 설비운영모니터링 — 하드코딩 페이지 * * 설비(equipment_mng) 목록 + 작업지시(work_instruction) 연결 * 실시간 카드 그리드 형태 모니터링 대시보드 */ 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 { cn } from "@/lib/utils"; import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; import { getMonitoringTheme } from "@/lib/monitoringTheme"; import { useTabStore } from "@/stores/tabStore"; import { RefreshCw, Clock, Loader2, Inbox, Wrench, Zap, Pause, Power, Settings2 } from "lucide-react"; /* ───── 상태 정의 ───── */ type OperationStatus = "running" | "idle" | "maintenance" | "off" | "unknown"; interface StatusConfig { label: string; color: string; bg: string; border: string; bar: string; icon: React.ReactNode; badgeBg: string; badgeText: string; cardGlow: string; } const STATUS_MAP: Record = { running: { label: "가동중", color: "text-emerald-400", bg: "bg-emerald-500/10", border: "border-emerald-500/30", bar: "bg-emerald-400", icon: , badgeBg: "bg-emerald-500/20", badgeText: "text-emerald-300", cardGlow: "shadow-emerald-500/5", }, idle: { label: "대기", color: "text-amber-400", bg: "bg-amber-500/10", border: "border-amber-500/30", bar: "bg-amber-400", icon: , badgeBg: "bg-amber-500/20", badgeText: "text-amber-300", cardGlow: "shadow-amber-500/5", }, maintenance: { label: "점검/수리", color: "text-red-400", bg: "bg-red-500/10", border: "border-red-500/30", bar: "bg-red-400", icon: , badgeBg: "bg-red-500/20", badgeText: "text-red-300", cardGlow: "shadow-red-500/5", }, off: { label: "비가동", color: "text-gray-400", bg: "bg-gray-500/10", border: "border-gray-500/30", bar: "bg-gray-500", icon: , badgeBg: "bg-gray-500/20", badgeText: "text-gray-400", cardGlow: "shadow-gray-500/5", }, unknown: { label: "미설정", color: "text-gray-500", bg: "bg-gray-500/10", border: "border-gray-600/30", bar: "bg-gray-600", icon: , badgeBg: "bg-gray-600/20", badgeText: "text-gray-500", cardGlow: "", }, }; /** operation_status 값 → 내부 키 매핑 */ function resolveStatus(raw: string | null | undefined): OperationStatus { if (!raw) return "unknown"; const v = raw.trim().toLowerCase(); if (["running", "가동", "가동중"].includes(v)) return "running"; if (["idle", "대기"].includes(v)) return "idle"; if (["maintenance", "점검", "수리", "점검/수리", "점검중"].includes(v)) return "maintenance"; if (["off", "비가동", "정지"].includes(v)) return "off"; return "unknown"; } /* ───── 타입 ───── */ interface Equipment { id: string; equipment_code: string; equipment_name: string; equipment_type: string; installation_location: string; operation_status: string; manufacturer: string; model_name: string; image_path: string; } interface WorkInstruction { id: string; instruction_number: string; item_name: string; equipment_id: string; worker_name: string; status: string; } /* ───── 컴포넌트 ───── */ export default function EquipmentMonitoringPage() { const { settings } = useMonitoringSettings("equipment"); const theme = getMonitoringTheme(settings.theme); const openTab = useTabStore((s) => s.openTab); const df = settings.displayFields; const [equipments, setEquipments] = useState([]); const [workInstructions, setWorkInstructions] = useState([]); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [filterStatus, setFilterStatus] = useState("all"); const autoRefreshRef = useRef(autoRefresh); // autoRefreshRef 동기화 useEffect(() => { autoRefreshRef.current = autoRefresh; }, [autoRefresh]); /* ── 시간 업데이트 ── */ useEffect(() => { const timer = setInterval(() => setCurrentTime(new Date()), 1000); return () => clearInterval(timer); }, []); /* ── 데이터 fetch ── */ const fetchData = useCallback(async () => { try { setLoading(true); const [equipRes, wiRes] = 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: [] } })), ]); const eqRows: Equipment[] = equipRes.data?.data?.rows ?? equipRes.data?.rows ?? []; setEquipments(eqRows); const wiRows: WorkInstruction[] = wiRes.data?.data ?? wiRes.data?.rows ?? []; setWorkInstructions(wiRows); } catch (err) { console.error("설비 모니터링 데이터 조회 실패:", err); } finally { setLoading(false); } }, []); useEffect(() => { fetchData(); }, [fetchData]); /* ── 자동 갱신 ── */ useEffect(() => { const interval = setInterval(() => { if (autoRefreshRef.current) fetchData(); }, settings.refreshInterval * 1000); 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); } }); 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 summaryCards: { label: string; count: number; status: OperationStatus | "total"; color: string; bg: string; border: string; icon: React.ReactNode; }[] = [ { label: "전체설비", count: stats.total, status: "total", color: "text-blue-400", bg: "bg-blue-500/10", border: "border-blue-500/30", icon: , }, { label: "가동중", count: stats.running, status: "running", color: "text-emerald-400", bg: "bg-emerald-500/10", border: "border-emerald-500/30", icon: , }, { label: "대기", count: stats.idle, status: "idle", color: "text-amber-400", bg: "bg-amber-500/10", border: "border-amber-500/30", icon: , }, { label: "점검/수리", count: stats.maintenance, status: "maintenance", color: "text-red-400", bg: "bg-red-500/10", border: "border-red-500/30", icon: , }, { label: "비가동", count: stats.off + stats.unknown, status: "off", color: "text-gray-400", bg: "bg-gray-500/10", border: "border-gray-500/30", icon: , }, ]; /* ── 필터 pill ── */ const filterPills: { label: string; value: OperationStatus | "all"; color: string }[] = [ { label: "전체", value: "all", color: "bg-blue-500/20 text-blue-700 hover:bg-blue-500/30" }, { label: "가동중", value: "running", color: "bg-emerald-500/20 text-emerald-700 hover:bg-emerald-500/30" }, { label: "대기", value: "idle", color: "bg-amber-500/20 text-amber-700 hover:bg-amber-500/30" }, { label: "점검/수리", value: "maintenance", color: "bg-red-500/20 text-red-700 hover:bg-red-500/30" }, { label: "비가동", value: "off", color: "bg-gray-500/20 text-gray-700 hover:bg-gray-500/30" }, ]; /* ── 포맷 ── */ const formatTime = (d: Date) => d.toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }); const formatDate = (d: Date) => d.toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit", weekday: "short" }); /* ────────────── 렌더 ────────────── */ return (
{/* ── 헤더 ── */}

설비운영모니터링

{/* 현재 시간 */}
{formatDate(currentTime)} {formatTime(currentTime)}
{/* 자동갱신 토글 */} {/* 새로고침 */} {/* 설정 */}
{/* ── 요약 카드 5개 ── */}
{summaryCards.map((card) => ( ))}
{/* ── 필터 pill ── */}
{filterPills.map((pill) => ( ))} {filteredEquipments.length}대 표시
{/* ── 로딩 ── */} {loading && equipments.length === 0 && (
설비 데이터를 불러오는 중...
)} {/* ── 데이터 없음 ── */} {!loading && equipments.length === 0 && (

등록된 설비가 없습니다.

)} {/* ── 설비 카드 그리드 ── */} {filteredEquipments.length > 0 && (
{filteredEquipments.map((eq) => { const status = resolveStatus(eq.operation_status); const cfg = STATUS_MAP[status]; const utilization = getUtilization(eq); const eqWIs = wiMap[eq.id] ?? []; return (
{/* 좌측 색상 바 */}
{/* 상단: 설비명 + 상태 배지 */}
{df.equipmentName && (

{eq.equipment_name || "이름 없음"}

)}

{df.equipmentType && (eq.equipment_type || "-")} {df.equipmentType && df.equipmentLocation && " · "} {df.equipmentLocation && (eq.installation_location || "-")}

{df.operationStatus && ( {cfg.icon} {cfg.label} )}
{/* 구분선 */}
{/* 정보 그리드 */}
{df.dailyOperationTime && (
금일 가동시간

-

)} {df.dailyProductionQty && (
생산수량

-

)} {df.worker && (
작업자

{eqWIs.length > 0 && eqWIs[0].worker_name ? eqWIs[0].worker_name : "-"}

)}
설비코드

{eq.equipment_code || "-"}

{/* 구분선 */}
{/* 가동률 프로그레스 */} {df.utilizationBar && (
가동률 {utilization !== null ? `${utilization}%` : "-"}
{utilization !== null && (
)}
)} {/* 구분선 */}
{/* 현재 작업지시 */} {df.currentWorkInstruction && (

현재 작업지시

{eqWIs.length > 0 ? (
{eqWIs.slice(0, 2).map((wi) => (
{wi.instruction_number || "-"} {wi.item_name || "-"}
))} {eqWIs.length > 2 &&

+{eqWIs.length - 2}건 더

}
) : (

배정된 작업 없음

)}
)} {/* 구분선 */}
{/* 센서 데이터 (PLC 미연동) */} {df.sensorData && (
온도 -
압력 -
RPM -
)}
); })}
)} {/* 필터 결과 없음 */} {!loading && equipments.length > 0 && filteredEquipments.length === 0 && (

해당 상태의 설비가 없습니다.

)}
); }