@uniai-fe/uds-templates 0.5.9 → 0.5.10

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/README.md CHANGED
@@ -60,6 +60,8 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
60
60
  - `weatherCoordinate`
61
61
  - `useWeatherKorea`
62
62
  - `useOpenWeatherMap`
63
+ - `WeatherPageHeaderContainerProps`
64
+ - `WeatherPageHeaderTexts`
63
65
  - `/service-inquiry`
64
66
  - `ServiceInquiry.Form`
65
67
  - `ServiceInquiry.OpenButton`
@@ -130,6 +132,7 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
130
132
  - 로그인 후 `farm_name`, `contact`를 auto-fill + readonly로 보여야 할 때는 `formContextOptions.defaultValues`와 `farmNameField.mode`, `contactField.mode`를 함께 전달한다.
131
133
  - `/weather/**`
132
134
  - page-frame header utility에 결합되는 weather header 템플릿, page-header 조각 export, weather data hook/API 도구를 제공한다.
135
+ - page-header는 `locale?: string`을 받으며 기본 지원값은 `ko`, `en`, `ja`다. 기본 문구 외 특수 문구는 `texts` prop으로 주입한다.
133
136
  - `/cctv/**`
134
137
  - finder/viewer/video/pagination 조합과 rtc/company-list API helper를 제공한다.
135
138
  - `/page-frame/**`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-templates",
3
- "version": "0.5.9",
3
+ "version": "0.5.10",
4
4
  "description": "UNIAI Design System; UI Templates Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -48,7 +48,16 @@ export default function CCTVPaginationListItem({
48
48
  >
49
49
  <CCTVVideoTemplate
50
50
  ref={videoRef}
51
+ cam={cam}
51
52
  className="cctv-pagination-list-video-container"
53
+ headerOptions={{
54
+ activeLiveState: true,
55
+ activeTitle: false,
56
+ isLive,
57
+ isShared:
58
+ typeof cam.cam_shared === "boolean" ? cam.cam_shared : true,
59
+ title: cam.cam_name,
60
+ }}
52
61
  footerOptions={{ cam }}
53
62
  {...{ isError, overlayMessage, isLive }}
54
63
  />
@@ -11,6 +11,22 @@ import type {
11
11
  } from "../../types";
12
12
  import { isValidGridCoordinate } from "../../utils/validate";
13
13
 
14
+ type WeatherQueryOptions = {
15
+ enabled?: boolean;
16
+ };
17
+
18
+ const getWeatherKoreaParams = ({
19
+ nx,
20
+ ny,
21
+ base_date,
22
+ base_time,
23
+ }: API_Req_WeatherKorea): API_Req_WeatherKorea => ({
24
+ nx,
25
+ ny,
26
+ ...(base_date ? { base_date } : {}),
27
+ ...(base_time ? { base_time } : {}),
28
+ });
29
+
14
30
  /**
15
31
  * 기상청 API; 현재날씨 fetch
16
32
  * @method GET
@@ -54,15 +70,22 @@ export const getWeatherKoreaAlert = async (params: API_Req_WeatherKoreaAlert) =>
54
70
  */
