import { useState, useEffect, useCallback } from 'react'; // ─── 타입 ────────────────────────────────────────────────── const SIGNAL_SOURCES = ['VTS', 'VTS-AIS', 'V-PASS', 'E-NAVI', 'S&P AIS'] as const; type SignalSource = (typeof SIGNAL_SOURCES)[number]; interface SignalSlot { time: string; // HH:mm sources: Record; } // ─── 상수 ────────────────────────────────────────────────── const SOURCE_COLORS: Record = { VTS: '#3b82f6', 'VTS-AIS': '#a855f7', 'V-PASS': '#22c55e', 'E-NAVI': '#f97316', 'S&P AIS': '#ec4899', }; const STATUS_COLOR: Record = { ok: '#22c55e', warn: '#eab308', error: '#ef4444', none: 'rgba(255,255,255,0.06)', }; const HOURS = Array.from({ length: 24 }, (_, i) => i); function generateTimeSlots(date: string): SignalSlot[] { const now = new Date(); const isToday = date === now.toISOString().slice(0, 10); const currentHour = isToday ? now.getHours() : 24; const currentMin = isToday ? now.getMinutes() : 0; const slots: SignalSlot[] = []; for (let h = 0; h < 24; h++) { for (let m = 0; m < 60; m += 10) { const time = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; const isPast = h < currentHour || (h === currentHour && m <= currentMin); const sources = {} as Record< SignalSource, { count: number; status: 'ok' | 'warn' | 'error' | 'none' } >; for (const src of SIGNAL_SOURCES) { if (!isPast) { sources[src] = { count: 0, status: 'none' }; } else { const rand = Math.random(); const count = Math.floor(Math.random() * 200) + 10; sources[src] = { count, status: rand > 0.15 ? 'ok' : rand > 0.05 ? 'warn' : 'error', }; } } slots.push({ time, sources }); } } return slots; } // ─── 타임라인 바 (10분 단위 셀) ──────────────────────────── function TimelineBar({ slots, source }: { slots: SignalSlot[]; source: SignalSource }) { if (slots.length === 0) return null; // 144개 슬롯을 각각 1칸씩 렌더링 (10분 = 1칸) return (
{slots.map((slot, i) => { const s = slot.sources[source]; const color = STATUS_COLOR[s.status] || STATUS_COLOR.none; const statusLabel = s.status === 'ok' ? '정상' : s.status === 'warn' ? '지연' : s.status === 'error' ? '오류' : '미수신'; return (
); })}
); } // ─── 메인 패널 ───────────────────────────────────────────── export default function VesselSignalPanel() { const [date, setDate] = useState(() => new Date().toISOString().slice(0, 10)); const [slots, setSlots] = useState([]); const [loading, setLoading] = useState(false); const load = useCallback(() => { setLoading(true); // TODO: 실제 API 연동 시 fetch 호출로 교체 setTimeout(() => { setSlots(generateTimeSlots(date)); setLoading(false); }, 300); }, [date]); useEffect(() => { const timer = setTimeout(() => load(), 0); return () => clearTimeout(timer); }, [load]); // 통계 계산 const stats = SIGNAL_SOURCES.map((src) => { let total = 0, ok = 0, warn = 0, error = 0; for (const slot of slots) { const s = slot.sources[src]; if (s.status !== 'none') { total++; if (s.status === 'ok') ok++; else if (s.status === 'warn') warn++; else error++; } } return { src, total, ok, warn, error, rate: total > 0 ? ((ok / total) * 100).toFixed(1) : '-' }; }); return (
{/* 헤더 */}

선박신호 수신 현황

setDate(e.target.value)} className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-fg" />
{/* 메인 콘텐츠 */}
{loading ? (
로딩 중...
) : (
{/* 좌측: 소스 라벨 고정 열 */}
{/* 시간축 높이 맞춤 빈칸 */}
{SIGNAL_SOURCES.map((src) => { const c = SOURCE_COLORS[src]; const st = stats.find((s) => s.src === src)!; return (
{src} {st.rate}%
); })}
{/* 우측: 시간축 + 타임라인 바 */}
{/* 시간 축 (상단) */}
{HOURS.map((h) => ( {String(h).padStart(2, '0')}시 ))} 24시
{/* 소스별 타임라인 바 */} {SIGNAL_SOURCES.map((src) => (
))}
)}
); }