@uniai-fe/uds-templates 0.1.16 → 0.1.17
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/dist/styles.css +139 -10
- package/package.json +18 -11
- package/src/auth/common/complete/Template.tsx +3 -0
- package/src/auth/common/complete/types.ts +4 -1
- package/src/auth/common/find/markup/CodeStep.tsx +2 -1
- package/src/auth/common/find/markup/InfoStep.tsx +2 -1
- package/src/auth/common/find/styles/result.scss +1 -1
- package/src/auth/find-password/markup/StepResetPassword.tsx +2 -1
- package/src/auth/login/markup/FormField.tsx +2 -1
- package/src/auth/signup/markup/AccountForm.tsx +2 -1
- package/src/auth/signup/markup/Complete.tsx +2 -1
- package/src/auth/signup/markup/UserInfoForm.tsx +2 -1
- package/src/auth/signup/markup/VerificationForm.tsx +4 -2
- package/src/index.scss +1 -0
- package/src/index.tsx +1 -0
- package/src/modal/core/components/FooterButtons.tsx +4 -1
- package/src/modal/templates/Dialog.tsx +4 -2
- package/src/modal/types/footer.ts +4 -2
- package/src/page-frame/container/index.scss +14 -8
- package/src/page-frame/desktop/components/header/Container.tsx +22 -0
- package/src/page-frame/desktop/components/header/Section.tsx +20 -0
- package/src/page-frame/desktop/components/header/index.tsx +19 -0
- package/src/page-frame/desktop/components/header/util/Button.tsx +23 -0
- package/src/page-frame/desktop/components/header/util/Container.tsx +18 -0
- package/src/page-frame/desktop/components/header/util/Item.tsx +13 -0
- package/src/page-frame/desktop/components/header/util/Logout.tsx +26 -0
- package/src/page-frame/desktop/components/header/util/setting/Button.tsx +39 -0
- package/src/page-frame/desktop/components/index.tsx +15 -0
- package/src/page-frame/desktop/components/nav/Button.tsx +24 -0
- package/src/page-frame/desktop/components/nav/Container.tsx +70 -0
- package/src/page-frame/desktop/components/nav/Logo.tsx +46 -0
- package/src/page-frame/desktop/components/nav/index.tsx +5 -0
- package/src/page-frame/desktop/components/page/Container.tsx +13 -0
- package/src/page-frame/desktop/components/page/ServiceFrame.tsx +15 -0
- package/src/page-frame/desktop/components/page/ServiceMain.tsx +13 -0
- package/src/page-frame/desktop/components/page/ServiceMainWrapper.tsx +24 -0
- package/src/page-frame/desktop/components/page/index.tsx +14 -0
- package/src/page-frame/desktop/components/popup/Container.tsx +18 -0
- package/src/page-frame/desktop/components/popup/frame/Body.tsx +14 -0
- package/src/page-frame/desktop/components/popup/frame/Container.tsx +16 -0
- package/src/page-frame/desktop/components/popup/frame/Header.tsx +26 -0
- package/src/page-frame/desktop/components/popup/frame/index.tsx +11 -0
- package/src/page-frame/desktop/components/popup/index.tsx +9 -0
- package/src/page-frame/desktop/index.scss +5 -0
- package/src/page-frame/desktop/index.tsx +5 -0
- package/src/page-frame/desktop/styles/page/common.scss +40 -0
- package/src/page-frame/desktop/styles/page/header.scss +49 -0
- package/src/page-frame/desktop/styles/page/nav.scss +128 -0
- package/src/page-frame/desktop/styles/popup/popup.scss +45 -0
- package/src/page-frame/desktop/styles/variables.scss +28 -0
- package/src/page-frame/desktop/types/nav.ts +77 -0
- package/src/page-frame/index.tsx +2 -0
- package/src/page-frame/mobile/index.scss +1 -1
- package/src/page-frame/types/index.ts +69 -0
- package/src/weather/apis/index.ts +4 -0
- package/src/weather/apis/korea/client.ts +93 -0
- package/src/weather/apis/korea/server.ts +253 -0
- package/src/weather/apis/open-weather-map/client.ts +69 -0
- package/src/weather/apis/open-weather-map/server.ts +134 -0
- package/src/weather/components/icon/Address.tsx +13 -0
- package/src/weather/components/icon/Weather.tsx +32 -0
- package/src/weather/components/index.tsx +7 -0
- package/src/weather/components/page-header/Address.tsx +21 -0
- package/src/weather/components/page-header/Alert.tsx +42 -0
- package/src/weather/components/page-header/Container.tsx +14 -0
- package/src/weather/components/page-header/Forecast.tsx +26 -0
- package/src/weather/components/page-header/NextDays.tsx +32 -0
- package/src/weather/components/page-header/Today.tsx +135 -0
- package/src/weather/context/mock.tsx +45 -0
- package/src/weather/data/alert-regions-meta.json +1286 -0
- package/src/weather/data/response.ts +36 -0
- package/src/weather/data/weather-regions-meta.json +9833 -0
- package/src/weather/hooks/index.ts +4 -0
- package/src/weather/hooks/useOpenWeatherMap.ts +42 -0
- package/src/weather/hooks/useWeatherKorea.ts +78 -0
- package/src/weather/hooks/useWeatherKoreaAlert.ts +36 -0
- package/src/weather/index.tsx +11 -0
- package/src/weather/jotai/coordinate.ts +16 -0
- package/src/weather/jotai/farm-idx.ts +5 -0
- package/src/weather/jotai/index.ts +2 -0
- package/src/weather/styles/weather.scss +151 -0
- package/src/weather/types/api.ts +215 -0
- package/src/weather/types/base.ts +50 -0
- package/src/weather/types/index.ts +4 -0
- package/src/weather/types/korea.ts +228 -0
- package/src/weather/types/open-weather-map.ts +164 -0
- package/src/weather/utils/alert.ts +30 -0
- package/src/weather/utils/date-time.ts +65 -0
- package/src/weather/utils/index.ts +4 -0
- package/src/weather/utils/location.ts +161 -0
- package/src/weather/utils/validate.ts +9 -0
- package/src/weather/utils/weather.ts +304 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
generateBackendQueryUrl_GET,
|
|
5
|
+
nextAPILog,
|
|
6
|
+
} from "@uniai-fe/util-functions";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 글로벌 날씨 API; 현재 날씨 URL
|
|
10
|
+
*/
|
|
11
|
+
const API_BASE_CURRENT = "https://api.openweathermap.org/data/2.5/weather";
|
|
12
|
+
/**
|
|
13
|
+
* 글로벌 날씨 API; 예보 날씨 URL
|
|
14
|
+
*/
|
|
15
|
+
const API_BASE_FORECAST =
|
|
16
|
+
"https://pro.openweathermap.org/data/2.5/weather/forecast/hourly";
|
|
17
|
+
|
|
18
|
+
const COMMON_OPTIONS = {
|
|
19
|
+
lang: "kr",
|
|
20
|
+
units: "metric", // 섭씨 온도
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 글로벌 날씨 API; 현재 날씨
|
|
25
|
+
* @method GET
|
|
26
|
+
*/
|
|
27
|
+
export const routeOpenWeatherMapNow = async ({
|
|
28
|
+
authKey,
|
|
29
|
+
routeUrl,
|
|
30
|
+
searchParams,
|
|
31
|
+
}: {
|
|
32
|
+
authKey: string;
|
|
33
|
+
routeUrl: string;
|
|
34
|
+
searchParams: URLSearchParams;
|
|
35
|
+
}): Promise<object> => {
|
|
36
|
+
const resDefault = {};
|
|
37
|
+
|
|
38
|
+
if (!authKey) {
|
|
39
|
+
nextAPILog("GET", routeUrl, "Open Weather Map 현재날씨 API", {
|
|
40
|
+
error: "API Key를 확인하세요.",
|
|
41
|
+
});
|
|
42
|
+
return resDefault;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const lat = searchParams.get("lat") || "";
|
|
46
|
+
const lon = searchParams.get("lng") || "";
|
|
47
|
+
if (!lat || !lon) {
|
|
48
|
+
nextAPILog("GET", routeUrl, "Open Weather Map 현재날씨 API", {
|
|
49
|
+
error: "위도와 경도를 입력하세요.",
|
|
50
|
+
lat,
|
|
51
|
+
lon,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const url = generateBackendQueryUrl_GET({
|
|
58
|
+
domain: API_BASE_CURRENT,
|
|
59
|
+
routeUrl,
|
|
60
|
+
queryUrl: "",
|
|
61
|
+
searchParams: new URLSearchParams({
|
|
62
|
+
lat,
|
|
63
|
+
lon,
|
|
64
|
+
appid: authKey,
|
|
65
|
+
...COMMON_OPTIONS,
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const res = await (await fetch(url)).json();
|
|
71
|
+
return res;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
nextAPILog("GET", routeUrl, url, { error });
|
|
74
|
+
|
|
75
|
+
return resDefault;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 글로벌 날씨 API; 예보 날씨
|
|
81
|
+
* @method GET
|
|
82
|
+
*/
|
|
83
|
+
export const routeOpenWeatherMapForecast = async ({
|
|
84
|
+
authKey,
|
|
85
|
+
routeUrl,
|
|
86
|
+
searchParams,
|
|
87
|
+
}: {
|
|
88
|
+
authKey: string;
|
|
89
|
+
routeUrl: string;
|
|
90
|
+
searchParams: URLSearchParams;
|
|
91
|
+
}): Promise<object> => {
|
|
92
|
+
const resDefault = {};
|
|
93
|
+
|
|
94
|
+
if (!authKey) {
|
|
95
|
+
nextAPILog("GET", routeUrl, "Open Weather Map 예보날씨 API", {
|
|
96
|
+
error: "API Key를 확인하세요.",
|
|
97
|
+
});
|
|
98
|
+
return resDefault;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const lat = searchParams.get("lat") || "";
|
|
102
|
+
const lon = searchParams.get("lng") || "";
|
|
103
|
+
if (!lat || !lon) {
|
|
104
|
+
nextAPILog("GET", routeUrl, "Open Weather Map 현재날씨 API", {
|
|
105
|
+
error: "위도와 경도를 입력하세요.",
|
|
106
|
+
lat,
|
|
107
|
+
lon,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const url = generateBackendQueryUrl_GET({
|
|
114
|
+
domain: API_BASE_FORECAST,
|
|
115
|
+
routeUrl,
|
|
116
|
+
queryUrl: "",
|
|
117
|
+
searchParams: new URLSearchParams({
|
|
118
|
+
lat,
|
|
119
|
+
lon,
|
|
120
|
+
appid: authKey,
|
|
121
|
+
...COMMON_OPTIONS,
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const res = await (await fetch(url)).json();
|
|
127
|
+
nextAPILog("GET", routeUrl, url, { searchParams, res });
|
|
128
|
+
return res;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
nextAPILog("GET", routeUrl, url, { error });
|
|
131
|
+
|
|
132
|
+
return resDefault;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
|
|
5
|
+
import assetUrl from "../../../../asset-url";
|
|
6
|
+
|
|
7
|
+
export default function WeatherAddressIcon() {
|
|
8
|
+
return (
|
|
9
|
+
<figure className="weather-address-icon">
|
|
10
|
+
<Image src={`${assetUrl}/img/weather/address.svg`} alt="날씨 위치" fill />
|
|
11
|
+
</figure>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Image from "next/image";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
|
|
4
|
+
import assetUrl from "../../../../asset-url";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 날씨; 아이콘
|
|
8
|
+
* @desc
|
|
9
|
+
* - 기상청 API 허브
|
|
10
|
+
* - 2024. 11. 28. 발효 기준
|
|
11
|
+
*
|
|
12
|
+
* @see https://apihub.kma.go.kr/getAttachFile.do?fileName=(20241128)%EB%8B%A8%EA%B8%B0%EC%98%88%EB%B3%B4%20%EC%84%9C%EB%B9%84%EC%8A%A4%20%EA%B0%9C%EC%84%A0%EC%97%90%20%EB%94%B0%EB%A5%B8%20API%20%EB%B3%80%EA%B2%BD%EC%82%AC%ED%95%AD.pdf
|
|
13
|
+
*/
|
|
14
|
+
export default function WeatherIcon({
|
|
15
|
+
code,
|
|
16
|
+
name,
|
|
17
|
+
}: Partial<{
|
|
18
|
+
code: string | null;
|
|
19
|
+
name: string | null;
|
|
20
|
+
}>) {
|
|
21
|
+
return (
|
|
22
|
+
code && (
|
|
23
|
+
<figure className={clsx("weather-base-icon", "weather-icon")}>
|
|
24
|
+
<Image
|
|
25
|
+
src={`${assetUrl}/img/weather/${code}.svg`}
|
|
26
|
+
alt={name || "날씨 아이콘"}
|
|
27
|
+
fill
|
|
28
|
+
/>
|
|
29
|
+
</figure>
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useAtomValue } from "jotai";
|
|
4
|
+
|
|
5
|
+
import { weatherCoordinate } from "../../jotai";
|
|
6
|
+
import WeatherAddressIcon from "../icon/Address";
|
|
7
|
+
|
|
8
|
+
export default function WeatherPageHeaderAddress() {
|
|
9
|
+
const { address } = useAtomValue(weatherCoordinate);
|
|
10
|
+
|
|
11
|
+
if (!address) return null;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="weather-address">
|
|
15
|
+
<WeatherAddressIcon />
|
|
16
|
+
<p className="weather-address-text">
|
|
17
|
+
<span>{address}</span>
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
|
|
5
|
+
import useWeatherKoreaAlert from "../../hooks/useWeatherKoreaAlert";
|
|
6
|
+
import { Alternate } from "@uniai-fe/uds-primitives";
|
|
7
|
+
|
|
8
|
+
export default function WeatherPageHeaderAlert() {
|
|
9
|
+
const { alert, isFetching } = useWeatherKoreaAlert();
|
|
10
|
+
|
|
11
|
+
const notice = useMemo((): string => {
|
|
12
|
+
if (alert.length === 0 || !alert?.[0]?.alert_type) return "";
|
|
13
|
+
|
|
14
|
+
const { alert_type, alert_level, alert_command } = alert?.[0] || {};
|
|
15
|
+
const level = () => {
|
|
16
|
+
if (alert_level.startsWith("예비")) return "예비특보";
|
|
17
|
+
if (alert_level.startsWith("주의")) return "주의보";
|
|
18
|
+
return alert_level;
|
|
19
|
+
};
|
|
20
|
+
const command = alert_command?.includes("해제") ? "해제" : "발령";
|
|
21
|
+
return `${alert_type}${level()} ${command}`;
|
|
22
|
+
}, [alert]);
|
|
23
|
+
|
|
24
|
+
if (!isFetching && (alert.length === 0 || !alert?.[0]?.alert_type)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="weather-alert">
|
|
30
|
+
<div className="weather-alert-text">
|
|
31
|
+
{isFetching ? (
|
|
32
|
+
// 특보 조회 중에도 영역을 확보해 레이아웃이 흔들리지 않도록 로딩 표시를 유지한다.
|
|
33
|
+
<Alternate.LoadingDefault direction="horizontal">
|
|
34
|
+
특보 불러오는 중...
|
|
35
|
+
</Alternate.LoadingDefault>
|
|
36
|
+
) : (
|
|
37
|
+
<span>{notice}</span>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import WeatherPageHeaderToday from "./Today";
|
|
2
|
+
import WeatherPageHeaderForecast from "./Forecast";
|
|
3
|
+
import WeatherPageHeaderAddress from "./Address";
|
|
4
|
+
import PageHeaderUtilityItem from "../../../page-frame/desktop/components/header/util/Item";
|
|
5
|
+
|
|
6
|
+
export default function WeatherPageHeaderContainer() {
|
|
7
|
+
return (
|
|
8
|
+
<PageHeaderUtilityItem>
|
|
9
|
+
<WeatherPageHeaderAddress />
|
|
10
|
+
<WeatherPageHeaderToday />
|
|
11
|
+
<WeatherPageHeaderForecast />
|
|
12
|
+
</PageHeaderUtilityItem>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Alternate } from "@uniai-fe/uds-primitives";
|
|
4
|
+
import { useWeatherKorea } from "../../hooks";
|
|
5
|
+
import WeatherPageHeaderNextDays from "./NextDays";
|
|
6
|
+
|
|
7
|
+
export default function WeatherPageHeaderForecast() {
|
|
8
|
+
const { forecast, isFetchingForecast } = useWeatherKorea();
|
|
9
|
+
|
|
10
|
+
if (isFetchingForecast) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="weather-next-days-container">
|
|
13
|
+
<Alternate.LoadingDefault direction="horizontal">
|
|
14
|
+
예보 날씨 불러오는 중...
|
|
15
|
+
</Alternate.LoadingDefault>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<WeatherPageHeaderNextDays title="내일" data={forecast?.day_1} />
|
|
23
|
+
<WeatherPageHeaderNextDays title="모레" data={forecast?.day_2} />
|
|
24
|
+
</>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { API_Res_WeatherKoreaNextDays } from "../../types";
|
|
2
|
+
import WeatherIcon from "../icon/Weather";
|
|
3
|
+
|
|
4
|
+
export default function WeatherPageHeaderNextDays({
|
|
5
|
+
title,
|
|
6
|
+
data,
|
|
7
|
+
}: {
|
|
8
|
+
title: string;
|
|
9
|
+
data?: API_Res_WeatherKoreaNextDays;
|
|
10
|
+
}) {
|
|
11
|
+
return (
|
|
12
|
+
<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>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { lengthFormat } from "@uniai-fe/util-functions";
|
|
5
|
+
|
|
6
|
+
import { Alternate } from "@uniai-fe/uds-primitives";
|
|
7
|
+
import WeatherIcon from "../icon/Weather";
|
|
8
|
+
import WeatherPageHeaderAlert from "./Alert";
|
|
9
|
+
import { useOpenWeatherMap, useWeatherKorea } from "../../hooks";
|
|
10
|
+
|
|
11
|
+
export default function WeatherPageHeaderToday() {
|
|
12
|
+
const { now, forecast, isFetchingNow } = useWeatherKorea();
|
|
13
|
+
const { now: nowAlt, isFetchingNow: isFetchingAlt } = useOpenWeatherMap();
|
|
14
|
+
|
|
15
|
+
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
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}, [
|
|
29
|
+
forecast?.today?.condition,
|
|
30
|
+
isFetchingAlt,
|
|
31
|
+
isFetchingNow,
|
|
32
|
+
now?.today?.condition,
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
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";
|
|
62
|
+
}
|
|
63
|
+
}, [forecast?.today?.condition, now?.today?.condition, nowAlt?.weather]);
|
|
64
|
+
|
|
65
|
+
const weatherStateName = useMemo(() => {
|
|
66
|
+
const kma_now = now?.today?.conditionName;
|
|
67
|
+
if (kma_now && !kma_now.endsWith("null")) return kma_now;
|
|
68
|
+
|
|
69
|
+
const kma_forecast = forecast?.today?.conditionName;
|
|
70
|
+
if (kma_forecast && !kma_forecast.endsWith("null")) return kma_forecast;
|
|
71
|
+
|
|
72
|
+
return nowAlt?.weather?.[0]?.description || "-";
|
|
73
|
+
}, [
|
|
74
|
+
forecast?.today?.conditionName,
|
|
75
|
+
now?.today?.conditionName,
|
|
76
|
+
nowAlt?.weather,
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
const temperature = useMemo(() => {
|
|
80
|
+
const kma_now = now?.today?.temperature;
|
|
81
|
+
if (kma_now && !Number.isNaN(Number(kma_now))) return Number(kma_now);
|
|
82
|
+
|
|
83
|
+
const kma_forecast = forecast?.today?.temperature;
|
|
84
|
+
if (kma_forecast && !Number.isNaN(Number(kma_forecast)))
|
|
85
|
+
return Number(kma_forecast);
|
|
86
|
+
|
|
87
|
+
const alt_now = nowAlt?.main?.temp;
|
|
88
|
+
return typeof alt_now === "number" ? Math.round(alt_now) : null;
|
|
89
|
+
}, [
|
|
90
|
+
forecast?.today?.temperature,
|
|
91
|
+
now?.today?.temperature,
|
|
92
|
+
nowAlt?.main?.temp,
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const humidity = useMemo(() => {
|
|
96
|
+
const kma_now = now?.today?.humidity;
|
|
97
|
+
if (kma_now && !Number.isNaN(Number(kma_now))) return Number(kma_now);
|
|
98
|
+
|
|
99
|
+
const kma_forecast = forecast?.today?.humidity;
|
|
100
|
+
if (kma_forecast && !Number.isNaN(Number(kma_forecast)))
|
|
101
|
+
return Number(kma_forecast);
|
|
102
|
+
|
|
103
|
+
const alt_now = nowAlt?.main?.humidity;
|
|
104
|
+
return typeof alt_now === "number" ? Math.round(alt_now) : null;
|
|
105
|
+
}, [forecast?.today?.humidity, now?.today?.humidity, nowAlt?.main?.humidity]);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className="weather-today-container">
|
|
109
|
+
{isFetchingAlternate ? (
|
|
110
|
+
<Alternate.LoadingDefault direction="horizontal">
|
|
111
|
+
현재 날씨 불러오는 중...
|
|
112
|
+
</Alternate.LoadingDefault>
|
|
113
|
+
) : (
|
|
114
|
+
<>
|
|
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>
|
|
130
|
+
</div>
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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);
|