Skip to content

Commit 163f90a

Browse files
authored
Merge pull request #133 from Luganic/fix/winelistcard
Fix/와인 카드 호버 영역 수정 및 상단 바로가기 버튼 생성
2 parents b839dca + 20f512c commit 163f90a

File tree

5 files changed

+92
-10
lines changed

5 files changed

+92
-10
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useEffect, useState } from 'react';
2+
3+
import NextIcon from '@/assets/icons/Next.svg';
4+
5+
export default function ScrollToTop() {
6+
const [isVisible, setIsVisible] = useState(false);
7+
8+
useEffect(() => {
9+
const handleScroll = () => {
10+
setIsVisible(window.scrollY > 3000);
11+
};
12+
13+
window.addEventListener('scroll', handleScroll);
14+
return () => window.removeEventListener('scroll', handleScroll);
15+
}, []);
16+
17+
const scrollToTop = () => {
18+
window.scrollTo({ top: 0, behavior: 'smooth' });
19+
};
20+
21+
if (!isVisible) return null;
22+
23+
/*뱃지 색깔: bg-primary-100 통일: bg-primary*/
24+
return (
25+
<button
26+
onClick={scrollToTop}
27+
className='
28+
fixed bottom-[157px] right-[20px] md:bottom-[90px] z-50
29+
rounded-full bg-gray-300
30+
hover:bg-primary
31+
p-[12px] shadow-lg transition
32+
group
33+
'
34+
aria-label='Scroll to top'
35+
>
36+
<NextIcon className='w-[30px] h-[30px] rotate-[-90deg] text-white group-hover:text-white' />
37+
</button>
38+
);
39+
}

src/components/common/winelist/WineCard.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,22 @@ interface WineCardProps {
99
image: string;
1010
name: string;
1111
rating: number;
12+
isCarouselEnd?: boolean;
1213
}
1314

