import { useState, useEffect, useCallback } from 'react'; // ─── 타입 ────────────────────────────────────────────────────────────────────── type PipelineStatus = '정상' | '지연' | '중단'; type ReceiveStatus = '수신완료' | '수신대기' | '수신실패' | '시간초과'; type ProcessStatus = '처리완료' | '처리중' | '대기' | '오류'; type DataSource = 'HYCOM' | '기상청' | '긴급구난시스템'; type AlertLevel = '경고' | '주의' | '정보'; interface PipelineNode { id: string; name: string; status: PipelineStatus; lastReceived: string; cycle: string; } interface DataLogRow { id: string; timestamp: string; source: DataSource; dataType: string; size: string; receiveStatus: ReceiveStatus; processStatus: ProcessStatus; } interface AlertItem { id: string; level: AlertLevel; message: string; timestamp: string; } // ─── Mock 데이터 ──────────────────────────────────────────────────────────────── const MOCK_PIPELINE: PipelineNode[] = [ { id: 'hycom', name: 'HYCOM 해양순환모델', status: '정상', lastReceived: '2026-04-11 06:00', cycle: '6시간 주기', }, { id: 'kma', name: '기상청 수치모델', status: '정상', lastReceived: '2026-04-11 06:00', cycle: '3시간 주기', }, { id: 'rescue', name: '해경 긴급구난 시스템', status: '정상', lastReceived: '2026-04-11 06:30', cycle: '내부 연계', }, { id: 'analysis', name: '구난 분석 연산', status: '정상', lastReceived: '2026-04-11 06:35', cycle: '연계 시작 즉시', }, { id: 'result', name: '결과 연계 수신', status: '정상', lastReceived: '2026-04-11 06:40', cycle: '분석 완료 즉시', }, ]; const MOCK_LOGS: DataLogRow[] = [ { id: 'log-01', timestamp: '2026-04-11 06:40', source: '긴급구난시스템', dataType: '구난 가능성 판단', size: '1.2 MB', receiveStatus: '수신완료', processStatus: '처리완료', }, { id: 'log-02', timestamp: '2026-04-11 06:35', source: '긴급구난시스템', dataType: '선체상태 분석', size: '3.4 MB', receiveStatus: '수신완료', processStatus: '처리완료', }, { id: 'log-03', timestamp: '2026-04-11 06:30', source: '긴급구난시스템', dataType: '사고선 위치정보', size: '0.8 MB', receiveStatus: '수신완료', processStatus: '처리완료', }, { id: 'log-04', timestamp: '2026-04-11 06:30', source: '긴급구난시스템', dataType: '비상배인력 정보', size: '0.5 MB', receiveStatus: '수신완료', processStatus: '처리완료', }, { id: 'log-05', timestamp: '2026-04-11 06:00', source: 'HYCOM', dataType: '해수면온도(SST)', size: '98 MB', receiveStatus: '수신완료', processStatus: '처리완료', }, { id: 'log-06', timestamp: '2026-04-11 06:00', source: 'HYCOM', dataType: '해류(U/V)', size: '142 MB', receiveStatus: '수신완료', processStatus: '처리완료', }, { id: 'log-07', timestamp: '2026-04-11 06:00', source: 'HYCOM', dataType: '해수면높이(SSH)', size: '54 MB', receiveStatus: '수신완료', processStatus: '처리완료', }, { id: 'log-08', timestamp: '2026-04-11 06:00', source: '기상청', dataType: '풍향/풍속', size: '38 MB', receiveStatus: '수신완료', processStatus: '처리완료', }, { id: 'log-09', timestamp: '2026-04-11 06:00', source: '기상청', dataType: '기압', size: '22 MB', receiveStatus: '수신완료', processStatus: '처리완료', }, { id: 'log-10', timestamp: '2026-04-11 06:00', source: '기상청', dataType: '기온', size: '19 MB', receiveStatus: '수신완료', processStatus: '처리완료', }, { id: 'log-11', timestamp: '2026-04-11 03:30', source: '긴급구난시스템', dataType: '선체상태 분석', size: '3.1 MB', receiveStatus: '수신완료', processStatus: '처리완료', }, { id: 'log-12', timestamp: '2026-04-11 03:30', source: '긴급구난시스템', dataType: '구난 가능성 판단', size: '1.1 MB', receiveStatus: '수신완료', processStatus: '처리완료', }, { id: 'log-13', timestamp: '2026-04-11 00:30', source: '긴급구난시스템', dataType: '사고선 위치정보', size: '0.8 MB', receiveStatus: '수신완료', processStatus: '처리완료', }, { id: 'log-14', timestamp: '2026-04-11 00:00', source: 'HYCOM', dataType: '해수면높이(SSH)', size: '53 MB', receiveStatus: '시간초과', processStatus: '오류', }, { id: 'log-15', timestamp: '2026-04-10 21:30', source: '긴급구난시스템', dataType: '비상배인력 정보', size: '0.4 MB', receiveStatus: '수신완료', processStatus: '처리완료', }, ]; const MOCK_ALERTS: AlertItem[] = [ { id: 'alert-01', level: '정보', message: '해경 긴급구난 시스템 정상 연계 중', timestamp: '2026-04-11 06:30', }, { id: 'alert-02', level: '정보', message: 'HYCOM 데이터 정상 수신', timestamp: '2026-04-11 06:00', }, { id: 'alert-03', level: '정보', message: '금일 긴급구난 분석 완료: 5회/6회', timestamp: '2026-04-11 06:40', }, ]; // ─── Mock fetch ───────────────────────────────────────────────────────────────── interface RescueData { pipeline: PipelineNode[]; logs: DataLogRow[]; alerts: AlertItem[]; } function fetchRescueData(): Promise { return new Promise((resolve) => { setTimeout( () => resolve({ pipeline: MOCK_PIPELINE, logs: MOCK_LOGS, alerts: MOCK_ALERTS, }), 300, ); }); } // ─── 유틸 ─────────────────────────────────────────────────────────────────────── function getPipelineStatusStyle(status: PipelineStatus): string { if (status === '정상') return 'text-emerald-400 bg-emerald-500/10'; if (status === '지연') return 'text-yellow-400 bg-yellow-500/10'; return 'text-red-400 bg-red-500/10'; } function getPipelineBorderStyle(status: PipelineStatus): string { if (status === '정상') return 'border-l-emerald-500'; if (status === '지연') return 'border-l-yellow-500'; return 'border-l-red-500'; } function getReceiveStatusStyle(status: ReceiveStatus): string { if (status === '수신완료') return 'text-emerald-400 bg-emerald-500/10'; if (status === '수신대기') return 'text-yellow-400 bg-yellow-500/10'; return 'text-red-400 bg-red-500/10'; } function getProcessStatusStyle(status: ProcessStatus): string { if (status === '처리완료') return 'text-emerald-400 bg-emerald-500/10'; if (status === '처리중') return 'text-cyan-400 bg-cyan-500/10'; if (status === '대기') return 'text-yellow-400 bg-yellow-500/10'; return 'text-red-400 bg-red-500/10'; } function getAlertStyle(level: AlertLevel): string { if (level === '경고') return 'text-red-400 bg-red-500/10 border-red-500/30'; if (level === '주의') return 'text-yellow-400 bg-yellow-500/10 border-yellow-500/30'; return 'text-cyan-400 bg-cyan-500/10 border-cyan-500/30'; } // ─── 파이프라인 카드 ───────────────────────────────────────────────────────────── function PipelineCard({ node }: { node: PipelineNode }) { const badgeStyle = getPipelineStatusStyle(node.status); const borderStyle = getPipelineBorderStyle(node.status); return (
{node.name}
{node.status}
최근 수신: {node.lastReceived}
{node.cycle}
); } function PipelineFlow({ nodes, loading }: { nodes: PipelineNode[]; loading: boolean }) { if (loading && nodes.length === 0) { return (
{Array.from({ length: 5 }).map((_, i) => (
{i < 4 && }
))}
); } return (
{nodes.map((node, idx) => (
{idx < nodes.length - 1 && }
))}
); } // ─── 수신 이력 테이블 ──────────────────────────────────────────────────────────── type FilterSource = 'all' | DataSource; type FilterReceive = 'all' | ReceiveStatus; type FilterPeriod = '6h' | '12h' | '24h'; const PERIOD_HOURS: Record = { '6h': 6, '12h': 12, '24h': 24 }; function filterLogs( rows: DataLogRow[], source: FilterSource, receive: FilterReceive, period: FilterPeriod, ): DataLogRow[] { const cutoff = new Date('2026-04-11T06:40:00'); const hours = PERIOD_HOURS[period]; const from = new Date(cutoff.getTime() - hours * 60 * 60 * 1000); return rows.filter((r) => { if (source !== 'all' && r.source !== source) return false; if (receive !== 'all' && r.receiveStatus !== receive) return false; const ts = new Date(r.timestamp.replace(' ', 'T')); if (ts < from) return false; return true; }); } const LOG_HEADERS = ['시간', '데이터소스', '데이터종류', '크기', '수신상태', '처리상태']; function DataLogTable({ rows, loading }: { rows: DataLogRow[]; loading: boolean }) { return (
{LOG_HEADERS.map((h) => ( ))} {loading && rows.length === 0 ? Array.from({ length: 8 }).map((_, i) => ( {LOG_HEADERS.map((_, j) => ( ))} )) : rows.map((row) => ( ))} {!loading && rows.length === 0 && ( )}
{h}
{row.timestamp} {row.source} {row.dataType} {row.size} {row.receiveStatus} {row.processStatus}
조회된 데이터가 없습니다.
); } // ─── 알림 목록 ─────────────────────────────────────────────────────────────────── function AlertList({ alerts, loading }: { alerts: AlertItem[]; loading: boolean }) { if (loading && alerts.length === 0) { return (
{Array.from({ length: 3 }).map((_, i) => (
))}
); } if (alerts.length === 0) { return

활성 알림이 없습니다.

; } return (
{alerts.map((alert) => (
[{alert.level}] {alert.message} {alert.timestamp}
))}
); } // ─── 메인 패널 ─────────────────────────────────────────────────────────────────── export default function RndRescuePanel() { const [pipeline, setPipeline] = useState([]); const [logs, setLogs] = useState([]); const [alerts, setAlerts] = useState([]); const [loading, setLoading] = useState(false); const [lastUpdate, setLastUpdate] = useState(null); // 필터 const [filterSource, setFilterSource] = useState('all'); const [filterReceive, setFilterReceive] = useState('all'); const [filterPeriod, setFilterPeriod] = useState('24h'); const fetchData = useCallback(async () => { setLoading(true); try { const data = await fetchRescueData(); setPipeline(data.pipeline); setLogs(data.logs); setAlerts(data.alerts); setLastUpdate(new Date()); } finally { setLoading(false); } }, []); useEffect(() => { let isMounted = true; void Promise.resolve().then(() => { if (isMounted) void fetchData(); }); return () => { isMounted = false; }; }, [fetchData]); const filteredLogs = filterLogs(logs, filterSource, filterReceive, filterPeriod); const totalReceived = logs.filter((r) => r.receiveStatus === '수신완료').length; const totalDelayed = logs.filter((r) => r.receiveStatus === '수신대기').length; const totalFailed = logs.filter( (r) => r.receiveStatus === '수신실패' || r.receiveStatus === '시간초과', ).length; return (
{/* ── 헤더 ── */}

긴급구난과제 연계 모니터링

{lastUpdate && ( 갱신:{' '} {lastUpdate.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit', })} )}
{/* 요약 통계 바 */}
정상 수신: {totalReceived}건 | 지연: {totalDelayed}건 | 실패: {totalFailed}건 | 금일 분석 완료: 5 / 6회
{/* ── 스크롤 영역 ── */}
{/* 파이프라인 현황 */}

데이터 파이프라인 현황

{/* 필터 바 + 수신 이력 테이블 */}

데이터 수신 이력

{/* 데이터소스 필터 */} {/* 수신상태 필터 */} {/* 기간 필터 */} {filteredLogs.length}건
{/* 알림 현황 */}

알림 현황

); }