@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
package/README.md CHANGED
@@ -60,6 +60,12 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
60
60
  - `weatherCoordinate`
61
61
  - `useWeatherKorea`
62
62
  - `useOpenWeatherMap`
63
+ - `resolveWeatherProvider`
64
+ - `WEATHER_PROVIDER_CAPABILITIES`
65
+ - `WeatherPageHeaderContainerProps`
66
+ - `WeatherPageHeaderTexts`
67
+ - `WeatherProviderKey`
68
+ - `WeatherProviderCapability`
63
69
  - `/service-inquiry`
64
70
  - `ServiceInquiry.Form`
65
71
  - `ServiceInquiry.OpenButton`
@@ -130,6 +136,7 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
130
136
  - 로그인 후 `farm_name`, `contact`를 auto-fill + readonly로 보여야 할 때는 `formContextOptions.defaultValues`와 `farmNameField.mode`, `contactField.mode`를 함께 전달한다.
131
137
  - `/weather/**`
132
138
  - page-frame header utility에 결합되는 weather header 템플릿, page-header 조각 export, weather data hook/API 도구를 제공한다.
139
+ - page-header는 `locale?: string`을 받으며 기본 지원값은 `ko`, `en`, `ja`다. 기본 문구 외 특수 문구는 `texts` prop으로 주입한다.
133
140
  - `/cctv/**`
134
141
  - finder/viewer/video/pagination 조합과 rtc/company-list API helper를 제공한다.
135
142
  - `/page-frame/**`
package/dist/styles.css CHANGED
@@ -67,6 +67,30 @@
67
67
  var(--spacing-padding-9, 32px) + env(safe-area-inset-top, 0px)
68
68
  );
69
69
  --auth-container-padding-bottom: var(--spacing-padding-10, 40px);
70
+ --weather-page-header-height: 2.4rem;
71
+ --weather-page-header-gap: var(--spacing-gap-6);
72
+ --weather-page-header-background: var(--color-surface-static-white);
73
+ --weather-icon-width: 2.5rem;
74
+ --weather-icon-height: 2.4rem;
75
+ --weather-address-icon-size: 1.6rem;
76
+ --weather-item-gap: var(--spacing-gap-4);
77
+ --weather-address-gap: var(--spacing-gap-2);
78
+ --weather-body-font-size: var(--font-body-xxsmall-size);
79
+ --weather-body-line-height: var(--font-body-xxsmall-line-height);
80
+ --weather-body-letter-spacing: var(--font-body-xxsmall-letter-spacing);
81
+ --weather-label-font-weight: 500;
82
+ --weather-label-value-gap: var(--spacing-gap-2);
83
+ --weather-value-font-weight: 600;
84
+ --weather-unit-font-size: 0.8385rem;
85
+ --weather-text-color: var(--color-label-standard);
86
+ --weather-label-color: var(--color-label-neutral);
87
+ --weather-alert-text-color: var(--color-primary-standard);
88
+ --weather-alert-background: var(--color-surface-static-blue);
89
+ --weather-alert-height: 2rem;
90
+ --weather-alert-padding-horizontal: var(--spacing-padding-3);
91
+ --weather-alert-radius: 0.6rem;
92
+ --weather-alert-font-size: var(--font-caption-medium-size);
93
+ --weather-alert-font-weight: 400;
70
94
  /* Card layout */
71
95
  --cctv-video-radius: 12px;
72
96
  --cctv-list-gap: var(--spacing-gap-5);
@@ -1471,129 +1495,165 @@
1471
1495
  letter-spacing: 0px;
1472
1496
  }
1473
1497
 
