Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/assets/icons/Star.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 0 additions & 3 deletions src/assets/icons/star.svg

This file was deleted.

40 changes: 19 additions & 21 deletions src/components/Modal/FilterModal/FilterModal.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,31 @@
import React, { useState } from 'react';
import React from 'react';

import Filterbtn from '@/assets/icons/filterbtn.svg';
import WineTypeFilter from '@/components/common/Filter/WineTypeFilter';
import BasicModal from '@/components/common/Modal/BasicModal';
import { Button } from '@/components/ui/button';
import useFilterStore from '@/stores/filterStore';

import BasicModal from '../../common/Modal/BasicModal';
import { Button } from '../../ui/button';

const FilterModal = () => {
const [showRegisterModal, setShowRegisterModal] = useState(false);

const FilterModal = ({
open,
onOpenChange,
}: {
open: boolean;
onOpenChange: (isOpen: boolean) => void;
}) => {
const reset = useFilterStore((state) => state.reset);

//모달창 끄면 리셋되게
const closeModalReset = (isOpen: boolean) => {
setShowRegisterModal(isOpen);
if (!isOpen) reset();
const handleApplyFilter = (e: React.FormEvent) => {
e.preventDefault();
onOpenChange(false); // 필터 적용 후 모달을 close
};
////

return (
<div>
<Filterbtn
className='w-[38px] md:w-[48px] h-[38px] md:h-[48px] border border-gray-300 text-gray-500 rounded-[8px] p-2 md:p-[11px] cursor-pointer'
onClick={() => setShowRegisterModal(true)}
/>
<BasicModal
type='filter'
title='필터'
open={showRegisterModal}
onOpenChange={closeModalReset}
open={open}
onOpenChange={onOpenChange}
buttons={
<div className='flex gap-2'>
<Button
Expand All @@ -41,7 +38,8 @@ const FilterModal = () => {
초기화
</Button>
<Button
type='submit'
type='button'
onClick={handleApplyFilter}
variant='purpleDark'
size='xl'
className='w-[223px] md:w-[223px]'
Expand All @@ -52,7 +50,7 @@ const FilterModal = () => {
</div>
}
>
<form>
<form onSubmit={handleApplyFilter}>
<WineTypeFilter className='mt-[20px] mb-[25px] ' showBorder={true} hasMargin={false} />
</form>
</BasicModal>
Expand Down
4 changes: 3 additions & 1 deletion src/components/common/Filter/WineTypeFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ const WineTypeFilter = ({
key={`${option}-${index}`}
variant='chooseWineType'
className={type === option ? 'bg-purpleDark text-white' : 'bg-white'}
onClick={() => setType(option)}
onClick={() => {
if (type !== option) setType(option);
}}
Comment on lines +37 to +39
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

필터 타입 선택 시 수정한 부분 확인했습니다

>
{option}
</Badge>
Expand Down
23 changes: 17 additions & 6 deletions src/components/common/winelist/WineFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { useState } from 'react';

import SearchButton from '@/assets/icons/SearchButton.svg';
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 { Button } from '@/components/ui/button';
import useSearchStore from '@/stores/searchStore';

export default function WineFilter() {
// const [isFilterOpen, setIsFilterOpen] = useState(false);
const [isFilterOpen, setIsFilterOpen] = useState(false);
const { searchTerm, setSearchTerm } = useSearchStore();

return (
<div className='w-full max-w-[1140px] mx-auto'>
{/* PC: 필터 + 검색창 + 등록 버튼 */}
<div className='hidden xl:flex max-w-[1140px] mx-auto mt-[30px] gap-[24px]'>
<div className='hidden xl:flex max-w-[1140px] mx-auto mt-[30px] gap-[24px]'>
<div className='flex-shrink-0 w-[260px] h-auto flex flex-col gap-[50px] ml-[-28px]'>
<div className='pt-[70px] '>
<WineTypeFilter className='h-[450px]' />
Expand All @@ -35,8 +40,9 @@ export default function WineFilter() {
xl:w-[800px] h-[48px] px-[20px] py-[14px] pl-[55px]
border border-gray-300 rounded-full
'
value={searchTerm} // 검색어 연결
onChange={(e) => setSearchTerm(e.target.value)}
/>
{/* WineListCard PC에서만 노출 */}
<div className='hidden xl:flex pt-[20px]'>
<WineListCard />
</div>
Expand All @@ -46,7 +52,7 @@ export default function WineFilter() {
{/* Tablet: 필터 버튼 + 검색창 + 등록 버튼 */}
<div className='hidden md:flex xl:hidden flex-row items-center px-[20px] mt-[24px] md:mt-[50px] md:mb-[80px]'>
<Button
// onClick={() => setIsFilterOpen(true)}
onClick={() => setIsFilterOpen(true)}
variant='white'
size={null}
width={null}
Expand All @@ -66,6 +72,8 @@ export default function WineFilter() {
h-[48px] px-[20px] py-[14px] pl-[55px]
border border-gray-300 rounded-full
'
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>

Expand All @@ -81,19 +89,21 @@ export default function WineFilter() {

{/* Mobile: 검색창 + 필터 버튼 */}
<div className='flex flex-col md:hidden gap-[8px] px-[16px] mt-[24px]'>
<div className='text-gray-500 [&_label]:top-[10px]'>
<div className='text-gray-500 [&_label]:top-[10px] min-w-[343px]'>
<Input
id='wine-search'
type='text'
placeholder='와인을 검색해 보세요'
variant='search'
className='w-full h-[38px] px-[15px] py-[14px] pl-[55px] border border-gray-300 rounded-full'
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>

<div className='w-fit mt-[15px] mb-[20px]'>
<Button
// onClick={() => setIsFilterOpen(true)}
onClick={() => setIsFilterOpen(true)}
variant='white'
size='sm'
width='xs'
Expand All @@ -103,6 +113,7 @@ export default function WineFilter() {
</Button>
</div>
</div>
{isFilterOpen && <FilterModal open={isFilterOpen} onOpenChange={setIsFilterOpen} />}

{/* Mobile: 하단 고정 등록 버튼 */}
<div className='fixed bottom-[20px] left-0 right-0 z-10 px-[16px] md:hidden'>
Expand Down
106 changes: 68 additions & 38 deletions src/components/common/winelist/WineListCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,29 @@ import StarIcon from '@/assets/icons/Star.svg';
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';

const mockWines = [
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.8,
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.',
},
Expand All @@ -24,6 +38,7 @@ const mockWines = [
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.',
},
Expand All @@ -33,7 +48,8 @@ const mockWines = [
region: 'Western Cape, South Africa',
image: '/images/image2.svg',
price: 59900,
rating: 4.6,
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.',
},
Expand All @@ -43,18 +59,58 @@ const mockWines = [
region: 'Western Cape, South Africa',
image: '/images/image4.svg',
price: 74000,
rating: 3.1,
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.',
},
];

export default function WineListCard() {
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 } = useSearchStore();

/* 별점 범위 필터 */
const ratingRangeMap: Record<string, [number, number]> = {
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],
};

const filteredWines = mockWines.filter((wine) => {
/* 종류 필터 */
if (type && wine.type !== type) return false;
/* 가격 범위 필터 */
if (wine.price < minPrice || wine.price > maxPrice) return false;
/* 평점 필터 */
if (rating !== 'all') {
const [min, max] = ratingRangeMap[rating] || [0, 5];
if (wine.rating < min || wine.rating > max) return false;
}
/* 검색어 필터 (이름 or 지역) */
if (searchTerm) {
const lowerCaseSearchTerm = searchTerm.toLowerCase();
if (
!wine.name.toLowerCase().includes(lowerCaseSearchTerm) &&
!wine.region.toLowerCase().includes(lowerCaseSearchTerm)
) {
return false;
}
}

return true;
});

return (
<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]'>
{mockWines.map((wine) => (
{filteredWines.map((wine) => (
<Link href={`/wines/${wine.id}`} key={wine.id} className='no-underline'>
{/* 카드 컨테이너 */}
<div className='w-full bg-white border border-gray-300 rounded-xl flex flex-col relative min-w-[320px]'>
<ImageCard
imageSrc={wine.image}
Expand All @@ -69,46 +125,25 @@ export default function WineListCard() {
rightSlot={null}
>
<div className='flex flex-col w-full px-[16px] md:px-0'>
{/* name, region, price 버튼 */}
<div
className='flex flex-col w-full ml-0 mt-[20px] gap-[8px]
md:w-[300px] md:ml-[36px] md:gap-[10px]'
>
<div
className='custom-text-xl-semibold text-gray-800 leading-[32px] h-auto
break-words max-w-[220px] mt-[5px] md:custom-text-3xl-semibold md:max-w-none'
>
<div className='flex flex-col w-full ml-0 mt-[20px] gap-[8px] md:w-[300px] md:ml-[36px] md:gap-[10px]'>
<div className='custom-text-xl-semibold text-gray-800 leading-[32px] h-auto break-words max-w-[220px] mt-[5px] md:custom-text-3xl-semibold md:max-w-none'>
{wine.name}
</div>
<div
className='custom-text-md-regular text-gray-500 leading-[24px] h-[24px]
md:text-[16px] md:leading-[26px] md:h-[26px] break-words'
>
<div className='custom-text-md-regular text-gray-500 leading-[24px] h-[24px] md:text-[16px] md:leading-[26px] md:h-[26px] break-words'>
{wine.region}
</div>
<Button
variant='purpleLight'
fontSize={null}
width={null}
size={null}
className='
text-[14px] text-purpleDark font-bold leading-[24px]
px-[10px] py-[2px] rounded-[10px] h-[30px]
w-full max-w-[86px] md:max-w-[114px]
md:text-[18px]
md:px-[2px] md:py-[2px]
md:rounded-[12px]
md:w-[114px] md:h-[42px] md:mt-[5px]'
className='text-[14px] text-purpleDark font-bold leading-[24px] px-[10px] py-[2px] rounded-[10px] h-[30px] w-full max-w-[86px] md:max-w-[114px] md:text-[18px] md:px-[2px] md:py-[2px] md:rounded-[12px] md:w-[114px] md:h-[42px] md:mt-[5px]'
>
₩ {wine.price.toLocaleString()}
</Button>
</div>

{/* 모바일용 rating */}
<div
className='flex flex-row items-start mt-[23px] w-full ml-0 gap-[6px]
md:hidden'
>
<div className='flex flex-row items-start mt-[23px] w-full ml-0 gap-[6px] md:hidden'>
<div className='text-[28px] font-extrabold text-gray-800 w-auto h-auto'>
{wine.rating.toFixed(1)}
</div>
Expand All @@ -130,7 +165,6 @@ export default function WineListCard() {
</div>
</div>

{/* 모바일용 NextIcon */}
<div className='flex-grow md:hidden flex justify-end items-center px-[16px] pr-[10px] mt-[-35px]'>
<button type='button' className='w-[36px] h-[36px] p-[4px] rounded'>
<NextIcon className='w-full h-full text-gray-300 hover:text-gray-500' />
Expand All @@ -139,7 +173,6 @@ export default function WineListCard() {
</div>
</ImageCard>

{/* 태블릿/PC용 rating */}
<div className='hidden md:flex flex-col items-start absolute top-[40px] right-[-10px] z-10'>
<div className='text-[48px] font-extrabold text-gray-800 leading-[48px] md:mt-[9px]'>
{wine.rating.toFixed(1)}
Expand All @@ -162,19 +195,16 @@ export default function WineListCard() {
</div>
</div>

{/* 태블릿/PC용 NextIcon */}
<button
type='button'
className='w-[40px] h-[40px] p-[4px] rounded flex-shrink-0 ml-0 hidden md:block absolute top-[185px] right-[50px] z-20'
>
<NextIcon className='w-full h-full text-gray-300 hover:text-gray-500' />
</button>

{/* 구분선 */}
<div className='w-full h-px bg-gray-300 mt-[-1px]' />

{/* 후기 영역 */}
<div className='px-[16px] py-[20px]'>
<div className='px-[25px] py-[20px]'>
<div className='text-[14px] font-semibold text-gray-800 mb-[4px] break-words'>
최신 후기
</div>
Expand Down
13 changes: 13 additions & 0 deletions src/stores/searchStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { create } from 'zustand';

type SearchState = {
searchTerm: string;
setSearchTerm: (term: string) => void;
};

const useSearchStore = create<SearchState>((set) => ({
searchTerm: '',
setSearchTerm: (term) => set({ searchTerm: term }),
}));

export default useSearchStore;