import { useState, useEffect, useCallback } from 'react'; import { getRecentObservation, OBS_STATION_CODES, type RecentObservation, } from '@tabs/weather/services/khoaApi'; import { getUltraShortForecast, getMarineForecast, convertToGridCoords, getCurrentBaseDateTime, MARINE_REGIONS, type WeatherForecastData, type MarineWeatherData, } from '@tabs/weather/services/weatherApi'; const KEY_TO_NAME: Record = { incheon: '인천', gunsan: '군산', mokpo: '목포', yeosu: '여수', tongyeong: '통영', ulsan: '울산', pohang: '포항', donghae: '동해', sokcho: '속초', jeju: '제주', }; // 조위관측소 목록 const STATIONS = Object.entries(OBS_STATION_CODES).map(([key, code]) => ({ key, code, name: KEY_TO_NAME[key] ?? key, })); // 기상청 초단기실황 지점 (위경도) const WEATHER_STATIONS = [ { name: '인천', lat: 37.4563, lon: 126.7052 }, { name: '군산', lat: 35.9679, lon: 126.7361 }, { name: '목포', lat: 34.8118, lon: 126.3922 }, { name: '부산', lat: 35.1028, lon: 129.0323 }, { name: '제주', lat: 33.5131, lon: 126.5297 }, ]; // 해역 목록 const MARINE_REGION_LIST = Object.entries(MARINE_REGIONS).map(([name, regId]) => ({ name, regId, })); type TabId = 'khoa' | 'kma-ultra' | 'kma-marine'; interface KhoaRow { stationName: string; data: RecentObservation | null; error: boolean; } interface KmaUltraRow { stationName: string; data: WeatherForecastData | null; error: boolean; } interface MarineRow { name: string; regId: string; data: MarineWeatherData | null; error: boolean; } const fmt = (v: number | null | undefined, digits = 1): string => v != null ? v.toFixed(digits) : '-'; function StatusBadge({ loading, errorCount, total }: { loading: boolean; errorCount: number; total: number }) { if (loading) { return ( 조회 중... ); } if (errorCount === total) { return ( 연계 오류 ); } if (errorCount > 0) { return ( 일부 오류 ({errorCount}/{total}) ); } return ( 정상 ); } function KhoaTable({ rows, loading }: { rows: KhoaRow[]; loading: boolean }) { const headers = ['관측소', '수온(°C)', '기온(°C)', '기압(hPa)', '풍향(°)', '풍속(m/s)', '유향(°)', '유속(m/s)', '조위(cm)', '상태']; return (
{headers.map((h) => ( ))} {loading && rows.length === 0 ? Array.from({ length: 5 }).map((_, i) => ( {headers.map((_, j) => ( ))} )) : rows.map((row) => ( ))}
{h}
{row.stationName} {fmt(row.data?.water_temp)} {fmt(row.data?.air_temp)} {fmt(row.data?.air_pres)} {fmt(row.data?.wind_dir, 0)} {fmt(row.data?.wind_speed)} {fmt(row.data?.current_dir, 0)} {fmt(row.data?.current_speed)} {fmt(row.data?.tide_level, 0)} {row.error ? ( 오류 ) : row.data ? ( 정상 ) : ( - )}
); } function KmaUltraTable({ rows, loading }: { rows: KmaUltraRow[]; loading: boolean }) { const headers = ['지점', '기온(°C)', '풍속(m/s)', '풍향(°)', '파고(m)', '강수(mm)', '습도(%)', '상태']; return (
{headers.map((h) => ( ))} {loading && rows.length === 0 ? Array.from({ length: 3 }).map((_, i) => ( {headers.map((_, j) => ( ))} )) : rows.map((row) => ( ))}
{h}
{row.stationName} {fmt(row.data?.temperature)} {fmt(row.data?.windSpeed)} {fmt(row.data?.windDirection, 0)} {fmt(row.data?.waveHeight)} {fmt(row.data?.precipitation)} {fmt(row.data?.humidity, 0)} {row.error ? ( 오류 ) : row.data ? ( 정상 ) : ( - )}
); } function MarineTable({ rows, loading }: { rows: MarineRow[]; loading: boolean }) { const headers = ['해역명', '파고(m)', '풍속(m/s)', '풍향', '수온(°C)', '상태']; return (
{headers.map((h) => ( ))} {loading && rows.length === 0 ? Array.from({ length: 4 }).map((_, i) => ( {headers.map((_, j) => ( ))} )) : rows.map((row) => ( ))}
{h}
{row.name} {fmt(row.data?.waveHeight)} {fmt(row.data?.windSpeed)} {row.data?.windDirection || '-'} {fmt(row.data?.temperature)} {row.error ? ( 오류 ) : row.data ? ( 정상 ) : ( - )}
); } export default function MonitorRealtimePanel() { const [activeTab, setActiveTab] = useState('khoa'); const [lastUpdate, setLastUpdate] = useState(null); // KHOA 조위관측소 const [khoaRows, setKhoaRows] = useState([]); const [khoaLoading, setKhoaLoading] = useState(false); // 기상청 초단기실황 const [kmaRows, setKmaRows] = useState([]); const [kmaLoading, setKmaLoading] = useState(false); // 기상청 해상예보 const [marineRows, setMarineRows] = useState([]); const [marineLoading, setMarineLoading] = useState(false); const fetchKhoa = useCallback(async () => { setKhoaLoading(true); const results = await Promise.allSettled( STATIONS.map((s) => getRecentObservation(s.code)) ); const rows: KhoaRow[] = STATIONS.map((s, i) => { const result = results[i]; if (result.status === 'fulfilled') { return { stationName: s.name, data: result.value, error: false }; } return { stationName: s.name, data: null, error: true }; }); setKhoaRows(rows); setKhoaLoading(false); setLastUpdate(new Date()); }, []); const fetchKmaUltra = useCallback(async () => { setKmaLoading(true); const { baseDate, baseTime } = getCurrentBaseDateTime(); const results = await Promise.allSettled( WEATHER_STATIONS.map((s) => { const { nx, ny } = convertToGridCoords(s.lat, s.lon); return getUltraShortForecast(nx, ny, baseDate, baseTime); }) ); const rows: KmaUltraRow[] = WEATHER_STATIONS.map((s, i) => { const result = results[i]; if (result.status === 'fulfilled' && result.value.length > 0) { return { stationName: s.name, data: result.value[0], error: false }; } return { stationName: s.name, data: null, error: result.status === 'rejected' }; }); setKmaRows(rows); setKmaLoading(false); setLastUpdate(new Date()); }, []); const fetchMarine = useCallback(async () => { setMarineLoading(true); const now = new Date(); const pad = (n: number) => String(n).padStart(2, '0'); const tmFc = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}00`; const results = await Promise.allSettled( MARINE_REGION_LIST.map((r) => getMarineForecast(r.regId, tmFc)) ); const rows: MarineRow[] = MARINE_REGION_LIST.map((r, i) => { const result = results[i]; if (result.status === 'fulfilled') { return { name: r.name, regId: r.regId, data: result.value, error: false }; } return { name: r.name, regId: r.regId, data: null, error: true }; }); setMarineRows(rows); setMarineLoading(false); setLastUpdate(new Date()); }, []); // 탭 전환 시 해당 데이터 로드 useEffect(() => { let isMounted = true; if (activeTab === 'khoa' && khoaRows.length === 0) { void Promise.resolve().then(() => { if (isMounted) void fetchKhoa(); }); } else if (activeTab === 'kma-ultra' && kmaRows.length === 0) { void Promise.resolve().then(() => { if (isMounted) void fetchKmaUltra(); }); } else if (activeTab === 'kma-marine' && marineRows.length === 0) { void Promise.resolve().then(() => { if (isMounted) void fetchMarine(); }); } return () => { isMounted = false; }; }, [activeTab, khoaRows.length, kmaRows.length, marineRows.length, fetchKhoa, fetchKmaUltra, fetchMarine]); const handleRefresh = () => { if (activeTab === 'khoa') fetchKhoa(); else if (activeTab === 'kma-ultra') fetchKmaUltra(); else fetchMarine(); }; const isLoading = activeTab === 'khoa' ? khoaLoading : activeTab === 'kma-ultra' ? kmaLoading : marineLoading; const currentRows = activeTab === 'khoa' ? khoaRows : activeTab === 'kma-ultra' ? kmaRows : marineRows; const errorCount = currentRows.filter((r) => r.error).length; const totalCount = activeTab === 'khoa' ? STATIONS.length : activeTab === 'kma-ultra' ? WEATHER_STATIONS.length : MARINE_REGION_LIST.length; const TABS: { id: TabId; label: string }[] = [ { id: 'khoa', label: 'KHOA 조위관측소' }, { id: 'kma-ultra', label: '기상청 초단기실황' }, { id: 'kma-marine', label: '기상청 해상예보' }, ]; return (
{/* 헤더 */}

실시간 관측자료 모니터링

{lastUpdate && ( 갱신: {lastUpdate.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit' })} )}
{/* 탭 */}
{TABS.map((tab) => ( ))}
{/* 상태 표시줄 */}
{activeTab === 'khoa' && `관측소 ${totalCount}개`} {activeTab === 'kma-ultra' && `지점 ${totalCount}개`} {activeTab === 'kma-marine' && `해역 ${totalCount}개`}
{/* 테이블 콘텐츠 */}
{activeTab === 'khoa' && ( )} {activeTab === 'kma-ultra' && ( )} {activeTab === 'kma-marine' && ( )}
); }