@uniai-fe/uds-templates 0.5.14 → 0.5.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -12,7 +12,10 @@ import type {
|
|
|
12
12
|
KMA_Res_WeatherNow,
|
|
13
13
|
} from "../../types";
|
|
14
14
|
import WEATHER_KOREA_RESPONSE from "../../data/response";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
getWeatherBaseMoments,
|
|
17
|
+
getWeatherDailyRangeBaseMoments,
|
|
18
|
+
} from "../../utils/date-time";
|
|
16
19
|
import { extractWeatherSummary } from "../../utils";
|
|
17
20
|
import type { NextResponse } from "next/server";
|
|
18
21
|
import { generateQueryUrl, nextAPILog } from "@uniai-fe/util-api";
|
|
@@ -63,6 +66,51 @@ const getWeatherParams = ({
|
|
|
63
66
|
};
|
|
64
67
|
};
|
|
65
68
|
|
|
69
|
+
const getWeatherDailyRangeParams = ({
|
|
70
|
+
authKey,
|
|
71
|
+
searchParams,
|
|
72
|
+
}: {
|
|
73
|
+
authKey: string;
|
|
74
|
+
searchParams: URLSearchParams;
|
|
75
|
+
}): KMA_Req_Weather => {
|
|
76
|
+
const { base_date, base_time } = getWeatherDailyRangeBaseMoments();
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
pageNo: "1",
|
|
80
|
+
numOfRows: "1000",
|
|
81
|
+
authKey,
|
|
82
|
+
dataType: "JSON",
|
|
83
|
+
nx: 0,
|
|
84
|
+
ny: 0,
|
|
85
|
+
...Object.fromEntries(searchParams.entries()),
|
|
86
|
+
base_date,
|
|
87
|
+
base_time,
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const hasTodayTemperatureRange = (
|
|
92
|
+
forecast: API_Res_WeatherKoreaForecast,
|
|
93
|
+
): boolean =>
|
|
94
|
+
forecast.today.max_temperature !== null &&
|
|
95
|
+
forecast.today.min_temperature !== null;
|
|
96
|
+
|
|
97
|
+
const fillTodayTemperatureRange = ({
|
|
98
|
+
forecast,
|
|
99
|
+
dailyRange,
|
|
100
|
+
}: {
|
|
101
|
+
forecast: API_Res_WeatherKoreaForecast;
|
|
102
|
+
dailyRange: API_Res_WeatherKoreaForecast;
|
|
103
|
+
}): API_Res_WeatherKoreaForecast => ({
|
|
104
|
+
...forecast,
|
|
105
|
+
today: {
|
|
106
|
+
...forecast.today,
|
|
107
|
+
max_temperature:
|
|
108
|
+
forecast.today.max_temperature ?? dailyRange.today.max_temperature,
|
|
109
|
+
min_temperature:
|
|
110
|
+
forecast.today.min_temperature ?? dailyRange.today.min_temperature,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
66
114
|
/**
|
|
67
115
|
* 기상청 API; 현재 날씨
|
|
68
116
|
* @method GET
|
|
@@ -185,7 +233,38 @@ export const routeWeatherKoreaForecast = async ({
|
|
|
185
233
|
try {
|
|
186
234
|
const res: KMA_Res_WeatherForecast = await (await fetch(url)).json();
|
|
187
235
|
resDefault.raw = res;
|
|
188
|
-
|
|
236
|
+
const forecast = extractWeatherSummary.forecast(resDefault, res);
|
|
237
|
+
|
|
238
|
+
if (hasTodayTemperatureRange(forecast)) {
|
|
239
|
+
return forecast;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const dailyRangeQueryParams = Object.fromEntries(
|
|
243
|
+
Object.entries(getWeatherDailyRangeParams({ authKey, searchParams })).map(
|
|
244
|
+
([key, value]) => [key, String(value)],
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
const dailyRangeUrl = generateQueryUrl({
|
|
248
|
+
domain,
|
|
249
|
+
routeUrl,
|
|
250
|
+
queryUrl: QUERY_URL.forecast,
|
|
251
|
+
searchParams: new URLSearchParams(dailyRangeQueryParams),
|
|
252
|
+
});
|
|
253
|
+
const dailyRangeRes: KMA_Res_WeatherForecast = await (
|
|
254
|
+
await fetch(dailyRangeUrl)
|
|
255
|
+
).json();
|
|
256
|
+
const dailyRange = extractWeatherSummary.forecast(
|
|
257
|
+
{
|
|
258
|
+
...resDefault,
|
|
259
|
+
raw: dailyRangeRes,
|
|
260
|
+
today: { ...resDefault.today },
|
|
261
|
+
day_1: { ...resDefault.day_1 },
|
|
262
|
+
day_2: { ...resDefault.day_2 },
|
|
263
|
+
},
|
|
264
|
+
dailyRangeRes,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
return fillTodayTemperatureRange({ forecast, dailyRange });
|
|
189
268
|
} catch (error) {
|
|
190
269
|
nextAPILog("GET", routeUrl, url, { error });
|
|
191
270
|
|
|
@@ -15,7 +15,7 @@ const API_BASE_CURRENT = "https://api.openweathermap.org/data/2.5/weather";
|
|
|
15
15
|
* 글로벌 날씨 API; 예보 날씨 URL
|
|
16
16
|
*/
|
|
17
17
|
const API_BASE_FORECAST =
|
|
18
|
-
"https://pro.openweathermap.org/data/2.5/
|
|
18
|
+
"https://pro.openweathermap.org/data/2.5/forecast/hourly";
|
|
19
19
|
|
|
20
20
|
const COMMON_OPTIONS = {
|
|
21
21
|
units: "metric", // 섭씨 온도
|
|
@@ -37,6 +37,29 @@ export const getWeatherBaseMoments = (
|
|
|
37
37
|
return { base_date, base_time };
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* 날씨 도구; KMA 오늘 일최고/일최저 조회 기준시각
|
|
42
|
+
* @util
|
|
43
|
+
* @return {KMA_Req_BaseMoments} 오늘 TMN/TMX 보강용 발표일자/발표시각
|
|
44
|
+
* @desc
|
|
45
|
+
* - KMA 최신 단기예보는 과거 예보시각의 오늘 TMN/TMX를 포함하지 않을 수 있다.
|
|
46
|
+
* - 기본은 전날 23시 발표를 사용하고, 23시 이후에는 최근 24시간 범위 안의 당일 02시 발표를 사용한다.
|
|
47
|
+
*/
|
|
48
|
+
export const getWeatherDailyRangeBaseMoments = (): KMA_Req_BaseMoments => {
|
|
49
|
+
const now = new Date();
|
|
50
|
+
const base = new Date(now);
|
|
51
|
+
const base_hour = now.getHours() >= 23 ? 2 : 23;
|
|
52
|
+
|
|
53
|
+
if (base_hour === 23) {
|
|
54
|
+
base.setDate(base.getDate() - 1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
base_date: dateFormat(base).replace(/-/g, ""),
|
|
59
|
+
base_time: `${base_hour.toString().padStart(2, "0")}00`,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
40
63
|
/**
|
|
41
64
|
* 날씨 도구; 현재 날씨/예보 시각
|
|
42
65
|
* @util
|
|
@@ -49,17 +72,18 @@ export const getWeatherBaseMoments = (
|
|
|
49
72
|
*/
|
|
50
73
|
export const getMoments = (): WeatherUtilDateTimeMoments => {
|
|
51
74
|
const now = new Date();
|
|
52
|
-
const today = now
|
|
75
|
+
const today = dateFormat(now).replace(/-/g, "");
|
|
53
76
|
|
|
54
|
-
const tomorrow = new Date(now.getTime() + 86400000)
|
|
55
|
-
|
|
56
|
-
.slice(0, 10)
|
|
57
|
-
.replace(/-/g, "");
|
|
77
|
+
const tomorrow = new Date(now.getTime() + 86400000);
|
|
78
|
+
const tomorrowKey = dateFormat(tomorrow).replace(/-/g, "");
|
|
58
79
|
|
|
59
|
-
const dayAfterTomorrow = new Date(now.getTime() + 2 * 86400000)
|
|
60
|
-
|
|
61
|
-
.slice(0, 10)
|
|
62
|
-
.replace(/-/g, "");
|
|
80
|
+
const dayAfterTomorrow = new Date(now.getTime() + 2 * 86400000);
|
|
81
|
+
const dayAfterTomorrowKey = dateFormat(dayAfterTomorrow).replace(/-/g, "");
|
|
63
82
|
|
|
64
|
-
return {
|
|
83
|
+
return {
|
|
84
|
+
now,
|
|
85
|
+
today,
|
|
86
|
+
tomorrow: tomorrowKey,
|
|
87
|
+
dayAfterTomorrow: dayAfterTomorrowKey,
|
|
88
|
+
};
|
|
65
89
|
};
|
|
@@ -264,8 +264,30 @@ const getLocalDateKey = (date: Date): string => {
|
|
|
264
264
|
return `${date.getFullYear()}-${month}-${day}`;
|
|
265
265
|
};
|
|
266
266
|
|
|
267
|
-
const
|
|
268
|
-
|
|
267
|
+
const getTimeZoneDateKey = (dateTimeSeconds: number, timezoneSeconds: number) =>
|
|
268
|
+
new Date((dateTimeSeconds + timezoneSeconds) * 1000)
|
|
269
|
+
.toISOString()
|
|
270
|
+
.slice(0, 10);
|
|
271
|
+
|
|
272
|
+
const getOpenWeatherMapDateKey = (
|
|
273
|
+
forecast?: OWM_Res_Weather_Forecast,
|
|
274
|
+
): string => {
|
|
275
|
+
const timezoneSeconds = forecast?.city?.timezone;
|
|
276
|
+
|
|
277
|
+
if (typeof timezoneSeconds === "number" && Number.isFinite(timezoneSeconds)) {
|
|
278
|
+
return getTimeZoneDateKey(Math.floor(Date.now() / 1000), timezoneSeconds);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return getLocalDateKey(new Date());
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const getForecastItemDateKey = (
|
|
285
|
+
item: OWM_Res_Weather_Forecast_Item,
|
|
286
|
+
timezoneSeconds = 0,
|
|
287
|
+
): string =>
|
|
288
|
+
Number.isFinite(item.dt)
|
|
289
|
+
? getTimeZoneDateKey(item.dt, timezoneSeconds)
|
|
290
|
+
: item.dt_txt?.slice(0, 10) || getLocalDateKey(new Date(item.dt * 1000));
|
|
269
291
|
|
|
270
292
|
const toOpenWeatherMapDayForecast = (
|
|
271
293
|
items: OWM_Res_Weather_Forecast_Item[],
|
|
@@ -311,11 +333,12 @@ export const getOpenWeatherMapNextDays = (
|
|
|
311
333
|
} => {
|
|
312
334
|
if (!Array.isArray(forecast?.list)) return {};
|
|
313
335
|
|
|
314
|
-
const
|
|
336
|
+
const timezoneSeconds = forecast.city?.timezone ?? 0;
|
|
337
|
+
const todayKey = getOpenWeatherMapDateKey(forecast);
|
|
315
338
|
const grouped = forecast.list.reduce<
|
|
316
339
|
Record<string, OWM_Res_Weather_Forecast_Item[]>
|
|
317
340
|
>((days, item) => {
|
|
318
|
-
const dateKey = getForecastItemDateKey(item);
|
|
341
|
+
const dateKey = getForecastItemDateKey(item, timezoneSeconds);
|
|
319
342
|
if (dateKey === todayKey) return days;
|
|
320
343
|
|
|
321
344
|
days[dateKey] = [...(days[dateKey] || []), item];
|
|
@@ -339,9 +362,10 @@ export const getOpenWeatherMapTodayForecast = (
|
|
|
339
362
|
): API_Res_WeatherKoreaNextDays | undefined => {
|
|
340
363
|
if (!Array.isArray(forecast?.list)) return undefined;
|
|
341
364
|
|
|
342
|
-
const
|
|
365
|
+
const timezoneSeconds = forecast.city?.timezone ?? 0;
|
|
366
|
+
const todayKey = getOpenWeatherMapDateKey(forecast);
|
|
343
367
|
const todayItems = forecast.list.filter(
|
|
344
|
-
item => getForecastItemDateKey(item) === todayKey,
|
|
368
|
+
item => getForecastItemDateKey(item, timezoneSeconds) === todayKey,
|
|
345
369
|
);
|
|
346
370
|
|
|
347
371
|
return toOpenWeatherMapDayForecast(todayItems);
|