summaryrefslogtreecommitdiff
path: root/components/common/video-background.tsx
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-03 10:39:51 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-03 10:39:51 +0900
commit1c5dd9c10ce4264ab157f4a2479e2055a3487de4 (patch)
treee05cefb406b87d6152a7c08a5b96057528c796b5 /components/common/video-background.tsx
parent98d178c8fe20a61a87e5d8f20e7d310ff6fefd6b (diff)
(김준회) 벤더 로그인화면 영상 더 크게 변경
Diffstat (limited to 'components/common/video-background.tsx')
-rw-r--r--components/common/video-background.tsx181
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>
+ );
+}
+