// data.go.kr 기상청 해양기상 API 서비스 // API Key를 환경변수에서 로드 (소스코드 노출 방지) const API_KEY = import.meta.env.VITE_DATA_GO_KR_API_KEY || ''; // API 베이스 URL const BASE_URL = 'https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0'; const MARINE_BASE_URL = 'https://apis.data.go.kr/1360000/SeaFcstInfoService'; import type { WeatherForecastData, MarineWeatherData } from '@interfaces/weather/WeatherInterface'; /** * 초단기 실황 조회 * 기상청 단기예보 API */ export async function getUltraShortForecast( nx: number, ny: number, baseDate: string, baseTime: string, ): Promise { try { const params = new URLSearchParams({ serviceKey: API_KEY, pageNo: '1', numOfRows: '100', dataType: 'JSON', base_date: baseDate, base_time: baseTime, nx: nx.toString(), ny: ny.toString(), }); const response = await fetch(`${BASE_URL}/getUltraSrtFcst?${params}`); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (data.response?.header?.resultCode !== '00') { throw new Error(`API Error: ${data.response?.header?.resultMsg}`); } const items = data.response?.body?.items?.item || []; // 데이터를 시간대별로 그룹화 const forecasts: WeatherForecastData[] = []; const grouped = new Map(); items.forEach((item: Record) => { const key = `${item.fcstDate}-${item.fcstTime}`; if (!grouped.has(key)) { grouped.set(key, { baseDate: item.baseDate, baseTime: item.baseTime, fcstDate: item.fcstDate, fcstTime: item.fcstTime, temperature: 0, windSpeed: 0, windDirection: 0, waveHeight: 0, precipitation: 0, humidity: 0, } as WeatherForecastData); } const forecast = grouped.get(key); if (!forecast) return; // 카테고리별 값 매핑 switch (item.category) { case 'T1H': // 기온 forecast.temperature = parseFloat(item.fcstValue); break; case 'WSD': // 풍속 forecast.windSpeed = parseFloat(item.fcstValue); break; case 'VEC': // 풍향 forecast.windDirection = parseFloat(item.fcstValue); break; case 'WAV': // 파고 forecast.waveHeight = parseFloat(item.fcstValue); break; case 'RN1': // 1시간 강수량 forecast.precipitation = parseFloat(item.fcstValue); break; case 'REH': // 습도 forecast.humidity = parseFloat(item.fcstValue); break; } }); grouped.forEach((value) => forecasts.push(value)); return forecasts; } catch (error) { console.error('초단기 실황 조회 오류:', error); throw error; } } /** * 해상 예보 조회 * 기상청 해상예보 API */ export async function getMarineForecast( regId: string, tmFc: string, ): Promise { try { const params = new URLSearchParams({ serviceKey: API_KEY, pageNo: '1', numOfRows: '10', dataType: 'JSON', regId: regId, // 해역 구역 ID tmFc: tmFc, // 발표시각 (YYYYMMDDHH24) }); const response = await fetch(`${MARINE_BASE_URL}/getWthrWrnList?${params}`); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (data.response?.header?.resultCode !== '00') { console.error('API Error:', data.response?.header?.resultMsg); return null; } const item = data.response?.body?.items?.item?.[0]; if (!item) return null; return { regId: item.regId, regName: item.regName || '', waveHeight: parseFloat(item.waveHeight || '0'), windSpeed: parseFloat(item.windSpeed || '0'), windDirection: item.windDirection || '', temperature: parseFloat(item.temperature || '0'), }; } catch (error) { console.error('해상 예보 조회 오류:', error); return null; } } /** * 지역 좌표를 격자 좌표로 변환 * 기상청 API는 격자 좌표(nx, ny)를 사용 */ export function convertToGridCoords(lat: number, lon: number): { nx: number; ny: number } { // 기상청 격자 변환 공식 const RE = 6371.00877; // 지구 반경(km) const GRID = 5.0; // 격자 간격(km) const SLAT1 = 30.0; // 표준위도1(degree) const SLAT2 = 60.0; // 표준위도2(degree) const OLON = 126.0; // 기준점 경도(degree) const OLAT = 38.0; // 기준점 위도(degree) const XO = 43; // 기준점 X좌표(GRID) const YO = 136; // 기준점 Y좌표(GRID) const DEGRAD = Math.PI / 180.0; const re = RE / GRID; const slat1 = SLAT1 * DEGRAD; const slat2 = SLAT2 * DEGRAD; const olon = OLON * DEGRAD; const olat = OLAT * DEGRAD; let sn = Math.tan(Math.PI * 0.25 + slat2 * 0.5) / Math.tan(Math.PI * 0.25 + slat1 * 0.5); sn = Math.log(Math.cos(slat1) / Math.cos(slat2)) / Math.log(sn); let sf = Math.tan(Math.PI * 0.25 + slat1 * 0.5); sf = (Math.pow(sf, sn) * Math.cos(slat1)) / sn; let ro = Math.tan(Math.PI * 0.25 + olat * 0.5); ro = (re * sf) / Math.pow(ro, sn); let ra = Math.tan(Math.PI * 0.25 + lat * DEGRAD * 0.5); ra = (re * sf) / Math.pow(ra, sn); let theta = lon * DEGRAD - olon; if (theta > Math.PI) theta -= 2.0 * Math.PI; if (theta < -Math.PI) theta += 2.0 * Math.PI; theta *= sn; const nx = Math.floor(ra * Math.sin(theta) + XO + 0.5); const ny = Math.floor(ro - ra * Math.cos(theta) + YO + 0.5); return { nx, ny }; } /** * 현재 날짜/시간을 API 형식으로 변환 */ export function getCurrentBaseDateTime(): { baseDate: string; baseTime: string } { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hour = now.getHours(); // 기상청 API는 매시 30분에 발표 // 현재 시각이 30분 이전이면 이전 시간 데이터 사용 const minutes = now.getMinutes(); let baseHour = hour; if (minutes < 30) { baseHour = hour - 1; if (baseHour < 0) baseHour = 23; } const baseDate = `${year}${month}${day}`; const baseTime = String(baseHour).padStart(2, '0') + '00'; return { baseDate, baseTime }; } /** * 해역 구역 ID 매핑 * 주요 해역별 ID */ export const MARINE_REGIONS = { 서해중부: '1100000000', 서해남부: '1200000000', 제주도해상: '1300000000', 남해서부: '2100000000', 남해동부: '2200000000', 동해중부: '3100000000', 동해남부: '3200000000', }; /** * 좌표에 가장 가까운 해역 구역 찾기 */ export function findNearestMarineRegion(lat: number, lon: number): string { // 간단한 매핑 (실제로는 더 정교한 로직 필요) if (lat >= 37 && lon <= 127) return MARINE_REGIONS.서해중부; if (lat < 37 && lat >= 35 && lon <= 126.5) return MARINE_REGIONS.서해남부; if (lat < 35 && lon >= 126 && lon <= 127) return MARINE_REGIONS.제주도해상; if (lat >= 34 && lat < 36 && lon >= 127 && lon <= 128.5) return MARINE_REGIONS.남해서부; if (lat >= 34 && lat < 36 && lon > 128.5) return MARINE_REGIONS.남해동부; if (lat >= 36 && lon >= 129) return MARINE_REGIONS.동해중부; if (lat < 36 && lon >= 129) return MARINE_REGIONS.동해남부; return MARINE_REGIONS.서해중부; // 기본값 }