-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
🎉 이미지 슬라이더 구현 #22
🎉 이미지 슬라이더 구현 #22
Changes from 7 commits
699f8f3
79b2426
77120bf
80ed5d6
1c80008
28af33c
fe519de
023b4a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import type { Meta, StoryObj } from '@storybook/react' | ||
import ImageSlider from './Slider' | ||
|
||
const meta = { | ||
title: 'DOMAIN/Slider', | ||
component: ImageSlider, | ||
tags: ['autodocs'], | ||
argTypes: {}, | ||
} satisfies Meta<typeof ImageSlider> | ||
|
||
const mockData = [ | ||
'https://cdn.pixabay.com/photo/2023/09/30/09/12/dachshund-8285220_1280.jpg', | ||
'https://cdn.pixabay.com/photo/2014/04/13/20/49/cat-323262_1280.jpg', | ||
'https://cdn.pixabay.com/photo/2018/10/01/09/21/pets-3715733_1280.jpg', | ||
] | ||
|
||
export default meta | ||
type Story = StoryObj<typeof meta> | ||
|
||
export const Slider: Story = { | ||
args: { | ||
slides: mockData, | ||
}, | ||
render: () => { | ||
return ( | ||
<div className="max-w-[640px] relative"> | ||
<ImageSlider slides={mockData} imageAspectRatio="1/1" /> | ||
</div> | ||
) | ||
}, | ||
} | ||
|
||
export const AutoSlider: Story = { | ||
args: { slides: mockData }, | ||
render: () => { | ||
return ( | ||
<div className="max-w-[640px] relative"> | ||
<ImageSlider | ||
slides={mockData} | ||
autoSlide={true} | ||
imageAspectRatio="1/1" | ||
/> | ||
</div> | ||
) | ||
}, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
'use client' | ||
|
||
import { useState, useEffect, useRef, useCallback } from 'react' | ||
import Image from 'next/image' | ||
import Assets from '@/config/assets' | ||
|
||
type SliderProps = { | ||
autoSlide?: boolean | ||
autoSlideInterval?: number | ||
slides: string[] | ||
imageAspectRatio?: string | ||
} | ||
|
||
const Slider = ({ | ||
autoSlide = false, | ||
autoSlideInterval = 3000, | ||
slides, | ||
imageAspectRatio = 'auto', | ||
}: SliderProps) => { | ||
const [currentIndex, setCurrentIndex] = useState(1) | ||
const [coordinate, setCoordinate] = useState({ start: 0, end: 0 }) | ||
const [style, setStyle] = useState({ | ||
transform: `translateX(-${currentIndex}00%)`, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 혹시 |
||
transition: `all 0.4s ease-out`, | ||
}) | ||
|
||
const ref = useRef<HTMLDivElement>(null) | ||
const imageList = [slides.at(-1), ...slides, slides[0]] | ||
|
||
const goPrevSlide = () => { | ||
setCurrentIndex(currentIndex - 1) | ||
setStyle({ | ||
transform: `translateX(-${currentIndex - 1}00%)`, | ||
transition: `all 0.4s ease-out`, | ||
}) | ||
} | ||
const goNextSlide = useCallback(() => { | ||
setCurrentIndex(currentIndex + 1) | ||
setStyle({ | ||
transform: `translateX(-${currentIndex + 1}00%)`, | ||
transition: `all 0.4s ease-out`, | ||
}) | ||
}, [currentIndex]) | ||
|
||
const handleTouchStart = (e: React.TouchEvent) => { | ||
setCoordinate({ | ||
...coordinate, | ||
start: e.touches[0].pageX, | ||
}) | ||
} | ||
|
||
const handleTouchMove = (e: React.TouchEvent) => { | ||
if (ref.current) { | ||
const current = ref.current.clientWidth * currentIndex | ||
const result = -current + (e.targetTouches[0].pageX - coordinate.start) | ||
setStyle({ | ||
transform: `translate3d(${result}px, 0px, 0px)`, | ||
transition: '0ms', | ||
}) | ||
} | ||
} | ||
|
||
const handleTouchEnd = (e: React.TouchEvent) => { | ||
const end = e.changedTouches[0].pageX | ||
const distance = Math.abs(coordinate.start - end) | ||
|
||
if (coordinate.start > end && distance > 2) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2라는 특정 숫자가 많이 나오는데, 코드에 대한 자세한 이해는 못했지만 매직넘버인 것 같아 상수화 하시면 좋을 것 같습니다. |
||
goNextSlide() | ||
} else if (coordinate.start < end && distance < 2) { | ||
goPrevSlide() | ||
} | ||
setCoordinate({ | ||
...coordinate, | ||
end, | ||
}) | ||
} | ||
|
||
useEffect(() => { | ||
if (!autoSlide) return | ||
const slideInterval = setInterval(goNextSlide, autoSlideInterval) | ||
return () => clearInterval(slideInterval) | ||
}, [autoSlide, autoSlideInterval, currentIndex, goNextSlide]) | ||
|
||
useEffect(() => { | ||
if (currentIndex === 0) { | ||
setCurrentIndex(imageList.length - 2) | ||
setTimeout(function () { | ||
setStyle({ | ||
transform: `translateX(-${imageList.length - 2}00%)`, | ||
transition: '0ms', | ||
}) | ||
}, 400) | ||
Comment on lines
+84
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. const SLIDE_TRANSITION_DELAY = 400; 으로 정의해줘도 좋을 것 같습니다. |
||
} | ||
Comment on lines
+78
to
+93
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 혹시 핸들러가 아니라 effect로 각 케이스를 따로 주어야 하는 이유가 있나요? (제가 제대로 이해를 못 한거 같아서) 궁금해서 남깁니다. |
||
|
||
if (currentIndex >= imageList.length - 1) { | ||
setCurrentIndex(1) | ||
setTimeout(() => { | ||
setStyle({ | ||
transform: `translateX(-100%)`, | ||
transition: '0ms', | ||
}) | ||
}, 400) | ||
} | ||
}, [currentIndex, imageList.length]) | ||
|
||
return ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 컴포넌트가 이미지, 버튼, 하단 인디케이터 세 가지로 분리되는 것 같은데 |
||
<div | ||
className="overflow-hidden relative" | ||
onTouchStart={handleTouchStart} | ||
onTouchMove={handleTouchMove} | ||
onTouchEnd={handleTouchEnd} | ||
> | ||
<div ref={ref} className="flex " style={style}> | ||
{imageList.map((url, i) => ( | ||
<Image | ||
src={url as string} | ||
alt="itemImage" | ||
key={i} | ||
priority | ||
width={0} | ||
height={0} | ||
className={`w-full cursor-pointer aspect-${imageAspectRatio}`} | ||
/> | ||
))} | ||
</div> | ||
<div className="absolute p-2 w-full flex justify-between top-[50%]"> | ||
<button onClick={goPrevSlide} className="p-1 rounded-full bg-white/20"> | ||
<Image src={Assets.leftIcon} alt="left" /> | ||
</button> | ||
|
||
<button onClick={goNextSlide} className="p-1 rounded-full bg-white/20"> | ||
<Image src={Assets.rightIcon} alt="right" priority /> | ||
</button> | ||
</div> | ||
<div className="absolute bottom-4 right-0 left-0"> | ||
<div className="flex items-center justify-center gap-2" ref={ref}> | ||
{slides.map((_, i) => ( | ||
<button | ||
key={i} | ||
className={` | ||
transition-all w-3 h-3 rounded-full | ||
${ | ||
currentIndex === i + 1 | ||
? 'w-4 h-4 bg-primary-color' | ||
: 'bg-white bg-opacity-50' | ||
} | ||
`} | ||
></button> | ||
))} | ||
</div> | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
export default Slider |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { default as ImageSlider } from './Slider' | ||
|
||
export default ImageSlider |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
물론 저희 기획 상 이미지가 있는 가정이지만, 해당 슬라이더는 그와 별개로 추상화되어야 하므로 이미지가 없을 때 기본 이미지가 있는게 좋을 것 같습니다! CLS 때문이라도 그게 좋을 것 같아요