@uniai-fe/uds-templates 0.5.28 → 0.6.0

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 (56) hide show
  1. package/README.md +3 -6
  2. package/dist/styles.css +1 -1
  3. package/package.json +1 -1
  4. package/src/cctv/styles/variables.scss +1 -1
  5. package/src/weather/_legacy/apis/index.ts +4 -0
  6. package/src/weather/_legacy/data/response.ts +36 -0
  7. package/src/weather/_legacy/hooks/index.ts +5 -0
  8. package/src/weather/{hooks → _legacy/hooks}/useOpenWeatherMap.ts +1 -1
  9. package/src/weather/{hooks → _legacy/hooks}/useWeatherKorea.ts +4 -7
  10. package/src/weather/{hooks → _legacy/hooks}/useWeatherKoreaAlert.ts +2 -4
  11. package/src/weather/_legacy/types/api.ts +221 -0
  12. package/src/weather/_legacy/types/base.ts +70 -0
  13. package/src/weather/_legacy/types/index.ts +4 -0
  14. package/src/weather/_legacy/utils/index.ts +5 -0
  15. package/src/weather/_legacy/utils/locale.ts +28 -0
  16. package/src/weather/_legacy/utils/location.ts +139 -0
  17. package/src/weather/_legacy/utils/weather.ts +460 -0
  18. package/src/weather/apis/client.ts +339 -0
  19. package/src/weather/apis/index.ts +2 -4
  20. package/src/weather/apis/server.ts +264 -0
  21. package/src/weather/components/icon/Address.tsx +7 -0
  22. package/src/weather/components/icon/Weather.tsx +7 -6
  23. package/src/weather/components/page-header/Address.tsx +14 -0
  24. package/src/weather/components/page-header/Alert.tsx +17 -13
  25. package/src/weather/components/page-header/Container.tsx +12 -19
  26. package/src/weather/components/page-header/Forecast.tsx +21 -28
  27. package/src/weather/components/page-header/NextDays.tsx +10 -0
  28. package/src/weather/components/page-header/Today.tsx +86 -158
  29. package/src/weather/components/page-header/index.ts +5 -0
  30. package/src/weather/data/response.ts +4 -23
  31. package/src/weather/hooks/index.ts +3 -3
  32. package/src/weather/hooks/useWeather.ts +52 -0
  33. package/src/weather/hooks/useWeatherAlert.ts +35 -0
  34. package/src/weather/index.tsx +2 -2
  35. package/src/weather/jotai/coordinate.ts +4 -0
  36. package/src/weather/jotai/farm-idx.ts +4 -0
  37. package/src/weather/types/api.ts +393 -114
  38. package/src/weather/types/base.ts +15 -32
  39. package/src/weather/types/index.ts +0 -3
  40. package/src/weather/types/page-header.ts +118 -68
  41. package/src/weather/utils/index.ts +6 -4
  42. package/src/weather/utils/locale.ts +7 -69
  43. package/src/weather/utils/location.ts +6 -141
  44. package/src/weather/utils/weather.ts +53 -456
  45. package/src/weather/data/alert-regions-meta.json +0 -1286
  46. package/src/weather/data/weather-regions-meta.json +0 -9833
  47. package/src/weather/types/provider.ts +0 -34
  48. package/src/weather/utils/alert.ts +0 -30
  49. /package/src/weather/{apis → _legacy/apis}/korea/client.ts +0 -0
  50. /package/src/weather/{apis → _legacy/apis}/korea/server.ts +0 -0
  51. /package/src/weather/{apis → _legacy/apis}/open-weather-map/client.ts +0 -0
  52. /package/src/weather/{apis → _legacy/apis}/open-weather-map/server.ts +0 -0
  53. /package/src/weather/{types → _legacy/types}/korea.ts +0 -0
  54. /package/src/weather/{types → _legacy/types}/open-weather-map.ts +0 -0
  55. /package/src/weather/{utils → _legacy/utils}/date-time.ts +0 -0
  56. /package/src/weather/{utils → _legacy/utils}/validate.ts +0 -0
