@uniai-fe/uds-templates 0.5.29 → 0.6.1

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.
Files changed (54) hide show
  1. package/README.md +3 -6
  2. package/package.json +1 -1
  3. package/src/weather/_legacy/apis/index.ts +4 -0
  4. package/src/weather/_legacy/data/response.ts +36 -0
  5. package/src/weather/_legacy/hooks/index.ts +5 -0
  6. package/src/weather/{hooks → _legacy/hooks}/useOpenWeatherMap.ts +1 -1
  7. package/src/weather/{hooks → _legacy/hooks}/useWeatherKorea.ts +4 -7
  8. package/src/weather/{hooks → _legacy/hooks}/useWeatherKoreaAlert.ts +2 -4
  9. package/src/weather/_legacy/types/api.ts +221 -0
  10. package/src/weather/_legacy/types/base.ts +70 -0
  11. package/src/weather/_legacy/types/index.ts +4 -0
  12. package/src/weather/_legacy/utils/index.ts +5 -0
  13. package/src/weather/_legacy/utils/locale.ts +28 -0
  14. package/src/weather/_legacy/utils/location.ts +139 -0
  15. package/src/weather/_legacy/utils/weather.ts +460 -0
  16. package/src/weather/apis/client.ts +459 -0
  17. package/src/weather/apis/index.ts +2 -4
  18. package/src/weather/apis/server.ts +373 -0
  19. package/src/weather/components/icon/Address.tsx +7 -0
  20. package/src/weather/components/icon/Weather.tsx +7 -6
  21. package/src/weather/components/page-header/Address.tsx +14 -0
  22. package/src/weather/components/page-header/Alert.tsx +17 -13
  23. package/src/weather/components/page-header/Container.tsx +12 -19
  24. package/src/weather/components/page-header/Forecast.tsx +21 -28
  25. package/src/weather/components/page-header/NextDays.tsx +10 -0
  26. package/src/weather/components/page-header/Today.tsx +86 -158
  27. package/src/weather/components/page-header/index.ts +5 -0
  28. package/src/weather/data/response.ts +4 -23
  29. package/src/weather/hooks/index.ts +3 -3
  30. package/src/weather/hooks/useWeather.ts +52 -0
  31. package/src/weather/hooks/useWeatherAlert.ts +35 -0
  32. package/src/weather/index.tsx +2 -2
  33. package/src/weather/jotai/coordinate.ts +4 -0
  34. package/src/weather/jotai/farm-idx.ts +4 -0
  35. package/src/weather/types/api.ts +442 -114
  36. package/src/weather/types/base.ts +31 -32
  37. package/src/weather/types/index.ts +0 -3
  38. package/src/weather/types/page-header.ts +118 -68
  39. package/src/weather/utils/index.ts +6 -4
  40. package/src/weather/utils/locale.ts +7 -69
  41. package/src/weather/utils/location.ts +47 -102
  42. package/src/weather/utils/weather.ts +53 -456
  43. package/src/weather/data/alert-regions-meta.json +0 -1286
  44. package/src/weather/data/weather-regions-meta.json +0 -9833
  45. package/src/weather/types/provider.ts +0 -34
  46. package/src/weather/utils/alert.ts +0 -30
  47. /package/src/weather/{apis → _legacy/apis}/korea/client.ts +0 -0
  48. /package/src/weather/{apis → _legacy/apis}/korea/server.ts +0 -0
  49. /package/src/weather/{apis → _legacy/apis}/open-weather-map/client.ts +0 -0
  50. /package/src/weather/{apis → _legacy/apis}/open-weather-map/server.ts +0 -0
  51. /package/src/weather/{types → _legacy/types}/korea.ts +0 -0
  52. /package/src/weather/{types → _legacy/types}/open-weather-map.ts +0 -0
  53. /package/src/weather/{utils → _legacy/utils}/date-time.ts +0 -0
  54. /package/src/weather/{utils → _legacy/utils}/validate.ts +0 -0
