kma 구현
This commit is contained in:
7
internal/pkg/kma/errors.go
Normal file
7
internal/pkg/kma/errors.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package kma
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrForecast = errors.New("error in get forecast")
|
||||
)
|
||||
179
internal/pkg/kma/forecaster.go
Normal file
179
internal/pkg/kma/forecaster.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package kma
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
UltraShortForecastInterval = time.Hour
|
||||
UltraShortForecastAvailableAfter = 15 * time.Minute
|
||||
|
||||
ShortTermForecastInterval = 3 * time.Hour
|
||||
ShortTermForecastAvailableAfter = 10 * time.Minute
|
||||
)
|
||||
|
||||
func GetUltraShortForecast(ctx context.Context, key string, base time.Time, nx int, ny int) (*ForecastResponse, error) {
|
||||
// 초단기 예보는 0:30, 1:30, ..., 11:30 단위로 제공된다.
|
||||
// 요청 시각(`base`)을 기준으로, 그보다 크지 않으면서 가장 가까운 기준 시각을 선택한다.
|
||||
selectedBase := selectUltraShortBase(base)
|
||||
|
||||
client := resty.New()
|
||||
|
||||
defer func() {
|
||||
err := client.Close()
|
||||
if err != nil {
|
||||
log.Println("error in close client", err)
|
||||
}
|
||||
}()
|
||||
|
||||
resp, err := client.R().
|
||||
SetContext(ctx).
|
||||
SetQueryParam("pageNo", "1").
|
||||
SetQueryParam("numOfRows", "1000").
|
||||
SetQueryParam("dataType", "JSON").
|
||||
SetQueryParam("base_date", selectedBase.Format("20060102")).
|
||||
SetQueryParam("base_time", selectedBase.Format("1504")).
|
||||
SetQueryParam("nx", strconv.Itoa(nx)).
|
||||
SetQueryParam("ny", strconv.Itoa(ny)).
|
||||
SetQueryParam("authKey", key).
|
||||
SetResult(&ForecastResponse{}). //nolint:exhaustruct
|
||||
Get("https://apihub.kma.go.kr/api/typ02/openApi/VilageFcstInfoService_2.0/getUltraSrtFcst")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in get forecast: %w", err)
|
||||
}
|
||||
|
||||
if resp.IsError() {
|
||||
return nil, fmt.Errorf("%w: %s, %s", ErrForecast, resp.Status(), resp.String())
|
||||
}
|
||||
|
||||
return resp.Result().(*ForecastResponse), nil //nolint:forcetypeassert
|
||||
}
|
||||
|
||||
func GetShortTermForecast(ctx context.Context, key string, base time.Time, nx int, ny int) (*ForecastResponse, error) {
|
||||
selectedBase := selectShortTermBase(base)
|
||||
|
||||
client := resty.New()
|
||||
|
||||
defer func() {
|
||||
err := client.Close()
|
||||
if err != nil {
|
||||
log.Println("error in close client", err)
|
||||
}
|
||||
}()
|
||||
|
||||
resp, err := client.R().SetDebug(true).
|
||||
SetContext(ctx).
|
||||
SetQueryParam("pageNo", "1").
|
||||
SetQueryParam("numOfRows", "1000").
|
||||
SetQueryParam("dataType", "JSON").
|
||||
SetQueryParam("base_date", selectedBase.Format("20060102")).
|
||||
SetQueryParam("base_time", selectedBase.Format("1504")).
|
||||
SetQueryParam("nx", strconv.Itoa(nx)).
|
||||
SetQueryParam("ny", strconv.Itoa(ny)).
|
||||
SetQueryParam("authKey", key).
|
||||
SetResult(&ForecastResponse{}). //nolint:exhaustruct
|
||||
Get("https://apihub.kma.go.kr/api/typ02/openApi/VilageFcstInfoService_2.0/getVilageFcst")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in get forecast: %w", err)
|
||||
}
|
||||
|
||||
if resp.IsError() {
|
||||
return nil, fmt.Errorf("%w: %s, %s", ErrForecast, resp.Status(), resp.String())
|
||||
}
|
||||
|
||||
return resp.Result().(*ForecastResponse), nil //nolint:forcetypeassert
|
||||
}
|
||||
|
||||
// 초단기 예보 기준 시각(0:30, 1:30, ..., 11:30) 중에서,
|
||||
// 주어진 시각 t 기준으로 t 보다 크지 않으면서 가장 가까운 시각을 반환한다.
|
||||
func selectUltraShortBase(base time.Time) time.Time {
|
||||
// 초단기 예보:
|
||||
// - 기준 시각 간격: 1시간
|
||||
// - 조회 가능 시각: 기준 시각 + 15분
|
||||
// - 후보 시작 시각: 이전날 23:30
|
||||
const (
|
||||
startHour = 23
|
||||
startMinute = 30
|
||||
)
|
||||
|
||||
return selectBase(
|
||||
base,
|
||||
UltraShortForecastInterval,
|
||||
UltraShortForecastAvailableAfter,
|
||||
startHour,
|
||||
startMinute,
|
||||
)
|
||||
}
|
||||
|
||||
// 단기 예보 기준 시각(02, 05, 08, 11, 14, 17, 20, 23) 중에서,
|
||||
// 주어진 시각 t 기준으로, t 시각에서 조회 가능한 가장 최근 기준 시각을 반환한다.
|
||||
// 각 기준 시각은 기준 시각 + 10분 이후부터 조회 가능하다.
|
||||
func selectShortTermBase(base time.Time) time.Time {
|
||||
// 단기 예보:
|
||||
// - 기준 시각 간격: 3시간
|
||||
// - 조회 가능 시각: 기준 시각 + 10분
|
||||
// - 후보 시작 시각: 이전날 23:00
|
||||
const (
|
||||
startHour = 23
|
||||
startMinute = 0
|
||||
)
|
||||
|
||||
return selectBase(
|
||||
base,
|
||||
ShortTermForecastInterval,
|
||||
ShortTermForecastAvailableAfter,
|
||||
startHour,
|
||||
startMinute,
|
||||
)
|
||||
}
|
||||
|
||||
func selectBase(base time.Time, interval, availableAfter time.Duration, startHour, startMinute int) time.Time {
|
||||
// KMA API 는 한국 표준시(Asia/Seoul)를 기준으로 동작하므로,
|
||||
// base 가 다른 타임존(예: UTC)인 경우 한국 시간으로 변환해서 계산한다.
|
||||
loc, err := time.LoadLocation("Asia/Seoul")
|
||||
if err != nil {
|
||||
// 로케일 정보를 불러오지 못하면, 기존 base 의 로케이션을 그대로 사용한다.
|
||||
loc = base.Location()
|
||||
}
|
||||
|
||||
baseInKST := base.In(loc)
|
||||
|
||||
prevDay := baseInKST.AddDate(0, 0, -1)
|
||||
|
||||
start := time.Date(prevDay.Year(), prevDay.Month(), prevDay.Day(), startHour, startMinute, 0, 0, loc)
|
||||
end := baseInKST
|
||||
|
||||
var candidates []time.Time
|
||||
|
||||
for t := start; !t.After(end); t = t.Add(interval) {
|
||||
candidates = append(candidates, t)
|
||||
}
|
||||
|
||||
return selectBaseFromCandidates(baseInKST, candidates, availableAfter)
|
||||
}
|
||||
|
||||
// 공통 기준 시각 선택 로직:
|
||||
// - candidates: 오름차순(과거 -> 미래) 기준 시각 목록
|
||||
// - 각 기준 시각은 availableAfter 이후부터 조회 가능
|
||||
// - base 시각 기준으로, 조회 가능한 가장 최근 기준 시각을 반환한다.
|
||||
func selectBaseFromCandidates(base time.Time, candidates []time.Time, availableAfter time.Duration) time.Time {
|
||||
// 가장 최근 기준 시각을 찾기 위해 역순으로 순회한다.
|
||||
for i := len(candidates) - 1; i >= 0; i-- {
|
||||
candidate := candidates[i]
|
||||
availableAt := candidate.Add(availableAfter)
|
||||
|
||||
if base.Before(availableAt) {
|
||||
continue
|
||||
}
|
||||
|
||||
return candidate
|
||||
}
|
||||
|
||||
panic("no available base time found")
|
||||
}
|
||||
38
internal/pkg/kma/model.go
Normal file
38
internal/pkg/kma/model.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package kma
|
||||
|
||||
type ForecastResponse struct {
|
||||
Response Response `json:"response,omitzero"`
|
||||
}
|
||||
|
||||
type Header struct {
|
||||
ResultCode string `json:"resultCode,omitempty"`
|
||||
ResultMsg string `json:"resultMsg,omitempty"`
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
BaseDate string `json:"baseDate,omitempty"`
|
||||
BaseTime string `json:"baseTime,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
FcstDate string `json:"fcstDate,omitempty"`
|
||||
FcstTime string `json:"fcstTime,omitempty"`
|
||||
FcstValue string `json:"fcstValue,omitempty"`
|
||||
Nx int `json:"nx,omitempty"`
|
||||
Ny int `json:"ny,omitempty"`
|
||||
}
|
||||
|
||||
type Items struct {
|
||||
Item []Item `json:"item,omitempty"`
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
DataType string `json:"dataType,omitempty"`
|
||||
Items Items `json:"items,omitzero"`
|
||||
PageNo int `json:"pageNo,omitempty"`
|
||||
NumOfRows int `json:"numOfRows,omitempty"`
|
||||
TotalCount int `json:"totalCount,omitempty"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Header Header `json:"header,omitzero"`
|
||||
Body Body `json:"body,omitzero"`
|
||||
}
|
||||
Reference in New Issue
Block a user