diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.module.less index c14be034..9ffffb01 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.module.less +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.module.less @@ -143,6 +143,16 @@ pointer-events: auto; } +.hideWhileFullscreen { + visibility: hidden; + pointer-events: none; +} + +.videoPortalHost { + width: 100%; + height: 100%; +} + // 전체화면 container (Portal, body에 항상 존재) .fullscreenContainer { position: fixed; diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v2.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v2.jsx index 7afbd9e3..8fe246c3 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v2.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v2.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState, useRef } from 'react'; import ReactDOM from 'react-dom'; import { useDispatch, useSelector } from 'react-redux'; import Spottable from '@enact/spotlight/Spottable'; @@ -24,7 +24,11 @@ import { stopMediaAutoClose, resetMediaAutoClose, } from '../../../../actions/mediaOverlayActions'; -import { pauseFullscreenVideo, resumeFullscreenVideo, clearAllVideoTimers } from '../../../../actions/playActions'; +import { + pauseFullscreenVideo, + resumeFullscreenVideo, + clearAllVideoTimers, +} from '../../../../actions/playActions'; import css from './ProductVideo.module.less'; const SpottableComponent = Spottable('div'); @@ -80,6 +84,16 @@ export function ProductVideoV2({ const videoPlayerWrapperRef = useRef(null); const normalContainerRef = useRef(null); const fullscreenContainerRef = useRef(null); + const videoPortalHostRef = useRef(null); + + const ensurePortalHost = useCallback(() => { + if (!videoPortalHostRef.current && typeof document !== 'undefined') { + const host = document.createElement('div'); + host.className = css.videoPortalHost; + videoPortalHostRef.current = host; + } + return videoPortalHostRef.current; + }, []); // 비디오 재생 가능 여부 체크 const canPlayVideo = useMemo(() => { @@ -149,33 +163,36 @@ export function ProductVideoV2({ }, [canPlayVideo, isPlaying]); // 썸네일 클릭 핸들러 - 비디오 재생 시작 + Redux dispatch + MediaPlayer 메서드 호출 - const handleThumbnailClick = useCallback((e) => { - console.log('🎬 [handleThumbnailClick] 썸네일 클릭됨', { - canPlayVideo, - isPlaying, - eventType: e?.type, - target: e?.target?.className, - bubbles: e?.bubbles, - timestamp: new Date().getTime(), - }); - if (canPlayVideo && !isPlaying) { - // console.log('[BgVideo] ProductVideoV2 - Starting video playback'); - console.log('🎬 [handleThumbnailClick] ✅ 비디오 재생 시작'); - setIsPlaying(true); + const handleThumbnailClick = useCallback( + (e) => { + console.log('🎬 [handleThumbnailClick] 썸네일 클릭됨', { + canPlayVideo, + isPlaying, + eventType: e?.type, + target: e?.target?.className, + bubbles: e?.bubbles, + timestamp: new Date().getTime(), + }); + if (canPlayVideo && !isPlaying) { + // console.log('[BgVideo] ProductVideoV2 - Starting video playback'); + console.log('🎬 [handleThumbnailClick] ✅ 비디오 재생 시작'); + setIsPlaying(true); - // 백그라운드 전체화면 비디오 일시정지 - // console.log('[BgVideo] ProductVideoV2 - Pausing background fullscreen video'); - dispatch(pauseFullscreenVideo()); + // 백그라운드 전체화면 비디오 일시정지 + // console.log('[BgVideo] ProductVideoV2 - Pausing background fullscreen video'); + dispatch(pauseFullscreenVideo()); - // Redux: mediaOverlay 상태 초기화 (MediaPlayer 전용) - dispatch(setMediaControlShow()); - dispatch(startMediaAutoClose(3000)); - // MediaPlayer 직접 제어: controls 표시 - setTimeout(() => { - videoPlayerRef.current?.showControls?.(); - }, 100); - } - }, [canPlayVideo, isPlaying, dispatch]); + // Redux: mediaOverlay 상태 초기화 (MediaPlayer 전용) + dispatch(setMediaControlShow()); + dispatch(startMediaAutoClose(3000)); + // MediaPlayer 직접 제어: controls 표시 + setTimeout(() => { + videoPlayerRef.current?.showControls?.(); + }, 100); + } + }, + [canPlayVideo, isPlaying, dispatch] + ); // 비디오 종료 핸들러 - 썸네일로 복귀 + Redux cleanup + MediaPlayer 메서드 호출 const handleVideoEnded = useCallback(() => { @@ -355,7 +372,7 @@ export function ProductVideoV2({ // FullScreen 모드에서의 MediaPlayer Click 핸들러 const handleVideoPlayerClick = useCallback( - (e) => { + (e) => { if (!isPlaying) return; if (!isFullscreen) { @@ -363,7 +380,7 @@ export function ProductVideoV2({ e.stopPropagation?.(); toggleFullscreen(); return; - } + } // fullscreen: overlay toggle e.preventDefault?.(); e.stopPropagation?.(); @@ -372,10 +389,10 @@ export function ProductVideoV2({ const isCurrentlyVisible = videoPlayerRef.current?.areControlsVisible?.(); if (isCurrentlyVisible) { videoPlayerRef.current?.hideControls?.(); - dispatch(setMediaControlHide()); // MediaOverlay Redux 상태 동기화 + dispatch(setMediaControlHide()); // MediaOverlay Redux 상태 동기화 } else { videoPlayerRef.current?.showControls?.(); - dispatch(setMediaControlShow()); // MediaOverlay Redux 상태 동기화 + dispatch(setMediaControlShow()); // MediaOverlay Redux 상태 동기화 } }, [isPlaying, isFullscreen, toggleFullscreen, dispatch, mediaOverlayState.controls?.visible] @@ -394,34 +411,40 @@ export function ProductVideoV2({ }, [mediaOverlayState.controls?.visible, dispatch]); // 전체 화면 컨테이너용 마우스 다운 핸들러 (Capture Phase) - const handleFullscreenContainerMouseDownCapture = useCallback((e) => { - if (!isPlaying || !isFullscreen) return; + const handleFullscreenContainerMouseDownCapture = useCallback( + (e) => { + if (!isPlaying || !isFullscreen) return; - e.preventDefault(); - e.stopPropagation(); - // toggleOverlayVisibility(); - }, [isPlaying, isFullscreen, toggleOverlayVisibility]); - - // 전체 화면 키보드 핸들러 - const handleFullscreenKeyDown = useCallback((e) => { - if (!isPlaying || !isFullscreen) return; - - // ESC 키만 오버레이 토글로 처리 - if (e.key === 'Escape' || e.keyCode === 27) { - console.log('🖥️ [Fullscreen Container] ESC 키 - 오버레이 토글 실행'); e.preventDefault(); e.stopPropagation(); - toggleOverlayVisibility(); - return; - } + // toggleOverlayVisibility(); + }, + [isPlaying, isFullscreen, toggleOverlayVisibility] + ); - // Enter 키는 기본 동작 허용 (포커스된 요소의 동작 수행) - if (e.key === 'Enter' || e.keyCode === 13) { - console.log('🖥️ [Fullscreen Container] Enter 키 - 포커스된 요소 동작 허용'); - // Enter 키는 preventDefault하지 않고 기본 동작 허용 - return; - } - }, [isPlaying, isFullscreen, toggleOverlayVisibility]); + // 전체 화면 키보드 핸들러 + const handleFullscreenKeyDown = useCallback( + (e) => { + if (!isPlaying || !isFullscreen) return; + + // ESC 키만 오버레이 토글로 처리 + if (e.key === 'Escape' || e.keyCode === 27) { + console.log('🖥️ [Fullscreen Container] ESC 키 - 오버레이 토글 실행'); + e.preventDefault(); + e.stopPropagation(); + toggleOverlayVisibility(); + return; + } + + // Enter 키는 기본 동작 허용 (포커스된 요소의 동작 수행) + if (e.key === 'Enter' || e.keyCode === 13) { + console.log('🖥️ [Fullscreen Container] Enter 키 - 포커스된 요소 동작 허용'); + // Enter 키는 preventDefault하지 않고 기본 동작 허용 + return; + } + }, + [isPlaying, isFullscreen, toggleOverlayVisibility] + ); // 마우스 다운 (클릭) 이벤트 - capture phase에서 처리 const handleVideoPlayerMouseDown = useCallback( @@ -466,20 +489,24 @@ export function ProductVideoV2({ useEffect(() => { const handleDocumentClick = (e) => { // ProductVideoV2 관련 요소인 경우만 로깅 - const isVideoElement = e.target?.closest('[class*="videoContainer"]') || - e.target?.closest('[class*="videoPlayer"]') || - e.target?.closest('[class*="videoThumbnail"]'); + const isVideoElement = + e.target?.closest('[class*="videoContainer"]') || + e.target?.closest('[class*="videoPlayer"]') || + e.target?.closest('[class*="videoThumbnail"]'); if (isVideoElement) { console.log('📄 [Document Level] 전역 클릭 감지됨', { eventPhase: e.eventPhase, bubbles: e.bubbles, target: e.target?.className, - eventPath: e.composedPath?.().slice(0, 6).map(el => ({ - tag: el.tagName, - className: el.className, - id: el.id - })) + eventPath: e + .composedPath?.() + .slice(0, 6) + .map((el) => ({ + tag: el.tagName, + className: el.className, + id: el.id, + })), }); } }; @@ -490,6 +517,36 @@ export function ProductVideoV2({ }; }, []); + useLayoutEffect(() => { + if (!isPlaying) return; + + const host = ensurePortalHost(); + const target = isFullscreen ? fullscreenContainerRef.current : normalContainerRef.current; + + if (host && target && host.parentNode !== target) { + target.appendChild(host); + } + }, [isPlaying, isFullscreen, ensurePortalHost]); + + useEffect(() => { + if (isPlaying) return; + + const host = videoPortalHostRef.current; + if (host?.parentNode) { + host.parentNode.removeChild(host); + } + }, [isPlaying]); + + useEffect(() => { + return () => { + const host = videoPortalHostRef.current; + if (host?.parentNode) { + host.parentNode.removeChild(host); + } + videoPortalHostRef.current = null; + }; + }, []); + // autoPlay 기능: 컴포넌트 마운트 후 500ms 후 자동 재생 useEffect(() => { if (autoPlay && canPlayVideo && !isPlaying) { @@ -595,17 +652,10 @@ export function ProductVideoV2({ const containerProps = useMemo(() => { const baseProps = { - onClick: handleVideoContainerClick, // 모든 상태에서 동일하게 적용 + onClick: handleVideoContainerClick, }; - if (isFullscreen) { - return { - ...baseProps, - spotlightRestrict: 'self-only', // 포커스가 밖으로 나가지 않도록 - spotlightId: 'product-video-v2-fullscreen', - onSpotlightDown: handleSpotlightDown, // 전체화면에서도 Down 키 동작 - }; - } else if (isPlaying) { + if (isPlaying) { return { ...baseProps, spotlightId: 'product-video-v2-playing', @@ -615,70 +665,89 @@ export function ProductVideoV2({ } else { return baseProps; } - }, [isFullscreen, isPlaying, handleVideoContainerClick, handleSpotlightDown, handleContainerKeyDown]); + }, [ + isFullscreen, + isPlaying, + handleVideoContainerClick, + handleSpotlightDown, + handleContainerKeyDown, + ]); // ⚠️ 간단한 해결책: 비디오 영역 클릭 시 직접 동작 실행 - const handleContainerClickFallback = useCallback((e) => { - const isThumbnailArea = e.target?.closest('[class*="videoThumbnail"]') || - e.target?.closest('[class*="playButton"]'); + const handleContainerClickFallback = useCallback( + (e) => { + const isThumbnailArea = + e.target?.closest('[class*="videoThumbnail"]') || + e.target?.closest('[class*="playButton"]'); - const isVideoPlayerArea = e.target?.closest('[class*="videoPlayer"]') || - e.target?.closest('[class*="VideoPlayer"]'); + const isVideoPlayerArea = + e.target?.closest('[class*="videoPlayer"]') || e.target?.closest('[class*="VideoPlayer"]'); - // 썸네일 클릭: 비디오 재생 시작 - if (isThumbnailArea && !isPlaying) { - console.log('🎬 [handleContainerClickFallback] 썸네일 클릭 → 비디오 재생 시작'); - handleThumbnailClick(e); - } + // 썸네일 클릭: 비디오 재생 시작 + if (isThumbnailArea && !isPlaying) { + console.log('🎬 [handleContainerClickFallback] 썸네일 클릭 → 비디오 재생 시작'); + handleThumbnailClick(e); + } - // 비디오 재생 중 클릭: 전체화면 토글 - if (isVideoPlayerArea && isPlaying && !isFullscreen) { - console.log('🎬 [handleContainerClickFallback] 비디오 클릭 → 전체화면 토글'); - toggleFullscreen(); - } - }, [isPlaying, isFullscreen, handleThumbnailClick, toggleFullscreen]); + // 비디오 재생 중 클릭: 전체화면 토글 + if (isVideoPlayerArea && isPlaying && !isFullscreen) { + console.log('🎬 [handleContainerClickFallback] 비디오 클릭 → 전체화면 토글'); + toggleFullscreen(); + } + }, + [isPlaying, isFullscreen, handleThumbnailClick, toggleFullscreen] + ); // ⚠️ 핵심: TScrollerDetail이 capture phase에서 이벤트를 막으므로, // ProductVideoV2 컨테이너 전체에서 capture phase를 다시 열어줌 (Hooks는 early return 전에) - const handleContainerMouseDownCapture = useCallback((e) => { - // 비디오 영역인지 확인 - const isVideoArea = e.currentTarget === containerRef.current; + const handleContainerMouseDownCapture = useCallback( + (e) => { + // 비디오 영역인지 확인 + const isVideoArea = e.currentTarget === containerRef.current; - console.log('🔓 [ProductVideoV2 Container] onMouseDownCapture - TScrollerDetail 차단 우회', { - isVideoArea, - target: e.target?.className, - isPlaying, - isFullscreen, - }); + console.log('🔓 [ProductVideoV2 Container] onMouseDownCapture - TScrollerDetail 차단 우회', { + isVideoArea, + target: e.target?.className, + isPlaying, + isFullscreen, + }); - // 전체 화면 모드가 아닐 때만 toggleFullscreen 호출 - if (!isFullscreen) { - toggleFullscreen(); + // 전체 화면 모드가 아닐 때만 toggleFullscreen 호출 + if (!isFullscreen) { + toggleFullscreen(); - // TScrollerDetail의 onClick 실행을 막기 위해 preventDefault 호출 - if (isPlaying) { - console.log('🔓 [ProductVideoV2] TScrollerDetail onClick 실행 차단'); - e.preventDefault(); - e.stopPropagation(); + // TScrollerDetail의 onClick 실행을 막기 위해 preventDefault 호출 + if (isPlaying) { + console.log('🔓 [ProductVideoV2] TScrollerDetail onClick 실행 차단'); + e.preventDefault(); + e.stopPropagation(); + } + } else { + // 전체 화면 모드에서는 오버레이(컨트롤)만 토글 + console.log('🖥️ [ProductVideoV2.OVERLAY-Toggle] 전체 화면 모드 - 오버레이 토글 시작'); + console.log( + '🖥️ [ProductVideoV2.OVERLAY-Toggle] 현재 오버레이 상태:', + overlayState.controls?.visible + ); + + try { + const result = dispatch(toggleControls()); + console.log( + '🖥️ [ProductVideoV2.OVERLAY-Toggle] toggleControls 액션 디스패치 결과:', + result + ); + + // 디스패치 후 상태 변화 확인 (setTimeout으로 비동기 처리) + setTimeout(() => { + console.log('🖥️ [ProductVideoV2.OVERLAY-Toggle] 액션 디스패치 후 상태 확인 필요'); + }, 10); + } catch (error) { + console.error('🖥️ [ProductVideoV2.OVERLAY-ERROR] toggleControls 디스패치 에러:', error); + } } - } else { - // 전체 화면 모드에서는 오버레이(컨트롤)만 토글 - console.log('🖥️ [ProductVideoV2.OVERLAY-Toggle] 전체 화면 모드 - 오버레이 토글 시작'); - console.log('🖥️ [ProductVideoV2.OVERLAY-Toggle] 현재 오버레이 상태:', overlayState.controls?.visible); - - try { - const result = dispatch(toggleControls()); - console.log('🖥️ [ProductVideoV2.OVERLAY-Toggle] toggleControls 액션 디스패치 결과:', result); - - // 디스패치 후 상태 변화 확인 (setTimeout으로 비동기 처리) - setTimeout(() => { - console.log('🖥️ [ProductVideoV2.OVERLAY-Toggle] 액션 디스패치 후 상태 확인 필요'); - }, 10); - } catch (error) { - console.error('🖥️ [ProductVideoV2.OVERLAY-ERROR] toggleControls 디스패치 에러:', error); - } - } - }, [isPlaying, isFullscreen, dispatch, toggleControls]); + }, + [isPlaying, isFullscreen, dispatch, toggleControls] + ); // Early return: 비디오 재생 불가능한 경우 if (!canPlayVideo) return null; @@ -687,17 +756,16 @@ export function ProductVideoV2({ // 전체화면: SpotlightContainer (self-only) // 일반 재생: SpottableComponent (포커스 가능) // 썸네일: div - const ContainerComponent = isFullscreen - ? SpotlightContainer - : isPlaying - ? SpottableComponent - : 'div'; + const ContainerComponent = isPlaying ? SpottableComponent : 'div'; // VideoPlayer 컴포넌트 생성 함수 const renderVideoPlayer = () => { if (!isPlaying) return null; - return ( + const host = ensurePortalHost(); + if (!host) return null; + + return ReactDOM.createPortal(