ERP-node/frontend/lib/registry/pop-components/pop-dashboard/modes/AutoSlideMode.tsx

183 lines
5.4 KiB
TypeScript

"use client";
/**
* 자동 슬라이드 표시 모드
*
* 전광판처럼 자동 전환, 터치 시 멈춤, 일정 시간 후 재개
* 컴포넌트 unmount 시 타이머 정리 필수
*/
import React, { useState, useEffect, useRef, useCallback } from "react";
/** 스와이프 감지 최소 거리(px) */
const SWIPE_THRESHOLD = 50;
// ===== Props =====
export interface AutoSlideModeProps {
/** 총 아이템 수 */
itemCount: number;
/** 자동 전환 간격 (초, 기본 5) */
interval?: number;
/** 터치 후 자동 재개까지 대기 시간 (초, 기본 3) */
resumeDelay?: number;
/** 페이지 인디케이터 표시 여부 */
showIndicator?: boolean;
/** 현재 인덱스에 해당하는 아이템 렌더링 */
renderItem: (index: number) => React.ReactNode;
}
// ===== 메인 컴포넌트 =====
export function AutoSlideModeComponent({
itemCount,
interval = 5,
resumeDelay = 3,
showIndicator = true,
renderItem,
}: AutoSlideModeProps) {
const [currentIndex, setCurrentIndex] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const resumeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const touchStartX = useRef<number | null>(null);
const touchStartY = useRef<number | null>(null);
// 타이머 정리 함수
const clearTimers = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (resumeTimerRef.current) {
clearTimeout(resumeTimerRef.current);
resumeTimerRef.current = null;
}
}, []);
// 자동 슬라이드 시작
const startAutoSlide = useCallback(() => {
clearTimers();
if (itemCount <= 1) return;
intervalRef.current = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % itemCount);
}, interval * 1000);
}, [itemCount, interval, clearTimers]);
// 터치/클릭으로 일시 정지
const handlePause = useCallback(() => {
setIsPaused(true);
clearTimers();
// resumeDelay 후 자동 재개
resumeTimerRef.current = setTimeout(() => {
setIsPaused(false);
startAutoSlide();
}, resumeDelay * 1000);
}, [resumeDelay, clearTimers, startAutoSlide]);
// 스와이프 핸들러: 터치로 페이지 넘김 + 자동 슬라이드 일시정지
const handleTouchStart = useCallback((e: React.TouchEvent) => {
touchStartX.current = e.touches[0].clientX;
touchStartY.current = e.touches[0].clientY;
}, []);
const handleTouchEnd = useCallback(
(e: React.TouchEvent) => {
if (touchStartX.current === null || touchStartY.current === null) return;
const deltaX = e.changedTouches[0].clientX - touchStartX.current;
const deltaY = e.changedTouches[0].clientY - touchStartY.current;
if (
Math.abs(deltaX) > SWIPE_THRESHOLD &&
Math.abs(deltaX) > Math.abs(deltaY)
) {
// 스와이프로 페이지 전환
if (deltaX < 0) {
setCurrentIndex((prev) => (prev < itemCount - 1 ? prev + 1 : 0));
} else {
setCurrentIndex((prev) => (prev > 0 ? prev - 1 : itemCount - 1));
}
handlePause(); // 스와이프 후 자동 슬라이드 일시정지
} else {
// 스와이프가 아닌 단순 터치 -> 일시정지만
handlePause();
}
touchStartX.current = null;
touchStartY.current = null;
},
[itemCount, handlePause]
);
// 마운트 시 자동 슬라이드 시작, unmount 시 정리
useEffect(() => {
if (!isPaused) {
startAutoSlide();
}
return clearTimers;
}, [isPaused, startAutoSlide, clearTimers]);
if (itemCount === 0) {
return (
<div className="flex h-full w-full items-center justify-center">
<span className="text-xs text-muted-foreground"> </span>
</div>
);
}
return (
<div
className="relative h-full w-full"
onClick={handlePause}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
role="presentation"
>
{/* 콘텐츠 (슬라이드 애니메이션) */}
<div className="absolute inset-0 overflow-hidden">
<div
className="flex h-full transition-transform duration-500 ease-in-out"
style={{
width: `${itemCount * 100}%`,
transform: `translateX(-${currentIndex * (100 / itemCount)}%)`,
}}
>
{Array.from({ length: itemCount }).map((_, i) => (
<div
key={i}
className="h-full"
style={{ width: `${100 / itemCount}%` }}
>
{renderItem(i)}
</div>
))}
</div>
</div>
{/* 인디케이터 (콘텐츠 하단에 겹침) */}
{showIndicator && itemCount > 1 && (
<div className="absolute bottom-1 left-0 right-0 z-10 flex items-center justify-center gap-1.5">
{isPaused && (
<span className="mr-2 text-[10px] text-muted-foreground/70">
</span>
)}
{Array.from({ length: itemCount }).map((_, i) => (
<span
key={i}
className={`h-1.5 rounded-full transition-all ${
i === currentIndex
? "w-4 bg-primary"
: "w-1.5 bg-muted-foreground/30"
}`}
/>
))}
</div>
)}
</div>
);
}