55
71
  export const useQueryWeatherKoreaNow = (
56
72
  params: API_Req_WeatherKorea,
57
- ): UseQueryResult<API_Res_WeatherKoreaNow> =>
58
- useQuery({
59
- queryKey: ["weather_korea_now", ...Object.values(params)],
60
- queryFn: () => getWeatherKoreaNow(params),
61
- enabled: isValidGridCoordinate({ nx: params.nx, ny: params.ny }),
73
+ options: WeatherQueryOptions = {},
74
+ ): UseQueryResult<API_Res_WeatherKoreaNow> => {
75
+ const { nx, ny, base_date, base_time } = params;
76
+
77
+ return useQuery({
78
+ queryKey: ["weather_korea_now", nx, ny, base_date, base_time],
79
+ queryFn: () =>
80
+ getWeatherKoreaNow(
81
+ getWeatherKoreaParams({ nx, ny, base_date, base_time }),
82
+ ),
83
+ enabled: (options.enabled ?? true) && isValidGridCoordinate({ nx, ny }),
62
84
  staleTime: 10 * 60 * 1000, // 10분
63
85
  refetchInterval: 5 * 60 * 1000, // 5분
64
86
  refetchOnWindowFocus: true,
65
87
  });
88
+ };
66
89
 
67
90
  /**
68
91
  * 기상청 API; 내일/모레 예보 react query
@@ -70,13 +93,20 @@ export const useQueryWeatherKoreaNow = (
70
93
  */
71
94
  export const useQueryWeatherKoreaForecast = (
72
95
  params: API_Req_WeatherKorea,
73
- ): UseQueryResult<API_Res_WeatherKoreaForecast> =>
74
- useQuery({
75
- queryKey: ["weather_korea_forecast", ...Object.values(params)],
76
- queryFn: () => getWeatherKoreaForecast(params),
77
- enabled: isValidGridCoordinate({ nx: params.nx, ny: params.ny }),
96
+ options: WeatherQueryOptions = {},
97
+ ): UseQueryResult<API_Res_WeatherKoreaForecast> => {
98
+ const { nx, ny, base_date, base_time } = params;
99
+
100
+ return useQuery({
101
+ queryKey: ["weather_korea_forecast", nx, ny, base_date, base_time],
102
+ queryFn: () =>
103
+ getWeatherKoreaForecast(
104
+ getWeatherKoreaParams({ nx, ny, base_date, base_time }),
105
+ ),
106
+ enabled: (options.enabled ?? true) && isValidGridCoordinate({ nx, ny }),
78
107
  staleTime: 30 * 60 * 1000, // 30분
79
108
  });
109
+ };
80
110
 
81
111
  /**
82
112
  * 기상청 API; 특보 react query
@@ -84,10 +114,15 @@ export const useQueryWeatherKoreaForecast = (
84
114
  */
85
115
  export const useQueryWeatherKoreaAlert = (
86
116
  params: API_Req_WeatherKoreaAlert,
87
- ): UseQueryResult<API_Res_WeatherKoreaAlert> =>
88
- useQuery({
89
- queryKey: ["weather_korea_alert", ...Object.values(params)],
90
- queryFn: () => getWeatherKoreaAlert(params),
91
- enabled: typeof params.farm_idx === "number" && params.farm_idx > 0,
117
+ options: WeatherQueryOptions = {},
118
+ ): UseQueryResult<API_Res_WeatherKoreaAlert> => {
119
+ const { farm_idx } = params;
120
+
121
+ return useQuery({
122
+ queryKey: ["weather_korea_alert", farm_idx],
123
+ queryFn: () => getWeatherKoreaAlert({ farm_idx }),
124
+ enabled:
125
+ (options.enabled ?? true) && typeof farm_idx === "number" && farm_idx > 0,
92
126
  staleTime: 60 * 60 * 1000, // 1시간
93
127
  });
128
+ };
@@ -147,8 +147,8 @@ export const routeWeatherKoreaForecast = async ({
147
147
  const resDefault: API_Res_WeatherKoreaForecast = {
148
148
  raw: API_RES_RAW as KMA_Res_WeatherForecast,
149
149
  today: { ...API_RES_BASE, temperature: null, humidity: null },
150
- day_1: FORECAST_DATA,
151
- day_2: FORECAST_DATA,
150
+ day_1: { ...FORECAST_DATA },
151
+ day_2: { ...FORECAST_DATA },
152
152
  };
153
153
 
154
154
  if (!authKey) {
@@ -4,11 +4,25 @@ import { useQuery, type UseQueryResult } from "@tanstack/react-query";
4
4
  import type {
5
5
  OWM_Res_Weather_Forecast,
6
6
  OWM_Res_Weather_Now,
7
- WeatherGeoCoordinate,
7
+ WeatherOpenWeatherMapParams,
8
8
  } from "../../types";
9
9
  import { getQueryString } from "@uniai-fe/util-api";
10
10
  import { isValidNumber } from "@uniai-fe/util-functions";
11
11
 
12
+ type WeatherQueryOptions = {
13
+ enabled?: boolean;
14
+ };
15
+
16
+ const getOpenWeatherMapParams = ({
17
+ lat,
18
+ lng,
19
+ locale,
20
+ }: WeatherOpenWeatherMapParams): WeatherOpenWeatherMapParams => ({
21
+ lat,
22
+ lng,
23
+ ...(locale ? { locale } : {}),
24
+ });
25
+
12
26
  /**
13
27
  * 글로벌 날씨 API; 현재 날씨 fetch
14
28
  * @method GET
@@ -19,7 +33,7 @@ import { isValidNumber } from "@uniai-fe/util-functions";
19
33
  * @param {number} params.lon - 경도
20
34
  */
21
35
  export const getWeatherOpenWeatherMapNow = async (
22
- params: WeatherGeoCoordinate,
36
+ params: WeatherOpenWeatherMapParams,
23
37
  ): Promise<OWM_Res_Weather_Now> =>
24
38
  await (
25
39
  await fetch(`/api/weather/open-weather-map/now${getQueryString(params)}`)
@@ -35,7 +49,7 @@ export const getWeatherOpenWeatherMapNow = async (
35
49
  * @param {number} params.lon - 경도
36
50
  */
37
51
  export const getWeatherOpenWeatherMapForecast = async (
38
- params: WeatherGeoCoordinate,
52
+ params: WeatherOpenWeatherMapParams,
39
53
  ): Promise<OWM_Res_Weather_Forecast> =>
40
54
  await (
41
55
  await fetch(
@@ -48,27 +62,43 @@ export const getWeatherOpenWeatherMapForecast = async (
48
62
  * @method GET
49
63
  */
50
64
  export const useQueryWeatherOpenWeatherMapNow = (
51
- params: WeatherGeoCoordinate,
52
- ): UseQueryResult<OWM_Res_Weather_Now> =>
53
- useQuery({
54
- queryKey: ["weather_open_weather_map_now", ...Object.values(params)],
55
- queryFn: () => getWeatherOpenWeatherMapNow(params),
56
- enabled: isValidNumber(params.lat) && isValidNumber(params.lng),
65
+ params: WeatherOpenWeatherMapParams,
66
+ options: WeatherQueryOptions = {},
67
+ ): UseQueryResult<OWM_Res_Weather_Now> => {
68
+ const { lat, lng, locale } = params;
69
+
70
+ return useQuery({
71
+ queryKey: ["weather_open_weather_map_now", lat, lng, locale],
72
+ queryFn: () =>
73
+ getWeatherOpenWeatherMapNow(
74
+ getOpenWeatherMapParams({ lat, lng, locale }),
75
+ ),
76
+ enabled:
77
+ (options.enabled ?? true) && isValidNumber(lat) && isValidNumber(lng),
57
78
  staleTime: 10 * 60 * 1000, // 10분
58
79
  refetchInterval: 5 * 60 * 1000, // 5분
59
80
  refetchOnWindowFocus: true,
60
81
  });
82
+ };
61
83
 
62
84
  /**
63
85
  * 글로벌 날씨 API; 예보 날씨 (4 days) react query
64
86
  * @method GET
65
87
  */
66
88
  export const useQueryWeatherOpenWeatherMapForecast = (
67
- params: WeatherGeoCoordinate,
68
- ): UseQueryResult<OWM_Res_Weather_Forecast> =>
69
- useQuery({
70
- queryKey: ["weather_open_weather_map_forecast", ...Object.values(params)],
71
- queryFn: () => getWeatherOpenWeatherMapForecast(params),
72
- enabled: isValidNumber(params.lat) && isValidNumber(params.lng),
89
+ params: WeatherOpenWeatherMapParams,
90
+ options: WeatherQueryOptions = {},
91
+ ): UseQueryResult<OWM_Res_Weather_Forecast> => {
92
+ const { lat, lng, locale } = params;
93
+
94
+ return useQuery({
95
+ queryKey: ["weather_open_weather_map_forecast", lat, lng, locale],
96
+ queryFn: () =>
97
+ getWeatherOpenWeatherMapForecast(
98
+ getOpenWeatherMapParams({ lat, lng, locale }),
99
+ ),
100
+ enabled:
101
+ (options.enabled ?? true) && isValidNumber(lat) && isValidNumber(lng),
73
102
  staleTime: 30 * 60 * 1000, // 30분
74
103
  });
104
+ };
@@ -5,6 +5,7 @@ import type {
5
5
  OWM_Res_Weather_Forecast,
6
6
  OWM_Res_Weather_Now,
7
7
  } from "../../types";
8
+ import { getOpenWeatherMapLang } from "../../utils/locale";
8
9
 
9
10
  /**
10
11
  * 글로벌 날씨 API; 현재 날씨 URL
@@ -17,7 +18,6 @@ const API_BASE_FORECAST =
17
18
  "https://pro.openweathermap.org/data/2.5/weather/forecast/hourly";
18
19
 
19
20
  const COMMON_OPTIONS = {
20
- lang: "kr",
21
21
  units: "metric", // 섭씨 온도
22
22
  };
23
23
 
@@ -63,6 +63,7 @@ export const routeOpenWeatherMapNow = async ({
63
63
  lat,
64
64
  lon,
65
65
  appid: authKey,
66
+ lang: getOpenWeatherMapLang(searchParams.get("locale") || undefined),
66
67
  ...COMMON_OPTIONS,
67
68
  }),
68
69
  });
@@ -119,13 +120,18 @@ export const routeOpenWeatherMapForecast = async ({
119
120
  lat,
120
121
  lon,
121
122
  appid: authKey,
123
+ lang: getOpenWeatherMapLang(searchParams.get("locale") || undefined),
122
124
  ...COMMON_OPTIONS,
123
125
  }),
124
126
  });
125
127
 
126
128
  try {
127
129
  const res = await (await fetch(url)).json();
128
- nextAPILog("GET", routeUrl, url, { searchParams, res });
130
+ nextAPILog("GET", routeUrl, "Open Weather Map 예보날씨 API", {
131
+ searchParams,
132
+ count: Array.isArray(res?.list) ? res.list.length : 0,
133
+ cod: res?.cod,
134
+ });
129
135
  return res;
130
136
  } catch (error) {
131
137
  nextAPILog("GET", routeUrl, url, { error });
@@ -1,13 +1,14 @@
1
1
  "use client";
2
2
 
3
- import Image from "next/image";
3
+ import type { WeatherAddressIconProps } from "../../types";
4
+ import MarkerIcon from "../../img/marker.svg";
4
5
 
5
- import assetUrl from "../../../asset-url";
6
-
7
- export default function WeatherAddressIcon() {
6
+ export default function WeatherAddressIcon({
7
+ alt,
8
+ }: WeatherAddressIconProps = {}) {
8
9
  return (
9
- <figure className="weather-address-icon">
10
- <Image src={`${assetUrl}/img/weather/address.svg`} alt="날씨 위치" fill />
10
+ <figure className="weather-address-icon" role="img" aria-label={alt}>
11
+ <MarkerIcon width={16} height={16} viewBox="0 0 16 16" />
11
12
  </figure>
12
13
  );
13
14
  }
@@ -4,6 +4,7 @@ import Image from "next/image";
4
4
  import clsx from "clsx";
5
5
 
6
6
  import assetUrl from "../../../asset-url";
7
+ import type { WeatherIconProps } from "../../types";
7
8
 
8
9
  // Storybook의 next/image mock과 동일한 client boundary에서 weather icon을 렌더한다.
9
10
 
@@ -18,16 +19,14 @@ import assetUrl from "../../../asset-url";
18
19
  export default function WeatherIcon({
19
20
  code,
20
21
  name,
21
- }: Partial<{
22
- code: string | null;
23
- name: string | null;
24
- }>) {
22
+ alt,
23
+ }: WeatherIconProps = {}) {
25
24
  return (
26
25
  code && (
27
26
  <figure className={clsx("weather-base-icon", "weather-icon")}>
28
27
  <Image
29
28
  src={`${assetUrl}/img/weather/${code}.svg`}
30
- alt={name || "날씨 아이콘"}
29
+ alt={name || alt || "날씨 아이콘"}
31
30
  fill
32
31
  />
33
32
  </figure>
@@ -1,20 +1,54 @@
1
1
  "use client";
2
2
 
3
+ import { useEffect, useState } from "react";
3
4
  import { useAtomValue } from "jotai";
4
5
 
5
6
  import { weatherCoordinate } from "../../jotai";
7
+ import type { WeatherPageHeaderAddressProps } from "../../types";
8
+ import { resolveWeatherPageHeaderTexts } from "../../utils";
6
9
  import WeatherAddressIcon from "../icon/Address";
7
10
 
8
- export default function WeatherPageHeaderAddress() {
11
+ const getRenderedAt = (): string => {
12
+ const now = new Date();
13
+ const month = String(now.getMonth() + 1).padStart(2, "0");
14
+ const date = String(now.getDate()).padStart(2, "0");
15
+ const hours = String(now.getHours()).padStart(2, "0");
16
+ const minutes = String(now.getMinutes()).padStart(2, "0");
17
+
18
+ return `${now.getFullYear()}-${month}-${date} ${hours}:${minutes}`;
19
+ };
20
+
21
+ export default function WeatherPageHeaderAddress({
22
+ texts: textOverrides,
23
+ }: WeatherPageHeaderAddressProps = {}) {
9
24
  const { address } = useAtomValue(weatherCoordinate);
25
+ const texts = resolveWeatherPageHeaderTexts(textOverrides);
26
+ const [renderedAt, setRenderedAt] = useState(getRenderedAt);
27
+
28
+ useEffect(() => {
29
+ const intervalId = window.setInterval(() => {
30
+ setRenderedAt(prevRenderedAt => {
31
+ const nextRenderedAt = getRenderedAt();
32
+
33
+ return prevRenderedAt === nextRenderedAt
34
+ ? prevRenderedAt
35
+ : nextRenderedAt;
36
+ });
37
+ }, 1000);
38
+
39
+ return () => {
40
+ window.clearInterval(intervalId);
41
+ };
42
+ }, []);
10
43
 
11
44
  if (!address) return null;
12
45
 
13
46
  return (
14
47
  <div className="weather-address">
15
- <WeatherAddressIcon />
48
+ <WeatherAddressIcon alt={texts.addressIconAlt} />
16
49
  <p className="weather-address-text">
17
50
  <span>{address}</span>
51
+ <span className="weather-address-date">({renderedAt})</span>
18
52
  </p>
19
53
  </div>
20
54
  );
@@ -2,40 +2,67 @@
2
2
 
3
3
  import { useMemo } from "react";
4
4
 
5
- import useWeatherKoreaAlert from "../../hooks/useWeatherKoreaAlert";
6
5
  import { Alternate } from "@uniai-fe/uds-primitives";
6
+ import useWeatherKoreaAlert from "../../hooks/useWeatherKoreaAlert";
7
+ import type { WeatherPageHeaderAlertProps } from "../../types";
8
+ import {
9
+ isKoreaWeatherLocale,
10
+ resolveWeatherPageHeaderTexts,
11
+ } from "../../utils";
7
12
 
8
- export default function WeatherPageHeaderAlert() {
9
- const { alert, isFetching } = useWeatherKoreaAlert();
13
+ export default function WeatherPageHeaderAlert({
14
+ locale,
15
+ texts: textOverrides,
16
+ }: WeatherPageHeaderAlertProps = {}) {
17
+ const isKoreaProvider = isKoreaWeatherLocale(locale);
18
+ const { alert, isFetching } = useWeatherKoreaAlert({
19
+ enabled: isKoreaProvider,
20
+ });
21
+ const texts = resolveWeatherPageHeaderTexts(textOverrides);
10
22
 
11
23
  const notice = useMemo((): string => {
12
24
  if (alert.length === 0 || !alert?.[0]?.alert_type) return "";
13
25
 
14
26
  const { alert_type, alert_level, alert_command } = alert?.[0] || {};
15
27
  const level = () => {
16
- if (alert_level.startsWith("예비")) return "예비특보";
17
- if (alert_level.startsWith("주의")) return "주의보";
28
+ if (alert_level.startsWith("예비"))
29
+ return texts.alertPreliminaryLevelLabel;
30
+ if (alert_level.startsWith("주의")) return texts.alertWatchLevelLabel;
18
31
  return alert_level;
19
32
  };
20
- const command = alert_command?.includes("해제") ? "해제" : "발령";
33
+ const command = alert_command?.includes("해제")
34
+ ? texts.alertCancelCommandLabel
35
+ : texts.alertIssueCommandLabel;
21
36
  return `${alert_type}${level()} ${command}`;
22
- }, [alert]);
37
+ }, [
38
+ alert,
39
+ texts.alertCancelCommandLabel,
40
+ texts.alertIssueCommandLabel,
41
+ texts.alertPreliminaryLevelLabel,
42
+ texts.alertWatchLevelLabel,
43
+ ]);
23
44
 
24
- if (!isFetching && (alert.length === 0 || !alert?.[0]?.alert_type)) {
45
+ if (
46
+ !isKoreaProvider ||
47
+ (!isFetching && (alert.length === 0 || !alert?.[0]?.alert_type))
48
+ ) {
25
49
  return null;
26
50
  }
27
51
 
52
+ if (isFetching) {
53
+ return (
54
+ <div className="weather-alert-loading">
55
+ <Alternate.LoadingDefault direction="horizontal">
56
+ {texts.alertLoading}
57
+ </Alternate.LoadingDefault>
58
+ </div>
59
+ );
60
+ }
61
+
28
62
  return (
29
63
  <div className="weather-alert">
30
64
  <div className="weather-alert-text">
31
- {isFetching ? (
32
- // 특보 조회 중에도 영역을 확보해 레이아웃이 흔들리지 않도록 로딩 표시를 유지한다.
33
- <Alternate.LoadingDefault direction="horizontal">
34
- 특보 불러오는 중...
35
- </Alternate.LoadingDefault>
36
- ) : (
37
- <span>{notice}</span>
38
- )}
65
+ <span>{notice}</span>
39
66
  </div>
40
67
  </div>
41
68
  );
@@ -1,14 +1,42 @@
1
+ "use client";
2
+
1
3
  import WeatherPageHeaderToday from "./Today";
2
4
  import WeatherPageHeaderForecast from "./Forecast";
3
5
  import WeatherPageHeaderAddress from "./Address";
4
6
  import PageHeaderUtilityItem from "../../../page-frame/desktop/components/header/util/Item";
7
+ import type { WeatherPageHeaderContainerProps } from "../../types";
8
+ import { useOpenWeatherMap, useWeatherKorea } from "../../hooks";
9
+ import { isKoreaWeatherLocale } from "../../utils";
10
+
11
+ export default function WeatherPageHeaderContainer({
12
+ locale,
13
+ texts,
14
+ }: 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
+ });
5
24
 
6
- export default function WeatherPageHeaderContainer() {
7
25
  return (
8
- <PageHeaderUtilityItem>
9
- <WeatherPageHeaderAddress />
10
- <WeatherPageHeaderToday />
11
- <WeatherPageHeaderForecast />
26
+ <PageHeaderUtilityItem className="weather-page-header">
27
+ <WeatherPageHeaderAddress locale={locale} texts={texts} />
28
+ <WeatherPageHeaderToday
29
+ locale={locale}
30
+ texts={texts}
31
+ koreaWeather={koreaWeather}
32
+ openWeatherMapWeather={openWeatherMapWeather}
33
+ />
34
+ <WeatherPageHeaderForecast
35
+ locale={locale}
36
+ texts={texts}
37
+ koreaWeather={koreaWeather}
38
+ openWeatherMapWeather={openWeatherMapWeather}
39
+ />
12
40
  </PageHeaderUtilityItem>
13
41
  );
14
42
  }
@@ -1,17 +1,50 @@
1
1
  "use client";
2
2
 
3
+ import { useMemo } from "react";
3
4
  import { Alternate } from "@uniai-fe/uds-primitives";
4
- import { useWeatherKorea } from "../../hooks";
5
+ import { useOpenWeatherMap, useWeatherKorea } from "../../hooks";
6
+ import type { WeatherPageHeaderForecastProps } from "../../types";
7
+ import {
8
+ getOpenWeatherMapNextDays,
9
+ isKoreaWeatherLocale,
10
+ resolveWeatherPageHeaderTexts,
11
+ } from "../../utils";
5
12
  import WeatherPageHeaderNextDays from "./NextDays";
6
13
 
7
- export default function WeatherPageHeaderForecast() {
8
- const { forecast, isFetchingForecast } = useWeatherKorea();
14
+ export default function WeatherPageHeaderForecast({
15
+ locale,
16
+ texts: textOverrides,
17
+ koreaWeather,
18
+ openWeatherMapWeather,
19
+ }: WeatherPageHeaderForecastProps = {}) {
20
+ const isKoreaProvider = isKoreaWeatherLocale(locale);
21
+ const koreaWeatherFallback = useWeatherKorea({
22
+ enabled: !koreaWeather && isKoreaProvider,
23
+ });
24
+ const openWeatherMapWeatherFallback = useOpenWeatherMap({
25
+ locale,
26
+ enabledNow: false,
27
+ enabledForecast: !openWeatherMapWeather && !isKoreaProvider,
28
+ });
29
+ const korea = koreaWeather ?? koreaWeatherFallback;
30
+ const openWeatherMap = openWeatherMapWeather ?? openWeatherMapWeatherFallback;
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;
9
42
 
10
- if (isFetchingForecast) {
43
+ if (isFetching) {
11
44
  return (
12
45
  <div className="weather-next-days-container">
13
46
  <Alternate.LoadingDefault direction="horizontal">
14
- 예보 날씨 불러오는 중...
47
+ {texts.forecastWeatherLoading}
15
48
  </Alternate.LoadingDefault>
16
49
  </div>
17
50
  );
@@ -19,8 +52,16 @@ export default function WeatherPageHeaderForecast() {
19
52
 
20
53
  return (
21
54
  <>
22
- <WeatherPageHeaderNextDays title="내일" data={forecast?.day_1} />
23
- <WeatherPageHeaderNextDays title="모레" data={forecast?.day_2} />
55
+ <WeatherPageHeaderNextDays
56
+ title={texts.tomorrowLabel}
57
+ data={forecastDays?.day_1}
58
+ texts={textOverrides}
59
+ />
60
+ <WeatherPageHeaderNextDays
61
+ title={texts.dayAfterTomorrowLabel}
62
+ data={forecastDays?.day_2}
63
+ texts={textOverrides}
64
+ />
24
65
  </>
25
66
  );
26
67
  }
@@ -1,32 +1,35 @@
1
- import type { API_Res_WeatherKoreaNextDays } from "../../types";
1
+ import { lengthFormat } from "@uniai-fe/util-functions";
2
+
3
+ import type { WeatherPageHeaderNextDaysProps } from "../../types";
4
+ import { resolveWeatherPageHeaderTexts } from "../../utils";
2
5
  import WeatherIcon from "../icon/Weather";
3
6
 
4
7
  export default function WeatherPageHeaderNextDays({
5
8
  title,
6
9
  data,
7
- }: {
8
- title: string;
9
- data?: API_Res_WeatherKoreaNextDays;
10
- }) {
10
+ texts: textOverrides,
11
+ }: WeatherPageHeaderNextDaysProps) {
12
+ const texts = resolveWeatherPageHeaderTexts(textOverrides);
13
+
11
14
  return (
12
15
  <div className="weather-next-days-container">
13
- <div className="weather-base-divider" aria-hidden="true" />
14
- <dl className="weather-next-days-text">
15
- <dt>
16
- <span>{title}</span>
17
- </dt>
18
- <dd>
19
- <WeatherIcon code={data?.condition} name={data?.conditionName} />
20
- </dd>
21
- <dd>
22
- <span>{data?.min_temperature ?? "-"}</span>
23
- <span className="unit">℃</span>
24
- </dd>
25
- <dd>
26
- <span>{data?.max_temperature ?? "-"}</span>
27
- <span className="unit">℃</span>
28
- </dd>
29
- </dl>
16
+ <WeatherIcon
17
+ code={data?.condition}
18
+ name={data?.conditionName}
19
+ alt={texts.weatherIconAlt}
20
+ />
21
+ <p className="weather-forecast-text">
22
+ <span className="weather-label">{title}</span>
23
+ <span className="weather-value">
24
+ {lengthFormat(data?.min_temperature, 0)}
25
+ </span>
26
+ <span className="weather-unit">℃</span>
27
+ <span className="weather-range">~</span>
28
+ <span className="weather-value">
29
+ {lengthFormat(data?.max_temperature, 0)}
30
+ </span>
31
+ <span className="weather-unit">℃</span>
32
+ </p>
30
33
  </div>
31
34
  );
32
35
  }