wing-ops/frontend/src/tabs/weather/services/khoaApi.ts
leedano 3743027ce7 feat(weather): 기상 정보 기상 레이어 업데이트 (#78)
## Summary
- 기상 맵 컨트롤 컴포넌트 추가 및 KHOA API 연동 개선
- KHOA API 엔드포인트 교체 및 해양예측 오버레이 Canvas 렌더링 전환

## 변경 파일
- OceanForecastOverlay.tsx
- WeatherMapOverlay.tsx
- WeatherView.tsx
- useOceanForecast.ts
- khoaApi.ts
- vite.config.ts

## Test plan
- [ ] 기상정보 -> 기상 레이어 -> 해황 예보도 클릭 -> 이미지 렌더링 확인
- [ ] 기상정보 -> 기상 레이어 -> 백터 바람 클릭 -> 백터 이미지 렌더링 확인

Co-authored-by: Nan Kyung Lee <nankyunglee@Nanui-Macmini.local>
Reviewed-on: #78
Co-authored-by: leedano <dnlee@gcsc.co.kr>
Co-committed-by: leedano <dnlee@gcsc.co.kr>
2026-03-11 11:14:25 +09:00

193 lines
5.6 KiB
TypeScript
Executable File

// KHOA (국립해양조사원) API 서비스
// API Key를 환경변수에서 로드 (소스코드 노출 방지)
const API_KEY = import.meta.env.VITE_DATA_GO_KR_API_KEY || ''
const BASE_URL = 'https://apis.data.go.kr/1192136/oceanCondition/GetOceanConditionApiService'
const RECENT_OBS_URL = 'https://apis.data.go.kr/1192136/dtRecent/GetDTRecentApiService'
// 지역 유형 (총 20개 지역)
export const OCEAN_REGIONS = {
: 'KOREA',
: 'INCHEON',
: 'GUNSAN',
: 'MOKPO',
: 'JEJU',
: 'YEOSU',
: 'TONGYEONG',
: 'BUSAN',
: 'ULSAN',
: 'POHANG',
: 'SOKCHO',
: 'DONGHAE'
} as const
export interface OceanForecastData {
imgFileNm: string // 이미지 파일명
imgFilePath: string // 이미지 파일경로
ofcBrnchId: string // 해황예보도 지점코드
ofcBrnchNm: string // 해황예보도 지점이름
ofcFrcstTm: string // 해황예보도 예보시각
ofcFrcstYmd: string // 해황예보도 예보일자
}
interface OceanForecastApiResponse {
header: { resultCode: string; resultMsg: string }
body: {
items: { item: OceanForecastData[] }
pageNo: number
numOfRows: number
totalCount: number
}
}
/**
* 해황예보도 조회
* @param regionType 지역 유형 (KOREA, INCHEON, BUSAN 등)
* @returns 해황예보 이미지 데이터 배열
*/
export async function getOceanForecast(
regionType: string = 'KOREA'
): Promise<OceanForecastData[]> {
try {
const params = new URLSearchParams({
serviceKey: API_KEY,
areaCode: regionType,
type: 'json',
})
const response = await fetch(`${BASE_URL}?${params}`)
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`)
}
const data = await response.json() as OceanForecastApiResponse
return data?.body?.items?.item ?? []
} catch (error) {
console.error('해황예보도 조회 오류:', error)
throw error
}
}
/**
* 현재 시간에 가장 가까운 예보 데이터 찾기
* @param forecasts 예보 데이터 배열
* @returns 가장 최근 예보 데이터
*/
export function getLatestForecast(forecasts: OceanForecastData[]): OceanForecastData | null {
if (!forecasts || forecasts.length === 0) return null
const sorted = [...forecasts].sort((a, b) => {
const dateTimeA = `${a.ofcFrcstYmd}${a.ofcFrcstTm}`
const dateTimeB = `${b.ofcFrcstYmd}${b.ofcFrcstTm}`
return dateTimeB.localeCompare(dateTimeA)
})
return sorted[0]
}
/**
* 특정 시간대의 예보 데이터 필터링
* @param forecasts 예보 데이터 배열
* @param targetDay 목표 날짜 (YYYYMMDD)
* @param targetHour 목표 시간 (HH)
* @returns 필터링된 예보 데이터
*/
export function getForecastByTime(
forecasts: OceanForecastData[],
targetDay: string,
targetHour: string
): OceanForecastData | null {
return (
forecasts.find((f) => f.ofcFrcstYmd === targetDay && f.ofcFrcstTm === targetHour) || null
)
}
// 조위관측소 코드 매핑
export const OBS_STATION_CODES: Record<string, string> = {
incheon: 'DT_0001',
gunsan: 'DT_0005',
mokpo: 'DT_0008',
yeosu: 'DT_0010',
tongyeong: 'DT_0012',
ulsan: 'DT_0015',
pohang: 'DT_0016',
donghae: 'DT_0018',
sokcho: 'DT_0019',
jeju: 'DT_0020',
}
export interface RecentObservation {
water_temp: number | null // 수온 (°C)
air_temp: number | null // 기온 (°C)
air_pres: number | null // 기압 (hPa)
wind_dir: number | null // 풍향 (°)
wind_speed: number | null // 풍속 (m/s)
current_dir: number | null // 유향 (°)
current_speed: number | null // 유속 (m/s)
tide_level: number | null // 조위 (cm)
}
/**
* 조위관측소 최신 관측데이터 조회
*/
export async function getRecentObservation(obsCode: string): Promise<RecentObservation | null> {
try {
const now = new Date()
const reqDate = formatDateForAPI(now)
const params = new URLSearchParams({
serviceKey: API_KEY,
obsCode,
type: 'json',
reqDate,
})
const response = await fetch(`${RECENT_OBS_URL}?${params}`)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data = await response.json()
const item = data.body ? data.body.items.item[0] : null
if (!item) return null
return {
water_temp: item.wtem != null ? parseFloat(item.wtem) : null,
air_temp: item.artmp != null ? parseFloat(item.artmp) : null,
air_pres: item.atmpr != null ? parseFloat(item.atmpr) : null,
wind_dir: item.wndrct != null ? parseFloat(item.wndrct) : null,
wind_speed: item.wspd != null ? parseFloat(item.wspd) : null,
current_dir: item.crdir != null ? parseFloat(item.crdir) : null,
current_speed: item.crsp != null ? parseFloat(item.crsp) : null,
tide_level: item.bscTdlvHgt != null ? parseFloat(item.bscTdlvHgt) : null,
}
} catch (error) {
console.error(`관측소 ${obsCode} 데이터 조회 오류:`, error)
return null
}
}
/**
* 날짜 문자열을 YYYYMMDD 형식으로 변환
*/
export function formatDateForAPI(date: Date): string {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}${month}${day}`
}
/**
* 시간을 HH 형식으로 변환 (3시간 단위로 반올림)
*/
export function formatHourForAPI(date: Date): string {
const hour = date.getHours()
// 3시간 단위로 반올림 (00, 03, 06, 09, 12, 15, 18, 21)
const roundedHour = Math.floor(hour / 3) * 3
return String(roundedHour).padStart(2, '0')
}