diff --git a/src/components/common/winelist/WineFilter.tsx b/src/components/common/winelist/WineFilter.tsx index 33d2bf51..7adeb3bf 100644 --- a/src/components/common/winelist/WineFilter.tsx +++ b/src/components/common/winelist/WineFilter.tsx @@ -5,12 +5,16 @@ import WineTypeFilter from '@/components/common/Filter/WineTypeFilter'; import Input from '@/components/common/Input'; import WineListCard from '@/components/common/winelist/WineListCard'; import FilterModal from '@/components/Modal/FilterModal/FilterModal'; + +import AddWineModal from '@/components/Modal/WineModal/AddWineModal'; import { Button } from '@/components/ui/button'; -import useSearchStore from '@/stores/searchStore'; +import useWineSearchKeywordStore from '@/stores/searchStore'; export default function WineFilter() { const [isFilterOpen, setIsFilterOpen] = useState(false); - const { searchTerm, setSearchTerm } = useSearchStore(); + const [showRegisterModal, setShowRegisterModal] = useState(false); + + const { searchTerm, setSearchTerm } = useWineSearchKeywordStore(); return (
@@ -24,6 +28,7 @@ export default function WineFilter() { variant='purpleDark' size='md' width={null} + onClick={() => setShowRegisterModal(true)} className='ml-[30px] mb-[200px] w-[284px]' > 와인 등록하기 @@ -81,6 +86,7 @@ export default function WineFilter() { variant='purpleDark' size={null} width={null} + onClick={() => setShowRegisterModal(true)} className='w-[220px] h-[48px] rounded-[16px] flex-shrink-0 ml-[16px]' > 와인 등록하기 @@ -121,11 +127,16 @@ export default function WineFilter() { variant='purpleDark' size='sm' width='full' + onClick={() => setShowRegisterModal(true)} className='w-full h-[48px] rounded-[12px] text-sm' > 와인 등록하기
+ ); } diff --git a/src/components/common/winelist/WineListCard.tsx b/src/components/common/winelist/WineListCard.tsx index 7d3e12a3..341fb4ca 100644 --- a/src/components/common/winelist/WineListCard.tsx +++ b/src/components/common/winelist/WineListCard.tsx @@ -6,65 +6,9 @@ import { ImageCard } from '@/components/common/card/ImageCard'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import useFilterStore from '@/stores/filterStore'; -import useSearchStore from '@/stores/searchStore'; - -interface Wine { - id: number; - name: string; - region: string; - image: string; - price: number; - rating: number; - type: 'Red' | 'White' | 'Sparkling'; - review: string; -} -const mockWines: Wine[] = [ - { - id: 1, - name: 'Sentinel Carbernet Sauvignon 2016', - region: 'Western Cape, South Africa', - image: '/images/image1.svg', - price: 64900, - rating: 4.5, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 2, - name: 'Palazzo della Torre 2017', - region: 'Western Cape, South Africa', - image: '/images/image3.svg', - price: 64900, - rating: 4.6, - type: 'White', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 3, - name: 'Sentinel Carbernet Sauvignon 2016', - region: 'Western Cape, South Africa', - image: '/images/image2.svg', - price: 59900, - rating: 3.6, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 4, - name: 'Palazzo della Torre 2017', - region: 'Western Cape, South Africa', - image: '/images/image4.svg', - price: 74000, - rating: 2.1, - type: 'Sparkling', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, -]; +import useWineSearchKeywordStore from '@/stores/searchStore'; +import useWineStore from '@/stores/wineAddStore'; export default function WineListCard() { const type = useFilterStore((state) => state.type); @@ -72,7 +16,10 @@ export default function WineListCard() { const maxPrice = useFilterStore((state) => state.maxPrice); const rating = useFilterStore((state) => state.rating); - const { searchTerm } = useSearchStore(); + const wines = useWineStore((state) => state.wines); // 와인 타입 정의, mock + + const { searchTerm } = useWineSearchKeywordStore(); + /* 별점 범위 필터 */ const ratingRangeMap: Record = { @@ -83,7 +30,9 @@ export default function WineListCard() { '3.1': [3.0, 3.5], }; - const filteredWines = mockWines.filter((wine) => { + + const filteredWines = wines.filter((wine) => { + /* 종류 필터 */ if (type && wine.type !== type) return false; /* 가격 범위 필터 */ diff --git a/src/hooks/useWineListQuery.ts b/src/hooks/useWineListQuery.ts new file mode 100644 index 00000000..529d42ab --- /dev/null +++ b/src/hooks/useWineListQuery.ts @@ -0,0 +1,27 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; + +import { getWines } from '@/lib/getWines'; +import useFilterStore from '@/stores/filterStore'; +import useWineSearchKeywordStore from '@/stores/searchStore'; + +const PAGE_LIMIT = 8; + +export function useWineListQuery() { + const type = useFilterStore((state) => state.type); + const minPrice = useFilterStore((state) => state.minPrice); + const maxPrice = useFilterStore((state) => state.maxPrice); + const rating = useFilterStore((state) => state.rating); + const searchTerm = useWineSearchKeywordStore((state) => state.searchTerm); + + return useInfiniteQuery({ + queryKey: ['wines', { type, minPrice, maxPrice, rating, searchTerm }], + queryFn: ({ pageParam = 0 }) => + getWines({ + cursor: pageParam, + limit: PAGE_LIMIT, + filters: { type, minPrice, maxPrice, rating, searchTerm }, + }), + initialPageParam: 0, + getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, + }); +} diff --git a/src/lib/getWines.ts b/src/lib/getWines.ts new file mode 100644 index 00000000..5dcc0304 --- /dev/null +++ b/src/lib/getWines.ts @@ -0,0 +1,60 @@ +import useWinAddStore, { Wine } from '@/stores/wineAddStore'; + +interface GetWinesParams { + cursor: number; + limit: number; + filters: { + type?: string; + minPrice?: number; + maxPrice?: number; + rating?: string; + searchTerm?: string; + }; +} + +interface GetWinesResponse { + list: Wine[]; + nextCursor: number | null; + totalCount: number; +} + +const ratingRangeMap: Record = { + all: [0, 5], + '4.6': [4.5, 5], + '4.1': [4.0, 4.5], + '3.6': [3.5, 4.0], + '3.1': [3.0, 3.5], +}; + +export function getWines({ cursor, limit, filters }: GetWinesParams): GetWinesResponse { + // zustand에서 mock 데이터 직접 가져옴 + const allWines = useWinAddStore.getState().wines; + + const { type, minPrice = 0, maxPrice = Infinity, rating = 'all', searchTerm = '' } = filters; + + const [minRating, maxRating] = ratingRangeMap[rating] ?? [0, 5]; + const keyword = searchTerm.toLowerCase(); + + const filtered = allWines.filter((wine) => { + if (type && wine.type !== type) return false; + if (wine.price < minPrice || wine.price > maxPrice) return false; + if (wine.rating < minRating || wine.rating > maxRating) return false; + if ( + keyword && + !wine.name.toLowerCase().includes(keyword) && + !wine.region.toLowerCase().includes(keyword) + ) + return false; + return true; + }); + + const totalCount = filtered.length; + const paged = filtered.slice(cursor, cursor + limit); + const nextCursor = cursor + limit < totalCount ? cursor + limit : null; + + return { + list: paged, + nextCursor, + totalCount, + }; +} diff --git a/src/stores/searchStore.ts b/src/stores/searchStore.ts index 5c0a1d68..74cf3156 100644 --- a/src/stores/searchStore.ts +++ b/src/stores/searchStore.ts @@ -5,9 +5,11 @@ type SearchState = { setSearchTerm: (term: string) => void; }; -const useSearchStore = create((set) => ({ +const useWineSearchKeywordStore = create((set) => ({ + searchTerm: '', setSearchTerm: (term) => set({ searchTerm: term }), })); -export default useSearchStore; +export default useWineSearchKeywordStore; + diff --git a/src/stores/wineAddStore.ts b/src/stores/wineAddStore.ts new file mode 100644 index 00000000..5bcf621b --- /dev/null +++ b/src/stores/wineAddStore.ts @@ -0,0 +1,73 @@ +import { create } from 'zustand'; + +export interface Wine { + id: number; + name: string; + region: string; + image: string; + price: number; + rating: number; + type: 'Red' | 'White' | 'Sparkling'; + review: string; +} + +interface WineStoreState { + wines: Wine[]; + addWine: (newWine: Wine) => void; +} + +const useWinAddStore = create((set) => ({ + wines: [ + { + id: 1, + name: 'Sentinel Carbernet Sauvignon 2016', + region: 'Western Cape, South Africa', + image: '/images/image1.svg', + price: 64900, + rating: 4.5, + type: 'Red', + review: + 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', + }, + { + id: 2, + name: 'Palazzo della Torre 2017', + region: 'Western Cape, South Africa', + image: '/images/image3.svg', + price: 64900, + rating: 4.6, + type: 'White', + review: + 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', + }, + { + id: 3, + name: 'Sentinel Carbernet Sauvignon 2016', + region: 'Western Cape, South Africa', + image: '/images/image2.svg', + price: 59900, + rating: 3.6, + type: 'Red', + review: + 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', + }, + { + id: 4, + name: 'Palazzo della Torre 2017', + region: 'Western Cape, South Africa', + image: '/images/image4.svg', + price: 74000, + rating: 2.1, + type: 'Sparkling', + review: + 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', + }, + ], + + addWine: (newWine) => + set((state) => ({ + wines: [newWine, ...state.wines], + })), +})); + +export default useWinAddStore;