@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
@@ -7,187 +7,115 @@ import { Alternate } from "@uniai-fe/uds-primitives";
7
7
  import WeatherIcon from "../icon/Weather";
8
8
  import WeatherPageHeaderAlert from "./Alert";
9
9
  import type { WeatherPageHeaderTodayProps } from "../../types";
10
- import { useOpenWeatherMap, useWeatherKorea } from "../../hooks";
10
+ import { useWeather } from "../../hooks";
11
11
  import {
12
- getOpenWeatherMapConditionCode,
13
- getOpenWeatherMapTodayForecast,
14
- isKoreaWeatherLocale,
12
+ isWeatherForecastResponse,
13
+ isWeatherNowResponse,
15
14
  resolveWeatherPageHeaderTexts,
16
15
  } from "../../utils";
17
16
 
17
+ /**
18
+ * Weather Page Header; 표시 가능한 숫자값으로 변환.
19
+ * @util
20
+ * @desc weather API가 숫자 또는 문자열로 내려주는 수치 값을 number/null로 정규화한다.
21
+ * @param {number|string|null|undefined} value 변환할 날씨 수치 값
22
+ * @return {number | null} 표시 가능한 숫자 값
23
+ */
24
+ const toWeatherNumber = (
25
+ value: number | string | null | undefined,
26
+ ): number | null => {
27
+ if (value === null || typeof value === "undefined" || value === "") {
28
+ return null;
29
+ }
30
+
31
+ const nextValue = Number(value);
32
+ return Number.isNaN(nextValue) ? null : nextValue;
33
+ };
34
+
35
+ /**
36
+ * Weather Page Header; 현재 날씨와 당일 최고/최저/습도를 표시하는 컴포넌트.
37
+ * @component
38
+ * @desc 통합 weather now/forecast 응답을 조합해 현재 기온, 최고/최저 기온, 습도를 표시한다.
39
+ * @param {WeatherPageHeaderTodayProps} [props] today props
40
+ * @param {string} [props.locale] 표시 언어 코드
41
+ * @param {Partial<WeatherPageHeaderTexts>} [props.texts] 서비스에서 주입한 표시 문구
42
+ * @param {WeatherPageHeaderWeather} [props.weather] 상위 container에서 준비한 통합 weather query 결과
43
+ */
18
44
  export default function WeatherPageHeaderToday({
19
45
  locale,
20
46
  texts: textOverrides,
21
- koreaWeather,
22
- openWeatherMapWeather,
47
+ weather,
23
48
  }: WeatherPageHeaderTodayProps = {}) {
24
- const isKoreaProvider = isKoreaWeatherLocale(locale);
25
- const koreaWeatherFallback = useWeatherKorea({
26
- enabled: !koreaWeather && isKoreaProvider,
27
- });
28
- const openWeatherMapWeatherFallback = useOpenWeatherMap({
49
+ const weatherFallback = useWeather({
29
50
  locale,
30
- enabledNow: !openWeatherMapWeather && !isKoreaProvider,
31
- enabledForecast: !openWeatherMapWeather && !isKoreaProvider,
51
+ enabled: !weather,
32
52
  });
33
- const korea = koreaWeather ?? koreaWeatherFallback;
34
- const openWeatherMap = openWeatherMapWeather ?? openWeatherMapWeatherFallback;
53
+ const currentWeather = weather ?? weatherFallback;
35
54
  const texts = resolveWeatherPageHeaderTexts(textOverrides);
36
- const openWeatherMapToday = useMemo(
37
- () => getOpenWeatherMapTodayForecast(openWeatherMap.forecast),
38
- [openWeatherMap.forecast],
55
+ const now = useMemo(
56
+ () =>
57
+ isWeatherNowResponse(currentWeather.now) ? currentWeather.now : undefined,
58
+ [currentWeather.now],
59
+ );
60
+ const forecast = useMemo(
61
+ () =>
62
+ isWeatherForecastResponse(currentWeather.forecast)
63
+ ? currentWeather.forecast
64
+ : undefined,
65
+ [currentWeather.forecast],
39
66
  );
40
67
 
41
- const isFetchingAlternate = useMemo(() => {
42
- if (!isKoreaProvider)
43
- return openWeatherMap.isFetchingNow || openWeatherMap.isFetchingForecast;
44
- if (korea.isFetchingNow) return true;
45
- return false;
46
- }, [
47
- isKoreaProvider,
48
- korea.isFetchingNow,
49
- openWeatherMap.isFetchingForecast,
50
- openWeatherMap.isFetchingNow,
51
- ]);
52
-
53
- const iconCode = useMemo(() => {
54
- if (isKoreaProvider) {
55
- const kma_now = korea.now?.today?.condition;
56
- if (kma_now && !kma_now.endsWith("null")) return kma_now;
57
-
58
- const kma_forecast = korea.forecast?.today?.condition;
59
- if (kma_forecast && !kma_forecast.endsWith("null")) return kma_forecast;
60
- }
61
-
62
- return getOpenWeatherMapConditionCode(
63
- openWeatherMap.now?.weather?.[0]?.icon,
64
- );
65
- }, [
66
- isKoreaProvider,
67
- korea.forecast?.today?.condition,
68
- korea.now?.today?.condition,
69
- openWeatherMap.now?.weather,
70
- ]);
71
-
72
- const weatherStateName = useMemo(() => {
73
- if (isKoreaProvider) {
74
- const kma_now = korea.now?.today?.conditionName;
75
- if (kma_now && !kma_now.endsWith("null"))
76
- return kma_now || texts.conditionFallbackLabel;
77
-
78
- const kma_forecast = korea.forecast?.today?.conditionName;
79
- if (kma_forecast && !kma_forecast.endsWith("null"))
80
- return kma_forecast || texts.conditionFallbackLabel;
81
- }
82
-
83
- return (
84
- openWeatherMap.now?.weather?.[0]?.description ||
85
- texts.conditionFallbackLabel
86
- );
87
- }, [
88
- isKoreaProvider,
89
- korea.forecast?.today?.conditionName,
90
- korea.now?.today?.conditionName,
91
- openWeatherMap.now?.weather,
92
- texts.conditionFallbackLabel,
93
- ]);
94
-
95
- const temperature = useMemo(() => {
96
- if (isKoreaProvider) {
97
- const kma_now = korea.now?.today?.temperature;
98
- if (kma_now && !Number.isNaN(Number(kma_now))) return Number(kma_now);
99
-
100
- const kma_forecast = korea.forecast?.today?.temperature;
101
- if (kma_forecast && !Number.isNaN(Number(kma_forecast)))
102
- return Number(kma_forecast);
103
- }
104
-
105
- const alt_now = openWeatherMap.now?.main?.temp;
106
- return typeof alt_now === "number" ? Math.round(alt_now) : null;
107
- }, [
108
- isKoreaProvider,
109
- korea.forecast?.today?.temperature,
110
- korea.now?.today?.temperature,
111
- openWeatherMap.now?.main?.temp,
112
- ]);
113
-
114
- const humidity = useMemo(() => {
115
- if (isKoreaProvider) {
116
- const kma_now = korea.now?.today?.humidity;
117
- if (kma_now && !Number.isNaN(Number(kma_now))) return Number(kma_now);
118
-
119
- const kma_forecast = korea.forecast?.today?.humidity;
120
- if (kma_forecast && !Number.isNaN(Number(kma_forecast)))
121
- return Number(kma_forecast);
122
- }
123
-
124
- const alt_now = openWeatherMap.now?.main?.humidity;
125
- return typeof alt_now === "number" ? Math.round(alt_now) : null;
126
- }, [
127
- isKoreaProvider,
128
- korea.forecast?.today?.humidity,
129
- korea.now?.today?.humidity,
130
- openWeatherMap.now?.main?.humidity,
131
- ]);
132
-
133
- const maxTemperature = useMemo(() => {
134
- if (isKoreaProvider) {
135
- const kma_forecast = korea.forecast?.today?.max_temperature;
136
- if (
137
- kma_forecast !== null &&
138
- kma_forecast !== undefined &&
139
- !Number.isNaN(Number(kma_forecast))
140
- )
141
- return Number(kma_forecast);
142
- }
68
+ const iconCode = useMemo(
69
+ () => now?.today.condition || forecast?.today.condition || "sky-1",
70
+ [forecast?.today.condition, now?.today.condition],
71
+ );
143
72
 
144
- const alt_forecast = openWeatherMapToday?.max_temperature;
145
- if (
146
- alt_forecast !== null &&
147
- alt_forecast !== undefined &&
148
- !Number.isNaN(Number(alt_forecast))
149
- )
150
- return Number(alt_forecast);
73
+ const weatherStateName = useMemo(
74
+ () =>
75
+ now?.today.conditionName ||
76
+ forecast?.today.conditionName ||
77
+ texts.conditionFallbackLabel,
78
+ [
79
+ forecast?.today.conditionName,
80
+ now?.today.conditionName,
81
+ texts.conditionFallbackLabel,
82
+ ],
83
+ );
151
84
 
152
- return null;
153
- }, [
154
- isKoreaProvider,
155
- korea.forecast?.today?.max_temperature,
156
- openWeatherMapToday?.max_temperature,
157
- ]);
85
+ const temperature = useMemo(
86
+ () =>
87
+ toWeatherNumber(now?.today.temperature) ??
88
+ toWeatherNumber(forecast?.today.temperature),
89
+ [forecast?.today.temperature, now?.today.temperature],
90
+ );
158
91
 
159
- const minTemperature = useMemo(() => {
160
- if (isKoreaProvider) {
161
- const kma_forecast = korea.forecast?.today?.min_temperature;
162
- if (
163
- kma_forecast !== null &&
164
- kma_forecast !== undefined &&
165
- !Number.isNaN(Number(kma_forecast))
166
- )
167
- return Number(kma_forecast);
168
- }
92
+ const humidity = useMemo(
93
+ () =>
94
+ toWeatherNumber(now?.today.humidity) ??
95
+ toWeatherNumber(forecast?.today.humidity),
96
+ [forecast?.today.humidity, now?.today.humidity],
97
+ );
169
98
 
170
- const alt_forecast = openWeatherMapToday?.min_temperature;
171
- if (
172
- alt_forecast !== null &&
173
- alt_forecast !== undefined &&
174
- !Number.isNaN(Number(alt_forecast))
175
- )
176
- return Number(alt_forecast);
99
+ const maxTemperature = useMemo(
100
+ () => toWeatherNumber(forecast?.today.max_temperature),
101
+ [forecast?.today.max_temperature],
102
+ );
177
103
 
178
- return null;
179
- }, [
180
- isKoreaProvider,
181
- korea.forecast?.today?.min_temperature,
182
- openWeatherMapToday?.min_temperature,
183
- ]);
104
+ const minTemperature = useMemo(
105
+ () => toWeatherNumber(forecast?.today.min_temperature),
106
+ [forecast?.today.min_temperature],
107
+ );
184
108
  const alert = (
185
- <WeatherPageHeaderAlert locale={locale} texts={textOverrides} />
109
+ <WeatherPageHeaderAlert
110
+ locale={locale}
111
+ texts={textOverrides}
112
+ weather={currentWeather}
113
+ />
186
114
  );
187
115
 
188
116
  return (
189
117
  <div className="weather-today-container">
190
- {isFetchingAlternate ? (
118
+ {currentWeather.isFetchingNow ? (
191
119
  <>
192
120
  {alert}
193
121
  <Alternate.LoadingDefault direction="horizontal">
@@ -6,6 +6,11 @@ import WeatherPageHeaderNextDays from "./NextDays";
6
6
  import WeatherPageHeaderToday from "./Today";
7
7
 
8
8
  // 기존 Container 호출 계약을 유지하면서, 서비스 재조합용 하위 조각 접근도 함께 연다.
9
+ /**
10
+ * Weather Page Header; 조합형 page-header namespace 컴포넌트.
11
+ * @component
12
+ * @desc 기존 container 호출 계약을 유지하고 서비스 재조합용 하위 조각을 정적 속성으로 노출한다.
13
+ */
9
14
  export const WeatherPageHeader = Object.assign(WeatherPageHeaderContainer, {
10
15
  Container: WeatherPageHeaderContainer,
11
16
  Address: WeatherPageHeaderAddress,
@@ -1,24 +1,6 @@
1
- import type { API_Res_WeatherKoreaBase } from "../types";
1
+ import type { API_Res_WeatherBase } from "../types";
2
2
 
3
- const API_RES_RAW = {
4
- response: {
5
- header: {
6
- resultCode: "",
7
- resultMsg: "",
8
- },
9
- body: {
10
- dataType: "",
11
- items: {
12
- item: [],
13
- },
14
- pageNo: 0,
15
- numOfRows: 0,
16
- totalCount: 0,
17
- },
18
- },
19
- };
20
-
21
- const API_RES_BASE: API_Res_WeatherKoreaBase = {
3
+ const API_RES_BASE: API_Res_WeatherBase = {
22
4
  sky: null,
23
5
  drop: null,
24
6
  rainAmount: null,
@@ -28,9 +10,8 @@ const API_RES_BASE: API_Res_WeatherKoreaBase = {
28
10
  conditionName: null,
29
11
  };
30
12
 
31
- const WEATHER_KOREA_RESPONSE = {
32
- API_RES_RAW,
13
+ const WEATHER_RESPONSE = {
33
14
  API_RES_BASE,
34
15
  };
35
16
 
36
- export default WEATHER_KOREA_RESPONSE;
17
+ export default WEATHER_RESPONSE;
@@ -1,4 +1,4 @@
1
- import useOpenWeatherMap from "./useOpenWeatherMap";
2
- import useWeatherKorea from "./useWeatherKorea";
1
+ import useWeather from "./useWeather";
2
+ import useWeatherAlert from "./useWeatherAlert";
3
3
 
4
- export { useWeatherKorea, useOpenWeatherMap };
4
+ export { useWeather, useWeatherAlert };
@@ -0,0 +1,52 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { useAtomValue } from "jotai";
5
+ import { useQueryWeatherSummary } from "../apis/client";
6
+ import { weatherCoordinate, weatherFarmIdx } from "../jotai";
7
+ import type {
8
+ API_Req_Weather,
9
+ UseWeatherOptions,
10
+ UseWeatherReturn,
11
+ } from "../types";
12
+
13
+ /**
14
+ * Weather Hook; 통합 weather summary API를 조회한다.
15
+ * @hook
16
+ * @desc weatherCoordinate/weatherFarmIdx atom 값을 통합 summary route 요청 파라미터로 변환해 조회한다.
17
+ * @param {UseWeatherOptions} [options] weather 조회 옵션
18
+ * @param {string} [options.locale] 요청 언어 코드
19
+ * @param {boolean} [options.enabled] query 실행 여부
20
+ * @return {UseWeatherReturn} 통합 weather query 결과
21
+ */
22
+ export default function useWeather({
23
+ locale,
24
+ enabled = true,
25
+ }: UseWeatherOptions = {}): UseWeatherReturn {
26
+ const coordinate = useAtomValue(weatherCoordinate);
27
+ const farm_idx = useAtomValue(weatherFarmIdx);
28
+ const params = useMemo<API_Req_Weather>(
29
+ () => ({
30
+ lat: coordinate.lat,
31
+ lng: coordinate.lng,
32
+ ...(farm_idx ? { farm_idx } : {}),
33
+ ...(locale ? { locale } : {}),
34
+ }),
35
+ [coordinate.lat, coordinate.lng, farm_idx, locale],
36
+ );
37
+ const { data, isFetching } = useQueryWeatherSummary(params, { enabled });
38
+
39
+ const alert = useMemo(() => data?.alert?.alerts || [], [data]);
40
+
41
+ return {
42
+ params,
43
+ coordinate,
44
+ farm_idx,
45
+ ...(data ? { summary: data, now: data.now, forecast: data.forecast } : {}),
46
+ alert,
47
+ isFetching,
48
+ isFetchingNow: isFetching,
49
+ isFetchingForecast: isFetching,
50
+ isFetchingAlert: isFetching,
51
+ };
52
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { useAtomValue } from "jotai";
5
+ import { useQueryWeatherAlert } from "../apis/client";
6
+ import { weatherFarmIdx } from "../jotai";
7
+ import type { UseWeatherAlertOptions, UseWeatherAlertReturn } from "../types";
8
+
9
+ /**
10
+ * Weather Hook; 통합 weather alert API를 조회한다.
11
+ * @hook
12
+ * @desc weatherFarmIdx atom 값을 통합 alert route 요청 파라미터로 변환해 조회한다.
13
+ * @param {UseWeatherAlertOptions} [options] 특보 조회 옵션
14
+ * @param {boolean} [options.enabled] query 실행 여부
15
+ * @return {UseWeatherAlertReturn} 특보 query 결과
16
+ */
17
+ export default function useWeatherAlert({
18
+ enabled = true,
19
+ }: UseWeatherAlertOptions = {}): UseWeatherAlertReturn {
20
+ const farm_idx = useAtomValue(weatherFarmIdx);
21
+ const { data, isFetching } = useQueryWeatherAlert(
22
+ {
23
+ farm_idx,
24
+ },
25
+ { enabled },
26
+ );
27
+
28
+ const alert = useMemo(() => data?.alerts || [], [data]);
29
+
30
+ return {
31
+ farm_idx,
32
+ alert,
33
+ isFetching,
34
+ };
35
+ }
@@ -2,12 +2,12 @@
2
2
  import "./index.scss";
3
3
 
4
4
  /**
5
- * Weather; page-header 템플릿과 api/hook/jotai 도구를 함께 제공하는 엔트리
5
+ * Weather; page-header 템플릿과 normalized API/hook/jotai 도구를 제공하는 엔트리
6
6
  */
7
7
  export * from "./utils";
8
8
  export * from "./apis";
9
- export * from "./jotai";
10
9
  export * from "./hooks";
10
+ export * from "./jotai";
11
11
  export * from "./types";
12
12
  export { default as WeatherComponents } from "./components";
13
13
  export { WeatherPageHeader } from "./components";
@@ -3,6 +3,10 @@
3
3
  import type { WeatherCoordinate } from "../types";
4
4
  import { atomWithReset } from "jotai/utils";
5
5
 
6
+ /**
7
+ * Weather Jotai; 현재 지역 좌표 atom.
8
+ * @desc service app에서 주입한 주소와 위경도를 weather template 전역 상태로 보관한다.
9
+ */
6
10
  export const weatherCoordinate = atomWithReset<WeatherCoordinate>({
7
11
  address: null,
8
12
  lat: null,
@@ -2,4 +2,8 @@
2
2
 
3
3
  import { atomWithReset } from "jotai/utils";
4
4
 
5
+ /**
6
+ * Weather Jotai; 현재 농장 식별자 atom.
7
+ * @desc weather alert 조회에 사용할 farm_idx를 weather template 전역 상태로 보관한다.
8
+ */
5
9
  export const weatherFarmIdx = atomWithReset<number | null>(null);