@uniai-fe/uds-templates 0.5.9 → 0.5.11

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 (30) hide show
  1. package/README.md +7 -0
  2. package/dist/styles.css +139 -79
  3. package/package.json +1 -1
  4. package/src/cctv/components/pagination/list/Item.tsx +9 -0
  5. package/src/weather/apis/korea/client.ts +50 -15
  6. package/src/weather/apis/korea/server.ts +2 -2
  7. package/src/weather/apis/open-weather-map/client.ts +45 -15
  8. package/src/weather/apis/open-weather-map/server.ts +8 -2
  9. package/src/weather/components/icon/Address.tsx +7 -6
  10. package/src/weather/components/icon/Weather.tsx +4 -5
  11. package/src/weather/components/page-header/Address.tsx +36 -2
  12. package/src/weather/components/page-header/Alert.tsx +43 -16
  13. package/src/weather/components/page-header/Container.tsx +33 -5
  14. package/src/weather/components/page-header/Forecast.tsx +48 -7
  15. package/src/weather/components/page-header/NextDays.tsx +25 -22
  16. package/src/weather/components/page-header/Today.tsx +134 -91
  17. package/src/weather/hooks/useOpenWeatherMap.ts +22 -3
  18. package/src/weather/hooks/useWeatherKorea.ts +16 -4
  19. package/src/weather/hooks/useWeatherKoreaAlert.ts +13 -4
  20. package/src/weather/img/marker.svg +4 -0
  21. package/src/weather/index.scss +1 -0
  22. package/src/weather/styles/variables.scss +30 -0
  23. package/src/weather/styles/weather.scss +116 -109
  24. package/src/weather/types/base.ts +20 -0
  25. package/src/weather/types/index.ts +2 -0
  26. package/src/weather/types/page-header.ts +277 -0
  27. package/src/weather/types/provider.ts +34 -0
  28. package/src/weather/utils/index.ts +1 -0
  29. package/src/weather/utils/locale.ts +110 -0
  30. package/src/weather/utils/weather.ts +112 -0
@@ -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
  }
@@ -6,127 +6,170 @@ import { lengthFormat } from "@uniai-fe/util-functions";
6
6
  import { Alternate } from "@uniai-fe/uds-primitives";
7
7
  import WeatherIcon from "../icon/Weather";
8
8
  import WeatherPageHeaderAlert from "./Alert";
9
+ import type { WeatherPageHeaderTodayProps } from "../../types";
9
10
  import { useOpenWeatherMap, useWeatherKorea } from "../../hooks";
10
-
11
- export default function WeatherPageHeaderToday() {
12
- const { now, forecast, isFetchingNow } = useWeatherKorea();
13
- const { now: nowAlt, isFetchingNow: isFetchingAlt } = useOpenWeatherMap();
11
+ import {
12
+ getOpenWeatherMapConditionCode,
13
+ isKoreaWeatherLocale,
14
+ resolveWeatherPageHeaderTexts,
15
+ } from "../../utils";
16
+
17
+ export default function WeatherPageHeaderToday({
18
+ locale,
19
+ texts: textOverrides,
20
+ koreaWeather,
21
+ openWeatherMapWeather,
22
+ }: WeatherPageHeaderTodayProps = {}) {
23
+ const isKoreaProvider = isKoreaWeatherLocale(locale);
24
+ const koreaWeatherFallback = useWeatherKorea({
25
+ enabled: !koreaWeather && isKoreaProvider,
26
+ });
27
+ const openWeatherMapWeatherFallback = useOpenWeatherMap({
28
+ locale,
29
+ enabledNow: !openWeatherMapWeather && !isKoreaProvider,
30
+ enabledForecast: false,
31
+ });
32
+ const korea = koreaWeather ?? koreaWeatherFallback;
33
+ const openWeatherMap = openWeatherMapWeather ?? openWeatherMapWeatherFallback;
34
+ const texts = resolveWeatherPageHeaderTexts(textOverrides);
14
35
 
15
36
  const isFetchingAlternate = useMemo(() => {
16
- if (isFetchingNow) return true;
17
- const kma_now = now?.today?.condition;
18
- const kma_forecast = forecast?.today?.condition;
19
- if (
20
- !kma_now ||
21
- kma_now.endsWith("null") ||
22
- !kma_forecast ||
23
- kma_forecast.endsWith("null")
24
- ) {
25
- return isFetchingAlt;
26
- }
37
+ if (!isKoreaProvider) return openWeatherMap.isFetchingNow;
38
+ if (korea.isFetchingNow) return true;
27
39
  return false;
28
- }, [
29
- forecast?.today?.condition,
30
- isFetchingAlt,
31
- isFetchingNow,
32
- now?.today?.condition,
33
- ]);
40
+ }, [isKoreaProvider, korea.isFetchingNow, openWeatherMap.isFetchingNow]);
34
41
 
