@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-templates",
3
- "version": "0.5.14",
3
+ "version": "0.5.15",
4
4
  "description": "UNIAI Design System; UI Templates Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -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 { getWeatherBaseMoments } from "../../utils/date-time";
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
- return extractWeatherSummary.forecast(resDefault, res);
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/weather/forecast/hourly";
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.toISOString().slice(0, 10).replace(/-/g, "");
75
+ const today = dateFormat(now).replace(/-/g, "");
53
76
 
54
- const tomorrow = new Date(now.getTime() + 86400000)
55
- .toISOString()
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
- .toISOString()
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 { now, today, tomorrow, dayAfterTomorrow };
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 getForecastItemDateKey = (item: OWM_Res_Weather_Forecast_Item): string =>
268
- item.dt_txt?.slice(0, 10) || getLocalDateKey(new Date(item.dt * 1000));
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 todayKey = getLocalDateKey(new Date());
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 todayKey = getLocalDateKey(new Date());
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);