- 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>
216 lines
6.5 KiB
TypeScript
Executable File
216 lines
6.5 KiB
TypeScript
Executable File
import axios from 'axios'
|
|
|
|
const API_KEY = import.meta.env.VITE_WEATHER_API_KEY
|
|
const BASE_URL = 'https://apihub.kma.go.kr/api/typ01/url'
|
|
|
|
// 위경도 → 격자 변환 (기상청 제공 알고리즘)
|
|
export function latLngToGrid(lat: number, lng: number) {
|
|
const RE = 6371.00877 // 지구 반경(km)
|
|
const GRID = 5.0 // 격자 간격(km)
|
|
const SLAT1 = 30.0 // 표준위도1
|
|
const SLAT2 = 60.0 // 표준위도2
|
|
const OLON = 126.0 // 기준점 경도
|
|
const OLAT = 38.0 // 기준점 위도
|
|
const XO = 43 // 기준점 X좌표
|
|
const YO = 136 // 기준점 Y좌표
|
|
|
|
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 = lng * DEGRAD - olon
|
|
if (theta > Math.PI) theta -= 2.0 * Math.PI
|
|
if (theta < -Math.PI) theta += 2.0 * Math.PI
|
|
theta *= sn
|
|
|
|
const x = Math.floor(ra * Math.sin(theta) + XO + 0.5)
|
|
const y = Math.floor(ro - ra * Math.cos(theta) + YO + 0.5)
|
|
|
|
return { nx: x, ny: y }
|
|
}
|
|
|
|
// 현재 날짜/시간을 기상청 API 형식으로 변환
|
|
export function getCurrentBaseTime() {
|
|
const now = new Date()
|
|
const hours = now.getHours()
|
|
const minutes = now.getMinutes()
|
|
|
|
// 기상청 API는 매시간 40분에 업데이트되므로, 40분 이전이면 이전 시간 사용
|
|
let baseHour = hours
|
|
if (minutes < 40) {
|
|
baseHour = hours - 1
|
|
if (baseHour < 0) baseHour = 23
|
|
}
|
|
|
|
const baseDate = now.toISOString().slice(0, 10).replace(/-/g, '')
|
|
const baseTime = String(baseHour).padStart(2, '0') + '00'
|
|
|
|
return { baseDate, baseTime }
|
|
}
|
|
|
|
// 초단기실황 조회 (현재 기상 상태)
|
|
export async function getUltraShortNowcast(lat: number, lng: number) {
|
|
const { nx, ny } = latLngToGrid(lat, lng)
|
|
const { baseDate, baseTime } = getCurrentBaseTime()
|
|
|
|
try {
|
|
const response = await axios.get(`${BASE_URL}/getUltraSrtNcst`, {
|
|
params: {
|
|
serviceKey: API_KEY,
|
|
numOfRows: 10,
|
|
pageNo: 1,
|
|
base_date: baseDate,
|
|
base_time: baseTime,
|
|
nx,
|
|
ny,
|
|
dataType: 'JSON',
|
|
},
|
|
})
|
|
|
|
const items = response.data.response.body.items.item
|
|
|
|
// 데이터 파싱
|
|
const weather: Record<string, number> = {}
|
|
items.forEach((item: Record<string, string>) => {
|
|
switch (item.category) {
|
|
case 'T1H': // 기온
|
|
weather.temperature = parseFloat(item.obsrValue)
|
|
break
|
|
case 'RN1': // 강수량
|
|
weather.rainfall = parseFloat(item.obsrValue)
|
|
break
|
|
case 'WSD': // 풍속
|
|
weather.windSpeed = parseFloat(item.obsrValue)
|
|
break
|
|
case 'VEC': // 풍향
|
|
weather.windDirection = parseInt(item.obsrValue)
|
|
break
|
|
case 'REH': // 습도
|
|
weather.humidity = parseInt(item.obsrValue)
|
|
break
|
|
case 'PTY': // 강수형태
|
|
weather.precipType = parseInt(item.obsrValue)
|
|
break
|
|
}
|
|
})
|
|
|
|
return weather
|
|
} catch (error) {
|
|
console.error('기상청 API 오류:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// 단기예보 조회 (최대 72시간)
|
|
export async function getShortForecast(lat: number, lng: number) {
|
|
const { nx, ny } = latLngToGrid(lat, lng)
|
|
const now = new Date()
|
|
|
|
// 단기예보 base_time: 02:00, 05:00, 08:00, 11:00, 14:00, 17:00, 20:00, 23:00
|
|
const baseHours = [2, 5, 8, 11, 14, 17, 20, 23]
|
|
const currentHour = now.getHours()
|
|
|
|
const baseHour = baseHours.reduce((prev, curr) =>
|
|
curr <= currentHour ? curr : prev
|
|
)
|
|
|
|
const baseDate = now.toISOString().slice(0, 10).replace(/-/g, '')
|
|
const baseTime = String(baseHour).padStart(2, '0') + '00'
|
|
|
|
try {
|
|
const response = await axios.get(`${BASE_URL}/getVilageFcst`, {
|
|
params: {
|
|
serviceKey: API_KEY,
|
|
numOfRows: 1000,
|
|
pageNo: 1,
|
|
base_date: baseDate,
|
|
base_time: baseTime,
|
|
nx,
|
|
ny,
|
|
dataType: 'JSON',
|
|
},
|
|
})
|
|
|
|
const items = response.data.response.body.items.item
|
|
|
|
// 시간별로 그룹화
|
|
const forecastByTime: Record<string, Record<string, string | number>> = {}
|
|
items.forEach((item: Record<string, string>) => {
|
|
const key = `${item.fcstDate}_${item.fcstTime}`
|
|
if (!forecastByTime[key]) {
|
|
forecastByTime[key] = {
|
|
date: item.fcstDate,
|
|
time: item.fcstTime,
|
|
}
|
|
}
|
|
|
|
switch (item.category) {
|
|
case 'TMP': // 기온
|
|
forecastByTime[key].temperature = parseFloat(item.fcstValue)
|
|
break
|
|
case 'WSD': // 풍속
|
|
forecastByTime[key].windSpeed = parseFloat(item.fcstValue)
|
|
break
|
|
case 'VEC': // 풍향
|
|
forecastByTime[key].windDirection = parseInt(item.fcstValue)
|
|
break
|
|
case 'SKY': // 하늘상태
|
|
forecastByTime[key].skyCondition = parseInt(item.fcstValue)
|
|
break
|
|
case 'POP': // 강수확률
|
|
forecastByTime[key].precipProbability = parseInt(item.fcstValue)
|
|
break
|
|
case 'WAV': // 파고
|
|
forecastByTime[key].waveHeight = parseFloat(item.fcstValue)
|
|
break
|
|
}
|
|
})
|
|
|
|
return Object.values(forecastByTime)
|
|
} catch (error) {
|
|
console.error('단기예보 API 오류:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// 풍향을 16방위로 변환
|
|
export function windDirectionToText(degree: number): string {
|
|
const directions = [
|
|
'N', 'NNE', 'NE', 'ENE',
|
|
'E', 'ESE', 'SE', 'SSE',
|
|
'S', 'SSW', 'SW', 'WSW',
|
|
'W', 'WNW', 'NW', 'NNW'
|
|
]
|
|
|
|
const index = Math.round((degree % 360) / 22.5) % 16
|
|
return directions[index]
|
|
}
|
|
|
|
// 해상 기상 정보 (Mock - 실제로는 해양기상청 API 사용)
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
export async function getMarineWeather(lat: number, lng: number) {
|
|
// TODO: 해양기상청 API 연동
|
|
// 현재는 Mock 데이터 반환
|
|
return {
|
|
waveHeight: 1.2, // 파고 (m)
|
|
waveDirection: 135, // 파향 (도)
|
|
wavePeriod: 5.5, // 주기 (초)
|
|
seaTemperature: 12.5, // 수온 (°C)
|
|
currentSpeed: 0.3, // 해류속도 (m/s)
|
|
currentDirection: 180, // 해류방향 (도)
|
|
visibility: 15, // 시정 (km)
|
|
}
|
|
}
|