Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
File renamed without changes
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);
}}
>
{option}
</Badge>
Expand Down
33 changes: 27 additions & 6 deletions src/components/common/winelist/WineFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
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 AddWineModal from '@/components/Modal/WineModal/AddWineModal';
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 [showRegisterModal, setShowRegisterModal] = 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 @@ -19,6 +27,7 @@ export default function WineFilter() {
variant='purpleDark'
size='md'
width={null}
onClick={() => setShowRegisterModal(true)}
className='ml-[30px] mb-[200px] w-[284px]'
>
와인 등록하기
Expand All @@ -35,8 +44,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 +56,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,13 +76,16 @@ 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>

<Button
variant='purpleDark'
size={null}
width={null}
onClick={() => setShowRegisterModal(true)}
className='w-[220px] h-[48px] rounded-[16px] flex-shrink-0 ml-[16px]'
>
와인 등록하기
Expand All @@ -81,19 +94,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,18 +118,24 @@ 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'>
<Button
variant='purpleDark'
size='sm'
width='full'
onClick={() => setShowRegisterModal(true)}
className='w-full h-[48px] rounded-[12px] text-sm'
>
와인 등록하기
</Button>
</div>
<AddWineModal
showRegisterModal={showRegisterModal}
setShowRegisterModal={setShowRegisterModal}
/>
</div>
);
}
129 changes: 52 additions & 77 deletions src/components/common/winelist/WineListCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,56 +5,57 @@ 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';

const mockWines = [
{
id: 1,
name: 'Sentinel Carbernet Sauvignon 2016',
region: 'Western Cape, South Africa',
image: '/images/image1.svg',
price: 64900,
rating: 4.8,
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,
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: 4.6,
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: 3.1,
review:
'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.',
},
];
import useFilterStore from '@/stores/filterStore';
import useSearchStore from '@/stores/searchStore';
import useWineStore from '@/stores/wineAddStore';

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 wines = useWineStore((state) => state.wines); // 와인 타입 정의, mock

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 = wines.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 +70,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 +110,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 +118,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 +140,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
Loading
Loading