35
42
  const iconCode = useMemo(() => {
36
- const kma_now = now?.today?.condition;
37
- if (kma_now && !kma_now.endsWith("null")) return kma_now;
38
-
39
- const kma_forecast = forecast?.today?.condition;
40
- if (kma_forecast && !kma_forecast.endsWith("null")) return kma_forecast;
41
-
42
- const alt_now = nowAlt?.weather?.[0]?.icon;
43
- switch (alt_now) {
44
- case "01d":
45
- return "sky-1";
46
- case "02d":
47
- case "03d":
48
- case "04d":
49
- return "sky-2";
50
- case "09d":
51
- return "drop-rain-3";
52
- case "10d":
53
- return "drop-rain-1";
54
- case "11d":
55
- return "drop-rain-thunder";
56
- case "13d":
57
- return "drop-snow";
58
- case "50d":
59
- return "sky-4";
60
- default:
61
- return "sky-1";
43
+ if (isKoreaProvider) {
44
+ const kma_now = korea.now?.today?.condition;
45
+ if (kma_now && !kma_now.endsWith("null")) return kma_now;
46
+
47
+ const kma_forecast = korea.forecast?.today?.condition;
48
+ if (kma_forecast && !kma_forecast.endsWith("null")) return kma_forecast;
62
49
  }
63
- }, [forecast?.today?.condition, now?.today?.condition, nowAlt?.weather]);
64
50
 
65
- const weatherStateName = useMemo(() => {
66
- const kma_now = now?.today?.conditionName;
67
- if (kma_now && !kma_now.endsWith("null")) return kma_now;
51
+ return getOpenWeatherMapConditionCode(
52
+ openWeatherMap.now?.weather?.[0]?.icon,
53
+ );
54
+ }, [
55
+ isKoreaProvider,
56
+ korea.forecast?.today?.condition,
57
+ korea.now?.today?.condition,
58
+ openWeatherMap.now?.weather,
59
+ ]);
68
60
 
69
- const kma_forecast = forecast?.today?.conditionName;
70
- if (kma_forecast && !kma_forecast.endsWith("null")) return kma_forecast;
61
+ const weatherStateName = useMemo(() => {
62
+ if (isKoreaProvider) {
63
+ const kma_now = korea.now?.today?.conditionName;
64
+ if (kma_now && !kma_now.endsWith("null"))
65
+ return kma_now || texts.conditionFallbackLabel;
66
+
67
+ const kma_forecast = korea.forecast?.today?.conditionName;
68
+ if (kma_forecast && !kma_forecast.endsWith("null"))
69
+ return kma_forecast || texts.conditionFallbackLabel;
70
+ }
71
71
 
72
- return nowAlt?.weather?.[0]?.description || "-";
72
+ return (
73
+ openWeatherMap.now?.weather?.[0]?.description ||
74
+ texts.conditionFallbackLabel
75
+ );
73
76
  }, [
74
- forecast?.today?.conditionName,
75
- now?.today?.conditionName,
76
- nowAlt?.weather,
77
+ isKoreaProvider,
78
+ korea.forecast?.today?.conditionName,
79
+ korea.now?.today?.conditionName,
80
+ openWeatherMap.now?.weather,
81
+ texts.conditionFallbackLabel,
77
82
  ]);
78
83
 
79
84
  const temperature = useMemo(() => {
80
- const kma_now = now?.today?.temperature;
81
- if (kma_now && !Number.isNaN(Number(kma_now))) return Number(kma_now);
85
+ if (isKoreaProvider) {
86
+ const kma_now = korea.now?.today?.temperature;
87
+ if (kma_now && !Number.isNaN(Number(kma_now))) return Number(kma_now);
82
88
 
83
- const kma_forecast = forecast?.today?.temperature;
84
- if (kma_forecast && !Number.isNaN(Number(kma_forecast)))
85
- return Number(kma_forecast);
89
+ const kma_forecast = korea.forecast?.today?.temperature;
90
+ if (kma_forecast && !Number.isNaN(Number(kma_forecast)))
91
+ return Number(kma_forecast);
92
+ }
86
93
 
87
- const alt_now = nowAlt?.main?.temp;
94
+ const alt_now = openWeatherMap.now?.main?.temp;
88
95
  return typeof alt_now === "number" ? Math.round(alt_now) : null;
89
96
  }, [
90
- forecast?.today?.temperature,
91
- now?.today?.temperature,
92
- nowAlt?.main?.temp,
97
+ isKoreaProvider,
98
+ korea.forecast?.today?.temperature,
99
+ korea.now?.today?.temperature,
100
+ openWeatherMap.now?.main?.temp,
93
101
  ]);
94
102
 
95
103
  const humidity = useMemo(() => {
96
- const kma_now = now?.today?.humidity;
97
- if (kma_now && !Number.isNaN(Number(kma_now))) return Number(kma_now);
104
+ if (isKoreaProvider) {
105
+ const kma_now = korea.now?.today?.humidity;
106
+ if (kma_now && !Number.isNaN(Number(kma_now))) return Number(kma_now);
98
107
 
99
- const kma_forecast = forecast?.today?.humidity;
100
- if (kma_forecast && !Number.isNaN(Number(kma_forecast)))
101
- return Number(kma_forecast);
108
+ const kma_forecast = korea.forecast?.today?.humidity;
109
+ if (kma_forecast && !Number.isNaN(Number(kma_forecast)))
110
+ return Number(kma_forecast);
111
+ }
102
112
 
103
- const alt_now = nowAlt?.main?.humidity;
113
+ const alt_now = openWeatherMap.now?.main?.humidity;
104
114
  return typeof alt_now === "number" ? Math.round(alt_now) : null;
105
- }, [forecast?.today?.humidity, now?.today?.humidity, nowAlt?.main?.humidity]);
115
+ }, [
116
+ isKoreaProvider,
117
+ korea.forecast?.today?.humidity,
118
+ korea.now?.today?.humidity,
119
+ openWeatherMap.now?.main?.humidity,
120
+ ]);
121
+
122
+ const maxTemperature = null;
123
+ const minTemperature = null;
124
+ const alert = (
125
+ <WeatherPageHeaderAlert locale={locale} texts={textOverrides} />
126
+ );
106
127
 
