176 lines
5.1 KiB
TypeScript
176 lines
5.1 KiB
TypeScript
// apps/dm/app/(landing)/reuse/_components/ImageSlider.tsx
|
|
"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; // 예: "max-w-[760px]"
|
|
};
|
|
|
|
export default function CaseImageSlider({
|
|
slides,
|
|
className = "",
|
|
maxWidthClassName = "max-w-[760px]",
|
|
}: Props) {
|
|
const safeSlides = useMemo(() => slides.filter(Boolean), [slides]);
|
|
const [idx, setIdx] = useState(0);
|
|
const [dragging, setDragging] = useState(false);
|
|
|
|
const startX = useRef(0);
|
|
const deltaX = useRef(0);
|
|
|
|
const total = safeSlides.length;
|
|
const canSlide = total > 1;
|
|
|
|
const go = (dir: number) => {
|
|
if (!canSlide) return;
|
|
setIdx((prev) => {
|
|
const next = prev + dir;
|
|
if (next < 0) return total - 1;
|
|
if (next >= total) return 0;
|
|
return next;
|
|
});
|
|
};
|
|
|
|
// ← → 키보드 지원(선택)
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (!canSlide) return;
|
|
if (e.key === "ArrowLeft") go(-1);
|
|
if (e.key === "ArrowRight") go(1);
|
|
};
|
|
window.addEventListener("keydown", onKey);
|
|
return () => window.removeEventListener("keydown", onKey);
|
|
}, [canSlide, total]);
|
|
|
|
// 드래그(마우스/터치)로 넘기기
|
|
const onPointerDown = (e: React.PointerEvent) => {
|
|
if (!canSlide) return;
|
|
setDragging(true);
|
|
startX.current = e.clientX;
|
|
deltaX.current = 0;
|
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
};
|
|
|
|
const onPointerMove = (e: React.PointerEvent) => {
|
|
if (!dragging) return;
|
|
deltaX.current = e.clientX - startX.current;
|
|
};
|
|
|
|
const onPointerUp = () => {
|
|
if (!dragging) return;
|
|
setDragging(false);
|
|
|
|
const threshold = 60; // 넘김 감도
|
|
if (deltaX.current > threshold) go(-1);
|
|
else if (deltaX.current < -threshold) go(1);
|
|
|
|
deltaX.current = 0;
|
|
};
|
|
|
|
if (!total) return null;
|
|
|
|
return (
|
|
<section className={className}>
|
|
<div className={`mx-auto ${maxWidthClassName}`}>
|
|
<div className="relative">
|
|
{/* 좌측 버튼 */}
|
|
<button
|
|
type="button"
|
|
onClick={() => go(-1)}
|
|
className="absolute left-[-60px] top-1/2 -translate-y-1/2 z-10
|
|
w-[80px] h-[80px] rounded-full border border-black/20 bg-white
|
|
grid place-items-center shadow-sm hover:bg-black/[0.03]"
|
|
>
|
|
<Image
|
|
src="/images/landing/icon_left_arrow.png"
|
|
alt="이전"
|
|
width={12}
|
|
height={22}
|
|
className="w-[12px] h-[22px]"
|
|
/>
|
|
</button>
|
|
|
|
|
|
{/* 우측 버튼 */}
|
|
<button
|
|
type="button"
|
|
onClick={() => go(1)}
|
|
className="absolute right-[-60px] top-1/2 -translate-y-1/2 z-10
|
|
w-[80px] h-[80px] rounded-full border border-black/20 bg-white
|
|
grid place-items-center shadow-sm hover:bg-black/[0.03]"
|
|
>
|
|
<Image
|
|
src="/images/landing/icon_right_arrow.png"
|
|
alt="다음"
|
|
width={12}
|
|
height={22}
|
|
className="w-[12px] h-[22px]"
|
|
/>
|
|
</button>
|
|
|
|
|
|
{/* 뷰포트 */}
|
|
<div
|
|
className={[
|
|
"overflow-hidden",
|
|
"select-none touch-pan-y",
|
|
"cursor-grab active:cursor-grabbing",
|
|
"w-[583px] h-[400px] mx-auto",
|
|
].join(" ")}
|
|
onPointerDown={onPointerDown}
|
|
onPointerMove={onPointerMove}
|
|
onPointerUp={onPointerUp}
|
|
onPointerCancel={onPointerUp}
|
|
>
|
|
<div
|
|
className="flex transition-transform duration-300 ease-out"
|
|
style={{ transform: `translateX(-${idx * 100}%)` }}
|
|
>
|
|
{safeSlides.map((s, i) => (
|
|
<div key={`${s.src}-${i}`} className="min-w-full">
|
|
<Image
|
|
src={s.src}
|
|
alt={s.alt ?? `slide-${i + 1}`}
|
|
width={583}
|
|
height={400}
|
|
className="w-full h-full object-contain"
|
|
style={{ width: "100%", height: "100%" }}
|
|
draggable={false}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 도트 */}
|
|
{canSlide && (
|
|
<div className="mt-3 flex items-center justify-center gap-2">
|
|
{safeSlides.map((_, i) => {
|
|
const active = i === idx;
|
|
return (
|
|
<button
|
|
key={i}
|
|
type="button"
|
|
onClick={() => setIdx(i)}
|
|
aria-label={`${i + 1}번 슬라이드`}
|
|
className={[
|
|
"h-2 w-2 rounded-full",
|
|
active ? "bg-black/70" : "bg-black/20 hover:bg-black/35",
|
|
].join(" ")}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|