@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 +3 -0
- package/package.json +1 -1
- package/src/cctv/components/pagination/list/Item.tsx +9 -0
- package/src/weather/apis/korea/client.ts +50 -15
- package/src/weather/apis/korea/server.ts +2 -2
- package/src/weather/apis/open-weather-map/client.ts +45 -15
- package/src/weather/apis/open-weather-map/server.ts +8 -2
- package/src/weather/components/icon/Address.tsx +7 -6
- package/src/weather/components/icon/Weather.tsx +4 -5
- package/src/weather/components/page-header/Address.tsx +36 -2
- package/src/weather/components/page-header/Alert.tsx +43 -16
- package/src/weather/components/page-header/Container.tsx +33 -5
- package/src/weather/components/page-header/Forecast.tsx +48 -7
- package/src/weather/components/page-header/NextDays.tsx +25 -22
- package/src/weather/components/page-header/Today.tsx +134 -91
- package/src/weather/hooks/useOpenWeatherMap.ts +22 -3
- package/src/weather/hooks/useWeatherKorea.ts +16 -4
- package/src/weather/hooks/useWeatherKoreaAlert.ts +13 -4
- package/src/weather/img/marker.svg +4 -0
- package/src/weather/styles/weather.scss +107 -108
- package/src/weather/types/base.ts +20 -0
- package/src/weather/types/index.ts +1 -0
- package/src/weather/types/page-header.ts +277 -0
- package/src/weather/utils/index.ts +1 -0
- package/src/weather/utils/locale.ts +73 -0
- package/src/weather/utils/weather.ts +112 -0
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
|
@@ -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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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,
|
|
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
|
|
3
|
+
import type { WeatherAddressIconProps } from "../../types";
|
|
4
|
+
import MarkerIcon from "../../img/marker.svg";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
export default function WeatherAddressIcon({
|
|
7
|
+
alt,
|
|
8
|
+
}: WeatherAddressIconProps = {}) {
|
|
8
9
|
return (
|
|
9
|
-
<figure className="weather-address-icon">
|
|
10
|
-
<
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("예비"))
|
|
17
|
-
|
|
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
|
-
}, [
|
|
37
|
+
}, [
|
|
38
|
+
alert,
|
|
39
|
+
texts.alertCancelCommandLabel,
|
|
40
|
+
texts.alertIssueCommandLabel,
|
|
41
|
+
texts.alertPreliminaryLevelLabel,
|
|
42
|
+
texts.alertWatchLevelLabel,
|
|
43
|
+
]);
|
|
23
44
|
|
|
24
|
-
if (
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
texts: textOverrides,
|
|
11
|
+
}: WeatherPageHeaderNextDaysProps) {
|
|
12
|
+
const texts = resolveWeatherPageHeaderTexts(textOverrides);
|
|
13
|
+
|
|
11
14
|
return (
|
|
12
15
|
<div className="weather-next-days-container">
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
</
|
|
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
|
}
|