Skip to content

Commit 934b400

Browse files
authored
feat/connect resort api (#41)
* feat: add resort and weather api * feat: update resorts data with api * feat: connect resort api in discovery detail page
1 parent 896dfa8 commit 934b400

15 files changed

+181
-62
lines changed
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { apiClient } from '@/shared/api/base';
2+
import type { Resort } from '../model';
3+
4+
export const getResorts = async (): Promise<Resort[]> => {
5+
const result = await apiClient.get<Resort[]>('/api/ski-resorts');
6+
7+
return result;
8+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { apiClient } from '@/shared/api/base';
2+
import type { WeatherResponse } from '../model';
3+
4+
export const getWeather = async (resortId: number): Promise<WeatherResponse> => {
5+
const result = await apiClient.get<WeatherResponse>(`/api/weather/${resortId}`);
6+
7+
return result;
8+
};

src/entities/discovery/api/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
export { discoveryQueries } from './discovery.queries';
1+
export { discoveryQueries } from './query/discovery.queries'
2+
export { resortQueries } from './query/resort.queries'
3+
export { weatherQueries } from './weather.queries';
24
export { postVote } from './post-vote';

src/entities/discovery/api/discovery.queries.ts src/entities/discovery/api/query/discovery.queries.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { queryOptions } from '@tanstack/react-query';
2-
import { getDiscoveries } from './get-discoveries';
3-
import { getVote } from './get-vote';
2+
import { getDiscoveries } from '../get-discoveries';
3+
import { getVote } from '../get-vote';
44

55
export const discoveryQueries = {
66
all: () => ['discovery'],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { queryOptions } from '@tanstack/react-query';
2+
import { getResorts } from '../get-resort';
3+
4+
export const resortQueries = {
5+
all: () => ['resort'],
6+
7+
listQueryKey: () => [...resortQueries.all(), 'list'],
8+
list: () =>
9+
queryOptions({
10+
queryKey: [...resortQueries.listQueryKey()],
11+
queryFn: () => getResorts(),
12+
}),
13+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { queryOptions } from '@tanstack/react-query';
2+
import { getWeather } from './get-weather';
3+
4+
export const weatherQueries = {
5+
all: () => ['weather'],
6+
7+
weatherQueryKey: (resortId: number) => [...weatherQueries.all(), resortId],
8+
weather: (resortId: number) =>
9+
queryOptions({
10+
queryKey: weatherQueries.weatherQueryKey(resortId),
11+
queryFn: () => getWeather(resortId),
12+
}),
13+
};

src/entities/discovery/model/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export { DiscoveryData } from './constants';
2-
export type { Weather, WeeklyWeather, Discovery, Vote } from './model';
2+
export type { Weather, Discovery, Vote, WeeklyWeather, WeatherResponse, Resort, Url } from './model';
+51-7
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,77 @@
11
export type Weather = 'sun' | 'cloud' | 'rain' | 'snow' | 'fog' | 'snow-rain';
22

3-
export type WeeklyWeather = {
3+
export type DuplicatedWeeklyWeather = {
44
weather: Weather;
55
temperature: {
66
lowest: string;
77
average: string;
88
};
99
};
1010

11+
export type Url = {
12+
bus: string;
13+
homepage: string;
14+
};
15+
1116
export type Discovery = {
1217
id: number;
1318
name: string;
1419
map: string;
1520
slope: number | null;
16-
url: {
17-
bus: string;
18-
homepage: string;
19-
};
21+
url: Url;
2022
weather: {
2123
weather: Weather;
2224
temperature: number;
2325
description: string;
2426
};
25-
weeklyWeather: WeeklyWeather[];
27+
weeklyWeather: DuplicatedWeeklyWeather[];
2628
};
2729

2830
export type Vote = {
2931
resortId: number;
3032
totalVotes: number;
3133
positiveVotes: number;
3234
status: string;
33-
};
35+
};
36+
37+
export type WeatherResponse = {
38+
resortId: number,
39+
currentWeather: {
40+
temperature: number,
41+
maxTemperature: number,
42+
minTemperature: number,
43+
feelsLike: number,
44+
description: string,
45+
condition: string
46+
},
47+
hourlyWeather: [],
48+
weeklyWeather: WeeklyWeather[]
49+
}
50+
51+
export type WeeklyWeather = {
52+
day: string;
53+
date: string;
54+
precipitationChance: string;
55+
maxTemperature: number;
56+
minTemperature: number;
57+
condition: string;
58+
};
59+
60+
export type SummarizedWeeklyWeather = {
61+
day: string;
62+
maxTemperature: number;
63+
minTemperature: number;
64+
description: string;
65+
};
66+
67+
export type Resort = {
68+
resortId: number,
69+
name: string,
70+
status: string,
71+
openSlopes: number,
72+
currentWeather: {
73+
temperature: number,
74+
description: string
75+
},
76+
weeklyWeather: SummarizedWeeklyWeather[]
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// eslint-disable-next-line boundaries/element-types
2+
import type { Weather } from "@/entities/discovery";
3+
4+
const weatherDescription = {
5+
sun: ['맑음'],
6+
cloud: ['구름', '구름많음'],
7+
rain: ['비', '구름많고 비', '흐리고 비', '구름많고 소나기', '흐리고 소나기'],
8+
snow: ['눈', '구름많고 눈', '흐리고 눈'],
9+
fog: ['안개', '흐림'],
10+
'snow-rain': ['흐리고 비/눈', '구름많고 비/눈']
11+
}
12+
13+
export const getWeatherFromDescription = (description: string): Weather => {
14+
for (const [weather, descriptions] of Object.entries(weatherDescription)) {
15+
if (descriptions.includes(description)) {
16+
return weather as Weather;
17+
}
18+
}
19+
20+
return 'sun';
21+
}

src/views/discovery-detail/ui/discovery-detail-page.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const DiscoveryDetailPage = ({ params }: { params: { resortId: string } }) => {
2626
const discovery = DiscoveryData.find(
2727
(discovery) => discovery.id === +params?.resortId
2828
) as Discovery;
29+
const { data: resortsData } = useQuery(discoveryApi.resortQueries.list());
30+
const resort = resortsData?.find((resort) => resort.resortId === +params?.resortId);
2931
const { data: voteData } = useQuery(discoveryApi.discoveryQueries.vote(+params?.resortId));
3032
const data = RESORT_DOMAIN[discovery?.map as keyof typeof RESORT_DOMAIN];
3133
const [selectedTab, setSelectedTab] = useState('webcam');
@@ -72,12 +74,12 @@ const DiscoveryDetailPage = ({ params }: { params: { resortId: string } }) => {
7274
}
7375
}, [isPositive, mutateAsync, params?.resortId]);
7476

75-
if (!discovery) return;
77+
if (!discovery || !resort) return;
7678

7779
return (
7880
<div className={cn('size-full')}>
79-
<Header resortName={discovery.name} hasBackButton hasShareButton />
80-
<DiscoverySummary {...discovery} />
81+
<Header resortName={resort.name} hasBackButton hasShareButton />
82+
<DiscoverySummary {...resort} {...discovery.url} />
8183
<ul className={cn('relative z-10 flex size-full h-[53px] bg-white')}>
8284
{DiscoveryContentTabList.map((tab) => (
8385
<li

src/views/discovery/ui/discovery-page.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import { discoveryApi } from '@/entities/discovery';
77
import { cn } from '@/shared/lib';
88

99
const DiscoveryPage = () => {
10-
const { data: discoveryData } = useQuery(discoveryApi.discoveryQueries.list());
10+
const { data: resorts } = useQuery(discoveryApi.resortQueries.list());
1111

12-
if (!discoveryData) return null;
12+
if (!resorts) return null;
1313

1414
return (
1515
<div className={cn('size-full bg-gradient-to-b from-[rgba(141,163,221,0.2)] to-transparent')}>
1616
<Header />
17-
<DiscoveryList discoveryData={discoveryData} />
17+
<DiscoveryList resorts={resorts} />
1818
</div>
1919
);
2020
};

src/widgets/discovery-detail/ui/discovery-summary.tsx

+22-16
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,43 @@
11
import Link from 'next/link';
22
import WeatherIcon from '@/features/discovery/ui/weather-icon';
33
import VoteDialog from '@/features/discovery-detail/ui/vote-dialog';
4-
import type { Discovery } from '@/entities/discovery';
4+
import type { Resort, Url } from '@/entities/discovery';
55
import { BusIcon, LiftIcon, VoteIcon } from '@/shared/icons';
66
import { cn } from '@/shared/lib';
7+
import { getWeatherFromDescription } from '@/shared/lib/getWeatherFromDescription';
78
import Card from '@/shared/ui/card';
89
import { DiscoverySummaryActionList } from '../model/constants';
910
import DiscoverySummaryAction from './discovery-summary-action';
1011

11-
const DiscoverySummary = ({ id, name, slope, url, weather }: Discovery) => {
12+
const DiscoverySummary = ({
13+
resortId,
14+
name,
15+
openSlopes,
16+
currentWeather,
17+
bus,
18+
homepage,
19+
}: Resort & Url) => {
1220
return (
1321
<div className={cn('flex w-full gap-[26px] px-5 pb-[34px] pt-[10px] md:px-[30px]')}>
1422
<Card
1523
className={cn(
16-
'flex flex-col justify-center gap-[5px] h-[134px] flex-1 items-center pl-[30px] pr-6 md:h-[123px]'
24+
'flex h-[134px] flex-1 flex-col items-center justify-center gap-[5px] pl-[30px] pr-6 md:h-[123px]'
1725
)}
1826
>
19-
<div className={cn('flex w-full justify-between items-center')}>
27+
<div className={cn('flex w-full items-center justify-between')}>
2028
<p className={cn('title1 text-gray-90')}>{name}</p>
21-
<div className={cn('flex gap-2 items-center')}>
22-
<WeatherIcon weather={weather.weather} />
23-
<p className={cn('font-semibold text-[30px] leading-tight')}>{weather.temperature}°</p>
29+
<div className={cn('flex items-center gap-2')}>
30+
<WeatherIcon weather={getWeatherFromDescription(currentWeather.description)} />
31+
<p className={cn('text-[30px] font-semibold leading-tight')}>
32+
{currentWeather.temperature}°
33+
</p>
2434
</div>
2535
</div>
26-
<div className={cn('flex w-full justify-between items-center')}>
36+
<div className={cn('flex w-full items-center justify-between')}>
2737
<p className={cn('body1-medium text-gray-60')}>
28-
{slope ? `운행중인 슬로프 ${slope}개` : '개장일이 곧 공개될 예정이에요'}
38+
{openSlopes ? `운행중인 슬로프 ${openSlopes}개` : '개장일이 곧 공개될 예정이에요'}
2939
</p>
30-
<p className={cn('body1-semibold text-gray-60')}>{weather.description}</p>
40+
<p className={cn('body1-semibold text-gray-60')}>{currentWeather.description}</p>
3141
</div>
3242
</Card>
3343
<Card className={cn('hidden h-[123px] items-center justify-center gap-[30px] px-5 md:flex')}>
@@ -36,19 +46,15 @@ const DiscoverySummary = ({ id, name, slope, url, weather }: Discovery) => {
3646
return (
3747
<VoteDialog
3848
key={action.name}
39-
id={id}
49+
id={resortId}
4050
trigger={
4151
<DiscoverySummaryAction key={action.name} {...action} icon={<VoteIcon />} />
4252
}
4353
/>
4454
);
4555
} else {
4656
return (
47-
<Link
48-
key={action.name}
49-
href={action.name === 'bus' ? url.bus : url.homepage}
50-
target="_blank"
51-
>
57+
<Link key={action.name} href={action.name === 'bus' ? bus : homepage} target="_blank">
5258
<DiscoverySummaryAction
5359
{...action}
5460
icon={action.name === 'bus' ? <BusIcon /> : <LiftIcon />}

src/widgets/discovery/ui/discovery-card.tsx

+15-19
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { useRouter } from 'next/navigation';
22
import WeatherIcon from '@/features/discovery/ui/weather-icon';
3-
import type { Discovery } from '@/entities/discovery';
3+
import type { Resort } from '@/entities/discovery';
44
import { cn } from '@/shared/lib';
5+
import { getWeatherFromDescription } from '@/shared/lib/getWeatherFromDescription';
56
import Card from '@/shared/ui/card';
6-
import { getTargetDateWeekday } from '../lib/getTargetDateWeekday';
77
import WeeklyWeather from './weekly-weather';
88

9-
const DiscoveryCard = ({ id, name, slope, weather, weeklyWeather }: Discovery) => {
9+
const DiscoveryCard = ({ resortId, name, openSlopes, currentWeather, weeklyWeather }: Resort) => {
1010
const router = useRouter();
1111

1212
return (
@@ -15,36 +15,32 @@ const DiscoveryCard = ({ id, name, slope, weather, weeklyWeather }: Discovery) =
1515
'box-border flex cursor-pointer flex-col gap-[15px] pb-4 pt-[34px] md:pb-[26px] md:pt-10',
1616
'transition-all hover:scale-[1.02] hover:border-2 hover:border-main-1/30 hover:pb-[15px] hover:pt-[33px] hover:md:pb-[25px] hover:md:pt-[39px]'
1717
)}
18-
onClick={() => router.push(`/${id}`)}
18+
onClick={() => router.push(`/${resortId}`)}
1919
>
2020
<div className={cn('mx-[30px] flex flex-col items-center gap-[5px] md:mx-[42px]')}>
2121
<div className={cn('flex w-full items-center justify-between')}>
2222
<h2 className={cn('title1 md:h2 text-gray-90')}>{name}</h2>
2323
<div className={cn('flex items-center gap-2')}>
24-
<WeatherIcon className={cn('origin-right scale-[1.17]')} weather={weather.weather} />
25-
<p className={cn('text-[30px] font-semibold leading-tight')}>{weather.temperature}°</p>
24+
<WeatherIcon
25+
className={cn('origin-right scale-[1.17]')}
26+
weather={getWeatherFromDescription(currentWeather.description)}
27+
/>
28+
<p className={cn('text-[30px] font-semibold leading-tight')}>
29+
{currentWeather.temperature}°
30+
</p>
2631
</div>
2732
</div>
2833
<div className={cn('flex w-full justify-between')}>
2934
<p className={cn('body1-medium text-gray-60')}>
30-
{slope ? `운행중인 슬로프 ${slope}개` : '개장일이 곧 공개될 예정이에요'}
35+
{openSlopes ? `운행중인 슬로프 ${openSlopes}개` : '개장일이 곧 공개될 예정이에요'}
3136
</p>
32-
<p className={cn('body1-semibold text-gray-60')}>{weather.description}</p>
37+
<p className={cn('body1-semibold text-gray-60')}>{currentWeather.description}</p>
3338
</div>
3439
</div>
3540
<hr className={cn('mx-[30px] border-gray-80 opacity-[0.04]')} />
36-
<ul
37-
className={cn(
38-
'flex w-full justify-between gap-[2px] overflow-x-scroll px-[30px] scrollbar-hide'
39-
)}
40-
>
41+
<ul className={cn('flex w-full gap-[2px] overflow-x-scroll px-[30px] scrollbar-hide')}>
4142
{weeklyWeather.map((weather, index) => (
42-
<WeeklyWeather
43-
key={index}
44-
{...weather}
45-
day={getTargetDateWeekday(index)}
46-
isToday={index === 0}
47-
/>
43+
<WeeklyWeather key={index} {...weather} isToday={index === 0} />
4844
))}
4945
</ul>
5046
</Card>

src/widgets/discovery/ui/discovery-list.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { Discovery } from '@/entities/discovery';
1+
import type { Resort } from '@/entities/discovery';
22
import { cn } from '@/shared/lib';
33
import DiscoveryCard from './discovery-card';
44

5-
const DiscoveryList = ({ discoveryData }: { discoveryData: Discovery[] }) => (
5+
const DiscoveryList = ({ resorts }: { resorts: Resort[] }) => (
66
<div className={cn('flex flex-col gap-4 px-5 py-4 md:px-8')}>
7-
{discoveryData.map((discovery) => (
8-
<DiscoveryCard key={discovery.id} {...discovery} />
7+
{resorts.map((resort) => (
8+
<DiscoveryCard key={resort.resortId} {...resort} />
99
))}
1010
</div>
1111
);

0 commit comments

Comments
 (0)