@uniai-fe/uds-templates 0.4.26 → 0.4.28
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 +7 -2
- package/package.json +1 -1
- package/src/weather/apis/korea/client.ts +1 -1
- package/src/weather/apis/korea/server.ts +5 -8
- package/src/weather/apis/open-weather-map/client.ts +10 -5
- package/src/weather/apis/open-weather-map/server.ts +11 -10
- package/src/weather/components/index.tsx +3 -2
- package/src/weather/components/page-header/index.ts +25 -0
- package/src/weather/hooks/useOpenWeatherMap.ts +2 -19
- package/src/weather/hooks/useWeatherKorea.ts +11 -18
- package/src/weather/hooks/useWeatherKoreaAlert.ts +3 -16
- package/src/weather/index.tsx +9 -2
- package/src/weather/jotai/coordinate.ts +0 -6
- package/src/weather/types/open-weather-map.ts +106 -4
- package/src/weather/utils/location.ts +4 -1
- package/src/weather/context/mock.tsx +0 -45
package/README.md
CHANGED
|
@@ -50,8 +50,13 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
|
|
|
50
50
|
- `UseModalReturn`
|
|
51
51
|
- `/weather`
|
|
52
52
|
- `WeatherComponents.PageHeader`
|
|
53
|
+
- `WeatherPageHeader`
|
|
53
54
|
- `WeatherPageHeaderContainer`
|
|
54
|
-
- `
|
|
55
|
+
- `WeatherPageHeaderAddress`
|
|
56
|
+
- `WeatherPageHeaderToday`
|
|
57
|
+
- `WeatherPageHeaderForecast`
|
|
58
|
+
- `WeatherPageHeaderAlert`
|
|
59
|
+
- `WeatherPageHeaderNextDays`
|
|
55
60
|
- `weatherCoordinate`
|
|
56
61
|
- `useWeatherKorea`
|
|
57
62
|
- `useOpenWeatherMap`
|
|
@@ -116,7 +121,7 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
|
|
|
116
121
|
- modal description은 기본으로 `"사용 중 문제가 있나요? 아래 내용을 적어 문의해 주세요."`를 사용하고 특수 문구가 필요할 때만 `dialogOptions.description`으로 덮어쓴다.
|
|
117
122
|
- 로그인 후 `farm_name`, `contact`를 auto-fill + readonly로 보여야 할 때는 `formContextOptions.defaultValues`와 `farmNameField.mode`, `contactField.mode`를 함께 전달한다.
|
|
118
123
|
- `/weather/**`
|
|
119
|
-
- page-frame header utility에 결합되는 weather header
|
|
124
|
+
- page-frame header utility에 결합되는 weather header 템플릿, page-header 조각 export, weather data hook/API 도구를 제공한다.
|
|
120
125
|
- `/cctv/**`
|
|
121
126
|
- finder/viewer/video/pagination 조합과 rtc/company-list API helper를 제공한다.
|
|
122
127
|
- `/page-frame/**`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
|
|
4
|
-
import { getQueryString } from "@uniai-fe/util-
|
|
4
|
+
import { getQueryString } from "@uniai-fe/util-api";
|
|
5
5
|
import type {
|
|
6
6
|
API_Req_WeatherKorea,
|
|
7
7
|
API_Req_WeatherKoreaAlert,
|
|
@@ -15,12 +15,9 @@ import WEATHER_KOREA_RESPONSE from "../../data/response";
|
|
|
15
15
|
import { getWeatherBaseMoments } from "../../utils/date-time";
|
|
16
16
|
import { extractWeatherSummary } from "../../utils";
|
|
17
17
|
import type { NextResponse } from "next/server";
|
|
18
|
-
import {
|
|
19
|
-
generateBackendQueryUrl_GET,
|
|
20
|
-
nextAPILog,
|
|
21
|
-
} from "@uniai-fe/util-functions";
|
|
18
|
+
import { generateQueryUrl, nextAPILog } from "@uniai-fe/util-api";
|
|
22
19
|
|
|
23
|
-
// const {
|
|
20
|
+
// const { generateQueryUrl } = weatherApi;
|
|
24
21
|
// const weatherLog = weatherApi.logger;
|
|
25
22
|
|
|
26
23
|
/**
|
|
@@ -106,7 +103,7 @@ export const routeWeatherKoreaNow = async ({
|
|
|
106
103
|
);
|
|
107
104
|
|
|
108
105
|
// 요청 URL 구성
|
|
109
|
-
const url =
|
|
106
|
+
const url = generateQueryUrl({
|
|
110
107
|
domain,
|
|
111
108
|
routeUrl,
|
|
112
109
|
queryUrl: QUERY_URL.now,
|
|
@@ -168,7 +165,7 @@ export const routeWeatherKoreaForecast = async ({
|
|
|
168
165
|
);
|
|
169
166
|
|
|
170
167
|
// 요청 URL 구성
|
|
171
|
-
const url =
|
|
168
|
+
const url = generateQueryUrl({
|
|
172
169
|
domain,
|
|
173
170
|
routeUrl,
|
|
174
171
|
queryUrl: QUERY_URL.forecast,
|
|
@@ -234,7 +231,7 @@ export const routeWeatherKoreaAlert = async ({
|
|
|
234
231
|
// fe: "f",
|
|
235
232
|
// tm: "",
|
|
236
233
|
});
|
|
237
|
-
const url =
|
|
234
|
+
const url = generateQueryUrl({
|
|
238
235
|
domain,
|
|
239
236
|
routeUrl,
|
|
240
237
|
queryUrl: QUERY_URL.alert,
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
|
|
4
|
-
import type {
|
|
5
|
-
|
|
4
|
+
import type {
|
|
5
|
+
OWM_Res_Weather_Forecast,
|
|
6
|
+
OWM_Res_Weather_Now,
|
|
7
|
+
WeatherGeoCoordinate,
|
|
8
|
+
} from "../../types";
|
|
9
|
+
import { getQueryString } from "@uniai-fe/util-api";
|
|
10
|
+
import { isValidNumber } from "@uniai-fe/util-functions";
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* 글로벌 날씨 API; 현재 날씨 fetch
|
|
@@ -15,7 +20,7 @@ import { getQueryString, isValidNumber } from "@uniai-fe/util-functions";
|
|
|
15
20
|
*/
|
|
16
21
|
export const getWeatherOpenWeatherMapNow = async (
|
|
17
22
|
params: WeatherGeoCoordinate,
|
|
18
|
-
) =>
|
|
23
|
+
): Promise<OWM_Res_Weather_Now> =>
|
|
19
24
|
await (
|
|
20
25
|
await fetch(`/api/weather/open-weather-map/now${getQueryString(params)}`)
|
|
21
26
|
).json();
|
|
@@ -31,7 +36,7 @@ export const getWeatherOpenWeatherMapNow = async (
|
|
|
31
36
|
*/
|
|
32
37
|
export const getWeatherOpenWeatherMapForecast = async (
|
|
33
38
|
params: WeatherGeoCoordinate,
|
|
34
|
-
) =>
|
|
39
|
+
): Promise<OWM_Res_Weather_Forecast> =>
|
|
35
40
|
await (
|
|
36
41
|
await fetch(
|
|
37
42
|
`/api/weather/open-weather-map/forecast${getQueryString(params)}`,
|
|
@@ -60,7 +65,7 @@ export const useQueryWeatherOpenWeatherMapNow = (
|
|
|
60
65
|
*/
|
|
61
66
|
export const useQueryWeatherOpenWeatherMapForecast = (
|
|
62
67
|
params: WeatherGeoCoordinate,
|
|
63
|
-
) =>
|
|
68
|
+
): UseQueryResult<OWM_Res_Weather_Forecast> =>
|
|
64
69
|
useQuery({
|
|
65
70
|
queryKey: ["weather_open_weather_map_forecast", ...Object.values(params)],
|
|
66
71
|
queryFn: () => getWeatherOpenWeatherMapForecast(params),
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { generateQueryUrl, nextAPILog } from "@uniai-fe/util-api";
|
|
4
|
+
import type {
|
|
5
|
+
OWM_Res_Weather_Forecast,
|
|
6
|
+
OWM_Res_Weather_Now,
|
|
7
|
+
} from "../../types";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* 글로벌 날씨 API; 현재 날씨 URL
|
|
@@ -32,8 +33,8 @@ export const routeOpenWeatherMapNow = async ({
|
|
|
32
33
|
authKey: string;
|
|
33
34
|
routeUrl: string;
|
|
34
35
|
searchParams: URLSearchParams;
|
|
35
|
-
}): Promise<
|
|
36
|
-
const resDefault = {};
|
|
36
|
+
}): Promise<OWM_Res_Weather_Now | Record<string, never>> => {
|
|
37
|
+
const resDefault: Record<string, never> = {};
|
|
37
38
|
|
|
38
39
|
if (!authKey) {
|
|
39
40
|
nextAPILog("GET", routeUrl, "Open Weather Map 현재날씨 API", {
|
|
@@ -54,7 +55,7 @@ export const routeOpenWeatherMapNow = async ({
|
|
|
54
55
|
return {};
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
const url =
|
|
58
|
+
const url = generateQueryUrl({
|
|
58
59
|
domain: API_BASE_CURRENT,
|
|
59
60
|
routeUrl,
|
|
60
61
|
queryUrl: "",
|
|
@@ -88,8 +89,8 @@ export const routeOpenWeatherMapForecast = async ({
|
|
|
88
89
|
authKey: string;
|
|
89
90
|
routeUrl: string;
|
|
90
91
|
searchParams: URLSearchParams;
|
|
91
|
-
}): Promise<
|
|
92
|
-
const resDefault = {};
|
|
92
|
+
}): Promise<OWM_Res_Weather_Forecast | Record<string, never>> => {
|
|
93
|
+
const resDefault: Record<string, never> = {};
|
|
93
94
|
|
|
94
95
|
if (!authKey) {
|
|
95
96
|
nextAPILog("GET", routeUrl, "Open Weather Map 예보날씨 API", {
|
|
@@ -110,7 +111,7 @@ export const routeOpenWeatherMapForecast = async ({
|
|
|
110
111
|
return {};
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
const url =
|
|
114
|
+
const url = generateQueryUrl({
|
|
114
115
|
domain: API_BASE_FORECAST,
|
|
115
116
|
routeUrl,
|
|
116
117
|
queryUrl: "",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { WeatherPageHeader } from "./page-header";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Weather Components; weather component namespace 집계
|
|
5
5
|
*/
|
|
6
6
|
const Weather = {
|
|
7
|
-
PageHeader:
|
|
7
|
+
PageHeader: WeatherPageHeader,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
export default Weather;
|
|
11
|
+
export { WeatherPageHeader };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import WeatherPageHeaderAddress from "./Address";
|
|
2
|
+
import WeatherPageHeaderAlert from "./Alert";
|
|
3
|
+
import WeatherPageHeaderContainer from "./Container";
|
|
4
|
+
import WeatherPageHeaderForecast from "./Forecast";
|
|
5
|
+
import WeatherPageHeaderNextDays from "./NextDays";
|
|
6
|
+
import WeatherPageHeaderToday from "./Today";
|
|
7
|
+
|
|
8
|
+
// 기존 Container 호출 계약을 유지하면서, 서비스 재조합용 하위 조각 접근도 함께 연다.
|
|
9
|
+
export const WeatherPageHeader = Object.assign(WeatherPageHeaderContainer, {
|
|
10
|
+
Container: WeatherPageHeaderContainer,
|
|
11
|
+
Address: WeatherPageHeaderAddress,
|
|
12
|
+
Today: WeatherPageHeaderToday,
|
|
13
|
+
Forecast: WeatherPageHeaderForecast,
|
|
14
|
+
Alert: WeatherPageHeaderAlert,
|
|
15
|
+
NextDays: WeatherPageHeaderNextDays,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
WeatherPageHeaderContainer,
|
|
20
|
+
WeatherPageHeaderAddress,
|
|
21
|
+
WeatherPageHeaderToday,
|
|
22
|
+
WeatherPageHeaderForecast,
|
|
23
|
+
WeatherPageHeaderAlert,
|
|
24
|
+
WeatherPageHeaderNextDays,
|
|
25
|
+
};
|
|
@@ -6,31 +6,14 @@ import {
|
|
|
6
6
|
useQueryWeatherOpenWeatherMapForecast,
|
|
7
7
|
useQueryWeatherOpenWeatherMapNow,
|
|
8
8
|
} from "../apis/open-weather-map/client";
|
|
9
|
-
import { useWeatherMock } from "../context/mock";
|
|
10
9
|
|
|
11
10
|
export default function useOpenWeatherMap() {
|
|
12
|
-
const mock = useWeatherMock();
|
|
13
11
|
const coordinate = useAtomValue(weatherCoordinate);
|
|
14
|
-
const shouldUseMock = Boolean(mock?.openWeather);
|
|
15
|
-
// mock 모드일 때는 Query가 실행되지 않도록 NaN 좌표를 전달한다.
|
|
16
|
-
const queryCoordinate = shouldUseMock
|
|
17
|
-
? { lat: Number.NaN, lng: Number.NaN }
|
|
18
|
-
: coordinate;
|
|
19
12
|
|
|
20
13
|
const { data: now, isFetching: isFetchingNow } =
|
|
21
|
-
useQueryWeatherOpenWeatherMapNow(
|
|
14
|
+
useQueryWeatherOpenWeatherMapNow(coordinate);
|
|
22
15
|
const { data: forecast, isFetching: isFetchingForecast } =
|
|
23
|
-
useQueryWeatherOpenWeatherMapForecast(
|
|
24
|
-
|
|
25
|
-
if (mock?.openWeather) {
|
|
26
|
-
return {
|
|
27
|
-
coordinate: mock.openWeather.coordinate,
|
|
28
|
-
now: mock.openWeather.now,
|
|
29
|
-
forecast: mock.openWeather.forecast,
|
|
30
|
-
isFetchingNow: mock.openWeather.isFetchingNow ?? false,
|
|
31
|
-
isFetchingForecast: mock.openWeather.isFetchingForecast ?? false,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
16
|
+
useQueryWeatherOpenWeatherMapForecast(coordinate);
|
|
34
17
|
|
|
35
18
|
return {
|
|
36
19
|
coordinate,
|
|
@@ -10,10 +10,8 @@ import type { API_Req_WeatherKorea } from "../types";
|
|
|
10
10
|
import { weatherCoordinate } from "../jotai/coordinate";
|
|
11
11
|
import { getWeatherBaseMoments } from "../utils/date-time";
|
|
12
12
|
import { getWeatherGridLocation } from "../utils/location";
|
|
13
|
-
import { useWeatherMock } from "../context/mock";
|
|
14
13
|
|
|
15
14
|
export default function useWeatherKorea() {
|
|
16
|
-
const mock = useWeatherMock();
|
|
17
15
|
const coordinate = useAtomValue(weatherCoordinate);
|
|
18
16
|
const [params, setParams] = useState<{
|
|
19
17
|
[categoryKey: string]: API_Req_WeatherKorea;
|
|
@@ -23,9 +21,18 @@ export default function useWeatherKorea() {
|
|
|
23
21
|
});
|
|
24
22
|
|
|
25
23
|
useEffect(() => {
|
|
26
|
-
if (mock?.korea) return;
|
|
27
24
|
if (typeof window === "undefined") return;
|
|
28
25
|
|
|
26
|
+
const hasInjectedCoordinate =
|
|
27
|
+
typeof coordinate.address === "string" ||
|
|
28
|
+
coordinate.lat !== null ||
|
|
29
|
+
coordinate.lng !== null;
|
|
30
|
+
|
|
31
|
+
if (!hasInjectedCoordinate) {
|
|
32
|
+
// 변경 설명: 초기 mount에서 coordinate 주입 전에는 geolocation/web API 준비를 시도하지 않는다.
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
const nowMoments = getWeatherBaseMoments("now");
|
|
30
37
|
const forecastMoments = getWeatherBaseMoments("forecast");
|
|
31
38
|
|
|
@@ -44,7 +51,7 @@ export default function useWeatherKorea() {
|
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
updater();
|
|
47
|
-
}, [coordinate
|
|
54
|
+
}, [coordinate]);
|
|
48
55
|
|
|
49
56
|
const { data: now, isFetching: isFetchingNow } = useQueryWeatherKoreaNow(
|
|
50
57
|
params.now,
|
|
@@ -53,20 +60,6 @@ export default function useWeatherKorea() {
|
|
|
53
60
|
const { data: forecast, isFetching: isFetchingForecast } =
|
|
54
61
|
useQueryWeatherKoreaForecast(params.forecast);
|
|
55
62
|
|
|
56
|
-
if (mock?.korea) {
|
|
57
|
-
return {
|
|
58
|
-
params: {
|
|
59
|
-
now: { nx: 0, ny: 0 },
|
|
60
|
-
forecast: { nx: 0, ny: 0 },
|
|
61
|
-
},
|
|
62
|
-
coordinate: mock.korea.coordinate,
|
|
63
|
-
now: mock.korea.now,
|
|
64
|
-
forecast: mock.korea.forecast,
|
|
65
|
-
isFetchingNow: mock.korea.isFetchingNow ?? false,
|
|
66
|
-
isFetchingForecast: mock.korea.isFetchingForecast ?? false,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
63
|
return {
|
|
71
64
|
params,
|
|
72
65
|
coordinate,
|
|
@@ -4,32 +4,19 @@ import { useMemo } from "react";
|
|
|
4
4
|
import { useAtomValue } from "jotai";
|
|
5
5
|
import { useQueryWeatherKoreaAlert } from "../apis/korea/client"; // server 번들을 피하기 위해 client 모듈에서 직접 import한다.
|
|
6
6
|
import { weatherFarmIdx } from "../jotai/farm-idx";
|
|
7
|
-
import { useWeatherMock } from "../context/mock";
|
|
8
7
|
|
|
9
8
|
export default function useWeatherKoreaAlert() {
|
|
10
|
-
const mock = useWeatherMock();
|
|
11
9
|
const farm_idx = useAtomValue(weatherFarmIdx);
|
|
12
|
-
const shouldUseMock = Boolean(mock?.koreaAlert);
|
|
13
|
-
const resolvedFarmIdx = farm_idx === 507 || !farm_idx ? 507 : (farm_idx ?? 0);
|
|
14
|
-
const queryFarmIdx = shouldUseMock ? 0 : resolvedFarmIdx;
|
|
15
10
|
|
|
11
|
+
// 특보 조회 식별자는 템플릿이 보정하지 않고, 서비스가 주입한 값만 그대로 사용한다.
|
|
16
12
|
const { data, isFetching } = useQueryWeatherKoreaAlert({
|
|
17
|
-
farm_idx
|
|
13
|
+
farm_idx,
|
|
18
14
|
});
|
|
19
15
|
|
|
20
16
|
const alert = useMemo(() => data?.alerts || [], [data]);
|
|
21
17
|
|
|
22
|
-
if (mock?.koreaAlert) {
|
|
23
|
-
// WeatherMockProvider를 사용하면 네트워크 호출 없이 스토리/테스트 전용 alert 데이터를 주입할 수 있다.
|
|
24
|
-
return {
|
|
25
|
-
farm_idx: null,
|
|
26
|
-
alert: mock.koreaAlert.alert?.alerts ?? [],
|
|
27
|
-
isFetching: mock.koreaAlert.isFetching ?? false,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
18
|
return {
|
|
32
|
-
farm_idx
|
|
19
|
+
farm_idx,
|
|
33
20
|
alert,
|
|
34
21
|
isFetching,
|
|
35
22
|
};
|
package/src/weather/index.tsx
CHANGED
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
import "./index.scss";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Weather; page-header 템플릿과 api/hook/jotai
|
|
5
|
+
* Weather; page-header 템플릿과 api/hook/jotai 도구를 함께 제공하는 엔트리
|
|
6
6
|
*/
|
|
7
7
|
export * from "./utils";
|
|
8
8
|
export * from "./apis";
|
|
9
9
|
export * from "./jotai";
|
|
10
10
|
export * from "./hooks";
|
|
11
|
-
export * from "./context/mock";
|
|
12
11
|
export * from "./types";
|
|
13
12
|
export { default as WeatherComponents } from "./components";
|
|
13
|
+
export { WeatherPageHeader } from "./components";
|
|
14
14
|
export { default as WeatherPageHeaderContainer } from "./components/page-header/Container";
|
|
15
|
+
export {
|
|
16
|
+
WeatherPageHeaderAddress,
|
|
17
|
+
WeatherPageHeaderAlert,
|
|
18
|
+
WeatherPageHeaderForecast,
|
|
19
|
+
WeatherPageHeaderNextDays,
|
|
20
|
+
WeatherPageHeaderToday,
|
|
21
|
+
} from "./components/page-header";
|
|
@@ -69,9 +69,9 @@ export type OWM_Res_Weather_Degrees = {
|
|
|
69
69
|
/** * 습도 (%) */
|
|
70
70
|
humidity: number;
|
|
71
71
|
/** * 해면기압 (hPa) */
|
|
72
|
-
sea_level
|
|
72
|
+
sea_level?: number;
|
|
73
73
|
/** * 현지기압 (hPa) */
|
|
74
|
-
grnd_level
|
|
74
|
+
grnd_level?: number;
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
/**
|
|
@@ -92,7 +92,7 @@ export type OWM_Res_Wind = {
|
|
|
92
92
|
*/
|
|
93
93
|
deg: number;
|
|
94
94
|
/** * 돌풍 (m/s) */
|
|
95
|
-
gust
|
|
95
|
+
gust?: number;
|
|
96
96
|
};
|
|
97
97
|
|
|
98
98
|
/**
|
|
@@ -116,6 +116,47 @@ export type OWM_Res_Sun = {
|
|
|
116
116
|
sunset: number; // 일몰 timestamp 1748342383;
|
|
117
117
|
};
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* OpenWeatherMap 도시 정보.
|
|
121
|
+
* @property {number} id 도시 식별자
|
|
122
|
+
* @property {string} name 도시 이름
|
|
123
|
+
* @property {OWM_Res_Coord} coord 도시 좌표
|
|
124
|
+
* @property {string} country 국가 코드
|
|
125
|
+
* @property {number} timezone UTC 오프셋(sec)
|
|
126
|
+
* @property {number} sunrise 일출 시각 timestamp
|
|
127
|
+
* @property {number} sunset 일몰 시각 timestamp
|
|
128
|
+
*/
|
|
129
|
+
export type OWM_Res_City = {
|
|
130
|
+
/** 도시 식별자 */
|
|
131
|
+
id: number;
|
|
132
|
+
/** 도시 이름 */
|
|
133
|
+
name: string;
|
|
134
|
+
/** 도시 좌표 */
|
|
135
|
+
coord: OWM_Res_Coord;
|
|
136
|
+
/** 국가 코드 */
|
|
137
|
+
country: string;
|
|
138
|
+
/** UTC 오프셋(sec) */
|
|
139
|
+
timezone: number;
|
|
140
|
+
/** 일출 시각 timestamp */
|
|
141
|
+
sunrise: number;
|
|
142
|
+
/** 일몰 시각 timestamp */
|
|
143
|
+
sunset: number;
|
|
144
|
+
/** 도시 인구 */
|
|
145
|
+
population?: number;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* OpenWeatherMap 강수량 정보.
|
|
150
|
+
* @property {number} ["1h"] 1시간 강수량(mm)
|
|
151
|
+
* @property {number} ["3h"] 3시간 강수량(mm)
|
|
152
|
+
*/
|
|
153
|
+
export type OWM_Res_Precipitation = {
|
|
154
|
+
/** 1시간 강수량(mm) */
|
|
155
|
+
"1h"?: number;
|
|
156
|
+
/** 3시간 강수량(mm) */
|
|
157
|
+
"3h"?: number;
|
|
158
|
+
};
|
|
159
|
+
|
|
119
160
|
/**
|
|
120
161
|
* OpenWeatherMap API 응답; 현재 날씨
|
|
121
162
|
* @see https://openweathermap.org/current
|
|
@@ -155,10 +196,71 @@ export type OWM_Res_Weather_Now = {
|
|
|
155
196
|
* @deprecated
|
|
156
197
|
*/
|
|
157
198
|
/** 도시 이름 */
|
|
158
|
-
name:
|
|
199
|
+
name: string;
|
|
159
200
|
/**
|
|
160
201
|
* API 응답 정보
|
|
161
202
|
*/
|
|
162
203
|
/** API 응답 코드 */
|
|
163
204
|
cod: number;
|
|
164
205
|
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* OpenWeatherMap API 응답; 예보 항목
|
|
209
|
+
* @property {number} dt 데이터 연산 시각 timestamp
|
|
210
|
+
* @property {OWM_Res_Weather_Degrees} main 측정 정보
|
|
211
|
+
* @property {OWM_Res_Condition[]} weather 개황 정보
|
|
212
|
+
* @property {OWM_Res_Clouds} clouds 구름량 정보
|
|
213
|
+
* @property {OWM_Res_Wind} wind 바람 정보
|
|
214
|
+
* @property {number} visibility 시정거리(m)
|
|
215
|
+
* @property {number} pop 강수 확률(0~1)
|
|
216
|
+
* @property {{ pod: string }} sys 낮/밤 구분
|
|
217
|
+
* @property {string} dt_txt 예보 시각 문자열
|
|
218
|
+
* @property {OWM_Res_Precipitation} [rain] 강수량 정보
|
|
219
|
+
* @property {OWM_Res_Precipitation} [snow] 적설량 정보
|
|
220
|
+
*/
|
|
221
|
+
export type OWM_Res_Weather_Forecast_Item = {
|
|
222
|
+
/** 데이터 연산 시각 timestamp */
|
|
223
|
+
dt: number;
|
|
224
|
+
/** 측정 정보 */
|
|
225
|
+
main: OWM_Res_Weather_Degrees;
|
|
226
|
+
/** 개황 정보 */
|
|
227
|
+
weather: OWM_Res_Condition[];
|
|
228
|
+
/** 구름량 정보 */
|
|
229
|
+
clouds: OWM_Res_Clouds;
|
|
230
|
+
/** 바람 정보 */
|
|
231
|
+
wind: OWM_Res_Wind;
|
|
232
|
+
/** 시정거리(m) */
|
|
233
|
+
visibility: number;
|
|
234
|
+
/** 강수 확률(0~1) */
|
|
235
|
+
pop: number;
|
|
236
|
+
/** 낮/밤 구분 */
|
|
237
|
+
sys: { pod: string };
|
|
238
|
+
/** 예보 시각 문자열 */
|
|
239
|
+
dt_txt: string;
|
|
240
|
+
/** 강수량 정보 */
|
|
241
|
+
rain?: OWM_Res_Precipitation;
|
|
242
|
+
/** 적설량 정보 */
|
|
243
|
+
snow?: OWM_Res_Precipitation;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* OpenWeatherMap API 응답; 예보 날씨
|
|
248
|
+
* @see https://openweathermap.org/forecast5
|
|
249
|
+
* @property {string} cod API 응답 코드
|
|
250
|
+
* @property {number} message 내부 메시지/계산값
|
|
251
|
+
* @property {number} cnt 예보 개수
|
|
252
|
+
* @property {OWM_Res_Weather_Forecast_Item[]} list 예보 목록
|
|
253
|
+
* @property {OWM_Res_City} city 도시 정보
|
|
254
|
+
*/
|
|
255
|
+
export type OWM_Res_Weather_Forecast = {
|
|
256
|
+
/** API 응답 코드 */
|
|
257
|
+
cod: string;
|
|
258
|
+
/** 내부 메시지/계산값 */
|
|
259
|
+
message: number;
|
|
260
|
+
/** 예보 개수 */
|
|
261
|
+
cnt: number;
|
|
262
|
+
/** 예보 목록 */
|
|
263
|
+
list: OWM_Res_Weather_Forecast_Item[];
|
|
264
|
+
/** 도시 정보 */
|
|
265
|
+
city: OWM_Res_City;
|
|
266
|
+
};
|
|
@@ -120,12 +120,15 @@ export async function getWeatherGridLocation(
|
|
|
120
120
|
latitude: coordinate?.lat ?? null,
|
|
121
121
|
longitude: coordinate?.lng ?? null,
|
|
122
122
|
};
|
|
123
|
+
const hasInjectedAddress =
|
|
124
|
+
typeof coordinate?.address === "string" && coordinate.address.trim() !== "";
|
|
123
125
|
|
|
124
126
|
// console.log("[getWeatherLocation] 특정 좌표지정 확인", coordinate);
|
|
125
127
|
|
|
126
|
-
//
|
|
128
|
+
// 변경 설명: 서비스가 address만 명시적으로 주입한 경우에는 geolocation fallback을 재시도하지 않는다.
|
|
127
129
|
if (
|
|
128
130
|
typeof window !== "undefined" &&
|
|
131
|
+
!hasInjectedAddress &&
|
|
129
132
|
(typeof coordinate === "undefined" ||
|
|
130
133
|
geo.latitude === null ||
|
|
131
134
|
geo.longitude === null)
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { createContext, useContext } from "react";
|
|
4
|
-
import type {
|
|
5
|
-
API_Res_WeatherKoreaAlert,
|
|
6
|
-
API_Res_WeatherKoreaForecast,
|
|
7
|
-
API_Res_WeatherKoreaNow,
|
|
8
|
-
WeatherCoordinate,
|
|
9
|
-
} from "../types";
|
|
10
|
-
|
|
11
|
-
type WeatherKoreaMock = {
|
|
12
|
-
now?: API_Res_WeatherKoreaNow;
|
|
13
|
-
forecast?: API_Res_WeatherKoreaForecast;
|
|
14
|
-
coordinate?: WeatherCoordinate;
|
|
15
|
-
isFetchingNow?: boolean;
|
|
16
|
-
isFetchingForecast?: boolean;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
type OpenWeatherMock = {
|
|
20
|
-
now?: {
|
|
21
|
-
weather?: Array<{ icon?: string; description?: string }>;
|
|
22
|
-
main?: { temp?: number | null; humidity?: number | null };
|
|
23
|
-
};
|
|
24
|
-
forecast?: unknown;
|
|
25
|
-
coordinate?: WeatherCoordinate;
|
|
26
|
-
isFetchingNow?: boolean;
|
|
27
|
-
isFetchingForecast?: boolean;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
type WeatherKoreaAlertMock = {
|
|
31
|
-
alert?: API_Res_WeatherKoreaAlert;
|
|
32
|
-
isFetching?: boolean;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export type WeatherMockValue = {
|
|
36
|
-
korea?: WeatherKoreaMock;
|
|
37
|
-
koreaAlert?: WeatherKoreaAlertMock;
|
|
38
|
-
openWeather?: OpenWeatherMock;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const WeatherMockContext = createContext<WeatherMockValue | null>(null);
|
|
42
|
-
|
|
43
|
-
export const WeatherMockProvider = WeatherMockContext.Provider;
|
|
44
|
-
|
|
45
|
-
export const useWeatherMock = () => useContext(WeatherMockContext);
|