import { useEffect, useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { PageContainer, PageHeader } from '@shared/components/layout'; import { Activity, AlertTriangle, Ship, Eye, Anchor, Radar, Shield, Bell, Clock, Target, ChevronRight } from 'lucide-react'; import type { LucideIcon } from 'lucide-react'; import { AreaChart, PieChart } from '@lib/charts'; import { useKpiStore } from '@stores/kpiStore'; import { useEventStore } from '@stores/eventStore'; import { getHourlyStats, type PredictionStatsHourly } from '@/services/kpi'; import { getKpiUi } from '@shared/constants/kpiUiMap'; import { formatDateTime } from '@shared/utils/dateFormat'; import { getViolationColor, getViolationLabel } from '@shared/constants/violationTypes'; import { type AlertLevel, getAlertLevelLabel, getAlertLevelIntent } from '@shared/constants/alertLevels'; import { useSettingsStore } from '@stores/settingsStore'; import { SystemStatusPanel } from './SystemStatusPanel'; /* SFR-12: 모니터링 및 경보 현황판(대시보드) */ // KPI UI 매핑 (icon, color는 store에 없으므로 라벨 기반 매핑) // KPI_UI_MAP은 shared/constants/kpiUiMap 공통 모듈 사용 export function MonitoringDashboard() { const { t } = useTranslation('dashboard'); const { t: tc } = useTranslation('common'); const lang = useSettingsStore((s) => s.language); const kpiStore = useKpiStore(); const eventStore = useEventStore(); const [hourlyStats, setHourlyStats] = useState([]); useEffect(() => { if (!kpiStore.loaded) kpiStore.load(); }, [kpiStore.loaded, kpiStore.load]); useEffect(() => { if (!eventStore.loaded) eventStore.load(); }, [eventStore.loaded, eventStore.load]); useEffect(() => { getHourlyStats(24).then(setHourlyStats).catch(() => setHourlyStats([])); }, []); // 24시간 위험도/경보 추이: hourly stats → 차트 데이터 const TREND = useMemo(() => hourlyStats.map((h) => { const hourLabel = h.statHour ? `${new Date(h.statHour).getHours()}시` : ''; // 위험도 점수: byRiskLevel 가중합 (CRITICAL=100, HIGH=70, MEDIUM=40, LOW=10) 정규화 let riskScore = 0; let total = 0; if (h.byRiskLevel) { const weights: Record = { CRITICAL: 100, HIGH: 70, MEDIUM: 40, LOW: 10 }; Object.entries(h.byRiskLevel).forEach(([k, v]) => { const cnt = Number(v) || 0; riskScore += (weights[k.toUpperCase()] ?? 0) * cnt; total += cnt; }); } const risk = total > 0 ? Math.round(riskScore / total) : 0; return { h: hourLabel, risk, alarms: (h.eventCount ?? 0) + (h.criticalCount ?? 0), }; }), [hourlyStats]); // KPI: store metrics + UI 매핑 const KPI = kpiStore.metrics.map((m) => ({ label: m.label, value: m.value, icon: getKpiUi(m.label).icon, color: getKpiUi(m.label).color, })); // PIE: store violationTypes → 공통 카탈로그 기반 라벨/색상 const PIE = kpiStore.violationTypes.map((v) => ({ name: getViolationLabel(v.type, tc, lang), value: v.pct, color: getViolationColor(v.type), })); // 이벤트: store events → 첫 6개, time은 KST로 포맷 const EVENTS = eventStore.events.slice(0, 6).map((e) => ({ time: formatDateTime(e.time), level: e.level, title: e.title, detail: e.detail, })); return ( {/* 백엔드 + prediction 분석 엔진 시스템 상태 (실시간) */}
{KPI.map(k => (
{k.value} {k.label}
))}
24시간 위험도·경보 추이
탐지 유형 분포
{PIE.map(d => (
{d.name}
{d.value}%
))}
실시간 이벤트 타임라인
{EVENTS.map((e, i) => (
{e.time} {getAlertLevelLabel(e.level, tc, lang)} {e.title} {e.detail}
))}
); }