diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-03 10:39:51 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-03 10:39:51 +0900 |
| commit | 1c5dd9c10ce4264ab157f4a2479e2055a3487de4 (patch) | |
| tree | e05cefb406b87d6152a7c08a5b96057528c796b5 /components/common | |
| parent | 98d178c8fe20a61a87e5d8f20e7d310ff6fefd6b (diff) | |
(김준회) 벤더 로그인화면 영상 더 크게 변경
Diffstat (limited to 'components/common')
| -rw-r--r-- | components/common/video-background.tsx | 181 |
1 files changed, 181 insertions, 0 deletions
diff --git a/components/common/video-background.tsx b/components/common/video-background.tsx new file mode 100644 index 00000000..96b9b4ac --- /dev/null +++ b/components/common/video-background.tsx @@ -0,0 +1,181 @@ +'use client'; + +import { useState, useEffect, useRef } from 'react'; +import { cn } from '@/lib/utils'; +import { registerServiceWorker, getCacheStatus } from '@/lib/service-worker/register'; + +interface VideoBackgroundProps { + videos: string[]; + className?: string; + overlayClassName?: string; + showIndicators?: boolean; + showCacheStatus?: boolean; + onVideoChange?: (index: number) => void; +} + +/** + * VideoBackground 컴포넌트 + * + * 특징: + * - 영상 프리로드로 부드러운 전환 + * - 자동 재생 및 순환 + * - Service Worker를 통한 영구 캐싱 + * - HTTP 캐시 헤더 지원 + * - 인디케이터 지원 + */ +export function VideoBackground({ + videos, + className, + overlayClassName, + showIndicators = true, + showCacheStatus = false, + onVideoChange, +}: VideoBackgroundProps) { + const [currentVideoIndex, setCurrentVideoIndex] = useState(0); + const [isVideoLoaded, setIsVideoLoaded] = useState(false); + const [cachedVideos, setCachedVideos] = useState(0); + const videoRef = useRef<HTMLVideoElement>(null); + const preloadedVideos = useRef<HTMLVideoElement[]>([]); + + // Service Worker 등록 + useEffect(() => { + const setupServiceWorker = async () => { + const registration = await registerServiceWorker(); + + if (registration) { + console.log('✅ Service Worker registered - Videos will be cached'); + + // 캐시 상태 확인 + setTimeout(async () => { + const status = await getCacheStatus(); + if (status) { + setCachedVideos(status.cached); + console.log(`📦 Cached videos: ${status.cached}/${status.total}`); + } + }, 2000); + } + }; + + setupServiceWorker(); + }, []); + + // 초기 랜덤 비디오 선택 + useEffect(() => { + const randomIndex = Math.floor(Math.random() * videos.length); + setCurrentVideoIndex(randomIndex); + }, [videos.length]); + + // 모든 비디오 프리로드 + useEffect(() => { + // 이미 프리로드된 경우 스킵 + if (preloadedVideos.current.length > 0) return; + + const preloadVideos = async () => { + const loadedVideos: HTMLVideoElement[] = []; + + for (let i = 0; i < videos.length; i++) { + const video = document.createElement('video'); + video.preload = 'auto'; + video.muted = true; + video.playsInline = true; + video.src = videos[i]; + + // 메타데이터 로드 대기 + await new Promise<void>((resolve) => { + video.addEventListener('loadedmetadata', () => resolve(), { once: true }); + // 타임아웃 설정 (10초) + setTimeout(() => resolve(), 10000); + }); + + loadedVideos.push(video); + } + + preloadedVideos.current = loadedVideos; + setIsVideoLoaded(true); + }; + + preloadVideos(); + + // 컴포넌트 언마운트 시 비디오 리소스 정리 + return () => { + preloadedVideos.current.forEach((video) => { + video.src = ''; + video.load(); + }); + preloadedVideos.current = []; + }; + }, [videos]); + + // 비디오 변경 시 콜백 호출 + useEffect(() => { + if (onVideoChange) { + onVideoChange(currentVideoIndex); + } + }, [currentVideoIndex, onVideoChange]); + + // 비디오 종료 시 다음 비디오로 전환 + const handleVideoEnd = () => { + setCurrentVideoIndex((prevIndex) => (prevIndex + 1) % videos.length); + }; + + // 인디케이터 클릭 핸들러 + const handleIndicatorClick = (index: number) => { + setCurrentVideoIndex(index); + // 비디오 즉시 재생 + if (videoRef.current) { + videoRef.current.currentTime = 0; + videoRef.current.play(); + } + }; + + return ( + <div className={cn('absolute inset-0 overflow-hidden', className)}> + {/* 비디오 배경 */} + <video + ref={videoRef} + key={currentVideoIndex} + autoPlay + muted + playsInline + onEnded={handleVideoEnd} + className="absolute inset-0 w-full h-full object-cover" + > + <source src={videos[currentVideoIndex]} type="video/mp4" /> + </video> + + {/* 오버레이 */} + <div className={cn('absolute inset-0 bg-black/40', overlayClassName)}></div> + + {/* 캐시 상태 표시 (개발용) */} + {showCacheStatus && cachedVideos > 0 && ( + <div className="absolute top-4 right-4 z-10 bg-black/70 text-white text-xs px-3 py-2 rounded-lg backdrop-blur-sm"> + <div className="flex items-center space-x-2"> + <span> + Cached: {cachedVideos}/{videos.length} + </span> + </div> + </div> + )} + + {/* 비디오 인디케이터 */} + {showIndicators && isVideoLoaded && ( + <div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 z-10 flex justify-center space-x-2"> + {videos.map((_, index) => ( + <button + key={index} + onClick={() => handleIndicatorClick(index)} + className={cn( + 'h-2 rounded-full transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-white/50', + index === currentVideoIndex + ? 'bg-white w-8' + : 'bg-white/50 hover:bg-white/75 w-2' + )} + aria-label={`비디오 ${index + 1}로 이동`} + /> + ))} + </div> + )} + </div> + ); +} + |