@@ -0,0 +1,373 @@
1
+ "use server";
2
+
3
+ import { generateQueryUrl, nextAPILog } from "@uniai-fe/util-api";
4
+ import { setDebugResponseHeaders } from "@uniai-fe/util-next";
5
+ import type { NextResponse } from "next/server";
6
+ import WEATHER_RESPONSE from "../data/response";
7
+ import { convertWeatherCoordinateToGrid } from "../utils";
8
+ import type {
9
+ API_Res_WeatherAlert,
10
+ API_Res_WeatherForecast,
11
+ API_Res_WeatherNextDays,
12
+ API_Res_WeatherNow,
13
+ API_Res_WeatherSummary,
14
+ API_Res_WeatherToday,
15
+ WeatherBackendFetchParams,
16
+ WeatherServerRouteParams,
17
+ } from "../types";
18
+
19
+ const QUERY_URL = {
20
+ now: "/avic/weather/now",
21
+ forecast: "/avic/weather/forecast",
22
+ summary: "/avic/weather/summary",
23
+ alert: "/avic/weather/alert",
24
+ };
25
+
26
+ const { API_RES_BASE } = WEATHER_RESPONSE;
27
+ type WeatherBackendRouteKind = keyof typeof QUERY_URL;
28
+
29
+ const BACKEND_COMMON_QUERY_KEYS = [
30
+ "farm_idx",
31
+ "lat",
32
+ "lng",
33
+ "locale",
34
+ "include_raw",
35
+ ] as const;
36
+ const BACKEND_DATE_TIME_QUERY_PAIRS = {
37
+ now: [["base_date", "base_time"]],
38
+ forecast: [["base_date", "base_time"]],
39
+ summary: [
40
+ ["now_base_date", "now_base_time"],
41
+ ["forecast_base_date", "forecast_base_time"],
42
+ ],
43
+ alert: [],
44
+ } as const;
45
+
46
+ /**
47
+ * Weather backend; query 값 존재 여부 확인.
48
+ * @param {string | null} value 확인할 query 값
49
+ * @return {boolean} query 값 존재 여부
50
+ */
51
+ const hasSearchParamValue = (value: string | null): value is string =>
52
+ value !== null && value !== "";
53
+
54
+ /**
55
+ * Weather backend; query 값을 숫자로 변환한다.
56
+ * @param {URLSearchParams} searchParams service app local route query
57
+ * @param {string} key 숫자로 읽을 query key
58
+ * @return {number | null} 변환된 숫자 또는 null
59
+ */
60
+ const getSearchParamNumber = (
61
+ searchParams: URLSearchParams,
62
+ key: string,
63
+ ): number | null => {
64
+ const value = searchParams.get(key);
65
+
66
+ if (!hasSearchParamValue(value)) return null;
67
+
68
+ const parsed = Number(value);
69
+
70
+ return Number.isFinite(parsed) ? parsed : null;
71
+ };
72
+
73
+ /**
74
+ * Weather backend; 재현용 date/time query를 pair 단위로 전달한다.
75
+ * @param {URLSearchParams} params backend 전달 query
76
+ * @param {URLSearchParams} searchParams service app local route query
77
+ * @param {WeatherBackendRouteKind} routeKind backend weather route 종류
78
+ */
79
+ const setBackendDateTimeSearchParams = (
80
+ params: URLSearchParams,
81
+ searchParams: URLSearchParams,
82
+ routeKind: WeatherBackendRouteKind,
83
+ ): void => {
84
+ BACKEND_DATE_TIME_QUERY_PAIRS[routeKind].forEach(([dateKey, timeKey]) => {
85
+ const date = searchParams.get(dateKey);
86
+ const time = searchParams.get(timeKey);
87
+
88
+ if (!hasSearchParamValue(date) || !hasSearchParamValue(time)) return;
89
+
90
+ params.set(dateKey, date);
91
+ params.set(timeKey, time);
92
+ });
93
+ };
94
+
95
+ /**
96
+ * Weather backend; nx/ny query를 직접 값 또는 lat/lng 변환값으로 전달한다.
97
+ * @param {URLSearchParams} params backend 전달 query
98
+ * @param {URLSearchParams} searchParams service app local route query
99
+ */
100
+ const setBackendGridSearchParams = (
101
+ params: URLSearchParams,
102
+ searchParams: URLSearchParams,
103
+ ): void => {
104
+ const providedNx = getSearchParamNumber(searchParams, "nx");
105
+ const providedNy = getSearchParamNumber(searchParams, "ny");
106
+ const { nx, ny } =
107
+ providedNx !== null && providedNy !== null
108
+ ? { nx: providedNx, ny: providedNy }
109
+ : convertWeatherCoordinateToGrid({
110
+ lat: getSearchParamNumber(searchParams, "lat"),
111
+ lng: getSearchParamNumber(searchParams, "lng"),
112
+ });
113
+
114
+ if (nx !== null && nx > 0) params.set("nx", String(nx));
115
+ if (ny !== null && ny > 0) params.set("ny", String(ny));
116
+ };
117
+
118
+ /**
119
+ * Weather backend; client route query를 backend 허용 query로 정리.
120
+ * @param {WeatherBackendRouteKind} routeKind backend weather route 종류
121
+ * @param {URLSearchParams} searchParams service app local route query
122
+ * @param {string} [searchParams.farm_idx] 농장 식별자
123
+ * @param {string} [searchParams.lat] 위도
124
+ * @param {string} [searchParams.lng] 경도
125
+ * @param {string} [searchParams.nx] backend 격자 X
126
+ * @param {string} [searchParams.ny] backend 격자 Y
127
+ * @param {string} [searchParams.base_date] 현재/예보 단일 route 재현용 날짜
128
+ * @param {string} [searchParams.base_time] 현재/예보 단일 route 재현용 시각
129
+ * @param {string} [searchParams.now_base_date] summary 현재 날씨 재현용 날짜
130
+ * @param {string} [searchParams.now_base_time] summary 현재 날씨 재현용 시각
131
+ * @param {string} [searchParams.forecast_base_date] summary 예보 날씨 재현용 날짜
132
+ * @param {string} [searchParams.forecast_base_time] summary 예보 날씨 재현용 시각
133
+ * @param {string} [searchParams.locale] 요청 언어 코드
134
+ * @param {string} [searchParams.include_raw] 원본 응답 포함 여부
135
+ * @return {URLSearchParams} backend 전달 query
136
+ */
137
+ const getBackendSearchParams = (
138
+ routeKind: WeatherBackendRouteKind,
139
+ searchParams: URLSearchParams,
140
+ ): URLSearchParams => {
141
+ const params = new URLSearchParams();
142
+
143
+ if (routeKind === "alert") {
144
+ const farmIdx = searchParams.get("farm_idx");
145
+ if (hasSearchParamValue(farmIdx)) params.set("farm_idx", farmIdx);
146
+ return params;
147
+ }
148
+
149
+ BACKEND_COMMON_QUERY_KEYS.forEach(key => {
150
+ const value = searchParams.get(key);
151
+ if (hasSearchParamValue(value)) params.set(key, value);
152
+ });
153
+
154
+ setBackendGridSearchParams(params, searchParams);
155
+ setBackendDateTimeSearchParams(params, searchParams, routeKind);
156
+
157
+ return params;
158
+ };
159
+
160
+ /**
161
+ * Weather backend; 실패 시 사용할 현재 날씨 기본값 생성.
162
+ * @return {API_Res_WeatherToday} 현재 날씨 기본값
163
+ */
164
+ const getWeatherTodayDefault = (): API_Res_WeatherToday => ({
165
+ ...API_RES_BASE,
166
+ temperature: null,
167
+ max_temperature: null,
168
+ min_temperature: null,
169
+ humidity: null,
170
+ });
171
+
172
+ /**
173
+ * Weather backend; 실패 시 사용할 내일/모레 예보 기본값 생성.
174
+ * @return {API_Res_WeatherNextDays} 내일/모레 예보 기본값
175
+ */
176
+ const getWeatherNextDaysDefault = (): API_Res_WeatherNextDays => ({
177
+ ...API_RES_BASE,
178
+ max_temperature: null,
179
+ min_temperature: null,
180
+ });
181
+
182
+ /**
183
+ * Weather backend; 실패 시 사용할 현재 날씨 응답 생성.
184
+ * @return {API_Res_WeatherNow} 현재 날씨 fallback 응답
185
+ */
186
+ const getWeatherNowDefault = (): API_Res_WeatherNow => ({
187
+ today: getWeatherTodayDefault(),
188
+ source: null,
189
+ fallback_used: true,
190
+ });
191
+
192
+ /**
193
+ * Weather backend; 실패 시 사용할 예보 날씨 응답 생성.
194
+ * @return {API_Res_WeatherForecast} 예보 날씨 fallback 응답
195
+ */
196
+ const getWeatherForecastDefault = (): API_Res_WeatherForecast => ({
197
+ today: getWeatherTodayDefault(),
198
+ day_1: getWeatherNextDaysDefault(),
199
+ day_2: getWeatherNextDaysDefault(),
200
+ source: null,
201
+ fallback_used: true,
202
+ });
203
+
204
+ /**
205
+ * Weather backend; 실패 시 사용할 특보 응답 생성.
206
+ * @return {API_Res_WeatherAlert} 특보 fallback 응답
207
+ */
208
+ const getWeatherAlertDefault = (): API_Res_WeatherAlert => ({
209
+ api_announcement_time: null,
210
+ total_count: 0,
211
+ alerts: [],
212
+ });
213
+
214
+ /**
215
+ * Weather backend; 실패 시 사용할 summary 응답 생성.
216
+ * @return {API_Res_WeatherSummary} summary fallback 응답
217
+ */
218
+ const getWeatherSummaryDefault = (): API_Res_WeatherSummary => ({
219
+ now: getWeatherNowDefault(),
220
+ forecast: getWeatherForecastDefault(),
221
+ alert: getWeatherAlertDefault(),
222
+ });
223
+
224
+ /**
225
+ * Weather backend; backend weather route 호출과 fallback 응답 처리.
226
+ * @param {WeatherBackendFetchParams<Data>} params route 호출 옵션
227
+ * @param {string} params.domain backend API domain
228
+ * @param {string} params.routeUrl local route URL
229
+ * @param {string} params.queryUrl backend weather route URL
230
+ * @param {"now"|"forecast"|"summary"|"alert"} params.routeKind backend weather route 종류
231
+ * @param {URLSearchParams} params.searchParams client route query
232
+ * @param {Data} params.fallback 실패 시 반환할 fallback 응답
233
+ * @return {Promise<Data>} backend 응답 또는 fallback
234
+ */
235
+ const fetchWeatherBackend = async <Data>({
236
+ domain,
237
+ routeUrl,
238
+ queryUrl,
239
+ routeKind,
240
+ searchParams,
241
+ fallback,
242
+ }: WeatherBackendFetchParams<Data>): Promise<Data> => {
243
+ if (!domain) {
244
+ nextAPILog("GET", routeUrl, queryUrl, {
245
+ error: "환경변수를 확인하세요.",
246
+ });
247
+ return fallback;
248
+ }
249
+
250
+ const backendParams = getBackendSearchParams(routeKind, searchParams);
251
+ const url = generateQueryUrl({
252
+ domain,
253
+ routeUrl,
254
+ queryUrl,
255
+ searchParams: backendParams,
256
+ });
257
+
258
+ try {
259
+ const res = await fetch(url);
260
+ if (!res.ok) {
261
+ nextAPILog("GET", routeUrl, url, {
262
+ status: res.status,
263
+ statusText: res.statusText,
264
+ });
265
+ return fallback;
266
+ }
267
+
268
+ return (await res.json()) as Data;
269
+ } catch (error) {
270
+ nextAPILog("GET", routeUrl, url, { error });
271
+ return fallback;
272
+ }
273
+ };
274
+
275
+ /**
276
+ * Weather backend; 현재 날씨 proxy.
277
+ * @method GET
278
+ * @param {WeatherServerRouteParams} params route 호출 옵션
279
+ * @param {string} params.domain backend API domain
280
+ * @param {string} params.routeUrl service app local route URL
281
+ * @param {URLSearchParams} params.searchParams service app local route query
282
+ * @return {Promise<API_Res_WeatherNow>} 현재 날씨 응답
283
+ */
284
+ export const routeWeatherNow = async ({
285
+ domain,
286
+ routeUrl,
287
+ searchParams,
288
+ }: WeatherServerRouteParams): Promise<API_Res_WeatherNow> =>
289
+ await fetchWeatherBackend({
290
+ domain,
291
+ routeUrl,
292
+ queryUrl: QUERY_URL.now,
293
+ routeKind: "now",
294
+ searchParams,
295
+ fallback: getWeatherNowDefault(),
296
+ });
297
+
298
+ /**
299
+ * Weather backend; 예보 날씨 proxy.
300
+ * @method GET
301
+ * @param {WeatherServerRouteParams} params route 호출 옵션
302
+ * @param {string} params.domain backend API domain
303
+ * @param {string} params.routeUrl service app local route URL
304
+ * @param {URLSearchParams} params.searchParams service app local route query
305
+ * @return {Promise<API_Res_WeatherForecast>} 예보 날씨 응답
306
+ */
307
+ export const routeWeatherForecast = async ({
308
+ domain,
309
+ routeUrl,
310
+ searchParams,
311
+ }: WeatherServerRouteParams): Promise<API_Res_WeatherForecast> =>
312
+ await fetchWeatherBackend({
313
+ domain,
314
+ routeUrl,
315
+ queryUrl: QUERY_URL.forecast,
316
+ routeKind: "forecast",
317
+ searchParams,
318
+ fallback: getWeatherForecastDefault(),
319
+ });
320
+
321
+ /**
322
+ * Weather backend; 현재/예보/특보 summary proxy.
323
+ * @method GET
324
+ * @param {WeatherServerRouteParams} params route 호출 옵션
325
+ * @param {string} params.domain backend API domain
326
+ * @param {string} params.routeUrl service app local route URL
327
+ * @param {URLSearchParams} params.searchParams service app local route query
328
+ * @return {Promise<API_Res_WeatherSummary>} summary 응답
329
+ */
330
+ export const routeWeatherSummary = async ({
331
+ domain,
332
+ routeUrl,
333
+ searchParams,
334
+ }: WeatherServerRouteParams): Promise<API_Res_WeatherSummary> =>
335
+ await fetchWeatherBackend({
336
+ domain,
337
+ routeUrl,
338
+ queryUrl: QUERY_URL.summary,
339
+ routeKind: "summary",
340
+ searchParams,
341
+ fallback: getWeatherSummaryDefault(),
342
+ });
343
+
344
+ /**
345
+ * Weather backend; 특보 proxy.
346
+ * @method GET
347
+ * @param {WeatherServerRouteParams} params route 호출 옵션
348
+ * @param {string} params.domain backend API domain
349
+ * @param {string} params.routeUrl service app local route URL
350
+ * @param {URLSearchParams} params.searchParams service app local route query
351
+ * @return {Promise<NextResponse<API_Res_WeatherAlert>>} 특보 응답
352
+ */
353
+ export const routeWeatherAlert = async ({
354
+ domain,
355
+ routeUrl,
356
+ searchParams,
357
+ }: WeatherServerRouteParams): Promise<NextResponse<API_Res_WeatherAlert>> => {
358
+ const res = await fetchWeatherBackend({
359
+ domain,
360
+ routeUrl,
361
+ queryUrl: QUERY_URL.alert,
362
+ routeKind: "alert",
363
+ searchParams,
364
+ fallback: getWeatherAlertDefault(),
365
+ });
366
+
367
+ return setDebugResponseHeaders<API_Res_WeatherAlert>({
368
+ res,
369
+ domain,
370
+ queryUrl: QUERY_URL.alert,
371
+ searchParams: getBackendSearchParams("alert", searchParams),
372
+ });
373
+ };
@@ -3,6 +3,13 @@
3
3
  import type { WeatherAddressIconProps } from "../../types";
