From e944c01db99c06a9f92594d2072b2d4c604e427b Mon Sep 17 00:00:00 2001 From: gahyeon Date: Tue, 10 Feb 2026 01:51:06 +0900 Subject: [PATCH 01/18] =?UTF-8?q?fix:=20=EA=B2=80=EC=83=89=EC=B0=BD=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=ED=8F=AC=EC=BB=A4=EC=8A=A4=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/search/page.tsx | 2 +- src/components/search/searchBar/SearchBar.tsx | 18 +++++++++++++++--- .../search/searchHeader/SearchHeader.tsx | 5 +++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index 5421d7ef..eef1d982 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/components/search/searchBar/SearchBar.tsx b/src/components/search/searchBar/SearchBar.tsx index c4b3d817..245a444e 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; + autoFocus?: boolean; } -const SearchBar = ({ searchText }: SearchBarProps) => { +const SearchBar = ({ searchText, autoFocus = false }: SearchBarProps) => { const [inputValue, setInputValue] = useState(searchText || ''); + const inputRef = useRef(null); + + useEffect(() => { + if (autoFocus && 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, autoFocus = false }: SearchHeader) => { const handleToBack = useNavigateTo(prevPath); return ( @@ -16,7 +17,7 @@ const SearchHeader = ({ searchText, prevPath }: SearchHeader) => { - + ); }; From 44d0d29c0f06f514369f7d27a598ff95ae7b52d2 Mon Sep 17 00:00:00 2001 From: gahyeon Date: Tue, 10 Feb 2026 01:55:05 +0900 Subject: [PATCH 02/18] =?UTF-8?q?fix:=20=ED=95=84=ED=84=B0=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=9C=84=EC=B9=98=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EA=B2=B0=EA=B3=BC=200=EA=B0=9C?= =?UTF-8?q?=EC=9D=BC=20=EB=95=8C=20=EB=B2=84=ED=8A=BC=20=EB=B9=84=ED=99=9C?= =?UTF-8?q?=EC=84=B1=ED=99=94=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/searchResult/searchResultPage.css.ts | 2 +- .../common/button/buttonBar/ButtonBar.tsx | 18 +++++++++++++----- .../common/button/buttonBar/buttonBar.css.ts | 13 +++++++------ .../FilterModalContent.tsx | 3 ++- 4 files changed, 23 insertions(+), 13 deletions(-) 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/common/button/buttonBar/ButtonBar.tsx b/src/components/common/button/buttonBar/ButtonBar.tsx index 541c78b1..7d491ef0 100644 --- a/src/components/common/button/buttonBar/ButtonBar.tsx +++ b/src/components/common/button/buttonBar/ButtonBar.tsx @@ -1,4 +1,4 @@ -import buttonBarContainer from '@components/common/button/buttonBar/buttonBar.css'; +import { buttonBarWrapper, buttonBarContainer } from './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'; @@ -10,6 +10,7 @@ interface ButtonBarProps { handleResetFilter?: () => void; liked?: boolean; onToggleWishlist?: () => void; + isDisabled?: boolean; } const ButtonBar = ({ @@ -19,6 +20,7 @@ const ButtonBar = ({ handleResetFilter = () => {}, liked, onToggleWishlist, + isDisabled = false, }: ButtonBarProps) => { const renderLeftButton = () => type === 'wish' ? ( @@ -34,11 +36,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) => { ); From c4c2797144ba830a76b1a03a3e73bd652a920acd Mon Sep 17 00:00:00 2001 From: gahyeon Date: Tue, 10 Feb 2026 01:56:28 +0900 Subject: [PATCH 03/18] =?UTF-8?q?fix:=20iOS=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EC=8B=9C=20=EC=9E=90=EB=8F=99=20=ED=99=95=EB=8C=80=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/search/searchBar/searchBar.css.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/search/searchBar/searchBar.css.ts b/src/components/search/searchBar/searchBar.css.ts index 2976bbe1..3a0ea4fd 100644 --- a/src/components/search/searchBar/searchBar.css.ts +++ b/src/components/search/searchBar/searchBar.css.ts @@ -13,7 +13,6 @@ export const searchBarContainer = style({ borderRadius: 8, backgroundColor: theme.COLORS.gray1, - ...theme.FONTS.b8M15, color: theme.COLORS.black, }); @@ -30,14 +29,19 @@ export const searchBarLayout = style({ export const inputStyle = style({ width: '21.9rem', backgroundColor: 'inherit', - + ...theme.FONTS.b7R16, + lineHeight: '1.5rem', + transform: 'scale(0.9375)', + transformOrigin: 'left center', selectors: { '&::placeholder': { color: theme.COLORS.gray5, + lineHeight: '1.5rem', + transform: 'scale(0.9375)', + transformOrigin: 'left center', }, }, }); - export const pointer = style({ cursor: 'pointer', display: 'flex', From ec65f793da20717c3093761d9c555e52adec9443 Mon Sep 17 00:00:00 2001 From: gahyeon Date: Tue, 10 Feb 2026 02:04:37 +0900 Subject: [PATCH 04/18] =?UTF-8?q?fix:=20=EC=83=88=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EC=8B=9C=20=ED=95=84=ED=84=B0=20=EB=B0=8F=20=EA=B0=80=EA=B2=A9?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=20=EC=B4=88=EA=B8=B0=ED=99=94=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=9D=B4=EC=8A=88=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useFilter.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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]) => { From 49c288bf5075750b729af16cb19aec65cf4319ad Mon Sep 17 00:00:00 2001 From: gahyeon Date: Tue, 10 Feb 2026 06:38:06 +0900 Subject: [PATCH 05/18] =?UTF-8?q?fix:=20=EB=AA=A8=EB=8B=AC=20z=20index=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B0=B0=EA=B2=BD=20=EC=BB=AC=EB=9F=AC=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/homePage.css.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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({ From cb53a6c9f8c9058514de8ced5d28e0d5a7d2aafe Mon Sep 17 00:00:00 2001 From: gahyeon Date: Tue, 10 Feb 2026 06:44:45 +0900 Subject: [PATCH 06/18] =?UTF-8?q?refactor:=20=EB=AC=B4=ED=95=9C=20?= =?UTF-8?q?=EB=A3=A8=ED=94=84=20=EB=B0=B0=EB=84=88=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/banner/MainBanner.tsx | 140 +++++---------------------- src/hooks/useInfiniteCarousel.ts | 123 +++++++++++++++++++++++ 2 files changed, 145 insertions(+), 118 deletions(-) create mode 100644 src/hooks/useInfiniteCarousel.ts diff --git a/src/components/banner/MainBanner.tsx b/src/components/banner/MainBanner.tsx index d37e2200..55ebd103 100644 --- a/src/components/banner/MainBanner.tsx +++ b/src/components/banner/MainBanner.tsx @@ -2,110 +2,31 @@ 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 }; +import useInfiniteCarousel from '@hooks/useInfiniteCarousel'; 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 [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); @@ -113,19 +34,6 @@ const MainBanner = () => { window.open(banner.link, '_blank'); } }; - - const handleKeyDown = (e: React.KeyboardEvent, banner: SlideItem) => { - if (e.key === 'Enter' || e.key === ' ') { - handleBannerClick(banner); - } - }; - - 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 */} @@ -135,19 +43,15 @@ const MainBanner = () => { transform: `translateX(calc(-${currentIndex * 100}% + ${dragOffset}px))`, transition: isAnimate ? 'transform 0.5s ease-in-out' : 'none', }} - onTransitionEnd={handleTransitionEnd} - onMouseDown={handleMouseDown} - onMouseMove={handleMouseMove} - onMouseUp={handleMouseUp} - onMouseLeave={handleMouseLeave}> - {slides.map((banner) => ( + {...handlers}> + {slides.map((banner, index) => (
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()}> { + data: T[]; + autoPlayInterval?: number; +} + +const useInfiniteCarousel = ({ data, autoPlayInterval }: UseInfiniteCarouselProps) => { + const totalOriginalSlides = data.length; + + const slides = useMemo( + () => [ + { ...data[data.length - 1], uniqueKey: 'clone-last' }, + ...data.map((item, idx) => ({ ...item, uniqueKey: `real-${idx}` })), + { ...data[0], uniqueKey: 'clone-first' }, + ], + [data], + ); + + const [currentIndex, setCurrentIndex] = useState(1); + const [isAnimate, setIsAnimate] = useState(true); + const [isDragging, setIsDragging] = useState(false); + const [startX, setStartX] = 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(() => { + if (!autoPlayInterval) return; + stopAutoSlide(); + timerRef.current = setInterval(moveNext, autoPlayInterval); + }, [moveNext, stopAutoSlide, autoPlayInterval]); + + useEffect(() => { + if (autoPlayInterval) { + startAutoSlide(); + return () => stopAutoSlide(); + } + }, [startAutoSlide, stopAutoSlide, currentIndex, autoPlayInterval]); + + const handleTransitionEnd = () => { + if (currentIndex === 0) { + setIsAnimate(false); + setCurrentIndex(totalOriginalSlides); + } else if (currentIndex === slides.length - 1) { + setIsAnimate(false); + setCurrentIndex(1); + } + }; + + const handleDragStart = (clientX: number) => { + stopAutoSlide(); + setIsDragging(true); + setStartX(clientX); + setCurrentX(clientX); // 시작할 때 현재 위치도 초기화 + setIsAnimate(false); + }; + + const handleDragMove = (clientX: number) => { + if (!isDragging) 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 displayIndex = + currentIndex === 0 + ? totalOriginalSlides + : currentIndex === slides.length - 1 + ? 1 + : currentIndex; + + const dragOffset = isDragging ? currentX - startX : 0; + + return { + slides, + currentIndex, + isAnimate, + dragOffset, + displayIndex, + totalOriginalSlides, + isSwiped: Math.abs(currentX - startX) > 5, + handlers: { + onMouseDown: (e: React.MouseEvent) => handleDragStart(e.clientX), + onMouseMove: (e: React.MouseEvent) => handleDragMove(e.clientX), + onMouseUp: handleDragEnd, + onMouseLeave: handleDragEnd, + onTransitionEnd: handleTransitionEnd, + }, + }; +}; + +export default useInfiniteCarousel; From 3191ed609f9de40cdc95a1ba66b7fadab2be9e5e Mon Sep 17 00:00:00 2001 From: gahyeon Date: Tue, 10 Feb 2026 06:46:20 +0900 Subject: [PATCH 07/18] =?UTF-8?q?fix:=20=EC=9D=B8=EA=B8=B0=20=ED=85=9C?= =?UTF-8?q?=ED=94=8C=EC=8A=A4=ED=85=8C=EC=9D=B4=20=EC=BA=90=EB=9F=AC?= =?UTF-8?q?=EC=85=80=20=EB=AC=B4=ED=95=9C=20=EB=A3=A8=ED=94=84=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../card/popularCard/PopularCard.tsx | 32 +++--- .../card/popularCard/popularCard.css.ts | 44 ++++---- .../card/popularCard/rankBtn.css.ts | 3 +- .../popularCarousel/CarouselIndex.tsx | 6 +- .../popularCarousel/PopularCarousel.tsx | 101 +++++++++--------- .../popularCarousel/popularCarousel.css.ts | 46 +++++--- src/stories/CarouselIndex.stories.ts | 4 +- 7 files changed, 134 insertions(+), 102 deletions(-) diff --git a/src/components/card/popularCard/PopularCard.tsx b/src/components/card/popularCard/PopularCard.tsx index 6b05cb53..92b57003 100644 --- a/src/components/card/popularCard/PopularCard.tsx +++ b/src/components/card/popularCard/PopularCard.tsx @@ -1,10 +1,9 @@ 'use client'; import Icon from '@assets/svgs'; -import RankBtn from '@components/card/popularCard/RankBtn'; import Image from 'next/image'; - import * as styles from './popularCard.css'; +import RankBtn from '@components/card/popularCard/RankBtn'; interface PopularCardProps { ranking: number; @@ -15,8 +14,9 @@ interface PopularCardProps { isLiked: boolean; onLikeToggle: (templestayId: number) => void; templestayId: number; - link: string; onClick: () => void; + link: string; + priority?: boolean; } const PopularCard = ({ @@ -30,6 +30,7 @@ const PopularCard = ({ templestayId, link, onClick, + priority = false, }: PopularCardProps) => { const handleLikeClick = (e: React.MouseEvent) => { e.stopPropagation(); @@ -39,22 +40,26 @@ 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..39d223b3 100644 --- a/src/components/card/popularCard/popularCard.css.ts +++ b/src/components/card/popularCard/popularCard.css.ts @@ -1,20 +1,29 @@ 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', }); -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 +31,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 +49,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/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..7356ec9e 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,42 @@ const PopularCarousel = ({ onRequireLogin }: PopularCarouselProps) => { }; return ( -
-
-
- {data && - data.map((temple) => ( - { - logClickEvent('click_popularity_card', { - label: temple.id, - }); - }} - /> - ))} -
+
+
+ {slides.map((temple, index) => ( + { + if (isSwiped) return; + + router.push(`/detail/${temple.id}`); + + logClickEvent('click_popularity_card', { + label: 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/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; From ccd224b1e6ec3dfaa623fc075d03f4749ea0e763 Mon Sep 17 00:00:00 2001 From: gahyeon Date: Tue, 10 Feb 2026 07:52:08 +0900 Subject: [PATCH 08/18] =?UTF-8?q?fix:=20Safari=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=ED=83=80=EC=9D=B4=ED=8B=80=202=EC=A4=84=20ellipsis=20=EA=B9=A8?= =?UTF-8?q?=EC=A7=90=20=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/card/templeStayCard/infoSection.css.ts | 1 - 1 file changed, 1 deletion(-) 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: { From e412194464b0029bf44e8385540407e82f253009 Mon Sep 17 00:00:00 2001 From: gahyeon Date: Tue, 10 Feb 2026 10:55:30 +0900 Subject: [PATCH 09/18] =?UTF-8?q?fix:=20=EB=B0=B0=EB=84=88=20=ED=8A=80?= =?UTF-8?q?=EC=96=B4=EB=82=98=EC=98=A4=EB=8A=94=20=EC=9D=B4=EC=8A=88=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filter/filterBottomSheetModal/filterModalContent.css.ts | 6 ++++++ src/styles/global.css.ts | 4 ++++ 2 files changed, 10 insertions(+) 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/styles/global.css.ts b/src/styles/global.css.ts index 490d956b..267bc609 100644 --- a/src/styles/global.css.ts +++ b/src/styles/global.css.ts @@ -4,6 +4,10 @@ globalStyle('*', { boxSizing: 'border-box', }); +globalStyle('html', { + paddingRight: '0px !important', +}); + globalStyle('html, body', { width: '100%', fontSize: '62.5%', From 7f3ee4c8fae672217b7218b05dc2f04e80e8794c Mon Sep 17 00:00:00 2001 From: bykbyk0401 Date: Fri, 6 Feb 2026 03:27:12 +0900 Subject: [PATCH 10/18] =?UTF-8?q?fix:=20=EB=B0=B0=EB=84=88=20=ED=84=B0?= =?UTF-8?q?=EC=B9=98=20=EC=8B=9C=20=EC=84=B8=EB=A1=9C=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/banner/MainBanner.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/banner/MainBanner.tsx b/src/components/banner/MainBanner.tsx index 55ebd103..462646dd 100644 --- a/src/components/banner/MainBanner.tsx +++ b/src/components/banner/MainBanner.tsx @@ -42,6 +42,7 @@ const MainBanner = () => { style={{ transform: `translateX(calc(-${currentIndex * 100}% + ${dragOffset}px))`, transition: isAnimate ? 'transform 0.5s ease-in-out' : 'none', + touchAction: 'pan-y', }} {...handlers}> {slides.map((banner, index) => ( From 453b86f77755ad19a0a26e4521b2d63ef7265009 Mon Sep 17 00:00:00 2001 From: gahyeon Date: Tue, 10 Feb 2026 06:44:45 +0900 Subject: [PATCH 11/18] =?UTF-8?q?refactor:=20=EB=AC=B4=ED=95=9C=20?= =?UTF-8?q?=EB=A3=A8=ED=94=84=20=EB=B0=B0=EB=84=88=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/banner/MainBanner.tsx | 4 ++-- src/hooks/useInfiniteCarousel.ts | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/components/banner/MainBanner.tsx b/src/components/banner/MainBanner.tsx index 462646dd..89fb2429 100644 --- a/src/components/banner/MainBanner.tsx +++ b/src/components/banner/MainBanner.tsx @@ -1,12 +1,12 @@ 'use client'; +import useInfiniteCarousel from '@hooks/useInfiniteCarousel'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; import React from 'react'; import BANNER_DATA from './bannerData'; import * as styles from './mainBanner.css'; -import useInfiniteCarousel from '@hooks/useInfiniteCarousel'; const MainBanner = () => { const router = useRouter(); @@ -45,7 +45,7 @@ const MainBanner = () => { touchAction: 'pan-y', }} {...handlers}> - {slides.map((banner, index) => ( + {slides.map((banner) => (
({ data, autoPlayInterval }: UseInfiniteCarouselP 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); @@ -62,16 +63,25 @@ const useInfiniteCarousel = ({ data, autoPlayInterval }: UseInfiniteCarouselP } }; - const handleDragStart = (clientX: number) => { + const handleDragStart = (clientX: number, clientY: number = 0) => { stopAutoSlide(); setIsDragging(true); setStartX(clientX); - setCurrentX(clientX); // 시작할 때 현재 위치도 초기화 + setStartY(clientY); + setCurrentX(clientX); setIsAnimate(false); }; - const handleDragMove = (clientX: number) => { + const handleDragMove = (clientX: number, clientY: number = 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); }; @@ -115,6 +125,11 @@ const useInfiniteCarousel = ({ data, autoPlayInterval }: UseInfiniteCarouselP 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, }, }; From 8c439ad1f731630f08b65c79bac4492c981c548f Mon Sep 17 00:00:00 2001 From: gahyeon Date: Thu, 12 Feb 2026 02:11:47 +0900 Subject: [PATCH 12/18] =?UTF-8?q?chore:=20=EB=A6=B0=ED=8A=B8=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/page.tsx | 4 ++-- src/app/search/page.tsx | 2 +- src/components/card/popularCard/PopularCard.tsx | 2 -- src/components/carousel/popularCarousel/PopularCarousel.tsx | 1 - src/components/common/button/buttonBar/ButtonBar.tsx | 3 ++- src/components/search/searchBar/SearchBar.tsx | 6 +++--- src/components/search/searchHeader/SearchHeader.tsx | 6 +++--- src/stories/PopularCard.stories.ts | 1 - 8 files changed, 11 insertions(+), 14 deletions(-) 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 eef1d982..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/components/card/popularCard/PopularCard.tsx b/src/components/card/popularCard/PopularCard.tsx index 92b57003..03bf9976 100644 --- a/src/components/card/popularCard/PopularCard.tsx +++ b/src/components/card/popularCard/PopularCard.tsx @@ -15,7 +15,6 @@ interface PopularCardProps { onLikeToggle: (templestayId: number) => void; templestayId: number; onClick: () => void; - link: string; priority?: boolean; } @@ -28,7 +27,6 @@ const PopularCard = ({ isLiked, onLikeToggle, templestayId, - link, onClick, priority = false, }: PopularCardProps) => { diff --git a/src/components/carousel/popularCarousel/PopularCarousel.tsx b/src/components/carousel/popularCarousel/PopularCarousel.tsx index 7356ec9e..a5272537 100644 --- a/src/components/carousel/popularCarousel/PopularCarousel.tsx +++ b/src/components/carousel/popularCarousel/PopularCarousel.tsx @@ -80,7 +80,6 @@ const PopularCarousel = ({ onRequireLogin }: PopularCarouselProps) => { templeName={temple.templeName} templestayId={temple.id} onLikeToggle={handleLikeToggle} - link={`/detail/${temple.id}`} onClick={() => { if (isSwiped) return; diff --git a/src/components/common/button/buttonBar/ButtonBar.tsx b/src/components/common/button/buttonBar/ButtonBar.tsx index 7d491ef0..7d75b928 100644 --- a/src/components/common/button/buttonBar/ButtonBar.tsx +++ b/src/components/common/button/buttonBar/ButtonBar.tsx @@ -1,8 +1,9 @@ -import { buttonBarWrapper, buttonBarContainer } from './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; diff --git a/src/components/search/searchBar/SearchBar.tsx b/src/components/search/searchBar/SearchBar.tsx index 245a444e..a55269b5 100644 --- a/src/components/search/searchBar/SearchBar.tsx +++ b/src/components/search/searchBar/SearchBar.tsx @@ -10,15 +10,15 @@ import * as styles from './searchBar.css'; interface SearchBarProps { searchText?: string; - autoFocus?: boolean; + inputAutoFocus?: boolean; } -const SearchBar = ({ searchText, autoFocus = false }: SearchBarProps) => { +const SearchBar = ({ searchText, inputAutoFocus = false }: SearchBarProps) => { const [inputValue, setInputValue] = useState(searchText || ''); const inputRef = useRef(null); useEffect(() => { - if (autoFocus && inputRef.current) { + if (inputAutoFocus && inputRef.current) { inputRef.current.focus(); } }, []); diff --git a/src/components/search/searchHeader/SearchHeader.tsx b/src/components/search/searchHeader/SearchHeader.tsx index d8e0d4ad..f5f2942c 100644 --- a/src/components/search/searchHeader/SearchHeader.tsx +++ b/src/components/search/searchHeader/SearchHeader.tsx @@ -6,10 +6,10 @@ import useNavigateTo from '@hooks/useNavigateTo'; interface SearchHeader { searchText?: string; prevPath: string | number; - autoFocus?: boolean; + inputAutoFocus?: boolean; } -const SearchHeader = ({ searchText, prevPath, autoFocus = false }: SearchHeader) => { +const SearchHeader = ({ searchText, prevPath, inputAutoFocus = false }: SearchHeader) => { const handleToBack = useNavigateTo(prevPath); return ( @@ -17,7 +17,7 @@ const SearchHeader = ({ searchText, prevPath, autoFocus = false }: SearchHeader) - + ); }; 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}`), From 97fc31586201783ba6a600c5d754f1e7577a7e5d Mon Sep 17 00:00:00 2001 From: gahyeon Date: Thu, 12 Feb 2026 02:12:24 +0900 Subject: [PATCH 13/18] =?UTF-8?q?style:=20=EC=8A=A4=ED=81=AC=EB=A1=A4?= =?UTF-8?q?=EB=B0=94=20=EC=88=A8=EA=B9=80=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/global.css.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/styles/global.css.ts b/src/styles/global.css.ts index 267bc609..30f0726c 100644 --- a/src/styles/global.css.ts +++ b/src/styles/global.css.ts @@ -4,6 +4,10 @@ globalStyle('*', { boxSizing: 'border-box', }); +globalStyle('::-webkit-scrollbar', { + display: 'none', +}); + globalStyle('html', { paddingRight: '0px !important', }); From af58a7b9f2f50ffea4ef8232c2c0d5b5b572c10b Mon Sep 17 00:00:00 2001 From: gahyeon Date: Thu, 12 Feb 2026 02:27:08 +0900 Subject: [PATCH 14/18] =?UTF-8?q?refactor:=20touch-action=20pan-y=EB=A5=BC?= =?UTF-8?q?=20=EC=8A=AC=EB=9D=BC=EC=9D=B4=EB=93=9C=20=EC=BB=A8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=84=88=EC=97=90=20=EC=A0=81=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/banner/MainBanner.tsx | 1 - src/components/banner/mainBanner.css.ts | 1 + src/components/card/popularCard/popularCard.css.ts | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/banner/MainBanner.tsx b/src/components/banner/MainBanner.tsx index 89fb2429..80a68183 100644 --- a/src/components/banner/MainBanner.tsx +++ b/src/components/banner/MainBanner.tsx @@ -42,7 +42,6 @@ const MainBanner = () => { style={{ transform: `translateX(calc(-${currentIndex * 100}% + ${dragOffset}px))`, transition: isAnimate ? 'transform 0.5s ease-in-out' : 'none', - touchAction: 'pan-y', }} {...handlers}> {slides.map((banner) => ( diff --git a/src/components/banner/mainBanner.css.ts b/src/components/banner/mainBanner.css.ts index 263cbc30..4d8aa741 100644 --- a/src/components/banner/mainBanner.css.ts +++ b/src/components/banner/mainBanner.css.ts @@ -7,6 +7,7 @@ export const container = style({ overflow: 'hidden', aspectRatio: '375 / 347', marginBottom: '40px', + touchAction: 'pan-y', }); export const slideList = style({ diff --git a/src/components/card/popularCard/popularCard.css.ts b/src/components/card/popularCard/popularCard.css.ts index 39d223b3..d595b513 100644 --- a/src/components/card/popularCard/popularCard.css.ts +++ b/src/components/card/popularCard/popularCard.css.ts @@ -4,6 +4,7 @@ import { style } from '@vanilla-extract/css'; export const container = style({ width: '100%', overflow: 'hidden', + touchAction: 'pan-y', }); export const slideList = style({ From 124a4616c55b7ddaeb0c0237960b21a42a179e60 Mon Sep 17 00:00:00 2001 From: gahyeon Date: Thu, 12 Feb 2026 02:28:51 +0900 Subject: [PATCH 15/18] =?UTF-8?q?fix:=20logClickEvent=20=ED=98=B8=EC=B6=9C?= =?UTF-8?q?=20=EC=88=9C=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/carousel/popularCarousel/PopularCarousel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/carousel/popularCarousel/PopularCarousel.tsx b/src/components/carousel/popularCarousel/PopularCarousel.tsx index a5272537..4f2e9ff3 100644 --- a/src/components/carousel/popularCarousel/PopularCarousel.tsx +++ b/src/components/carousel/popularCarousel/PopularCarousel.tsx @@ -83,11 +83,11 @@ const PopularCarousel = ({ onRequireLogin }: PopularCarouselProps) => { onClick={() => { if (isSwiped) return; - router.push(`/detail/${temple.id}`); - logClickEvent('click_popularity_card', { label: temple.id, }); + + router.push(`/detail/${temple.id}`); }} priority={index === 1} /> From d2e9d6cf3ec32aaf951988a5cb30f8a85a158ad1 Mon Sep 17 00:00:00 2001 From: gahyeon Date: Thu, 12 Feb 2026 02:38:54 +0900 Subject: [PATCH 16/18] =?UTF-8?q?fix:=20useInfiniteCarousel=20=EB=B9=88=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useInfiniteCarousel.ts | 98 ++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/src/hooks/useInfiniteCarousel.ts b/src/hooks/useInfiniteCarousel.ts index d513ecc6..9d990e18 100644 --- a/src/hooks/useInfiniteCarousel.ts +++ b/src/hooks/useInfiniteCarousel.ts @@ -7,15 +7,7 @@ interface UseInfiniteCarouselProps { const useInfiniteCarousel = ({ data, autoPlayInterval }: UseInfiniteCarouselProps) => { const totalOriginalSlides = data.length; - - const slides = useMemo( - () => [ - { ...data[data.length - 1], uniqueKey: 'clone-last' }, - ...data.map((item, idx) => ({ ...item, uniqueKey: `real-${idx}` })), - { ...data[0], uniqueKey: 'clone-first' }, - ], - [data], - ); + const hasSlides = totalOriginalSlides > 0; const [currentIndex, setCurrentIndex] = useState(1); const [isAnimate, setIsAnimate] = useState(true); @@ -26,34 +18,51 @@ const useInfiniteCarousel = ({ data, autoPlayInterval }: UseInfiniteCarouselP 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) return; + if (!autoPlayInterval || !hasSlides) return; + stopAutoSlide(); timerRef.current = setInterval(moveNext, autoPlayInterval); - }, [moveNext, stopAutoSlide, autoPlayInterval]); + }, [moveNext, stopAutoSlide, autoPlayInterval, hasSlides]); useEffect(() => { - if (autoPlayInterval) { - startAutoSlide(); - return () => stopAutoSlide(); - } - }, [startAutoSlide, stopAutoSlide, currentIndex, autoPlayInterval]); + if (!autoPlayInterval || !hasSlides) return; + + startAutoSlide(); + return () => stopAutoSlide(); + }, [startAutoSlide, stopAutoSlide, autoPlayInterval, hasSlides]); const handleTransitionEnd = () => { + if (!hasSlides) return; + if (currentIndex === 0) { setIsAnimate(false); setCurrentIndex(totalOriginalSlides); @@ -63,7 +72,9 @@ const useInfiniteCarousel = ({ data, autoPlayInterval }: UseInfiniteCarouselP } }; - const handleDragStart = (clientX: number, clientY: number = 0) => { + const handleDragStart = (clientX: number, clientY = 0) => { + if (!hasSlides) return; + stopAutoSlide(); setIsDragging(true); setStartX(clientX); @@ -72,7 +83,7 @@ const useInfiniteCarousel = ({ data, autoPlayInterval }: UseInfiniteCarouselP setIsAnimate(false); }; - const handleDragMove = (clientX: number, clientY: number = 0) => { + const handleDragMove = (clientX: number, clientY = 0) => { if (!isDragging) return; const diffX = Math.abs(clientX - startX); @@ -94,17 +105,14 @@ const useInfiniteCarousel = ({ data, autoPlayInterval }: UseInfiniteCarouselP setIsDragging(false); if (autoPlayInterval) startAutoSlide(); - if (diff < -threshold) { - moveNext(); - } else if (diff > threshold) { - movePrev(); - } else { - setIsAnimate(true); - } + if (diff < -threshold) moveNext(); + else if (diff > threshold) movePrev(); + else setIsAnimate(true); }; - const displayIndex = - currentIndex === 0 + const displayIndex = !hasSlides + ? 0 + : currentIndex === 0 ? totalOriginalSlides : currentIndex === slides.length - 1 ? 1 @@ -114,24 +122,26 @@ const useInfiniteCarousel = ({ data, autoPlayInterval }: UseInfiniteCarouselP return { slides, - currentIndex, - isAnimate, + currentIndex: hasSlides ? currentIndex : 0, + isAnimate: hasSlides ? isAnimate : false, dragOffset, displayIndex, totalOriginalSlides, - isSwiped: Math.abs(currentX - startX) > 5, - 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, - }, + isSwiped: hasSlides && Math.abs(currentX - startX) > 5, + handlers: hasSlides + ? { + 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, + } + : {}, }; }; From 9e36ea4d114657b55e9ce9a029e74c49fb3ba50d Mon Sep 17 00:00:00 2001 From: gahyeon Date: Thu, 12 Feb 2026 02:56:54 +0900 Subject: [PATCH 17/18] =?UTF-8?q?fix:=20onKeyDown=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=9F=AC=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/card/popularCard/PopularCard.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/card/popularCard/PopularCard.tsx b/src/components/card/popularCard/PopularCard.tsx index 03bf9976..d58557d1 100644 --- a/src/components/card/popularCard/PopularCard.tsx +++ b/src/components/card/popularCard/PopularCard.tsx @@ -43,7 +43,12 @@ const PopularCard = ({ onClick={onClick} role="button" tabIndex={0} - onKeyDown={(e) => e.key === 'Enter' || (e.key === ' ' && onClick())} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClick(); + } + }} aria-label={`${templestayName} 로 이동`} onDragStart={(e) => e.preventDefault()}>
From 618cfb2f2d148d3c21d6f5f96469d456eb6194aa Mon Sep 17 00:00:00 2001 From: gahyeon Date: Thu, 12 Feb 2026 03:06:11 +0900 Subject: [PATCH 18/18] =?UTF-8?q?refactor:=20handlers=EB=A5=BC=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useInfiniteCarousel.ts | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/hooks/useInfiniteCarousel.ts b/src/hooks/useInfiniteCarousel.ts index 9d990e18..7d87892b 100644 --- a/src/hooks/useInfiniteCarousel.ts +++ b/src/hooks/useInfiniteCarousel.ts @@ -110,6 +110,19 @@ const useInfiniteCarousel = ({ data, autoPlayInterval }: UseInfiniteCarouselP 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 @@ -118,30 +131,15 @@ const useInfiniteCarousel = ({ data, autoPlayInterval }: UseInfiniteCarouselP ? 1 : currentIndex; - const dragOffset = isDragging ? currentX - startX : 0; - return { slides, currentIndex: hasSlides ? currentIndex : 0, isAnimate: hasSlides ? isAnimate : false, - dragOffset, + dragOffset: isDragging ? currentX - startX : 0, displayIndex, totalOriginalSlides, isSwiped: hasSlides && Math.abs(currentX - startX) > 5, - handlers: hasSlides - ? { - 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, - } - : {}, + handlers: hasSlides ? handlers : {}, }; };