wing-ops/frontend/src/tabs/hns/hooks/useWeatherFetch.ts
Nan Kyung Lee 8f98f63aa5 feat(aerial): CCTV 실시간 HLS 스트림 + HNS 분석 고도화
CCTV 실시간 영상:
- CCTVPlayer 컴포넌트 (hls.js 기반 HLS/MJPEG/MP4 재생)
- 백엔드 HLS 프록시 엔드포인트 (CORS 우회, m3u8 URL 재작성)
- KHOA 15개 + KBS 6개 실제 해안 CCTV 연동
- Vite dev proxy, 스트림 타입 자동 감지 유틸리티

HNS 분석:
- HNS 시나리오 저장/불러오기/재계산 기능
- 물질 DB 검색 및 상세 정보 연동
- 좌표/파라미터 입력 UI 개선
- Python 확산 모델 스크립트 (hns_dispersion.py)

공통:
- 3D 지도 토글, 보고서 생성 개선
- useSubMenu 훅, mapUtils 확장
- ESLint set-state-in-effect 수정

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 17:21:41 +09:00

126 lines
3.7 KiB
TypeScript

import { useState, useEffect, useRef } from 'react';
import {
convertToGridCoords,
getUltraShortForecast,
getCurrentBaseDateTime,
} from '@tabs/weather/services/weatherApi';
import type { StabilityClass, WeatherFetchResult } from '../utils/dispersionTypes';
/**
* Turner 간이법으로 Pasquill-Gifford 안정도 산출
* 풍속 + 시간대(주간/야간) 기반
*/
function deriveStabilityClass(windSpeed: number, hour: number): StabilityClass {
const isNight = hour < 6 || hour >= 18;
if (isNight) {
if (windSpeed < 2) return 'F';
if (windSpeed < 3) return 'E';
return 'D';
}
// 주간 (중간 수준 일사량 가정)
if (windSpeed < 2) return 'A';
if (windSpeed < 3) return 'B';
if (windSpeed < 5) return 'C';
return 'D';
}
/** 풍향(도) → 16방위 문자열 */
export function windDirToCompass(deg: number): string {
const dirs = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
const idx = Math.round(((deg % 360) + 360) % 360 / 22.5) % 16;
return dirs[idx];
}
const DEFAULT_WEATHER: WeatherFetchResult = {
windSpeed: 5.0,
windDirection: 270,
temperature: 15,
humidity: 60,
stability: 'D',
isLoading: false,
error: null,
lastUpdate: null,
};
/**
* 위치 기반 기상정보 자동조회 훅
* KMA 초단기실황 API 활용, 500ms 디바운스
* baseDate: 'YYYY-MM-DD' (선택), baseTime: 'HH:mm' (선택)
* 미제공 시 getCurrentBaseDateTime()으로 현재 시각 사용
*/
export function useWeatherFetch(lat: number, lon: number, baseDate?: string, baseTime?: string): WeatherFetchResult {
const [weather, setWeather] = useState<WeatherFetchResult>({
...DEFAULT_WEATHER,
isLoading: true,
});
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(async () => {
setWeather(prev => ({ ...prev, isLoading: true, error: null }));
try {
const { nx, ny } = convertToGridCoords(lat, lon);
let apiBaseDate: string;
let apiBaseTime: string;
let stabilityHour: number;
if (baseDate && baseTime) {
// 'YYYY-MM-DD' → 'YYYYMMDD'
apiBaseDate = baseDate.replace(/-/g, '');
// 'HH:mm' → 'HH00'
apiBaseTime = baseTime.slice(0, 2) + '00';
stabilityHour = parseInt(baseTime.slice(0, 2), 10);
} else {
const current = getCurrentBaseDateTime();
apiBaseDate = current.baseDate;
apiBaseTime = current.baseTime;
stabilityHour = new Date().getHours();
}
const forecasts = await getUltraShortForecast(nx, ny, apiBaseDate, apiBaseTime);
if (forecasts.length > 0) {
const f = forecasts[0];
const stability = deriveStabilityClass(f.windSpeed, stabilityHour);
setWeather({
windSpeed: f.windSpeed,
windDirection: f.windDirection,
temperature: f.temperature,
humidity: f.humidity,
stability,
isLoading: false,
error: null,
lastUpdate: new Date(),
});
} else {
setWeather({
...DEFAULT_WEATHER,
isLoading: false,
error: 'API 응답 데이터 없음',
lastUpdate: new Date(),
});
}
} catch {
setWeather({
...DEFAULT_WEATHER,
isLoading: false,
error: 'KMA API 조회 실패 (기본값 사용)',
lastUpdate: new Date(),
});
}
}, 500);
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [lat, lon, baseDate, baseTime]);
return weather;
}