From 1c5dd9c10ce4264ab157f4a2479e2055a3487de4 Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Mon, 3 Nov 2025 10:39:51 +0900
Subject: (김준회) 벤더 로그인화면 영상 더 크게 변경
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/common/video-background.tsx | 181 +++++++++++++++++++++++++++++++++
 1 file changed, 181 insertions(+)
 create mode 100644 components/common/video-background.tsx

(limited to 'components/common/video-background.tsx')

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>
+  );
+}
+
-- 
cgit v1.2.3

