158 lines
4.5 KiB
TypeScript
158 lines
4.5 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
type Slide = { src: string; alt?: string };
|
|
|
|
type Props = {
|
|
slides: Slide[];
|
|
className?: string;
|
|
maxWidthClassName?: string;
|
|
autoPlayMs?: number;
|
|
imageHeightClassName?: string;
|
|
};
|
|
|
|
const PER_VIEW = 2;
|
|
|
|
export default function CaseImageSlider({
|
|
slides,
|
|
className = "",
|
|
maxWidthClassName = "max-w-[700px]",
|
|
autoPlayMs = 4500,
|
|
imageHeightClassName = "h-[340px]",
|
|
}: Props) {
|
|
const safeSlides = useMemo(() => slides.filter(Boolean), [slides]);
|
|
|
|
const [page, setPage] = useState(0);
|
|
const [dragging, setDragging] = useState(false);
|
|
const [isHover, setIsHover] = useState(false);
|
|
|
|
const startX = useRef(0);
|
|
const deltaX = useRef(0);
|
|
|
|
const pageCount = Math.ceil(safeSlides.length / PER_VIEW);
|
|
const canSlide = pageCount > 1;
|
|
|
|
const goTo = (nextPage: number) => {
|
|
if (!canSlide) return;
|
|
|
|
if (nextPage < 0) setPage(pageCount - 1);
|
|
else if (nextPage >= pageCount) setPage(0);
|
|
else setPage(nextPage);
|
|
};
|
|
|
|
const goNext = () => goTo(page + 1);
|
|
const goPrev = () => goTo(page - 1);
|
|
|
|
// ✅ 자동롤링 (hover 시 정지 포함)
|
|
useEffect(() => {
|
|
if (!canSlide || dragging || isHover) return;
|
|
|
|
const timer = window.setInterval(() => {
|
|
setPage((prev) => (prev + 1 >= pageCount ? 0 : prev + 1));
|
|
}, autoPlayMs);
|
|
|
|
return () => window.clearInterval(timer);
|
|
}, [canSlide, dragging, isHover, pageCount, autoPlayMs]);
|
|
|
|
// ✅ swipe
|
|
const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
if (!canSlide) return;
|
|
|
|
setDragging(true);
|
|
startX.current = e.clientX;
|
|
deltaX.current = 0;
|
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
};
|
|
|
|
const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
if (!dragging) return;
|
|
deltaX.current = e.clientX - startX.current;
|
|
};
|
|
|
|
const onPointerUp = () => {
|
|
if (!dragging) return;
|
|
|
|
setDragging(false);
|
|
|
|
const threshold = 50;
|
|
|
|
if (deltaX.current > threshold) goPrev();
|
|
if (deltaX.current < -threshold) goNext();
|
|
|
|
deltaX.current = 0;
|
|
};
|
|
|
|
if (!safeSlides.length) return null;
|
|
|
|
return (
|
|
<section className={className}>
|
|
<div className={`mx-auto ${maxWidthClassName}`}>
|
|
<div
|
|
className="overflow-hidden select-none touch-pan-y cursor-grab active:cursor-grabbing"
|
|
onMouseEnter={() => setIsHover(true)} // ✅ hover 시작 → 정지
|
|
onMouseLeave={() => setIsHover(false)} // ✅ hover 끝 → 재시작
|
|
onPointerDown={onPointerDown}
|
|
onPointerMove={onPointerMove}
|
|
onPointerUp={onPointerUp}
|
|
onPointerCancel={onPointerUp}
|
|
>
|
|
<div
|
|
className="flex transition-transform duration-500 ease-out"
|
|
style={{
|
|
transform: `translateX(-${page * 100}%)`,
|
|
}}
|
|
>
|
|
{Array.from({ length: pageCount }).map((_, pageIndex) => {
|
|
const pageSlides = safeSlides.slice(
|
|
pageIndex * PER_VIEW,
|
|
pageIndex * PER_VIEW + PER_VIEW
|
|
);
|
|
|
|
return (
|
|
<div
|
|
key={pageIndex}
|
|
className="grid min-w-full grid-cols-2 gap-3"
|
|
>
|
|
{pageSlides.map((slide, i) => (
|
|
<div
|
|
key={`${slide.src}-${i}`}
|
|
className={`relative ${imageHeightClassName} overflow-hidden rounded-[20px] bg-neutral-100`}
|
|
>
|
|
<Image
|
|
src={slide.src}
|
|
alt={slide.alt ?? `slide-${pageIndex * PER_VIEW + i + 1}`}
|
|
fill
|
|
className="object-cover"
|
|
sizes="(max-width: 768px) 50vw, 340px"
|
|
draggable={false}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 도트 */}
|
|
{canSlide && (
|
|
<div className="mt-5 flex items-center justify-center gap-[6px]">
|
|
{Array.from({ length: pageCount }).map((_, i) => (
|
|
<button
|
|
key={i}
|
|
type="button"
|
|
onClick={() => setPage(i)}
|
|
className={[
|
|
"h-[8px] w-[8px] rounded-full transition-all",
|
|
page === i ? "bg-[#18BCEB]" : "bg-[#D8DDE3]",
|
|
].join(" ")}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
);
|
|
} |