import { useState, useEffect, useCallback } from 'react'; import { getNumericalDataStatus, type NumericalDataStatus, } from '../services/monitorApi'; type TabId = 'all' | 'ocean' | 'koast'; const TABS: { id: TabId; label: string }[] = [ { id: 'all', label: '전체' }, { id: 'ocean', label: '기상·해양 모델' }, { id: 'koast', label: 'KOAST' }, ]; const OCEAN_MODELS = ['HYCOM', 'GFS', 'WW3']; const KOAST_MODELS = ['KOAST POS_WIND', 'KOAST POS_HYDR', 'KOAST POS_WAVE']; function filterByTab(rows: NumericalDataStatus[], tab: TabId): NumericalDataStatus[] { if (tab === 'ocean') return rows.filter((r) => OCEAN_MODELS.includes(r.modelName)); if (tab === 'koast') return rows.filter((r) => KOAST_MODELS.includes(r.modelName)); return rows; } function formatDuration(sec: number | null): string { if (sec == null) return '-'; const m = Math.floor(sec / 60); const s = sec % 60; return m > 0 ? `${m}분 ${s}초` : `${s}초`; } function formatDatetime(iso: string | null): string { if (!iso) return '-'; const d = new Date(iso); const mm = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); const hh = String(d.getHours()).padStart(2, '0'); const min = String(d.getMinutes()).padStart(2, '0'); return `${mm}-${dd} ${hh}:${min}`; } function formatTime(iso: string | null): string { if (!iso) return '-'; const d = new Date(iso); const hh = String(d.getHours()).padStart(2, '0'); const min = String(d.getMinutes()).padStart(2, '0'); return `${hh}:${min}`; } function StatusCell({ row }: { row: NumericalDataStatus }) { if (row.lastStatus === 'COMPLETED') { return 정상; } if (row.lastStatus === 'FAILED') { return ( 오류{row.consecutiveFailures > 0 ? ` (${row.consecutiveFailures}회 연속)` : ''} ); } if (row.lastStatus === 'STARTED') { return ( 실행 중 ); } return -; } function StatusBadge({ loading, errorCount, total, }: { loading: boolean; errorCount: number; total: number; }) { if (loading) { return ( 조회 중... ); } if (errorCount === total && total > 0) { return ( 연계 오류 ); } if (errorCount > 0) { return ( 일부 오류 ({errorCount}/{total}) ); } return ( 정상 ); } const TABLE_HEADERS = [ '모델명', '데이터 기준일', '마지막 다운로드', '상태', '소요 시간', '다음 예정', ]; function ForecastTable({ rows, loading, }: { rows: NumericalDataStatus[]; loading: boolean; }) { return (
{TABLE_HEADERS.map((h) => ( ))} {loading && rows.length === 0 ? Array.from({ length: 6 }).map((_, i) => ( {TABLE_HEADERS.map((_, j) => ( ))} )) : rows.map((row) => ( ))}
{h}
{row.modelName} {row.lastDataDate ?? '-'} {formatDatetime(row.lastDownloadedAt)} {formatDuration(row.durationSec)} {formatTime(row.nextScheduledAt)}
); } export default function MonitorForecastPanel() { const [activeTab, setActiveTab] = useState('all'); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(false); const [lastUpdate, setLastUpdate] = useState(null); const fetchData = useCallback(async () => { setLoading(true); try { const data = await getNumericalDataStatus(); setRows(data); setLastUpdate(new Date()); } catch { // 오류 시 기존 데이터 유지 } finally { setLoading(false); } }, []); useEffect(() => { void fetchData(); }, [fetchData]); const visibleRows = filterByTab(rows, activeTab); const errorCount = visibleRows.filter( (r) => r.lastStatus === 'FAILED' ).length; const totalCount = visibleRows.length; return (
{/* 헤더 */}

수치예측자료 모니터링

{lastUpdate && ( 갱신:{' '} {lastUpdate.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit', })} )}
{/* 탭 */}
{TABS.map((tab) => ( ))}
{/* 상태 표시줄 */}
{!loading && totalCount > 0 && ( 모델 {totalCount}개 )}
{/* 테이블 */}
); }