Skip to content
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

Merged
merged 8 commits into from
Nov 7, 2023
3 changes: 3 additions & 0 deletions public/images/icon-left.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: 3 additions & 0 deletions public/images/icon-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 46 additions & 0 deletions src/components/domain/Slider/Slider.stories.tsx
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>
)
},
}
156 changes: 156 additions & 0 deletions src/components/domain/Slider/Slider.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

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

물론 저희 기획 상 이미지가 있는 가정이지만, 해당 슬라이더는 그와 별개로 추상화되어야 하므로 이미지가 없을 때 기본 이미지가 있는게 좋을 것 같습니다! CLS 때문이라도 그게 좋을 것 같아요

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%)`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

혹시 currentIndex * 100같은 연산으로 처리하는거 어떨까요??

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

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

const SLIDE_TRANSITION_DELAY = 400; 으로 정의해줘도 좋을 것 같습니다.

}
Comment on lines +78 to +93
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 (
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
3 changes: 3 additions & 0 deletions src/components/domain/Slider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { default as ImageSlider } from './Slider'

export default ImageSlider
6 changes: 6 additions & 0 deletions src/config/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import ArrowLeftIcon from '/public/images/arrow-left.svg'
import AlarmIcon from '/public/images/bell.svg'
import FilterIcon from '/public/images/filter.svg'
import GoogleIcon from '/public/images/google.png'
import LeftIcon from '/public/images/icon-left.svg'
import RightIcon from '/public/images/icon-right.svg'
import ActiveHeartIcon from '/public/images/icon-heart-active.svg'
import InActiveHeartIcon from '/public/images/icon-heart-inactive.svg'
import LeftIcon from '/public/images/icon-left.svg'
import MarkerIcon from '/public/images/icon-marker.svg'
import MoneyIcon from '/public/images/icon-money.svg'
import RightIcon from '/public/images/icon-right.svg'
import UsersIcon from '/public/images/icon-users.svg'
import KakaoIcon from '/public/images/kakao.png'
import Logo from '/public/images/logo.svg'
Expand All @@ -20,6 +24,8 @@ const Assets = {
arrowLeftIcon: ArrowLeftIcon,
filterIcon: FilterIcon,
alarmIcon: AlarmIcon,
leftIcon: LeftIcon,
rightIcon: RightIcon,
googleIcon: GoogleIcon,
kakaoIcon: KakaoIcon,
logo: Logo,
Expand Down
Loading