- frontend: ESLint 에러 86건 수정 (unused-vars, set-state-in-effect, static-components 등) - backend: simulation.ts req.params 타입 단언 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
264 lines
7.7 KiB
TypeScript
Executable File
264 lines
7.7 KiB
TypeScript
Executable File
// 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<WeatherForecastData[]> {
|
|
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<string, Record<string, unknown>>()
|
|
|
|
items.forEach((item: Record<string, string>) => {
|
|
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<MarineWeatherData | null> {
|
|
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.서해중부 // 기본값
|
|
}
|