107
128
  return (
108
129
  <div className="weather-today-container">
109
130
  {isFetchingAlternate ? (
110
- <Alternate.LoadingDefault direction="horizontal">
111
- 현재 날씨 불러오는 중...
112
- </Alternate.LoadingDefault>
131
+ <>
132
+ {alert}
133
+ <Alternate.LoadingDefault direction="horizontal">
134
+ {texts.currentWeatherLoading}
135
+ </Alternate.LoadingDefault>
136
+ </>
113
137
  ) : (
114
138
  <>
115
- <WeatherIcon code={iconCode} name={weatherStateName} />
116
- <div className="weather-today-temperature">
117
- <span>{lengthFormat(temperature, 0)}</span>
118
- <span className="unit">℃</span>
119
- </div>
120
- <WeatherPageHeaderAlert />
121
- <div className="weather-today-humidity">
122
- <dl className="weather-base-text-info">
123
- <dt>
124
- <span>습도</span>
125
- </dt>
126
- <dd>
127
- <span>{lengthFormat(humidity, 0)}%</span>
128
- </dd>
129
- </dl>
139
+ <WeatherIcon
140
+ code={iconCode}
141
+ name={weatherStateName}
142
+ alt={texts.weatherIconAlt}
143
+ />
144
+ <div className="weather-today-text">
145
+ {alert}
146
+ <p className="weather-temperature-text">
147
+ <span className="weather-label">{texts.currentLabel}</span>
148
+ <span className="weather-value">
149
+ {lengthFormat(temperature, 0)}
150
+ </span>
151
+ <span className="weather-unit">℃</span>
152
+ </p>
153
+ <p className="weather-temperature-text">
154
+ <span className="weather-label">{texts.maxTemperatureLabel}</span>
155
+ <span className="weather-value">
156
+ {lengthFormat(maxTemperature, 0)}
157
+ </span>
158
+ <span className="weather-unit">℃</span>
159
+ </p>
160
+ <p className="weather-temperature-text">
161
+ <span className="weather-label">{texts.minTemperatureLabel}</span>
162
+ <span className="weather-value">
163
+ {lengthFormat(minTemperature, 0)}
164
+ </span>
165
+ <span className="weather-unit">℃</span>
166
+ </p>
167
+ <p className="weather-humidity-text">
168
+ <span className="weather-label">{texts.humidityLabel}</span>
169
+ <span className="weather-value">
170
+ {lengthFormat(humidity, 0)}%
171
+ </span>
172
+ </p>
130
173
  </div>
131
174
  </>
132
175
  )}
@@ -6,14 +6,33 @@ import {
6
6
  useQueryWeatherOpenWeatherMapForecast,
7
7
  useQueryWeatherOpenWeatherMapNow,
8
8
  } from "../apis/open-weather-map/client";
9
+ import type {
10
+ WeatherApiLocaleOptions,
11
+ WeatherOpenWeatherMapParams,
12
+ } from "../types";
9
13
 
10
- export default function useOpenWeatherMap() {
14
+ type UseOpenWeatherMapOptions = WeatherApiLocaleOptions & {
15
+ enabledNow?: boolean;
16
+ enabledForecast?: boolean;
17
+ };
18
+
19
+ export default function useOpenWeatherMap({
20
+ locale,
21
+ enabledNow = true,
22
+ enabledForecast = true,
23
+ }: UseOpenWeatherMapOptions = {}) {
11
24
  const coordinate = useAtomValue(weatherCoordinate);
25
+ const params: WeatherOpenWeatherMapParams = {
26
+ ...coordinate,
27
+ ...(locale ? { locale } : {}),
28
+ };
12
29
 
13
30
  const { data: now, isFetching: isFetchingNow } =
14
- useQueryWeatherOpenWeatherMapNow(coordinate);
31
+ useQueryWeatherOpenWeatherMapNow(params, { enabled: enabledNow });
15
32
  const { data: forecast, isFetching: isFetchingForecast } =
16
- useQueryWeatherOpenWeatherMapForecast(coordinate);
33
+ useQueryWeatherOpenWeatherMapForecast(params, {
34
+ enabled: enabledForecast,
35
+ });
17
36
 
18
37
  return {
19
38
  coordinate,
@@ -11,7 +11,13 @@ import { weatherCoordinate } from "../jotai/coordinate";
11
11
  import { getWeatherBaseMoments } from "../utils/date-time";
12
12
  import { getWeatherGridLocation } from "../utils/location";
13
13
 
14
- export default function useWeatherKorea() {
14
+ type UseWeatherKoreaOptions = {
15
+ enabled?: boolean;
16
+ };
17
+
18
+ export default function useWeatherKorea({
19
+ enabled = true,
20
+ }: UseWeatherKoreaOptions = {}) {
15
21
  const coordinate = useAtomValue(weatherCoordinate);
16
22
  const [params, setParams] = useState<{
17
23
  [categoryKey: string]: API_Req_WeatherKorea;
@@ -21,7 +27,8 @@ export default function useWeatherKorea() {
21
27
  });
22
28
 
23
29
  useEffect(() => {
24
- if (typeof window === "undefined") return;
30
+ if (typeof window === "undefined" || !enabled) return;
31
+ let isActive = true;
25
32
 
26
33
  const hasInjectedCoordinate =
27
34
  typeof coordinate.address === "string" ||
@@ -38,6 +45,7 @@ export default function useWeatherKorea() {
38
45
 
39
46
  async function updater() {
40
47
  const grid = await getWeatherGridLocation(coordinate);
48
+ if (!isActive) return;
41
49
  setParams({
42
50
  now: {
43
51
  ...grid,
@@ -51,14 +59,18 @@ export default function useWeatherKorea() {
51
59
  }
52
60
 
53
61
  updater();
54
- }, [coordinate]);
62
+ return () => {
63
+ isActive = false;
64
+ };
65
+ }, [coordinate, enabled]);
55
66
 
56
67
  const { data: now, isFetching: isFetchingNow } = useQueryWeatherKoreaNow(
57
68
  params.now,
69
+ { enabled },
58
70
  );
59
71
 
60
72
  const { data: forecast, isFetching: isFetchingForecast } =
61
- useQueryWeatherKoreaForecast(params.forecast);
73
+ useQueryWeatherKoreaForecast(params.forecast, { enabled });
62
74
 
63
75
  return {
64
76
  params,