14-
export default function WineCard({ id, image, name, rating }: WineCardProps) {
15+
export default function WineCard({ id, image, name, rating, isCarouselEnd }: WineCardProps) {
1516
return (
16-
<Link href={`/wines/${id}`} key={id} className='no-underline'>
17+
<Link
18+
href={`/wines/${id}`}
19+
key={id}
20+
className='no-underline'
21+
onClick={(e) => {
22+
if (isCarouselEnd) {
23+
e.preventDefault();
24+
e.stopPropagation();
25+
}
26+
}}
27+
>
1728
<div
1829
className={cn(
1930
'flex h-[160px] bg-white p-2 border border-gray-200 ',

src/components/common/winelist/WineListCard.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@ export default function WineListCard() {
5454

5555
return (
5656
<div
57-
className='flex flex-col gap-[24px] px-[16px] mt-[12px] min-w-[370px] h-[390px]
57+
className='flex flex-col gap-[24px] px-[16px] mt-[12px] min-w-[370px]
5858
md:px-[20px] md:mt-[24px]
5959
xl:px-0 max-w-[1140px] mx-auto xl:max-w-[800px] xl:800px'
6060
>
6161
{wineList?.map((wine) => (
6262
<Link href={`/wines/${wine.id}`} key={wine.id} className='no-underline'>
63-
<div className='w-full bg-white border border-gray-300 rounded-xl flex flex-col relative min-w-[320px]'>
63+
<div className='w-full bg-white border border-gray-300 rounded-xl flex flex-col relative min-w-[320px] group'>
6464
<ImageCard
6565
imageSrc={wine.image}
6666
className='pt-[20px] pb-0 border-none md:pl-[30px]'
@@ -114,7 +114,7 @@ export default function WineListCard() {
114114
</div>
115115
</div>
116116
<button type='button' className='w-[36px] h-[36px] p-[4px] rounded'>
117-
<NextIcon className='w-full h-full text-gray-300 hover:text-gray-500' />
117+
<NextIcon className='w-full h-full text-gray-300 group-hover:text-gray-700' />
118118
</button>
119119
</div>
120120
</div>
@@ -144,7 +144,7 @@ export default function WineListCard() {
144144
type='button'
145145
className='w-[40px] h-[40px] p-[4px] rounded flex-shrink-0 ml-10 hidden md:block absolute top-[185px] right-[20px]'
146146
>
147-
<NextIcon className='w-full h-full text-gray-300 hover:text-gray-500' />
147+
<NextIcon className='w-full h-full text-gray-300 group-hover:text-gray-700' />
148148
</button>
149149
<div className='w-full h-px bg-gray-300 mt-[-1px]' />
150150
<div className='px-[25px] py-[20px] xl:w-[800px] xl:h-[110px]'>

src/components/common/winelist/WineSlider.tsx

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect } from 'react';
1+
import { useEffect, useState } from 'react';
22

33
import { useQuery, useQueryClient } from '@tanstack/react-query';
44

@@ -9,6 +9,7 @@ import {
99
CarouselItem,
1010
CarouselPrevious,
1111
CarouselNext,
12+
type CarouselApi,
1213
} from '@/components/ui/carousel';
1314
import { getRecommendedWines } from '@/lib/wineApi';
1415
import { RecommendedWineResponse } from '@/types/wineListType';
@@ -40,6 +41,33 @@ export default function WineSlider() {
4041
});
4142
}, [data]);
4243

44+
// useState 훅들을 여기에 선언합니다.
45+
const [api, setApi] = useState<CarouselApi | undefined>();
46+
const [isAtStart, setIsAtStart] = useState(true);
47+
const [isAtEnd, setIsAtEnd] = useState(false);
48+
49+
// setApi prop에 직접 콜백을 작성하는 대신,
50+
// api 상태가 변경될 때마다 캐러셀 이벤트를 관리하는 useEffect 훅을 사용합니다.
51+
useEffect(() => {
52+
if (!api) return;
53+
54+
const handleSelect = () => {
55+
setIsAtStart(!api.canScrollPrev());
56+
setIsAtEnd(!api.canScrollNext());
57+
};
58+
59+
// 초기 렌더링 시에도 상태를 업데이트합니다.
60+
handleSelect();
61+
62+
api.on('select', handleSelect);
63+
api.on('reInit', handleSelect);
64+
65+
return () => {
66+
api.off('select', handleSelect);
67+
api.off('reInit', handleSelect);
68+
};
69+
}, [api]);
70+
4371
return (
4472
<div className='mx-auto px-[16px] md:px-[20px] xl:px-0 max-w-[1140px] min-w-[365px] mt-[20px] mb-[24px]'>
4573
<section className='w-full min-h-[241px] rounded-[12px] bg-gray-100 py-[20px] md:min-h-[285px]'>
@@ -65,21 +93,23 @@ export default function WineSlider() {
6593
align: 'start',
6694
slidesToScroll: 2,
6795
}}
96+
setApi={setApi} // <-- useState로 선언한 setApi 함수를 prop으로 전달합니다.
6897
>
6998
<CarouselContent>
70-
{filteredWines.map((wine) => (
99+
{filteredWines.map((wine, index) => (
71100
<CarouselItem key={wine.id} className='basis-auto flex items-start '>
72101
<WineCard
73102
id={wine.id}
74103
image={wine.image}
75104
name={wine.name}
76105
rating={wine.avgRating}
106+
isCarouselEnd={isAtEnd && index >= filteredWines.length - 2}
77107
/>
78108
</CarouselItem>
79109
))}
80110
</CarouselContent>
81-
<CarouselPrevious />
82-
<CarouselNext />
111+
<CarouselPrevious disabled={isAtStart} className='z-50' />
112+
<CarouselNext disabled={isAtEnd} className='z-50' />
83113
</Carousel>
84114
);
85115
})()

src/pages/wines/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect } from 'react';
22

3+
import ScrollToTop from '@/components/common/scrollToTop';
34
import WineFilter from '@/components/common/winelist/WineFilter';
45
import WineListCard from '@/components/common/winelist/WineListCard';
56
import WineSlider from '@/components/common/winelist/WineSlider';
@@ -23,6 +24,7 @@ export default function Wines() {
2324
<div className='xl:hidden'>
2425
<WineListCard />
2526
</div>
27+
<ScrollToTop />
2628
</div>
2729
);
2830
}

0 commit comments

Comments
 (0)