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 +++++++++++++++++++++++++++++++ components/login/login-form.tsx | 187 +++++++++++++-------------------- 2 files changed, 256 insertions(+), 112 deletions(-) create mode 100644 components/common/video-background.tsx (limited to 'components') 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(null); + const preloadedVideos = useRef([]); + + // 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((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 ( +
+ {/* 비디오 배경 */} + + + {/* 오버레이 */} +
+ + {/* 캐시 상태 표시 (개발용) */} + {showCacheStatus && cachedVideos > 0 && ( +
+
+ + Cached: {cachedVideos}/{videos.length} + +
+
+ )} + + {/* 비디오 인디케이터 */} + {showIndicators && isVideoLoaded && ( +
+ {videos.map((_, index) => ( +
+ )} +
+ ); +} + diff --git a/components/login/login-form.tsx b/components/login/login-form.tsx index 51d54531..fae5b8c9 100644 --- a/components/login/login-form.tsx +++ b/components/login/login-form.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from "react"; import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" -import { Ship, InfoIcon, GlobeIcon, ChevronDownIcon, ArrowLeft } from "lucide-react"; +import { InfoIcon, GlobeIcon, ChevronDownIcon, ArrowLeft } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem } from "@/components/ui/dropdown-menu" import { useTranslation } from '@/i18n/client' @@ -21,6 +21,7 @@ import { import { requestPasswordResetAction } from "@/lib/users/auth/partners-auth"; import { checkEmailAndStartAuth, resendEmailOtp } from "@/lib/users/auth/email-auth"; import Loading from "../common/loading/loading"; +import { VideoBackground } from "@/components/common/video-background"; // 배경 영상 목록 const BACKGROUND_VIDEOS = [ @@ -44,9 +45,6 @@ export function LoginForm() { const { toast } = useToast(); const { data: session, status } = useSession(); - // 배경 영상 상태 - const [currentVideoIndex, setCurrentVideoIndex] = useState(0); - // 상태 관리 const [isFirstAuthLoading, setIsFirstAuthLoading] = useState(false); const [showForgotPassword, setShowForgotPassword] = useState(false); @@ -83,14 +81,11 @@ export function LoginForm() { // 호스트명 확인 상태 추가 const [isDataRoomHost, setIsDataRoomHost] = useState(false); - // 컴포넌트 마운트 시 호스트명 확인 및 랜덤 비디오 선택 + // 컴포넌트 마운트 시 호스트명 확인 useEffect(() => { if (typeof window !== 'undefined') { const hostname = window.location.hostname; setIsDataRoomHost(hostname.includes('shidataroom')); - - // 랜덤한 비디오 인덱스 선택 - setCurrentVideoIndex(Math.floor(Math.random() * BACKGROUND_VIDEOS.length)); } }, []); @@ -168,11 +163,6 @@ export function LoginForm() { router.push(`/${lng}/partners/repository`); }; - // 비디오 종료 시 다음 비디오로 전환 - const handleVideoEnd = () => { - setCurrentVideoIndex((prevIndex) => (prevIndex + 1) % BACKGROUND_VIDEOS.length); - }; - // MFA 카운트다운 효과 useEffect(() => { if (mfaCountdown > 0) { @@ -576,47 +566,59 @@ export function LoginForm() { } return ( -
- {/* Left Content */} -
- {/* Top bar with Logo + eVCP (left) and "Request Vendor Repository" (right) */} -
-
- - {isDataRoomHost ? "Data Room" : "eVCP"} +
+ {/* 전체 화면 배경 영상 */} + + + {/* 로그인 카드 */} +
+ {/* 불투명한 카드 배경 */} +
+ {/* 헤더 */} +
+
+
+ {/* */} + {isDataRoomHost ? "Data Room" : "eVCP"} +
+ {!isDataRoomHost && ( + + + {t('registerVendor')} + + )} +
- {!isDataRoomHost && ( - - - {t('registerVendor')} - - )} -
- {/* Content section that occupies remaining space, centered vertically */} -
-
-
-
- {/* Header */} -
+ {/* 로그인 폼 컨텐츠 */} +
+
+ {/* Header */} +
{!showMfaForm ? ( <> -

{isDataRoomHost ? t('loginMessageDataRoom') :t('loginMessage')}

+

{isDataRoomHost ? t('loginMessageDataRoom') :t('loginMessage')}

{isDataRoomHost?t('loginDescriptionDataRoom') :t('loginDescription')}

) : ( <> -
+
{mfaType === 'email' ? '📧' : '🔐'}
-

+

{mfaType === 'email' ? t('emailVerification') : t('smsVerification')}

@@ -674,7 +676,7 @@ export function LoginForm() { variant="ghost" size="sm" onClick={handleBackToLogin} - className="text-blue-600 hover:text-blue-800" + className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" > {t('backToLogin')} @@ -682,9 +684,9 @@ export function LoginForm() {

{/* 이메일 표시 */} -
-

{t('loginEmail')}

-

{emailInput}

+
+

{t('loginEmail')}

+

{emailInput}

{/* 패스워드 입력 폼 */} @@ -722,7 +724,7 @@ export function LoginForm() {
{/* OTP 재전송 섹션 (SMS/Email) */} -
-

+
+

{t('resendCode')}

-

+

{mfaType === 'email' ? t('didNotReceiveEmail') : t('didNotReceiveCode')} @@ -797,7 +799,7 @@ export function LoginForm() {

-