import { useState, useEffect, useCallback, memo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { X, ChevronLeft, ChevronRight, Download } from 'lucide-react'; import LightboxIndicator from './LightboxIndicator'; /** * 라이트박스 공통 컴포넌트 * 이미지/비디오 갤러리를 전체 화면으로 표시 * * @param {string[]} images - 이미지/비디오 URL 배열 * @param {Object[]} photos - 메타데이터 포함 사진 배열 (선택적) * @param {string} photos[].title - 컨셉 이름 * @param {string} photos[].members - 멤버 이름 (쉼표 구분) * @param {Object[]} teasers - 티저 정보 배열 (비디오 여부 확인용) * @param {string} teasers[].media_type - 'video' 또는 'image' * @param {number} currentIndex - 현재 인덱스 * @param {boolean} isOpen - 열림 상태 * @param {function} onClose - 닫기 콜백 * @param {function} onIndexChange - 인덱스 변경 콜백 * @param {boolean} showCounter - 카운터 표시 여부 (기본: true) * @param {boolean} showDownload - 다운로드 버튼 표시 여부 (기본: true) */ function Lightbox({ images, photos, teasers, currentIndex, isOpen, onClose, onIndexChange, showCounter = true, showDownload = true, }) { const [imageLoaded, setImageLoaded] = useState(false); const [slideDirection, setSlideDirection] = useState(0); // 이전/다음 네비게이션 const goToPrev = useCallback(() => { if (images.length <= 1) return; setImageLoaded(false); setSlideDirection(-1); onIndexChange((currentIndex - 1 + images.length) % images.length); }, [images.length, currentIndex, onIndexChange]); const goToNext = useCallback(() => { if (images.length <= 1) return; setImageLoaded(false); setSlideDirection(1); onIndexChange((currentIndex + 1) % images.length); }, [images.length, currentIndex, onIndexChange]); const goToIndex = useCallback( (index) => { if (index === currentIndex) return; setImageLoaded(false); setSlideDirection(index > currentIndex ? 1 : -1); onIndexChange(index); }, [currentIndex, onIndexChange] ); // 이미지 다운로드 const downloadImage = useCallback(async () => { const imageUrl = images[currentIndex]; if (!imageUrl) return; try { const response = await fetch(imageUrl); const blob = await response.blob(); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `image_${currentIndex + 1}.jpg`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } catch (error) { console.error('이미지 다운로드 실패:', error); } }, [images, currentIndex]); // 라이트박스 열릴 때 body 스크롤 숨기기 useEffect(() => { if (isOpen) { document.documentElement.style.overflow = 'hidden'; document.body.style.overflow = 'hidden'; } else { document.documentElement.style.overflow = ''; document.body.style.overflow = ''; } return () => { document.documentElement.style.overflow = ''; document.body.style.overflow = ''; }; }, [isOpen]); // 키보드 이벤트 핸들러 useEffect(() => { if (!isOpen) return; const handleKeyDown = (e) => { switch (e.key) { case 'ArrowLeft': goToPrev(); break; case 'ArrowRight': goToNext(); break; case 'Escape': onClose(); break; default: break; } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isOpen, goToPrev, goToNext, onClose]); // 이미지가 바뀔 때 로딩 상태 리셋 useEffect(() => { setImageLoaded(false); }, [currentIndex]); // 현재 사진의 메타데이터 const currentPhoto = photos?.[currentIndex]; const photoTitle = currentPhoto?.title; const hasValidTitle = photoTitle && photoTitle.trim() && photoTitle !== 'Default'; const photoMembers = currentPhoto?.members; const hasMembers = photoMembers && String(photoMembers).trim(); return ( {isOpen && images.length > 0 && ( {/* 내부 컨테이너 */}
{/* 카운터 */} {showCounter && images.length > 1 && (
{currentIndex + 1} / {images.length}
)} {/* 상단 버튼들 */}
{showDownload && ( )}
{/* 이전 버튼 */} {images.length > 1 && ( )} {/* 로딩 스피너 */} {!imageLoaded && (
)} {/* 이미지/비디오 + 메타데이터 */}
{teasers?.[currentIndex]?.media_type === 'video' ? ( e.stopPropagation()} onCanPlay={() => setImageLoaded(true)} initial={{ x: slideDirection * 100 }} animate={{ x: 0 }} transition={{ duration: 0.25, ease: 'easeOut' }} controls autoPlay /> ) : ( e.stopPropagation()} onLoad={() => setImageLoaded(true)} initial={{ x: slideDirection * 100 }} animate={{ x: 0 }} transition={{ duration: 0.25, ease: 'easeOut' }} /> )} {/* 컨셉/멤버 정보 */} {imageLoaded && (hasValidTitle || hasMembers) && (
{hasValidTitle && ( {photoTitle} )} {hasMembers && (
{String(photoMembers) .split(',') .map((member, idx) => ( {member.trim()} ))}
)}
)}
{/* 다음 버튼 */} {images.length > 1 && ( )} {/* 인디케이터 */} {images.length > 1 && ( )}
)}
); } export default Lightbox;