/** * carousel.js * 모달 이미지 캐러셀: 무한 루프 슬라이드, 드래그/스와이프 및 UI 동기화 */ // ========================================================================== // 1. 유틸리티 및 계산 함수 (Internal Utils) // ========================================================================== /** 현재 스크롤 위치를 바탕으로 실제 이미지 인덱스(0 ~ length-1)를 반환 */ export function getRealIndex(container, originalLength) { const rawIndex = Math.round(container.scrollLeft / container.clientWidth); let index = rawIndex - 1; if (index < 0) index = originalLength - 1; if (index >= originalLength) index = 0; return index; } /** 드래그 종료 시 관성 및 이동 거리를 계산하여 다음 인덱스 결정 */ function calculateTargetIndex(delta, elapsed, currentScroll, width, originalLength) { const minMove = Math.min(width * 0.05, 20); const hasMoved = Math.abs(delta) >= minMove; // 빠른 스와이프(200ms 미만) 또는 일정 거리 이상 드래그 시 방향 확정 const direction = hasMoved && (Math.abs(delta) > width * 0.1 || elapsed < 200) ? (delta > 0 ? 1 : -1) : 0; let index = Math.round(currentScroll / width) + direction; return Math.max(0, Math.min(originalLength + 1, index)); } // ========================================================================== // 2. UI 동기화 및 스크롤 (UI & Scrolling) // ========================================================================== export function syncModalUI(originalLength) { const container = document.getElementById('modal-main-carousel-container'); if (!container) return; const index = getRealIndex(container, originalLength); // 썸네일 & 도트 업데이트 로직 (생략 - 기존과 동일) updateThumbnailUI(index); updateDotUI(index); ensureThumbnailVisible(index); } /** 특정 인덱스로 부드럽게 이동 */ export function scrollToImage(index) { const container = document.getElementById('modal-main-carousel-container'); if (!container) return; container.scrollTo({ left: container.clientWidth * (index + 1), behavior: 'smooth', }); } // ========================================================================== // 3. 캐러셀 엔진 (Core Engine) // ========================================================================== export function initBetterCarousel(container, originalLength) { let state = { isDragging: false, startX: 0, startScroll: 0, startTime: 0, }; const getX = (e) => (e.touches ? e.touches[0].pageX : e.pageX); // 핸들러 분리: 시작 const handleStart = (e) => { state.isDragging = true; state.startX = getX(e); state.startScroll = container.scrollLeft; state.startTime = Date.now(); }; // 핸들러 분리: 이동 const handleMove = (e) => { if (!state.isDragging) return; container.scrollLeft = state.startScroll - (getX(e) - state.startX); }; // 핸들러 분리: 종료 const handleEnd = () => { if (!state.isDragging) return; state.isDragging = false; const w = container.clientWidth; if (w <= 0) return; const delta = container.scrollLeft - state.startScroll; const elapsed = Date.now() - state.startTime; const index = calculateTargetIndex(delta, elapsed, container.scrollLeft, w, originalLength); finalizeScroll(container, index, w, originalLength); }; /** 드래그 종료 후 스냅 애니메이션 및 루프 처리 */ function finalizeScroll(container, index, width, originalLength) { requestAnimationFrame(() => { const targetLeft = index * width; const useSmooth = Math.abs(container.scrollLeft - targetLeft) > width * 0.03; container.style.scrollBehavior = useSmooth ? 'smooth' : 'auto'; container.scrollTo({ left: targetLeft }); // 루프 워프 처리 setTimeout( () => { container.style.scrollBehavior = 'auto'; if (index === 0) container.scrollLeft = width * originalLength; if (index === originalLength + 1) container.scrollLeft = width; syncModalUI(originalLength); }, useSmooth ? 320 : 0, ); }); } // 리스너 등록 container.addEventListener('mousedown', handleStart); container.addEventListener('touchstart', handleStart, { passive: true }); container.addEventListener('mousemove', handleMove); container.addEventListener('touchmove', handleMove, { passive: false }); container.addEventListener('mouseup', handleEnd); container.addEventListener('mouseleave', handleEnd); container.addEventListener('touchend', handleEnd); }