4
4
  import MarkerIcon from "../../img/marker.svg";
5
5
 
6
+ /**
7
+ * Weather Address Icon; 위치 아이콘 컴포넌트.
8
+ * @component
9
+ * @desc weather address 앞에 표시할 marker SVG를 접근성 label과 함께 렌더한다.
10
+ * @param {WeatherAddressIconProps} [props] 위치 아이콘 props
11
+ * @param {string} [props.alt] 아이콘 대체 텍스트
12
+ */
6
13
  export default function WeatherAddressIcon({
7
14
  alt,
8
15
  }: WeatherAddressIconProps = {}) {
@@ -9,12 +9,13 @@ import type { WeatherIconProps } from "../../types";
9
9
  // Storybook의 next/image mock과 동일한 client boundary에서 weather icon을 렌더한다.
10
10
 
11
11
  /**
12
- * 날씨; 아이콘
13
- * @desc
14
- * - 기상청 API 허브
15
- * - 2024. 11. 28. 발효 기준
16
- *
17
- * @see https://apihub.kma.go.kr/getAttachFile.do?fileName=(20241128)%EB%8B%A8%EA%B8%B0%EC%98%88%EB%B3%B4%20%EC%84%9C%EB%B9%84%EC%8A%A4%20%EA%B0%9C%EC%84%A0%EC%97%90%20%EB%94%B0%EB%A5%B8%20API%20%EB%B3%80%EA%B2%BD%EC%82%AC%ED%95%AD.pdf
12
+ * Weather Icon; 날씨 상태 아이콘 컴포넌트.
13
+ * @component
14
+ * @desc normalized weather condition code를 asset icon으로 렌더하고 상태 이름을 대체 텍스트로 사용한다.
15
+ * @param {WeatherIconProps} [props] 날씨 아이콘 props
16
+ * @param {string | null} [props.code] 날씨 아이콘 코드
17
+ * @param {string | null} [props.name] 날씨 상태 이름
18
+ * @param {string} [props.alt] 아이콘 대체 텍스트 fallback
18
19
  */
19
20
  export default function WeatherIcon({
20
21
  code,
@@ -8,6 +8,12 @@ import type { WeatherPageHeaderAddressProps } from "../../types";
8
8
  import { resolveWeatherPageHeaderTexts } from "../../utils";
9
9
  import WeatherAddressIcon from "../icon/Address";
10
10
 
11
+ /**
12
+ * Weather Page Header; 현재 렌더링 시각을 표시 문자열로 변환.
13
+ * @util
14
+ * @desc 클라이언트 시각을 weather address 보조 텍스트 형식으로 변환한다.
15
+ * @return {string} yyyy-mm-dd hh:mm 형식의 렌더링 시각
16
+ */
11
17
  const getRenderedAt = (): string => {
12
18
  const now = new Date();
13
19
  const month = String(now.getMonth() + 1).padStart(2, "0");
@@ -18,6 +24,14 @@ const getRenderedAt = (): string => {
18
24
  return `${now.getFullYear()}-${month}-${date} ${hours}:${minutes}`;
19
25
  };
20
26
 
27
+ /**
28
+ * Weather Page Header; 주소와 렌더링 시각을 표시하는 컴포넌트.
29
+ * @component
30
+ * @desc weatherCoordinate atom에 주입된 주소와 클라이언트 렌더링 시각을 표시한다.
31
+ * @param {WeatherPageHeaderAddressProps} [props] address props
32
+ * @param {string} [props.locale] 표시 언어 코드
33
+ * @param {Partial<WeatherPageHeaderTexts>} [props.texts] 서비스에서 주입한 표시 문구
34
+ */
21
35
  export default function WeatherPageHeaderAddress({
22
36
  texts: textOverrides,
23
37
  }: WeatherPageHeaderAddressProps = {}) {
@@ -3,21 +3,28 @@
3
3
  import { useMemo } from "react";
4
4
 
5
5
  import { Alternate } from "@uniai-fe/uds-primitives";
6
- import useWeatherKoreaAlert from "../../hooks/useWeatherKoreaAlert";
6
+ import { useWeatherAlert } from "../../hooks";
7
7
  import type { WeatherPageHeaderAlertProps } from "../../types";
8
- import {
9
- isKoreaWeatherLocale,
10
- resolveWeatherPageHeaderTexts,
11
- } from "../../utils";
8
+ import { resolveWeatherPageHeaderTexts } from "../../utils";
12
9
 
10
+ /**
11
+ * Weather Page Header; 기상 특보를 표시하는 컴포넌트.
12
+ * @component
13
+ * @desc 통합 weather alert query 결과 또는 상위 weather props의 특보 목록을 표시한다.
14
+ * @param {WeatherPageHeaderAlertProps} [props] alert props
15
+ * @param {string} [props.locale] 표시 언어 코드
16
+ * @param {Partial<WeatherPageHeaderTexts>} [props.texts] 서비스에서 주입한 표시 문구
17
+ * @param {WeatherPageHeaderWeather} [props.weather] 상위 container에서 준비한 통합 weather query 결과
18
+ */
13
19
  export default function WeatherPageHeaderAlert({
14
- locale,
15
20
  texts: textOverrides,
21
+ weather,
16
22
  }: WeatherPageHeaderAlertProps = {}) {
17
- const isKoreaProvider = isKoreaWeatherLocale(locale);
18
- const { alert, isFetching } = useWeatherKoreaAlert({
19
- enabled: isKoreaProvider,
23
+ const alertFallback = useWeatherAlert({
24
+ enabled: !weather,
20
25
  });
26
+ const alert = weather?.alert ?? alertFallback.alert;
27
+ const isFetching = weather?.isFetchingAlert ?? alertFallback.isFetching;
21
28
  const texts = resolveWeatherPageHeaderTexts(textOverrides);
22
29
 
23
30
  const notice = useMemo((): string => {
@@ -42,10 +49,7 @@ export default function WeatherPageHeaderAlert({
42
49
  texts.alertWatchLevelLabel,
43
50
  ]);
44
51
 
45
- if (
46
- !isKoreaProvider ||
47
- (!isFetching && (alert.length === 0 || !alert?.[0]?.alert_type))
48
- ) {
52
+ if (!isFetching && (alert.length === 0 || !alert?.[0]?.alert_type)) {
49
53
  return null;
50
54
  }
51
55
 
@@ -5,37 +5,30 @@ import WeatherPageHeaderForecast from "./Forecast";
5
5
  import WeatherPageHeaderAddress from "./Address";
6
6
  import PageHeaderUtilityItem from "../../../page-frame/desktop/components/header/util/Item";
7
7
  import type { WeatherPageHeaderContainerProps } from "../../types";
8
- import { useOpenWeatherMap, useWeatherKorea } from "../../hooks";
9
- import { isKoreaWeatherLocale } from "../../utils";
8
+ import { useWeather } from "../../hooks";
10
9
 
10
+ /**
11
+ * Weather Page Header; address/today/forecast를 조합하는 container 컴포넌트.
12
+ * @component
13
+ * @desc 통합 weather summary query를 한 번 실행하고 address, today, forecast leaf에 전달한다.
14
+ * @param {WeatherPageHeaderContainerProps} [props] container props
15
+ * @param {string} [props.locale] 표시 언어 코드
16
+ * @param {Partial<WeatherPageHeaderTexts>} [props.texts] 서비스에서 주입한 표시 문구
17
+ */
11
18
  export default function WeatherPageHeaderContainer({
12
19
  locale,
13
20
  texts,
14
21
  }: WeatherPageHeaderContainerProps = {}) {
15
- const isKoreaProvider = isKoreaWeatherLocale(locale);
16
- const koreaWeather = useWeatherKorea({
17
- enabled: isKoreaProvider,
18
- });
19
- const openWeatherMapWeather = useOpenWeatherMap({
20
- locale,
21
- enabledNow: !isKoreaProvider,
22
- enabledForecast: !isKoreaProvider,
23
- });
22
+ const weather = useWeather({ locale });
24
23
 
25
24
  return (
26
25
  <PageHeaderUtilityItem className="weather-page-header">
27
26
  <WeatherPageHeaderAddress locale={locale} texts={texts} />
28
- <WeatherPageHeaderToday
29
- locale={locale}
30
- texts={texts}
31
- koreaWeather={koreaWeather}
32
- openWeatherMapWeather={openWeatherMapWeather}
33
- />
27
+ <WeatherPageHeaderToday locale={locale} texts={texts} weather={weather} />
34
28
  <WeatherPageHeaderForecast
35
29
  locale={locale}
36
30
  texts={texts}
37
- koreaWeather={koreaWeather}
38
- openWeatherMapWeather={openWeatherMapWeather}
31
+ weather={weather}
39
32
  />
40
33
  </PageHeaderUtilityItem>
41
34
  );
@@ -1,46 +1,39 @@
1
1
  "use client";
2
2
 
3
- import { useMemo } from "react";
4
3
  import { Alternate } from "@uniai-fe/uds-primitives";
5
- import { useOpenWeatherMap, useWeatherKorea } from "../../hooks";
4
+ import { useWeather } from "../../hooks";
6
5
  import type { WeatherPageHeaderForecastProps } from "../../types";
7
6
  import {
8
- getOpenWeatherMapNextDays,
9
- isKoreaWeatherLocale,
7
+ isWeatherForecastResponse,
10
8
  resolveWeatherPageHeaderTexts,
11
9
  } from "../../utils";
12
10
  import WeatherPageHeaderNextDays from "./NextDays";
13
11
 
12
+ /**
13
+ * Weather Page Header; 내일/모레 예보를 표시하는 컴포넌트.
14
+ * @component
15
+ * @desc 통합 weather forecast 응답에서 day_1/day_2를 추출해 하루 단위 예보로 표시한다.
16
+ * @param {WeatherPageHeaderForecastProps} [props] forecast props
17
+ * @param {string} [props.locale] 표시 언어 코드
18
+ * @param {Partial<WeatherPageHeaderTexts>} [props.texts] 서비스에서 주입한 표시 문구
19
+ * @param {WeatherPageHeaderWeather} [props.weather] 상위 container에서 준비한 통합 weather query 결과
20
+ */
14
21
  export default function WeatherPageHeaderForecast({
15
22
  locale,
16
23
  texts: textOverrides,
17
- koreaWeather,
18
- openWeatherMapWeather,
24
+ weather,
19
25
  }: WeatherPageHeaderForecastProps = {}) {
20
- const isKoreaProvider = isKoreaWeatherLocale(locale);
21
- const koreaWeatherFallback = useWeatherKorea({
22
- enabled: !koreaWeather && isKoreaProvider,
23
- });
24
- const openWeatherMapWeatherFallback = useOpenWeatherMap({
26
+ const weatherFallback = useWeather({
25
27
  locale,
26
- enabledNow: false,
27
- enabledForecast: !openWeatherMapWeather && !isKoreaProvider,
28
+ enabled: !weather,
28
29
  });
29
- const korea = koreaWeather ?? koreaWeatherFallback;
30
- const openWeatherMap = openWeatherMapWeather ?? openWeatherMapWeatherFallback;
30
+ const currentWeather = weather ?? weatherFallback;
31
31
  const texts = resolveWeatherPageHeaderTexts(textOverrides);
32
- const openWeatherMapNextDays = useMemo(
33
- () => getOpenWeatherMapNextDays(openWeatherMap.forecast),
34
- [openWeatherMap.forecast],
35
- );
36
- const forecastDays = isKoreaProvider
37
- ? korea.forecast
38
- : openWeatherMapNextDays;
39
- const isFetching = isKoreaProvider
40
- ? korea.isFetchingForecast
41
- : openWeatherMap.isFetchingForecast;
32
+ const forecast = isWeatherForecastResponse(currentWeather.forecast)
33
+ ? currentWeather.forecast
34
+ : undefined;
42
35
 
43
- if (isFetching) {
36
+ if (currentWeather.isFetchingForecast) {
44
37
  return (
45
38
  <div className="weather-next-days-container">
46
39
  <Alternate.LoadingDefault direction="horizontal">
@@ -54,12 +47,12 @@ export default function WeatherPageHeaderForecast({
54
47
  <>
55
48
  <WeatherPageHeaderNextDays
56
49
  title={texts.tomorrowLabel}
57
- data={forecastDays?.day_1}
50
+ data={forecast?.day_1}
58
51
  texts={textOverrides}
59
52
  />
60
53
  <WeatherPageHeaderNextDays
61
54
  title={texts.dayAfterTomorrowLabel}
62
- data={forecastDays?.day_2}
55
+ data={forecast?.day_2}
63
56
  texts={textOverrides}
64
57
  />
65
58
  </>
@@ -4,6 +4,16 @@ import type { WeatherPageHeaderNextDaysProps } from "../../types";
4
4
  import { resolveWeatherPageHeaderTexts } from "../../utils";
5
5
  import WeatherIcon from "../icon/Weather";
6
6
 
7
+ /**
8
+ * Weather Page Header; 하루 단위 예보를 표시하는 컴포넌트.
9
+ * @component
10
+ * @desc title과 normalized forecast day 데이터를 조합해 날씨 아이콘과 최저/최고 기온을 표시한다.
11
+ * @param {WeatherPageHeaderNextDaysProps} props next days props
12
+ * @param {string} props.title 날짜 라벨
13
+ * @param {API_Res_WeatherNextDays} [props.data] 예보 데이터
14
+ * @param {string} [props.locale] 표시 언어 코드
15
+ * @param {Partial<WeatherPageHeaderTexts>} [props.texts] 서비스에서 주입한 표시 문구
16
+ */
7
17
  export default function WeatherPageHeaderNextDays({
8
18
  title,
9
19
  data,