@@ -0,0 +1,460 @@
1
+ import type {
2
+ API_Res_WeatherKoreaBase,
3
+ API_Res_WeatherKoreaForecast,
4
+ API_Res_WeatherKoreaNextDays,
5
+ API_Res_WeatherKoreaNow,
6
+ KMA_Res_WeatherItem,
7
+ KMA_Res_WeatherForecast,
8
+ KMA_Res_WeatherNow,
9
+ OWM_Res_Weather_Forecast,
10
+ OWM_Res_Weather_Forecast_Item,
11
+ } from "../types";
12
+ import { getMoments } from "./date-time";
13
+
14
+ /**
15
+ * 날씨 도구; 현재 날씨/예보 값 추출
16
+ * @param {KMA_Res_WeatherItem[]} items - 현재 날씨 아이템
17
+ * @param {string} category - 카테고리
18
+ * @return {string | null} 현재 날씨 값
19
+ */
20
+ const getValueNow = (
21
+ items: KMA_Res_WeatherItem[],
22
+ category: string,
23
+ ): string | null => items.find(i => i.category === category)?.fcstValue || null;
24
+
25
+ /**
26
+ * 날씨 도구; 예보 값 추출
27
+ * @param {KMA_Res_WeatherItem[]} items - 예보 아이템
28
+ * @param {string} date - 날짜
29
+ * @param {string} category - 카테고리
30
+ * @return {string | null} 예보 값
31
+ */
32
+ const getValueForecast = (
33
+ items: KMA_Res_WeatherItem[],
34
+ date: string,
35
+ category: string,
36
+ ): string | null =>
37
+ items.find(i => i.fcstDate === date && i.category === category)?.fcstValue ||
38
+ null;
39
+
40
+ /**
41
+ * 강수량 코드
42
+ * @desc
43
+ * - 1(약한 비); 3mm/h 미만
44
+ * - 2(보통 비); 3mm/h 이상 15mm/h 미만
45
+ * - 3(강한 비); 15mm/h 이상
46
+ * @param {string} props.pty - 강수형태 코드
47
+ * @param {string} props.pcp - 1시간 강수량
48
+ * @return {string | null} 강수량 코드; 1(약한비), 2(보통비), 3(강한비)
49
+ */
50
+ const getRainAmount = ({
51
+ pty,
52
+ pcp,
53
+ }: {
54
+ pty: string | null;
55
+ pcp: string | null;
56
+ }): string | null => {
57
+ if (!pty || !pcp) return null;
58
+
59
+ if (Number(pcp) < 3) return "1";
60
+ else if (3 <= Number(pcp) && Number(pcp) < 15) return "2";
61
+ else return "3";
62
+ };
63
+
64
+ /**
65
+ * 강설량 코드
66
+ * @desc
67
+ * - 1(보통 눈); 1cm/h 미만
68
+ * - 2(많은 눈); 1cm/h 이상
69
+ * @param {string} props.pty - 강수형태 코드
70
+ * @param {string} props.snow - 1시간 신적설
71
+ * @return {string | null} 강설량 코드; 1(보통눈), 2(많은눈)
72
+ */
73
+ const getSnowAmount = ({
74
+ pty,
75
+ snow,
76
+ }: {
77
+ pty: string | null;
78
+ snow: string | null;
79
+ }): string | null => {
80
+ if (!pty || !snow) return null;
81
+
82
+ if (Number(snow) < 1) return "1";
83
+ else return "2";
84
+ };
85
+
86
+ /**
87
+ * 풍속 코드
88
+ * @desc
89
+ * - 1(약한 바람); 4m/s 미만
90
+ * - 2(약간 강한 바람); 4m/s 이상 9m/s 미만
91
+ * - 3(강한 바람); 9m/s 이상
92
+ * @param {string} props.wind - 풍속
93
+ * @return {string | null} 풍속 코드; 1(약한바람), 2(약간강한바람), 3(강한바람)
94
+ */
95
+ const getWindSpeed = ({ wind }: { wind: string | null }): string | null => {
96
+ if (!wind) return null;
97
+ if (Number(wind) < 4) return "1";
98
+ else if (4 <= Number(wind) && Number(wind) < 9) return "2";
99
+ else return "3";
100
+ };
101
+
102
+ /**
103
+ * 날씨 상태 코드
104
+ * @desc
105
+ * - sky-1(맑음)
106
+ * - sky-2(구름조금)
107
+ * - sky-3(구름많음)
108
+ * - sky-4(흐림)
109
+ * - drop-rain-shower(소나기)
110
+ * - drop-rain(비)
111
+ * - drop-rain-1(약한 비)
112
+ * - drop-rain-2(보통 비)
113
+ * - drop-rain-3(강한 비)
114
+ * - drop-rain-snow(비/눈)
115
+ * - drop-rain-snow-1(약한 비/눈)
116
+ * - drop-rain-snow-2(보통 비/눈)
117
+ * - drop-rain-snow-3(강한 비/눈)
118
+ * - drop-snow(눈)
119
+ * - drop-snow-1(보통 눈)
120
+ * - drop-snow-2(많은 눈)
121
+ * @param {string} props.sky - 하늘상태 코드; 1(맑음), 2(구름조금), 3(구름많음), 4(흐림)
122
+ * @param {string} props.pty - 강수형태 코드; 0(없음), 1(비), 2(비/눈), 3(눈), 4(소나기)
123
+ * @param {string} props.pcp - 1시간 강수량
124
+ * @param {string} props.snow - 1시간 신적설
125
+ */
126
+ const getWeatherCondition = ({
127
+ sky,
128
+ pty,
129
+ pcp,
130
+ snow,
131
+ }: {
132
+ sky: string | null;
133
+ pty: string | null;
134
+ pcp: string | null;
135
+ snow: string | null;
136
+ }): { code: string | null; name: string | null } => {
137
+ const dropType = () => {
138
+ if (!pty || isNaN(Number(pty)) || pty === "0") return null;
139
+ switch (Number(pty)) {
140
+ case 1:
141
+ return "rain";
142
+ case 2:
143
+ return "rain-snow";
144
+ case 3:
145
+ return "snow";
146
+ case 4:
147
+ return "shower";
148
+ default:
149
+ return null;
150
+ }
151
+ };
152
+
153
+ if (!dropType()) {
154
+ const skyCode = sky === null ? null : sky;
155
+ const skyName = () => {
156
+ switch (Number(skyCode)) {
157
+ case 1:
158
+ return "맑음";
159
+ case 2:
160
+ return "구름조금";
161
+ case 3:
162
+ return "구름많음";
163
+ case 4:
164
+ return "흐림";
165
+ default:
166
+ return null;
167
+ }
168
+ };
169
+
170
+ return {
171
+ code: `sky-${skyCode}`,
172
+ name: skyName(),
173
+ };
174
+ }
175
+
176
+ const rainAmount = getRainAmount({ pty, pcp });
177
+ const snowAmount = getSnowAmount({ pty, snow });
178
+
179
+ const dropStrength = (): string | null => {
180
+ const type = dropType();
181
+ if (type === "rain") return rainAmount;
182
+ else if (type === "snow") return snowAmount;
183
+ else if (type === "rain-snow") {
184
+ if (rainAmount === null) return snowAmount;
185
+ if (snowAmount === null) return rainAmount;
186
+ // rainAmount와 snowAmount 중 큰 값을 선택
187
+ return Math.max(Number(rainAmount), Number(snowAmount)).toString();
188
+ }
189
+ // 소나기
190
+ else if (type === "shower") return type;
191
+ return null;
192
+ };
193
+
194
+ const dropCode = `drop-${dropType()}${dropStrength() ? `-${dropStrength()}` : ""}`;
195
+ const dropName = () => {
196
+ const type = dropType();
197
+ if (type === "rain") {
198
+ if (dropStrength() === null) return "비";
199
+ if (rainAmount === "1") return "약한 비";
200
+ else if (rainAmount === "2") return "보통 비";
201
+ else if (rainAmount === "3") return "강한 비";
202
+ } else if (type === "snow") {
203
+ if (dropStrength() === null) return "눈";
204
+ if (snowAmount === "1") return "보통 눈";
205
+ else if (snowAmount === "2") return "많은 눈";
206
+ } else if (type === "rain-snow") {
207
+ if (dropStrength() === null) return "비/눈";
208
+ if (dropStrength() === "1") return "약한 비/눈";
209
+ else if (dropStrength() === "2") return "보통 비/눈";
210
+ else if (dropStrength() === "3") return "강한 비/눈";
211
+ } else if (type === "shower") {
212
+ return "소나기";
213
+ }
214
+ return null;
215
+ };
216
+
217
+ return {
218
+ code: dropCode,
219
+ name: dropName(),
220
+ };
221
+ };
222
+
223
+ /**
224
+ * 날씨 도구; OpenWeatherMap 아이콘 코드를 weather icon 코드로 변환
225
+ * @param {string} [icon] OpenWeatherMap icon 코드
226
+ * @return {string} weather icon 코드
227
+ */
228
+ export const getOpenWeatherMapConditionCode = (icon?: string): string => {
229
+ switch (icon) {
230
+ case "01d":
231
+ case "01n":
232
+ return "sky-1";
233
+ case "02d":
234
+ case "02n":
235
+ case "03d":
236
+ case "03n":
237
+ case "04d":
238
+ case "04n":
239
+ return "sky-2";
240
+ case "09d":
241
+ case "09n":
242
+ return "drop-rain-3";
243
+ case "10d":
244
+ case "10n":
245
+ return "drop-rain-1";
246
+ case "11d":
247
+ case "11n":
248
+ return "drop-rain-thunder";
249
+ case "13d":
250
+ case "13n":
251
+ return "drop-snow";
252
+ case "50d":
253
+ case "50n":
254
+ return "sky-4";
255
+ default:
256
+ return "sky-1";
257
+ }
258
+ };
259
+
260
+ const getLocalDateKey = (date: Date): string => {
261
+ const month = String(date.getMonth() + 1).padStart(2, "0");
262
+ const day = String(date.getDate()).padStart(2, "0");
263
+
264
+ return `${date.getFullYear()}-${month}-${day}`;
265
+ };
266
+
267
+ const getTimeZoneDateKey = (dateTimeSeconds: number, timezoneSeconds: number) =>
268
+ new Date((dateTimeSeconds + timezoneSeconds) * 1000)
269
+ .toISOString()
270
+ .slice(0, 10);
271
+
272
+ const getOpenWeatherMapDateKey = (
273
+ forecast?: OWM_Res_Weather_Forecast,
274
+ ): string => {
275
+ const timezoneSeconds = forecast?.city?.timezone;
276
+
277
+ if (typeof timezoneSeconds === "number" && Number.isFinite(timezoneSeconds)) {
278
+ return getTimeZoneDateKey(Math.floor(Date.now() / 1000), timezoneSeconds);
279
+ }
280
+
281
+ return getLocalDateKey(new Date());
282
+ };
283
+
284
+ const getForecastItemDateKey = (
285
+ item: OWM_Res_Weather_Forecast_Item,
286
+ timezoneSeconds = 0,
287
+ ): string =>
288
+ Number.isFinite(item.dt)
289
+ ? getTimeZoneDateKey(item.dt, timezoneSeconds)
290
+ : item.dt_txt?.slice(0, 10) || getLocalDateKey(new Date(item.dt * 1000));
291
+
292
+ const toOpenWeatherMapDayForecast = (
293
+ items: OWM_Res_Weather_Forecast_Item[],
294
+ ): API_Res_WeatherKoreaNextDays | undefined => {
295
+ if (items.length === 0) return undefined;
296
+
297
+ const conditionItem =
298
+ items.find(item => item.dt_txt?.includes("12:00")) ||
299
+ items[Math.floor(items.length / 2)];
300
+ const temperatures = items
301
+ .flatMap(item => [item.main.temp_min, item.main.temp_max, item.main.temp])
302
+ .filter((temperature): temperature is number =>
303
+ Number.isFinite(temperature),
304
+ );
305
+
306
+ return {
307
+ sky: null,
308
+ drop: null,
309
+ rainAmount: null,
310
+ snowAmount: null,
311
+ windSpeed: null,
312
+ condition: getOpenWeatherMapConditionCode(
313
+ conditionItem?.weather?.[0]?.icon,
314
+ ),
315
+ conditionName: conditionItem?.weather?.[0]?.description || null,
316
+ max_temperature:
317
+ temperatures.length > 0 ? Math.round(Math.max(...temperatures)) : null,
318
+ min_temperature:
319
+ temperatures.length > 0 ? Math.round(Math.min(...temperatures)) : null,
320
+ };
321
+ };
322
+
323
+ /**
324
+ * 날씨 도구; OpenWeatherMap 예보 응답을 page-header next days 형태로 변환
325
+ * @param {OWM_Res_Weather_Forecast} [forecast] OpenWeatherMap 예보 응답
326
+ * @return {{ day_1?: API_Res_WeatherKoreaNextDays; day_2?: API_Res_WeatherKoreaNextDays }} 내일/모레 예보
327
+ */
328
+ export const getOpenWeatherMapNextDays = (
329
+ forecast?: OWM_Res_Weather_Forecast,
330
+ ): {
331
+ day_1?: API_Res_WeatherKoreaNextDays;
332
+ day_2?: API_Res_WeatherKoreaNextDays;
333
+ } => {
334
+ if (!Array.isArray(forecast?.list)) return {};
335
+
336
+ const timezoneSeconds = forecast.city?.timezone ?? 0;
337
+ const todayKey = getOpenWeatherMapDateKey(forecast);
338
+ const grouped = forecast.list.reduce<
339
+ Record<string, OWM_Res_Weather_Forecast_Item[]>
340
+ >((days, item) => {
341
+ const dateKey = getForecastItemDateKey(item, timezoneSeconds);
342
+ if (dateKey === todayKey) return days;
343
+
344
+ days[dateKey] = [...(days[dateKey] || []), item];
345
+ return days;
346
+ }, {});
347
+ const [day1Key, day2Key] = Object.keys(grouped).sort();
348
+
349
+ return {
350
+ day_1: day1Key ? toOpenWeatherMapDayForecast(grouped[day1Key]) : undefined,
351
+ day_2: day2Key ? toOpenWeatherMapDayForecast(grouped[day2Key]) : undefined,
352
+ };
353
+ };
354
+
355
+ /**
356
+ * 날씨 도구; OpenWeatherMap 예보 응답을 오늘 최고/최저 형태로 변환
357
+ * @param {OWM_Res_Weather_Forecast} [forecast] OpenWeatherMap 예보 응답
358
+ * @return {API_Res_WeatherKoreaNextDays | undefined} 오늘 최고/최저 예보
359
+ */
360
+ export const getOpenWeatherMapTodayForecast = (
361
+ forecast?: OWM_Res_Weather_Forecast,
362
+ ): API_Res_WeatherKoreaNextDays | undefined => {
363
+ if (!Array.isArray(forecast?.list)) return undefined;
364
+
365
+ const timezoneSeconds = forecast.city?.timezone ?? 0;
366
+ const todayKey = getOpenWeatherMapDateKey(forecast);
367
+ const todayItems = forecast.list.filter(
368
+ item => getForecastItemDateKey(item, timezoneSeconds) === todayKey,
369
+ );
370
+
371
+ return toOpenWeatherMapDayForecast(todayItems);
372
+ };
373
+
374
+ export function now(
375
+ outputResponse: API_Res_WeatherKoreaNow,
376
+ apiResponse: KMA_Res_WeatherNow,
377
+ ): API_Res_WeatherKoreaNow {
378
+ const res: API_Res_WeatherKoreaNow = { ...outputResponse };
379
+
380
+ const items = apiResponse?.response?.body?.items?.item;
381
+ if (!items || !Array.isArray(items) || items.length === 0) return res;
382
+
383
+ const sky = getValueNow(items, "SKY"); // 없음
384
+ const pty = getValueNow(items, "PTY"); // 강수형태
385
+ const pcp = getValueNow(items, "RN1"); // 1시간 강수량 PCP없음
386
+ const snow = getValueNow(items, "SNO"); // 없음
387
+ const wind = getValueNow(items, "WSD");
388
+
389
+ const condition = getWeatherCondition({ sky, pty, pcp, snow });
390
+
391
+ const getDailyState: API_Res_WeatherKoreaBase = {
392
+ sky,
393
+ drop: pty,
394
+ rainAmount: getRainAmount({ pty, pcp }),
395
+ snowAmount: getSnowAmount({ pty, snow }),
396
+ windSpeed: getWindSpeed({ wind }),
397
+ condition: condition.code,
398
+ conditionName: condition.name,
399
+ };
400
+
401
+ res.today.temperature = getValueNow(items, "T1H");
402
+ res.today.humidity = getValueNow(items, "REH");
403
+
404
+ Object.assign(res.today, getDailyState);
405
+
406
+ return res;
407
+ }
408
+
409
+ export function forecast(
410
+ outputResponse: API_Res_WeatherKoreaForecast,
411
+ apiResponse: KMA_Res_WeatherForecast,
412
+ ): API_Res_WeatherKoreaForecast {
413
+ const res: API_Res_WeatherKoreaForecast = { ...outputResponse };
414
+
415
+ const items = apiResponse?.response?.body?.items?.item;
416
+ if (!items || !Array.isArray(items) || items.length === 0) return res;
417
+
418
+ const { today, tomorrow, dayAfterTomorrow } = getMoments();
419
+
420
+ const getDailyState = (date: string): API_Res_WeatherKoreaBase => {
421
+ const sky = getValueForecast(items, date, "SKY");
422
+ const pty = getValueForecast(items, date, "PTY");
423
+ const pcp = getValueForecast(items, date, "PCP");
424
+ const snow = getValueForecast(items, date, "SNO");
425
+ const wind = getValueForecast(items, date, "WSD");
426
+
427
+ const condition = getWeatherCondition({ sky, pty, pcp, snow });
428
+
429
+ return {
430
+ sky,
431
+ drop: pty,
432
+ rainAmount: getRainAmount({ pty, pcp }),
433
+ snowAmount: getSnowAmount({ pty, snow }),
434
+ windSpeed: getWindSpeed({ wind }),
435
+ condition: condition.code,
436
+ conditionName: condition.name,
437
+ };
438
+ };
439
+
440
+ res.today.temperature = getValueForecast(items, today, "TMP");
441
+ res.today.max_temperature = getValueForecast(items, today, "TMX");
442
+ res.today.min_temperature = getValueForecast(items, today, "TMN");
443
+ res.today.humidity = getValueForecast(items, today, "REH");
444
+ Object.assign(res.today, getDailyState(today));
445
+
446
+ res.day_1.max_temperature = getValueForecast(items, tomorrow, "TMX");
447
+ res.day_1.min_temperature = getValueForecast(items, tomorrow, "TMN");
448
+ Object.assign(res.day_1, getDailyState(tomorrow));
449
+
450
+ res.day_2.max_temperature = getValueForecast(items, dayAfterTomorrow, "TMX");
451
+ res.day_2.min_temperature = getValueForecast(items, dayAfterTomorrow, "TMN");
452
+ Object.assign(res.day_2, getDailyState(dayAfterTomorrow));
453
+
454
+ return res;
455
+ }
456
+
457
+ export const extractWeatherSummary = {
458
+ forecast,
459
+ now,
460
+ };