// data.go.kr 기상청 해양기상 API 서비스 // API Key를 환경변수에서 로드 (소스코드 노출 방지) const API_KEY = import.meta.env.VITE_DATA_GO_KR_API_KEY || '' // API 베이스 URL const BASE_URL = 'http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0' const MARINE_BASE_URL = 'http://apis.data.go.kr/1360000/SeaFcstInfoService' export interface WeatherForecastData { baseDate: string baseTime: string fcstDate: string fcstTime: string temperature: number windSpeed: number windDirection: number waveHeight: number precipitation: number humidity: number } export interface MarineWeatherData { regId: string // 구역 ID regName: string // 구역명 waveHeight: number // 파고 (m) windSpeed: number // 풍속 (m/s) windDirection: string // 풍향 temperature: number // 수온 (°C) } /** * 초단기 실황 조회 * 기상청 단기예보 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 }) } const forecast = grouped.get(key) // 카테고리별 값 매핑 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.서해중부 // 기본값 }