Skip to content

Commit 3781778

Browse files
authored
Merge pull request #104 from Luganic/listpage
Listpage
2 parents d327327 + a8ce331 commit 3781778

File tree

6 files changed

+223
-56
lines changed

6 files changed

+223
-56
lines changed

src/components/common/winelist/WineFilter.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export default function WineFilter() {
3333
와인 등록하기
3434
</Button>
3535
</div>
36-
3736
<div className='flex-1 flex flex-col items-end gap-[18px] text-gray-500 [&_label]:top-[10px] md:[&_label]:top-[14px] xl:[&_label]:top-[14px]'>
3837
<Input
3938
id='wine-search'
@@ -52,7 +51,6 @@ export default function WineFilter() {
5251
</div>
5352
</div>
5453
</div>
55-
5654
{/* Tablet: 필터 버튼 + 검색창 + 등록 버튼 */}
5755
<div className='hidden md:flex xl:hidden flex-row items-center px-[20px] mt-[24px] md:mt-[50px] md:mb-[80px]'>
5856
<Button
@@ -64,7 +62,6 @@ export default function WineFilter() {
6462
>
6563
<SearchButton className='w-[20px] h-[20px] text-gray-500' />
6664
</Button>
67-
6865
<div className='flex-grow ml-[24px] text-gray-500 [&_label]:top-[14px]'>
6966
<Input
7067
id='wine-search'
@@ -80,7 +77,6 @@ export default function WineFilter() {
8077
onChange={(e) => setSearchTerm(e.target.value)}
8178
/>
8279
</div>
83-
8480
<Button
8581
variant='purpleDark'
8682
size={null}
@@ -91,7 +87,6 @@ export default function WineFilter() {
9187
와인 등록하기
9288
</Button>
9389
</div>
94-
9590
{/* Mobile: 검색창 + 필터 버튼 */}
9691
<div className='flex flex-col md:hidden gap-[8px] px-[16px] mt-[24px]'>
9792
<div className='text-gray-500 [&_label]:top-[10px] min-w-[343px]'>
@@ -105,7 +100,6 @@ export default function WineFilter() {
105100
onChange={(e) => setSearchTerm(e.target.value)}
106101
/>
107102
</div>
108-
109103
<div className='w-fit mt-[15px] mb-[20px]'>
110104
<Button
111105
onClick={() => setIsFilterOpen(true)}
@@ -119,7 +113,6 @@ export default function WineFilter() {
119113
</div>
120114
</div>
121115
{isFilterOpen && <FilterModal open={isFilterOpen} onOpenChange={setIsFilterOpen} />}
122-
123116
{/* Mobile: 하단 고정 등록 버튼 */}
124117
<div className='fixed bottom-[20px] left-0 right-0 z-10 px-[16px] md:hidden'>
125118
<Button

src/components/common/winelist/WineListCard.tsx

Lines changed: 21 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,38 @@
1+
import { useRef } from 'react';
2+
13
import Link from 'next/link';
24

35
import NextIcon from '@/assets/icons/Next.svg';
46
import StarIcon from '@/assets/icons/star.svg';
57
import { ImageCard } from '@/components/common/card/ImageCard';
68
import { Button } from '@/components/ui/button';
9+
import { useInfiniteScroll } from '@/hooks/useInfiniteScroll';
10+
import { useWineListQuery } from '@/hooks/useWineListQuery';
711
import { cn } from '@/lib/utils';
8-
import useFilterStore from '@/stores/filterStore';
9-
import useWineSearchKeywordStore from '@/stores/searchStore';
10-
import useWineStore from '@/stores/wineAddStore';
1112

1213
export default function WineListCard() {
13-
const type = useFilterStore((state) => state.type);
14-
const minPrice = useFilterStore((state) => state.minPrice);
15-
const maxPrice = useFilterStore((state) => state.maxPrice);
16-
const rating = useFilterStore((state) => state.rating);
17-
18-
const wines = useWineStore((state) => state.wines); // 와인 타입 정의, mock
14+
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError } =
15+
useWineListQuery();
1916

20-
const { searchTerm } = useWineSearchKeywordStore();
17+
const observerRef = useRef<HTMLDivElement | null>(null);
2118

22-
/* 별점 범위 필터 */
23-
const ratingRangeMap: Record<string, [number, number]> = {
24-
all: [0, 5],
25-
'4.6': [4.5, 5],
26-
'4.1': [4.0, 4.5],
27-
'3.6': [3.5, 4.0],
28-
'3.1': [3.0, 3.5],
29-
};
19+
useInfiniteScroll({
20+
targetRef: observerRef,
21+
hasNextPage,
22+
fetchNextPage,
23+
isFetching: isFetchingNextPage,
24+
threshold: 0.3,
25+
});
3026

31-
const filteredWines = wines.filter((wine) => {
32-
/* 종류 필터 */
33-
if (type && wine.type !== type) return false;
34-
/* 가격 범위 필터 */
35-
if (wine.price < minPrice || wine.price > maxPrice) return false;
36-
/* 평점 필터 */
37-
if (rating !== 'all') {
38-
const [min, max] = ratingRangeMap[rating] || [0, 5];
39-
if (wine.rating < min || wine.rating > max) return false;
40-
}
41-
/* 검색어 필터 (이름 or 지역) */
42-
if (searchTerm) {
43-
const lowerCaseSearchTerm = searchTerm.toLowerCase();
44-
if (
45-
!wine.name.toLowerCase().includes(lowerCaseSearchTerm) &&
46-
!wine.region.toLowerCase().includes(lowerCaseSearchTerm)
47-
) {
48-
return false;
49-
}
50-
}
27+
if (isLoading) return <p>불러오는 중...</p>;
28+
if (isError || !data) return <p>와인 데이터를 불러올 수 없습니다.</p>;
5129

52-
return true;
53-
});
30+
/* 전체 와인 리스트 조합 */
31+
const wineList = data.pages.flatMap((page) => page.list);
5432

5533
return (
5634
<div className='flex flex-col gap-[24px] px-[16px] mt-[12px] min-w-[370px] md:px-[20px] md:mt-[24px] xl:px-0 max-w-[1140px] mx-auto xl:max-w-[800px]'>
57-
{filteredWines.map((wine) => (
35+
{wineList.map((wine) => (
5836
<Link href={`/wines/${wine.id}`} key={wine.id} className='no-underline'>
5937
<div className='w-full bg-white border border-gray-300 rounded-xl flex flex-col relative min-w-[320px]'>
6038
<ImageCard
@@ -160,6 +138,8 @@ export default function WineListCard() {
160138
</div>
161139
</Link>
162140
))}
141+
{/* 무한 스크롤 감지 */}
142+
<div ref={observerRef} className='h-[1px]' />
163143
</div>
164144
);
165145
}

src/hooks/useWineListQuery.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import useWineSearchKeywordStore from '@/stores/searchStore';
77
const PAGE_LIMIT = 8;
88

99
export function useWineListQuery() {
10+
/* 각 필터 상태 */
1011
const type = useFilterStore((state) => state.type);
1112
const minPrice = useFilterStore((state) => state.minPrice);
1213
const maxPrice = useFilterStore((state) => state.maxPrice);

src/lib/getWines.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import useWinAddStore, { Wine } from '@/stores/wineAddStore';
1+
import useWineAddStore, { Wine } from '@/stores/wineAddStore';
22

33
interface GetWinesParams {
44
cursor: number;
@@ -18,6 +18,7 @@ interface GetWinesResponse {
1818
totalCount: number;
1919
}
2020

21+
/* 평점 필터링 */
2122
const ratingRangeMap: Record<string, [number, number]> = {
2223
all: [0, 5],
2324
'4.6': [4.5, 5],
@@ -26,9 +27,9 @@ const ratingRangeMap: Record<string, [number, number]> = {
2627
'3.1': [3.0, 3.5],
2728
};
2829

30+
/* 필터링 + 페이징 */
2931
export function getWines({ cursor, limit, filters }: GetWinesParams): GetWinesResponse {
30-
// zustand에서 mock 데이터 직접 가져옴
31-
const allWines = useWinAddStore.getState().wines;
32+
const allWines = useWineAddStore.getState().wines;
3233

3334
const { type, minPrice = 0, maxPrice = Infinity, rating = 'all', searchTerm = '' } = filters;
3435

src/pages/wines/index.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import WineFilter from '@/components/common/winelist/WineFilter';
2+
import WineListCard from '@/components/common/winelist/WineListCard';
3+
import WineSlider from '@/components/common/winelist/WineSlider';
4+
5+
////
6+
export default function Wines() {
7+
return (
8+
<div>
9+
<WineSlider />
10+
<WineFilter />
11+
<div className='xl:hidden'>
12+
<WineListCard />
13+
</div>
14+
</div>
15+
);
16+
}

0 commit comments

Comments
 (0)