Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e944c01
fix: 검색창 자동 포커스 처리
maylh Feb 9, 2026
44d0d29
fix: 필터 버튼 위치 버그 수정 및 결과 0개일 때 버튼 비활성화 처리
maylh Feb 9, 2026
c4c2797
fix: iOS 입력 시 자동 확대 방지 처리
maylh Feb 9, 2026
ec65f79
fix: 새 검색 시 필터 및 가격 상태 초기화되지 않는 이슈 수정
maylh Feb 9, 2026
49c288b
fix: 모달 z index 및 배경 컬러 중복 코드 수정
maylh Feb 9, 2026
cb53a6c
refactor: 무한 루프 배너 커스텀 훅으로 로직 분리
maylh Feb 9, 2026
3191ed6
fix: 인기 템플스테이 캐러셀 무한 루프로 수정
maylh Feb 9, 2026
ccd224b
fix: Safari에서 타이틀 2줄 ellipsis 깨짐 현상 수정
maylh Feb 9, 2026
e412194
fix: 배너 튀어나오는 이슈 해결
maylh Feb 10, 2026
7f3ee4c
fix: 배너 터치 시 세로스크롤 관리
bykbyk0401 Feb 5, 2026
453b86f
refactor: 무한 루프 배너 커스텀 훅으로 로직 분리
maylh Feb 9, 2026
8c439ad
chore: 린트 에러 수정
maylh Feb 11, 2026
97fc315
style: 스크롤바 숨김 처리
maylh Feb 11, 2026
af58a7b
refactor: touch-action pan-y를 슬라이드 컨테이너에 적용하도록 위치 변경
maylh Feb 11, 2026
124a461
fix: logClickEvent 호출 순서 변경
maylh Feb 11, 2026
d2e9d6c
fix: useInfiniteCarousel 빈 데이터 예외 처리 추가
maylh Feb 11, 2026
a1ca37e
Merge branch 'develop' into fix/#350/3sp-qa2
maylh Feb 11, 2026
9e36ea4
fix: onKeyDown 핸들러 로직 수정
maylh Feb 11, 2026
618cfb2
refactor: handlers를 변수로 분리
maylh Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/app/homePage.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
4 changes: 2 additions & 2 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
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';
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();
Expand Down
2 changes: 1 addition & 1 deletion src/app/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import leftPaddingStyle from './searchPage.css';
const SearchPage = () => {
return (
<>
<SearchHeader prevPath={'/'} />
<SearchHeader prevPath={'/'} inputAutoFocus={true} />
<div className={leftPaddingStyle}>
<RecentBtnBox />
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/searchResult/searchResultPage.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const headerContainer = style({
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
paddingTop: '1rem',
paddingTop: '1.2rem',
marginBottom: '1rem',
zIndex: 3,
});
Expand Down
191 changes: 21 additions & 170 deletions src/components/banner/MainBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,181 +1,39 @@
'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<NodeJS.Timeout | null>(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);
} else {
window.open(banner.link, '_blank');
}
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>, 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 (
<div className={styles.container}>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
Expand All @@ -186,22 +44,15 @@ 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) => (
<div
key={banner.uniqueKey}
className={styles.slideItem}
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()}>
<Image
Expand Down
1 change: 1 addition & 0 deletions src/components/banner/mainBanner.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const container = style({
overflow: 'hidden',
aspectRatio: '375 / 347',
marginBottom: '40px',
touchAction: 'pan-y',
});

export const slideList = style({
Expand Down
32 changes: 18 additions & 14 deletions src/components/card/popularCard/PopularCard.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,8 +14,8 @@ interface PopularCardProps {
isLiked: boolean;
onLikeToggle: (templestayId: number) => void;
templestayId: number;
link: string;
onClick: () => void;
priority?: boolean;
}

const PopularCard = ({
Expand All @@ -28,8 +27,8 @@ const PopularCard = ({
isLiked,
onLikeToggle,
templestayId,
link,
onClick,
priority = false,
}: PopularCardProps) => {
const handleLikeClick = (e: React.MouseEvent) => {
e.stopPropagation();
Expand All @@ -39,22 +38,26 @@ const PopularCard = ({
};

return (
<a
href={link}
className={styles.cardWrapper}
draggable={false}
onDragStart={(e) => e.preventDefault()}
onClick={onClick}>
<div>
<div className={styles.imgBox}>
<div
className={styles.slideItem}
onClick={onClick}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' || (e.key === ' ' && onClick())}
aria-label={`${templestayName} 로 이동`}
onDragStart={(e) => e.preventDefault()}>
<div className={styles.slideContent}>
<div className={styles.imageWrapper}>
<Image
src={templeImg}
alt={`${templestayName} 대표 이미지`}
fill
style={{ objectFit: 'cover' }}
style={{ objectFit: 'cover', objectPosition: 'center' }}
priority={priority}
/>
<RankBtn ranking={ranking} />
</div>

<div className={styles.bottomWrapper}>
<div className={styles.bottomContainer}>
<h3 className={styles.templestayName}>{templestayName}</h3>
Expand All @@ -64,12 +67,13 @@ const PopularCard = ({
<span>{templeName}</span>
</div>
</div>

<button className={styles.likeBtn} onClick={handleLikeClick}>
{isLiked ? <Icon.IcnFlowerPink /> : <Icon.IcnFlowerGray />}
</button>
</div>
</div>
</a>
</div>
);
};

Expand Down
Loading
Loading