Skip to content

Commit d6c57dc

Browse files
authored
[FE] 건물 상세페이지를 구현한다. (#1224)
1 parent 2452d9b commit d6c57dc

15 files changed

Lines changed: 540 additions & 10 deletions

File tree

frontend/src/apis/building.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fetcher from '@/apis/fetcher';
22
import { BASE_URL, ENDPOINT } from '@/apis/url';
3-
import { PaginationParams } from '@/types/api';
4-
import { BuildingsResponse } from '@/types/building';
3+
import { InfinitePaginationParams, PaginationParams } from '@/types/api';
4+
import { BuildingDetailResponse, BuildingsResponse } from '@/types/building';
55

66
export const getBuildingList = async (
77
searchParams: { search?: string; subways?: string },
@@ -14,3 +14,12 @@ export const getBuildingList = async (
1414
const data = await response.json();
1515
return data as BuildingsResponse;
1616
};
17+
18+
export const getBuildingDetail = async (buildingId: number, params: InfinitePaginationParams) => {
19+
const response = await fetcher.get({
20+
url: BASE_URL + ENDPOINT.BUILDING_DETAIL(buildingId),
21+
params: {...params}
22+
});
23+
const data = await response.json();
24+
return data as BuildingDetailResponse;
25+
};

frontend/src/apis/url.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const ENDPOINT = {
2828
ARTICLE_ID: (id: number) => `/articles/${id}`,
2929
// building
3030
BUILDING_LIST: '/buildings',
31+
BUILDING_DETAIL: (buildingId: number) => `/buildings/${buildingId}/checklists`,
3132
// kakao login
3233
OAUTH_LOGIN: '/oauth/login',
3334
// basic login
Lines changed: 2 additions & 2 deletions
Loading

frontend/src/components/Building/BuildingCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Building } from '@/types/building';
1010

1111
function BuildingCard({ buildingId, buildingName, checklistCount, station, isLiked, thumbnail }: Building) {
1212
return (
13-
<Link to={`${ROUTE_PATH.buildingList}/${buildingId}`}>
13+
<Link to={`${ROUTE_PATH.buildingDetail(buildingId)}`}>
1414
<FlexBox.Horizontal padding="0.8rem 0" gap="1.6rem" onClick={() => {}}>
1515
<S.Image alt="건물 이미지" src={thumbnail} />
1616

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import useMouseDrag from "@/hooks/useMouseDrag";
2+
import styled from "@emotion/styled";
3+
import { useRef, useState } from "react";
4+
5+
export const Carousel = ({images}:{images:string[]}) => {
6+
const scrollRef = useRef<HTMLDivElement>(null);
7+
const [currentIndex, setCurrentIndex] = useState(0);
8+
9+
const maxIndex = images.length ;
10+
11+
const mod = (n:number, m:number) => (n % m + m) % m;
12+
useMouseDrag(scrollRef, (start, end) => {
13+
if (end.x - start.x > 100) {
14+
setCurrentIndex(mod(currentIndex - 1,maxIndex));
15+
} else if (start.x - end.x > 100) {
16+
setCurrentIndex(mod(currentIndex + 1,maxIndex));
17+
}
18+
});
19+
20+
21+
return (
22+
<SC.Wrapper ref={scrollRef} >
23+
<SC.ScrollBox >
24+
{[...images].map(image => (
25+
<SC.Slide key={image}>
26+
<img src={image} draggable={false} alt="image" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
27+
28+
</SC.Slide>
29+
))}
30+
</SC.ScrollBox>
31+
32+
<SC.DotContainer>
33+
{[...images].map((_, idx) => (
34+
<SC.Dot key={idx} active={idx === currentIndex} />
35+
))}
36+
</SC.DotContainer>
37+
</SC.Wrapper>
38+
);
39+
};
40+
41+
42+
const SC = {
43+
Wrapper: styled.section`
44+
display: flex;
45+
position: relative;
46+
width: 100%;
47+
flex-direction: column;
48+
align-items: center;
49+
`,
50+
51+
ScrollBox: styled.div`
52+
display: flex;
53+
overflow-x: auto;
54+
scroll-snap-type: x mandatory;
55+
-webkit-overflow-scrolling: touch;
56+
width: 100%;
57+
scroll-behavior: smooth;
58+
59+
&::-webkit-scrollbar {
60+
display: none;
61+
}
62+
`,
63+
Slide: styled.div`
64+
flex-shrink: 0;
65+
scroll-snap-align: start;
66+
width: 100%;
67+
min-width: 100%;
68+
height: 313px;
69+
`,
70+
DotContainer: styled.div`
71+
display: flex;
72+
position: absolute;
73+
bottom: 10px;
74+
margin-top: 1rem;
75+
gap: 0.5rem;
76+
`,
77+
Dot: styled.div<{ active: boolean }>`
78+
width: 8px;
79+
height: 8px;
80+
border-radius: 50%;
81+
82+
background-color: ${({ active, theme }) => (active ? theme.color.gray[500] : theme.color.gray[100])};
83+
transition: background-color 0.3s;
84+
`,
85+
};
86+
87+

frontend/src/constants/queryKeys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ export const QUERY_KEYS = {
1111
ROOM_COMPARE: 'room/compare',
1212
ROOM_CATEGORY_DETAIL: '/room/category/detail',
1313
BUILDING_LIST: 'building-list',
14+
BUILDING_DETAIL: 'building-detail',
1415
};

frontend/src/constants/routePath.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const ROUTE_PATH = {
2323
/* building-list */
2424
buildingList: '/building-list',
2525
buildingMap: '/building-map',
26-
26+
buildingDetail: (id:string|number)=>`/building-detail/${id}`,
2727
/* etc */
2828
location: '/location',
2929
myPage: '/my-page',
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
3+
import { getBuildingDetail } from '@/apis/building';
4+
import { QUERY_KEYS } from '@/constants/queryKeys';
5+
import { InfinitePaginationParams } from '@/types/api';
6+
7+
export const useGetBuildingDetailQuery = (
8+
buildingId: number,
9+
params: InfinitePaginationParams,
10+
) => {
11+
return useQuery({
12+
queryKey: [QUERY_KEYS.BUILDING_DETAIL, buildingId, params],
13+
queryFn: () => getBuildingDetail(buildingId, params),
14+
});
15+
};
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { BuildingDetailResponse } from "@/types/building";
2+
3+
export const buildingDetailResponse: BuildingDetailResponse = {
4+
"buildingId": 1,
5+
"buildingName": "방끗 빌라",
6+
"address": "서울특별시 강서구 허준로 121",
7+
"latitude": 34.156478,
8+
"longitude": 34.156478,
9+
"checklistCount": 45,
10+
"stations": [
11+
{
12+
"stationName": "잠실",
13+
"stationLine": "2호선",
14+
"walkingTime": 11
15+
},
16+
{
17+
"stationName": "잠실",
18+
"stationLine": "8호선",
19+
"walkingTime": 12
20+
}
21+
],
22+
"isLiked": false,
23+
"photos": [
24+
"http://placehold.co/393x313",
25+
"http://placehold.co/393x313"
26+
],
27+
"checklists": [
28+
{
29+
"checklistId": 2,
30+
"userName": "bangggood123",
31+
"roomName": "살기 좋은 방",
32+
"deposit": 2000,
33+
"rent": 50,
34+
"optionCount": 5,
35+
"categories": [
36+
{
37+
"categoryId": 1,
38+
"categoryName": '방 컨디션',
39+
"score": 70
40+
},
41+
{
42+
"categoryId": 2,
43+
"categoryName": "편의시설",
44+
"score": 60
45+
}
46+
],
47+
"createdAt": "2025-10-01"
48+
},
49+
{
50+
"checklistId": 1,
51+
"userName": "bangggood123",
52+
"roomName": "살기 좋은 방",
53+
"deposit": 2000,
54+
"rent": 50,
55+
"optionCount": 5,
56+
"categories": [
57+
{
58+
"categoryId": 1,
59+
"categoryName": "청결",
60+
"score": 70
61+
},
62+
{
63+
"categoryId": 2,
64+
"categoryName": "편의시설",
65+
"score": 60
66+
}
67+
],
68+
"createdAt": "2025-10-01"
69+
}
70+
],
71+
"lastCursor": "2025-10-01"
72+
}
73+

frontend/src/mocks/handlers/building.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { http, HttpResponse } from 'msw';
22

33
import { BASE_URL, ENDPOINT } from '@/apis/url';
4+
import { buildingDetailResponse } from '@/mocks/fixtures/building';
45
import { buildings } from '@/mocks/fixtures/buildingList';
56

67
export const buildingHandlers = [
@@ -24,4 +25,7 @@ export const buildingHandlers = [
2425
totalElements: filteredBuildings.length,
2526
});
2627
}),
28+
http.get(BASE_URL + ENDPOINT.BUILDING_DETAIL(1), () => {
29+
return HttpResponse.json(buildingDetailResponse, { status: 200 });
30+
}),
2731
];

0 commit comments

Comments
 (0)