diff --git a/src/app/homePage.css.ts b/src/app/homePage.css.ts index c59f9d62..c93c8fbd 100644 --- a/src/app/homePage.css.ts +++ b/src/app/homePage.css.ts @@ -35,10 +35,9 @@ export const modalOverlay = style({ width: '100%', height: 'calc(100% + 1.2rem)', marginTop: '-1.2rem', - backgroundColor: theme.COLORS.black60, justifyContent: 'center', alignItems: 'center', - zIndex: 1, + zIndex: 100, }); export const titleWithIconStyle = style({ diff --git a/src/app/page.tsx b/src/app/page.tsx index b431e946..1dead267 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,6 @@ import HomeClient from '@app/HomeClient'; import RecommendTempleClient from '@app/RecommendTempleClient'; +import Icon from '@assets/svgs'; import MainBanner from '@components/banner/MainBanner'; import DetailTitle from '@components/detailTitle/DetailTitle'; import FilterTypeBoxClient from '@components/filter/filterTypeBox/FilterTypeBoxClient'; @@ -7,10 +8,9 @@ import Footer from '@components/footer/Footer'; import Header from '@components/header/Header'; import TestBanner from '@components/test/testBanner/TestBanner'; import { cookies } from 'next/headers'; +import Link from 'next/link'; import * as styles from './homePage.css'; -import Link from 'next/link'; -import Icon from '@assets/svgs'; const HomePage = async () => { const cookieStore = await cookies(); diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index 5421d7ef..c17313b0 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -8,7 +8,7 @@ import leftPaddingStyle from './searchPage.css'; const SearchPage = () => { return ( <> - +
diff --git a/src/app/searchResult/searchResultPage.css.ts b/src/app/searchResult/searchResultPage.css.ts index 863f9f0c..e41fefbe 100644 --- a/src/app/searchResult/searchResultPage.css.ts +++ b/src/app/searchResult/searchResultPage.css.ts @@ -17,7 +17,7 @@ export const headerContainer = style({ flexDirection: 'column', justifyContent: 'center', alignItems: 'center', - paddingTop: '1rem', + paddingTop: '1.2rem', marginBottom: '1rem', zIndex: 3, }); diff --git a/src/components/banner/MainBanner.tsx b/src/components/banner/MainBanner.tsx index ac53be44..89fb2429 100644 --- a/src/components/banner/MainBanner.tsx +++ b/src/components/banner/MainBanner.tsx @@ -1,112 +1,32 @@ 'use client'; +import useInfiniteCarousel from '@hooks/useInfiniteCarousel'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; -import React, { useState, useEffect, useRef, useCallback } from 'react'; +import React from 'react'; -import BANNER_DATA, { BannerItem } from './bannerData'; +import BANNER_DATA from './bannerData'; import * as styles from './mainBanner.css'; -type SlideItem = BannerItem & { uniqueKey: string }; - const MainBanner = () => { const router = useRouter(); - const slides = [ - { ...BANNER_DATA[BANNER_DATA.length - 1], uniqueKey: 'clone-last' }, - ...BANNER_DATA.map((item) => ({ ...item, uniqueKey: `real-${item.id}` })), - { ...BANNER_DATA[0], uniqueKey: 'clone-first' }, - ]; - - const totalOriginalSlides = BANNER_DATA.length; - - const [currentIndex, setCurrentIndex] = useState(1); - const [isAnimate, setIsAnimate] = useState(true); - const [isDragging, setIsDragging] = useState(false); - const [startX, setStartX] = useState(0); - const [startY, setStartY] = useState(0); - const [currentX, setCurrentX] = useState(0); - - const timerRef = useRef(null); - - const moveNext = useCallback(() => { - setIsAnimate(true); - setCurrentIndex((prev) => prev + 1); - }, []); - - const movePrev = useCallback(() => { - setIsAnimate(true); - setCurrentIndex((prev) => prev - 1); - }, []); - - const stopAutoSlide = useCallback(() => { - if (timerRef.current) { - clearInterval(timerRef.current); - } - }, []); - - const startAutoSlide = useCallback(() => { - stopAutoSlide(); - timerRef.current = setInterval(() => { - moveNext(); - }, 4000); - }, [moveNext, stopAutoSlide]); - - useEffect(() => { - startAutoSlide(); - return () => stopAutoSlide(); - }, [startAutoSlide, stopAutoSlide, currentIndex]); - - const handleTransitionEnd = () => { - if (currentIndex === 0) { - setIsAnimate(false); - setCurrentIndex(totalOriginalSlides); - } else if (currentIndex === slides.length - 1) { - setIsAnimate(false); - setCurrentIndex(1); - } - }; - - const handleMouseDown = (e: React.MouseEvent) => { - stopAutoSlide(); - setIsDragging(true); - setStartX(e.clientX); - setCurrentX(e.clientX); - setIsAnimate(false); - }; - - const handleMouseMove = (e: React.MouseEvent) => { - if (!isDragging) return; - setCurrentX(e.clientX); - }; - - const handleMouseUp = () => { - if (!isDragging) return; - setIsDragging(false); - startAutoSlide(); - - const diff = currentX - startX; - const threshold = 50; - - if (diff < -threshold) { - moveNext(); - } else if (diff > threshold) { - movePrev(); - } else { - setIsAnimate(true); - } - }; - - const handleMouseLeave = () => { - if (isDragging) { - setIsDragging(false); - setIsAnimate(true); - startAutoSlide(); - } - }; - - const handleBannerClick = (banner: SlideItem) => { - if (Math.abs(currentX - startX) > 5) return; + const { + slides, + currentIndex, + isAnimate, + dragOffset, + displayIndex, + totalOriginalSlides, + isSwiped, + handlers, + } = useInfiniteCarousel({ + data: BANNER_DATA, + autoPlayInterval: 4000, + }); + + const handleBannerClick = (banner: (typeof BANNER_DATA)[0]) => { + if (isSwiped) return; if (banner.type === 'internal') { router.push(banner.link); @@ -114,68 +34,6 @@ const MainBanner = () => { window.open(banner.link, '_blank'); } }; - - const handleKeyDown = (e: React.KeyboardEvent, banner: SlideItem) => { - if (e.key === 'Enter' || e.key === ' ') { - handleBannerClick(banner); - } - }; - - const handleTouchStart = (e: React.TouchEvent) => { - stopAutoSlide(); - setIsDragging(true); - const touchX = e.touches[0].clientX; - const touchY = e.touches[0].clientY; - setStartX(touchX); - setStartY(touchY); - setCurrentX(touchX); - setIsAnimate(false); - }; - - const handleTouchMove = (e: React.TouchEvent) => { - if (!isDragging) return; - const touchX = e.touches[0].clientX; - const touchY = e.touches[0].clientY; - - const diffX = Math.abs(touchX - startX); - const diffY = Math.abs(touchY - startY); - - // 스크롤 의도 파악 - if (diffY > diffX && diffY > 10) { - setIsDragging(false); - return; - } - - setCurrentX(touchX); - }; - - const handleTouchEnd = () => { - if (!isDragging) { - startAutoSlide(); - return; - } - - setIsDragging(false); - startAutoSlide(); - - const diff = currentX - startX; - const threshold = 50; - - if (diff < -threshold) { - moveNext(); - } else if (diff > threshold) { - movePrev(); - } else { - setIsAnimate(true); - } - }; - - let displayIndex = currentIndex; - if (currentIndex === 0) displayIndex = totalOriginalSlides; - else if (currentIndex === slides.length - 1) displayIndex = 1; - - const dragOffset = isDragging ? currentX - startX : 0; - return (
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} @@ -186,14 +44,7 @@ const MainBanner = () => { transition: isAnimate ? 'transform 0.5s ease-in-out' : 'none', touchAction: 'pan-y', }} - onTransitionEnd={handleTransitionEnd} - onMouseDown={handleMouseDown} - onMouseMove={handleMouseMove} - onMouseUp={handleMouseUp} - onMouseLeave={handleMouseLeave} - onTouchStart={handleTouchStart} - onTouchMove={handleTouchMove} - onTouchEnd={handleTouchEnd}> + {...handlers}> {slides.map((banner) => (
{ onClick={() => handleBannerClick(banner)} role="button" tabIndex={0} - onKeyDown={(e) => handleKeyDown(e, banner)} + onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ') && handleBannerClick(banner)} aria-label={`${banner.alt} 배너로 이동`} onDragStart={(e) => e.preventDefault()}> void; templestayId: number; - link: string; onClick: () => void; + priority?: boolean; } const PopularCard = ({ @@ -28,8 +27,8 @@ const PopularCard = ({ isLiked, onLikeToggle, templestayId, - link, onClick, + priority = false, }: PopularCardProps) => { const handleLikeClick = (e: React.MouseEvent) => { e.stopPropagation(); @@ -39,22 +38,31 @@ const PopularCard = ({ }; return ( - e.preventDefault()} - onClick={onClick}> -
- ); }; diff --git a/src/components/card/popularCard/popularCard.css.ts b/src/components/card/popularCard/popularCard.css.ts index 8f49f021..d595b513 100644 --- a/src/components/card/popularCard/popularCard.css.ts +++ b/src/components/card/popularCard/popularCard.css.ts @@ -1,20 +1,30 @@ import theme from '@styles/theme.css'; import { style } from '@vanilla-extract/css'; -export const cardWrapper = style({ - width: '33.5rem', - cursor: 'pointer', +export const container = style({ + width: '100%', + overflow: 'hidden', + touchAction: 'pan-y', }); -export const cardContainer = style({ +export const slideList = style({ display: 'flex', - flexDirection: 'column', - gap: '0.8rem', + width: '33.5rem', + gap: '2rem', }); -export const templeInfoBox = style({ - display: 'flex', - flexDirection: 'column', +export const slideItem = style({ + minWidth: '100%', + height: '100%', + cursor: 'pointer', +}); + +export const imageWrapper = style({ + position: 'relative', + width: '100%', + height: '137px', + overflow: 'hidden', + borderRadius: 8, }); export const templestayName = style({ @@ -22,6 +32,12 @@ export const templestayName = style({ textAlign: 'left', }); +export const slideContent = style({ + display: 'flex', + + flexDirection: 'column', +}); + export const bottomBox = style({ display: 'flex', color: theme.COLORS.gray8, @@ -34,17 +50,6 @@ export const likeBtn = style({ padding: '1rem', }); -export const imgBox = style({ - height: '13.7rem', - borderRadius: 8, - display: 'flex', - justifyContent: 'flex-end', - color: theme.COLORS.white, - overflow: 'hidden', - backgroundPosition: 'center', - position: 'relative', -}); - export const bottomContainer = style({ display: 'flex', flexDirection: 'column', diff --git a/src/components/card/popularCard/rankBtn.css.ts b/src/components/card/popularCard/rankBtn.css.ts index 2b9e278f..02015df3 100644 --- a/src/components/card/popularCard/rankBtn.css.ts +++ b/src/components/card/popularCard/rankBtn.css.ts @@ -10,7 +10,8 @@ const rankBox = style({ borderRadius: '0 0 8px 8px', backgroundColor: theme.COLORS.black60, ...theme.FONTS.c2R14, - marginRight: '2rem', + color: theme.COLORS.white, + right: '2rem', position: 'absolute', zIndex: 1, }); diff --git a/src/components/card/templeStayCard/infoSection.css.ts b/src/components/card/templeStayCard/infoSection.css.ts index d63040ea..54024016 100644 --- a/src/components/card/templeStayCard/infoSection.css.ts +++ b/src/components/card/templeStayCard/infoSection.css.ts @@ -60,7 +60,6 @@ export const title = recipe({ variants: { size: { default: { - height: '4.8rem', ...theme.FONTS.h5Sb16, }, small: { diff --git a/src/components/carousel/popularCarousel/CarouselIndex.tsx b/src/components/carousel/popularCarousel/CarouselIndex.tsx index ece63382..73f08d64 100644 --- a/src/components/carousel/popularCarousel/CarouselIndex.tsx +++ b/src/components/carousel/popularCarousel/CarouselIndex.tsx @@ -3,16 +3,16 @@ import React from 'react'; interface CarouselIndexProps { total: number; - currentIndex: number; + displayIndex: number; } -const CarouselIndex = ({ total, currentIndex }: CarouselIndexProps) => { +const CarouselIndex = ({ total, displayIndex }: CarouselIndexProps) => { return (
{Array.from({ length: total }).map((_, index) => (
))}
diff --git a/src/components/carousel/popularCarousel/PopularCarousel.tsx b/src/components/carousel/popularCarousel/PopularCarousel.tsx index 85afb7fd..4f2e9ff3 100644 --- a/src/components/carousel/popularCarousel/PopularCarousel.tsx +++ b/src/components/carousel/popularCarousel/PopularCarousel.tsx @@ -3,11 +3,11 @@ import { useAddWishlistV2, useRemoveWishlistV2 } from '@apis/wish'; import PopularCard from '@components/card/popularCard/PopularCard'; import CarouselIndex from '@components/carousel/popularCarousel/CarouselIndex'; import ExceptLayout from '@components/except/exceptLayout/ExceptLayout'; -import useCarousel from '@hooks/useCarousel'; +import { useRouter } from 'next/navigation'; import { useQueryClient } from '@tanstack/react-query'; -import registDragEvent from '@utils/registDragEvent'; import { getCookie } from 'cookies-next'; import useEventLogger from 'src/gtm/hooks/useEventLogger'; +import useInfiniteCarousel from '@hooks/useInfiniteCarousel'; import * as styles from './popularCarousel.css'; @@ -19,24 +19,26 @@ const PopularCarousel = ({ onRequireLogin }: PopularCarouselProps) => { const queryClient = useQueryClient(); const addWishlistMutation = useAddWishlistV2(); const removeWishlistMutation = useRemoveWishlistV2(); + const { logClickEvent } = useEventLogger('home_popularity_component'); + const router = useRouter(); const { data, isLoading, isError } = useGetRanking(); - const { currentIndex, carouselRef, transformStyle, handleDragChange, handleDragEnd } = - useCarousel({ - itemCount: data?.length || 0, - moveDistance: 355, - }); - - const { logClickEvent } = useEventLogger('home_popularity_component'); - - if (isLoading) { - return ; - } + const { + slides, + currentIndex, + isAnimate, + dragOffset, + displayIndex, + totalOriginalSlides, + isSwiped, + handlers, + } = useInfiniteCarousel({ + data: data || [], + }); - if (isError) { - return ; - } + if (isLoading) return ; + if (isError) return ; const handleLikeToggle = (templestayId: number) => { const userNickname = getCookie('userNickname'); @@ -59,39 +61,41 @@ const PopularCarousel = ({ onRequireLogin }: PopularCarouselProps) => { }; return ( -
-
-
- {data && - data.map((temple) => ( - { - logClickEvent('click_popularity_card', { - label: temple.id, - }); - }} - /> - ))} -
+
+
+ {slides.map((temple, index) => ( + { + if (isSwiped) return; + + logClickEvent('click_popularity_card', { + label: temple.id, + }); + + router.push(`/detail/${temple.id}`); + }} + priority={index === 1} + /> + ))}
- -
+ + +
); }; diff --git a/src/components/carousel/popularCarousel/popularCarousel.css.ts b/src/components/carousel/popularCarousel/popularCarousel.css.ts index e17c1252..5d45f697 100644 --- a/src/components/carousel/popularCarousel/popularCarousel.css.ts +++ b/src/components/carousel/popularCarousel/popularCarousel.css.ts @@ -1,30 +1,46 @@ +import theme from '@styles/theme.css'; + import { style } from '@vanilla-extract/css'; -export const carouselWrapper = style({ +export const container = style({ + width: '100%', + overflow: 'hidden', display: 'flex', flexDirection: 'column', alignItems: 'center', - justifyContent: 'center', - gap: '1.5rem', }); -export const carouselContainer = style({ - width: '33.5rem', - overflow: 'hidden', +export const slideList = style({ display: 'flex', + width: '33.5rem', + gap: '2rem', }); -export const carouselBox = style({ - display: 'flex', - gap: '2rem', +export const slideItem = style({ + minWidth: '100%', + height: '100%', + cursor: 'pointer', +}); + +export const imageWrapper = style({ + position: 'relative', + + width: '100%', + + height: '137px', + + overflow: 'hidden', + + borderRadius: 8, }); -export const carouselItem = style({ - flexShrink: 0, +export const templestayName = style({ + ...theme.FONTS.h3Sb18, + textAlign: 'left', }); -export const emptyBox = style({ - width: '2rem', - height: '100%', - flexShrink: 0, +export const slideContent = style({ + display: 'flex', + + flexDirection: 'column', }); diff --git a/src/components/common/button/buttonBar/ButtonBar.tsx b/src/components/common/button/buttonBar/ButtonBar.tsx index 541c78b1..7d75b928 100644 --- a/src/components/common/button/buttonBar/ButtonBar.tsx +++ b/src/components/common/button/buttonBar/ButtonBar.tsx @@ -1,8 +1,9 @@ -import buttonBarContainer from '@components/common/button/buttonBar/buttonBar.css'; import FlowerBtn from '@components/common/button/flowerBtn/FlowerBtn'; import PageBottomBtn from '@components/common/button/pageBottomBtn/PageBottomBtn'; import TextBtn from '@components/common/button/textBtn/TextBtn'; +import { buttonBarWrapper, buttonBarContainer } from './buttonBar.css'; + interface ButtonBarProps { type: 'reset' | 'wish'; label: string; @@ -10,6 +11,7 @@ interface ButtonBarProps { handleResetFilter?: () => void; liked?: boolean; onToggleWishlist?: () => void; + isDisabled?: boolean; } const ButtonBar = ({ @@ -19,6 +21,7 @@ const ButtonBar = ({ handleResetFilter = () => {}, liked, onToggleWishlist, + isDisabled = false, }: ButtonBarProps) => { const renderLeftButton = () => type === 'wish' ? ( @@ -34,11 +37,17 @@ const ButtonBar = ({ ); return ( -
- {renderLeftButton()} - +
+
+ {renderLeftButton()} + +
); }; - export default ButtonBar; diff --git a/src/components/common/button/buttonBar/buttonBar.css.ts b/src/components/common/button/buttonBar/buttonBar.css.ts index 890a3dee..22f68932 100644 --- a/src/components/common/button/buttonBar/buttonBar.css.ts +++ b/src/components/common/button/buttonBar/buttonBar.css.ts @@ -1,23 +1,24 @@ import theme from '@styles/theme.css'; import { style } from '@vanilla-extract/css'; -const buttonBarContainer = style({ +export const buttonBarWrapper = style({ position: 'fixed', bottom: 0, - left: '50%', - transform: 'translateX(-50%)', + width: '100%', + display: 'flex', + justifyContent: 'center', zIndex: 100, +}); +export const buttonBarContainer = style({ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '37.5rem', + maxWidth: '100%', height: '7.2rem', padding: '1rem 2rem', boxSizing: 'border-box', background: theme.COLORS.white, - boxShadow: `0px -4px 16px 0px rgba(0, 0, 0, 0.05)`, }); - -export default buttonBarContainer; diff --git a/src/components/filter/filterBottomSheetModal/FilterModalContent.tsx b/src/components/filter/filterBottomSheetModal/FilterModalContent.tsx index 1872ad69..3e783d98 100644 --- a/src/components/filter/filterBottomSheetModal/FilterModalContent.tsx +++ b/src/components/filter/filterBottomSheetModal/FilterModalContent.tsx @@ -93,9 +93,10 @@ const FilterModalContent = ({ onComplete, scrollRef, searchText }: Props) => { ); diff --git a/src/components/filter/filterBottomSheetModal/filterModalContent.css.ts b/src/components/filter/filterBottomSheetModal/filterModalContent.css.ts index 6d49e37d..cb623482 100644 --- a/src/components/filter/filterBottomSheetModal/filterModalContent.css.ts +++ b/src/components/filter/filterBottomSheetModal/filterModalContent.css.ts @@ -13,4 +13,10 @@ export const main = style({ overflowY: 'auto', maxHeight: 'calc(100vh - 280px)', height: '100vh', + + selectors: { + '&::-webkit-scrollbar': { + display: 'none', + }, + }, }); diff --git a/src/components/search/searchBar/SearchBar.tsx b/src/components/search/searchBar/SearchBar.tsx index c4b3d817..a55269b5 100644 --- a/src/components/search/searchBar/SearchBar.tsx +++ b/src/components/search/searchBar/SearchBar.tsx @@ -3,17 +3,25 @@ import Icon from '@assets/svgs'; import useFilter from '@hooks/useFilter'; import { usePathname } from 'next/navigation'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import useEventLogger from 'src/gtm/hooks/useEventLogger'; import * as styles from './searchBar.css'; interface SearchBarProps { searchText?: string; + inputAutoFocus?: boolean; } -const SearchBar = ({ searchText }: SearchBarProps) => { +const SearchBar = ({ searchText, inputAutoFocus = false }: SearchBarProps) => { const [inputValue, setInputValue] = useState(searchText || ''); + const inputRef = useRef(null); + + useEffect(() => { + if (inputAutoFocus && inputRef.current) { + inputRef.current.focus(); + } + }, []); const { handleSearch } = useFilter(); const { logClickEvent } = useEventLogger('search_bar'); @@ -27,8 +35,11 @@ const SearchBar = ({ searchText }: SearchBarProps) => { const handleClearInput = () => { setInputValue(''); - logClickEvent('click_delete', { label: inputValue }); + + if (inputRef.current) { + inputRef.current.focus(); + } }; const handleClickSearch = () => { @@ -66,6 +77,7 @@ const SearchBar = ({ searchText }: SearchBarProps) => {
{ +const SearchHeader = ({ searchText, prevPath, inputAutoFocus = false }: SearchHeader) => { const handleToBack = useNavigateTo(prevPath); return ( @@ -16,7 +17,7 @@ const SearchHeader = ({ searchText, prevPath }: SearchHeader) => { - + ); }; diff --git a/src/hooks/useFilter.ts b/src/hooks/useFilter.ts index e4b20a85..cfeba1be 100644 --- a/src/hooks/useFilter.ts +++ b/src/hooks/useFilter.ts @@ -1,9 +1,10 @@ import useLocalStorage from '@hooks/useLocalStorage'; import { getCookie } from 'cookies-next'; +import { useSetAtom } from 'jotai'; import { useRouter } from 'next/navigation'; import { useCallback } from 'react'; import queryClient from 'src/queryClient'; -import { filterListInstance } from 'src/store/store'; +import { filterListInstance, priceAtom } from 'src/store/store'; type FilterQueryParams = { region?: string[]; @@ -22,6 +23,7 @@ const isLoggedIn = getCookie('userNickname'); const useFilter = () => { const { addStorageValue } = useLocalStorage(); + const setPrice = useSetAtom(priceAtom); // 필터 상태 토글 const toggleFilter = async (filterName: string) => { @@ -37,6 +39,9 @@ const useFilter = () => { const handleSearch = useCallback( (params: FilterQueryParams = {}) => { + filterListInstance.resetAllStates(); + setPrice({ minPrice: 0, maxPrice: 30 }); + const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { diff --git a/src/hooks/useInfiniteCarousel.ts b/src/hooks/useInfiniteCarousel.ts new file mode 100644 index 00000000..7d87892b --- /dev/null +++ b/src/hooks/useInfiniteCarousel.ts @@ -0,0 +1,146 @@ +import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; + +interface UseInfiniteCarouselProps { + data: T[]; + autoPlayInterval?: number; +} + +const useInfiniteCarousel = ({ data, autoPlayInterval }: UseInfiniteCarouselProps) => { + const totalOriginalSlides = data.length; + const hasSlides = totalOriginalSlides > 0; + + const [currentIndex, setCurrentIndex] = useState(1); + const [isAnimate, setIsAnimate] = useState(true); + const [isDragging, setIsDragging] = useState(false); + const [startX, setStartX] = useState(0); + const [startY, setStartY] = useState(0); + const [currentX, setCurrentX] = useState(0); + + const timerRef = useRef(null); + + const slides = useMemo(() => { + if (!hasSlides) return []; + + return [ + { ...data[data.length - 1], uniqueKey: 'clone-last' }, + ...data.map((item, idx) => ({ ...item, uniqueKey: `real-${idx}` })), + { ...data[0], uniqueKey: 'clone-first' }, + ]; + }, [data, hasSlides]); + + const moveNext = useCallback(() => { + if (!hasSlides) return; + + setIsAnimate(true); + setCurrentIndex((prev) => prev + 1); + }, [hasSlides]); + + const movePrev = useCallback(() => { + if (!hasSlides) return; + + setIsAnimate(true); + setCurrentIndex((prev) => prev - 1); + }, [hasSlides]); + + const stopAutoSlide = useCallback(() => { + if (timerRef.current) clearInterval(timerRef.current); + }, []); + + const startAutoSlide = useCallback(() => { + if (!autoPlayInterval || !hasSlides) return; + + stopAutoSlide(); + timerRef.current = setInterval(moveNext, autoPlayInterval); + }, [moveNext, stopAutoSlide, autoPlayInterval, hasSlides]); + + useEffect(() => { + if (!autoPlayInterval || !hasSlides) return; + + startAutoSlide(); + return () => stopAutoSlide(); + }, [startAutoSlide, stopAutoSlide, autoPlayInterval, hasSlides]); + + const handleTransitionEnd = () => { + if (!hasSlides) return; + + if (currentIndex === 0) { + setIsAnimate(false); + setCurrentIndex(totalOriginalSlides); + } else if (currentIndex === slides.length - 1) { + setIsAnimate(false); + setCurrentIndex(1); + } + }; + + const handleDragStart = (clientX: number, clientY = 0) => { + if (!hasSlides) return; + + stopAutoSlide(); + setIsDragging(true); + setStartX(clientX); + setStartY(clientY); + setCurrentX(clientX); + setIsAnimate(false); + }; + + const handleDragMove = (clientX: number, clientY = 0) => { + if (!isDragging) return; + + const diffX = Math.abs(clientX - startX); + const diffY = Math.abs(clientY - startY); + if (diffY > diffX && diffY > 10) { + setIsDragging(false); + return; + } + + setCurrentX(clientX); + }; + + const handleDragEnd = () => { + if (!isDragging) return; + + const diff = currentX - startX; + const threshold = 50; + + setIsDragging(false); + if (autoPlayInterval) startAutoSlide(); + + if (diff < -threshold) moveNext(); + else if (diff > threshold) movePrev(); + else setIsAnimate(true); + }; + + const handlers = { + onMouseDown: (e: React.MouseEvent) => handleDragStart(e.clientX), + onMouseMove: (e: React.MouseEvent) => handleDragMove(e.clientX), + onMouseUp: handleDragEnd, + onMouseLeave: handleDragEnd, + onTouchStart: (e: React.TouchEvent) => + handleDragStart(e.touches[0].clientX, e.touches[0].clientY), + onTouchMove: (e: React.TouchEvent) => + handleDragMove(e.touches[0].clientX, e.touches[0].clientY), + onTouchEnd: handleDragEnd, + onTransitionEnd: handleTransitionEnd, + }; + + const displayIndex = !hasSlides + ? 0 + : currentIndex === 0 + ? totalOriginalSlides + : currentIndex === slides.length - 1 + ? 1 + : currentIndex; + + return { + slides, + currentIndex: hasSlides ? currentIndex : 0, + isAnimate: hasSlides ? isAnimate : false, + dragOffset: isDragging ? currentX - startX : 0, + displayIndex, + totalOriginalSlides, + isSwiped: hasSlides && Math.abs(currentX - startX) > 5, + handlers: hasSlides ? handlers : {}, + }; +}; + +export default useInfiniteCarousel; diff --git a/src/stories/CarouselIndex.stories.ts b/src/stories/CarouselIndex.stories.ts index 12937cd9..e1936ef1 100644 --- a/src/stories/CarouselIndex.stories.ts +++ b/src/stories/CarouselIndex.stories.ts @@ -12,13 +12,13 @@ const meta = { total: { control: { type: 'number' }, }, - currentIndex: { + displayIndex: { control: { type: 'number' }, }, }, args: { total: 3, - currentIndex: 2, + displayIndex: 2, }, } satisfies Meta; diff --git a/src/stories/PopularCard.stories.ts b/src/stories/PopularCard.stories.ts index 8bc33195..0ebb7dcc 100644 --- a/src/stories/PopularCard.stories.ts +++ b/src/stories/PopularCard.stories.ts @@ -39,7 +39,6 @@ const meta = { templeImg: 'https://img.danawa.com/images/descFiles/6/110/5109431_agiLaciMHn_1659098198501.jpeg', templeName: '봉선사', - link: 'https://www.gototemplestay.com/', isLiked: false, templestayId: 123, onLikeToggle: (id) => alert(`Toggled like for ID: ${id}`), diff --git a/src/styles/global.css.ts b/src/styles/global.css.ts index 490d956b..30f0726c 100644 --- a/src/styles/global.css.ts +++ b/src/styles/global.css.ts @@ -4,6 +4,14 @@ globalStyle('*', { boxSizing: 'border-box', }); +globalStyle('::-webkit-scrollbar', { + display: 'none', +}); + +globalStyle('html', { + paddingRight: '0px !important', +}); + globalStyle('html, body', { width: '100%', fontSize: '62.5%',