1474
- .weather-base-container, .weather-next-days-container, .weather-today-container {
1475
- width: fit-content;
1498
+
1499
+
1500
+ .weather-page-header {
1476
1501
  display: flex;
1477
1502
  align-items: center;
1478
- }
1479
- .weather-base-container span, .weather-next-days-container span, .weather-today-container span {
1480
- font-size: 1.4rem;
1503
+ gap: var(--weather-page-header-gap);
1504
+ width: fit-content;
1505
+ height: var(--weather-page-header-height);
1506
+ background: var(--weather-page-header-background);
1507
+ white-space: nowrap;
1481
1508
  }
1482
1509
 
1483
1510
  .weather-base-icon {
1484
- width: 2.5rem;
1485
- height: 2.4rem;
1486
1511
  position: relative;
1512
+ width: var(--weather-icon-width);
1513
+ height: var(--weather-icon-height);
1514
+ flex: 0 0 auto;
1487
1515
  }
1488
1516
 
1489
- .weather-base-text-info, .weather-next-days-text {
1517
+ .weather-address {
1490
1518
  display: flex;
1491
1519
  align-items: center;
1520
+ gap: var(--weather-address-gap);
1521
+ width: fit-content;
1492
1522
  }
1493
- .weather-base-text-info dt, .weather-next-days-text dt {
1494
- margin-right: 0.8rem;
1495
- font-size: 0;
1496
- }
1497
- .weather-base-text-info dt span, .weather-next-days-text dt span {
1498
- color: var(--color-cool-gray-35);
1499
- }
1500
- .weather-base-text-info dd, .weather-next-days-text dd {
1501
- font-size: 0;
1523
+
1524
+ .weather-address-icon {
1525
+ position: relative;
1502
1526
  display: flex;
1503
- align-items: flex-start;
1527
+ align-items: center;
1528
+ justify-content: center;
1529
+ width: var(--weather-address-icon-size);
1530
+ height: var(--weather-address-icon-size);
1531
+ flex: 0 0 auto;
1504
1532
  }
1505
- .weather-base-text-info dd span, .weather-next-days-text dd span {
1506
- color: var(--color-cool-gray-10);
1507
- font-weight: 600;
1533
+
1534
+ .weather-address-text {
1535
+ display: flex;
1536
+ align-items: center;
1537
+ gap: var(--weather-address-gap);
1538
+ margin: 0;
1539
+ color: var(--weather-text-color);
1540
+ font-size: var(--weather-body-font-size);
1541
+ font-weight: var(--weather-label-font-weight);
1542
+ line-height: var(--weather-body-line-height);
1543
+ letter-spacing: var(--weather-body-letter-spacing);
1508
1544
  }
1509
1545
 
1510
- .weather-base-divider {
1511
- width: 1px;
1512
- height: 1.3rem;
1513
- margin: 0 0.8rem;
1514
- background: var(--color-cool-gray-85);
1546
+ .weather-address-date {
1547
+ color: var(--weather-label-color);
1515
1548
  }
1516
1549
 
1517
- .weather-address {
1550
+ .weather-today-container {
1518
1551
  display: flex;
1519
1552
  align-items: center;
1553
+ gap: var(--weather-item-gap);
1520
1554
  width: fit-content;
1521
1555
  }
1522
- .weather-address-icon {
1523
- width: 1.6rem;
1524
- height: 1.6rem;
1525
- margin-right: 0.4rem;
1526
- position: relative;
1527
- }
1528
- .weather-address-text {
1556
+
1557
+ .weather-today-text {
1558
+ display: flex;
1559
+ align-items: center;
1560
+ gap: var(--weather-item-gap);
1529
1561
  width: fit-content;
1530
- margin-right: 1rem;
1562
+ }
1563
+
1564
+ .weather-next-days-container {
1531
1565
  display: flex;
1532
1566
  align-items: center;
1567
+ gap: var(--weather-item-gap);
1568
+ width: fit-content;
1533
1569
  }
1534
- .weather-address-text span {
1535
- font-size: 1.6rem;
1536
- color: var(--color-cool-gray-10);
1537
- font-weight: 600;
1538
- white-space: nowrap;
1539
- transform: translateY(0.1rem);
1570
+
1571
+ .weather-temperature-text {
1572
+ margin: 0;
1573
+ color: var(--weather-text-color);
1574
+ font-size: 0;
1575
+ font-weight: var(--weather-label-font-weight);
1576
+ line-height: var(--weather-body-line-height);
1577
+ letter-spacing: var(--weather-body-letter-spacing);
1540
1578
  }
1541
1579
 
1542
- .weather-today-container span {
1543
- white-space: nowrap;
1580
+ .weather-humidity-text {
1581
+ margin: 0;
1582
+ color: var(--weather-text-color);
1583
+ font-size: 0;
1584
+ font-weight: var(--weather-label-font-weight);
1585
+ line-height: var(--weather-body-line-height);
1586
+ letter-spacing: var(--weather-body-letter-spacing);
1544
1587
  }
1545
- .weather-today-container .empty-text {
1546
- font-size: 1.4rem;
1547
- padding-right: 4rem;
1588
+
1589
+ .weather-forecast-text {
1590
+ margin: 0;
1591
+ color: var(--weather-text-color);
1592
+ font-size: 0;
1593
+ font-weight: var(--weather-label-font-weight);
1594
+ line-height: var(--weather-body-line-height);
1595
+ letter-spacing: var(--weather-body-letter-spacing);
1548
1596
  }
1549
1597
 
1550
- .weather-today-temperature {
1551
- margin-left: 1rem;
1552
- display: flex;
1553
- align-items: flex-start;
1598
+ .weather-label {
1599
+ color: var(--weather-label-color);
1600
+ font-size: var(--weather-body-font-size);
1601
+ font-weight: var(--weather-label-font-weight);
1602
+ margin-right: var(--weather-label-value-gap);
1554
1603
  }
1555
- .weather-today-temperature span {
1556
- font-size: 2rem;
1557
- font-weight: 700;
1558
- color: var(--color-blue-55);
1604
+
1605
+ .weather-value {
1606
+ color: var(--weather-text-color);
1607
+ font-size: var(--weather-body-font-size);
1608
+ font-weight: var(--weather-value-font-weight);
1559
1609
  }
1560
- .weather-today-temperature .unit {
1561
- font-size: 1.2rem;
1610
+
1611
+ .weather-unit {
1612
+ color: var(--weather-text-color);
1613
+ font-size: var(--weather-unit-font-size);
1614
+ font-weight: var(--weather-value-font-weight);
1562
1615
  }
1563
1616
 
1564
- .weather-today-humidity {
1565
- margin-left: 1.4rem;
1617
+ .weather-range {
1618
+ color: var(--weather-text-color);
1619
+ font-size: var(--weather-body-font-size);
1620
+ font-weight: var(--weather-value-font-weight);
1566
1621
  }
1567
1622
 
1568
1623
  .weather-alert {
1624
+ display: flex;
1625
+ align-items: center;
1626
+ justify-content: center;
1569
1627
  width: fit-content;
1570
- height: 2.6rem;
1571
- padding: 0 1rem;
1572
- margin-left: 1rem;
1573
- border-radius: 0.8rem;
1574
- background-color: var(--color-cool-gray-95, #f4f6f8);
1628
+ height: var(--weather-alert-height);
1629
+ padding: 0 var(--weather-alert-padding-horizontal);
1630
+ border-radius: var(--weather-alert-radius);
1631
+ background-color: var(--weather-alert-background);
1632
+ box-sizing: border-box;
1633
+ }
1634
+
1635
+ .weather-alert-loading {
1575
1636
  display: flex;
1576
1637
  align-items: center;
1638
+ width: fit-content;
1639
+ height: var(--weather-page-header-height);
1577
1640
  }
1641
+
1578
1642
  .weather-alert-text {
1579
- height: fit-content;
1580
- font-size: 0;
1581
- }
1582
- .weather-alert-text span {
1583
- font-size: 1.2rem;
1584
- color: var(--color-blue-55, #007cdb);
1585
- font-weight: 600;
1586
- line-height: 1.6;
1643
+ display: flex;
1644
+ align-items: center;
1645
+ color: var(--weather-alert-text-color);
1646
+ font-size: var(--weather-alert-font-size);
1647
+ font-weight: var(--weather-alert-font-weight);
1648
+ line-height: var(--weather-body-line-height);
1649
+ letter-spacing: var(--weather-body-letter-spacing);
1587
1650
  }
1588
1651
 
1589
- .weather-next-days-text dt {
1590
- margin-right: 0.8rem;
1591
- }
1592
- .weather-next-days-text dd:nth-of-type(n + 2) {
1593
- margin-left: 0.8rem;
1594
- }
1595
- .weather-next-days-text dd:nth-of-type(n + 2) .unit {
1596
- font-size: 1rem;
1652
+ .weather-alert-text span {
1653
+ color: inherit;
1654
+ font-size: inherit;
1655
+ font-weight: inherit;
1656
+ line-height: inherit;
1597
1657
  }
1598
1658
 
1599
1659
 
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.11",
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>