diff --git a/public/images/icon-left.svg b/public/images/icon-left.svg new file mode 100644 index 00000000..bcf021e5 --- /dev/null +++ b/public/images/icon-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/icon-right.svg b/public/images/icon-right.svg new file mode 100644 index 00000000..5704297c --- /dev/null +++ b/public/images/icon-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/domain/Slider/Slider.stories.tsx b/src/components/domain/Slider/Slider.stories.tsx new file mode 100644 index 00000000..4e25654b --- /dev/null +++ b/src/components/domain/Slider/Slider.stories.tsx @@ -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 + +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 + +export const Slider: Story = { + args: { + slides: mockData, + }, + render: () => { + return ( +
+ +
+ ) + }, +} + +export const AutoSlider: Story = { + args: { slides: mockData }, + render: () => { + return ( +
+ +
+ ) + }, +} diff --git a/src/components/domain/Slider/Slider.tsx b/src/components/domain/Slider/Slider.tsx new file mode 100644 index 00000000..1801a044 --- /dev/null +++ b/src/components/domain/Slider/Slider.tsx @@ -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%)`, + transition: `all 0.4s ease-out`, + }) + + const ref = useRef(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) { + 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) + } + + if (currentIndex >= imageList.length - 1) { + setCurrentIndex(1) + setTimeout(() => { + setStyle({ + transform: `translateX(-100%)`, + transition: '0ms', + }) + }, 400) + } + }, [currentIndex, imageList.length]) + + return ( +
+
+ {imageList.map((url, i) => ( + itemImage + ))} +
+
+ + + +
+
+
+ {slides.map((_, i) => ( + + ))} +
+
+
+ ) +} + +export default Slider diff --git a/src/components/domain/Slider/index.tsx b/src/components/domain/Slider/index.tsx new file mode 100644 index 00000000..ae160c26 --- /dev/null +++ b/src/components/domain/Slider/index.tsx @@ -0,0 +1,3 @@ +import { default as ImageSlider } from './Slider' + +export default ImageSlider diff --git a/src/config/assets.ts b/src/config/assets.ts index c67a3665..861a16f3 100644 --- a/src/config/assets.ts +++ b/src/config/assets.ts @@ -4,8 +4,10 @@ import FilterIcon from '/public/images/filter.svg' import GoogleIcon from '/public/images/google.png' 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' @@ -20,6 +22,8 @@ const Assets = { arrowLeftIcon: ArrowLeftIcon, filterIcon: FilterIcon, alarmIcon: AlarmIcon, + leftIcon: LeftIcon, + rightIcon: RightIcon, googleIcon: GoogleIcon, kakaoIcon: KakaoIcon, logo: Logo,