[251112] feat: ProductVideoV2 fullScreen ovelay toggle

🕐 커밋 시간: 2025. 11. 12. 17:35:22

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +57줄
  • 삭제: -28줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v2.jsx

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 소규모 기능 개선
This commit is contained in:
2025-11-12 17:35:24 +09:00
parent 1166311e4b
commit b17aa89c28
2 changed files with 97 additions and 52 deletions

View File

@@ -65,7 +65,7 @@ const isEnter = is('enter');
const isLeft = is('left'); const isLeft = is('left');
const isRight = is('right'); const isRight = is('right');
const jumpBackKeyCode = 3 const jumpBackKeyCode = 3;
const jumpForwardKeyCode = 39; const jumpForwardKeyCode = 39;
const controlsHandleAboveSelectionKeys = [13, 16777221, jumpBackKeyCode, jumpForwardKeyCode]; const controlsHandleAboveSelectionKeys = [13, 16777221, jumpBackKeyCode, jumpForwardKeyCode];
const getControlsHandleAboveHoldConfig = ({ frequency, time }) => ({ const getControlsHandleAboveHoldConfig = ({ frequency, time }) => ({
@@ -706,6 +706,7 @@ const VideoPlayerBase = class extends React.Component {
reactPlayerConfig: PropTypes.any, //for ReactPlayer reactPlayerConfig: PropTypes.any, //for ReactPlayer
qrCurrentItem: PropTypes.any, qrCurrentItem: PropTypes.any,
modalScale: PropTypes.number, modalScale: PropTypes.number,
notifyOnClickWhenNotModal: PropTypes.bool,
}; };
static contextType = FloatingLayerContext; static contextType = FloatingLayerContext;
@@ -718,6 +719,7 @@ const VideoPlayerBase = class extends React.Component {
jumpDelay: 200, jumpDelay: 200,
mediaControlsComponent: MediaControls, mediaControlsComponent: MediaControls,
miniFeedbackHideDelay: 2000, miniFeedbackHideDelay: 2000,
notifyOnClickWhenNotModal: false,
playbackRateHash: { playbackRateHash: {
fastForward: ['2', '4', '8', '16'], fastForward: ['2', '4', '8', '16'],
rewind: ['-2', '-4', '-8', '-16'], rewind: ['-2', '-4', '-8', '-16'],
@@ -1429,7 +1431,13 @@ const VideoPlayerBase = class extends React.Component {
} }
// 🎬 실시간 재생상태 로깅 // 🎬 실시간 재생상태 로깅
if (ev.type === 'onUpdate' || ev.type === 'onPlay' || ev.type === 'onPause' || ev.type === 'onStart' || ev.type === 'onLoadStart') { if (
ev.type === 'onUpdate' ||
ev.type === 'onPlay' ||
ev.type === 'onPause' ||
ev.type === 'onStart' ||
ev.type === 'onLoadStart'
) {
console.log('🎬 [MediaPlayer] 재생상태 실시간 로깅', { console.log('🎬 [MediaPlayer] 재생상태 실시간 로깅', {
eventType: ev.type, eventType: ev.type,
currentTime: el.currentTime, currentTime: el.currentTime,
@@ -1933,14 +1941,20 @@ const VideoPlayerBase = class extends React.Component {
// //
// Player Interaction events // Player Interaction events
// //
onVideoClick = () => { onVideoClick = (ev) => {
console.log('[MediaPlayer] onVideoClick 호출'); console.log('[MediaPlayer] onVideoClick 호출');
// modal 상태일 때 외부 onClick 핸들러가 있으면 호출 const shouldNotify =
if (this.props.panelInfo?.modal && this.props.onClick) { (this.props.panelInfo?.modal || this.props.notifyOnClickWhenNotModal) &&
console.log('[MediaPlayer] modal에서 onClick 호출'); typeof this.props.onClick === 'function';
this.props.onClick();
return; if (shouldNotify) {
console.log('[MediaPlayer] 외부 onClick 콜백 호출');
this.props.onClick(ev);
if (this.props.panelInfo?.modal && !this.props.notifyOnClickWhenNotModal) {
return;
}
} }
// MediaPanel에서는 controls 토글만 // MediaPanel에서는 controls 토글만

View File

@@ -86,6 +86,11 @@ export function ProductVideoV2({
const fullscreenContainerRef = useRef(null); const fullscreenContainerRef = useRef(null);
const videoPortalHostRef = useRef(null); const videoPortalHostRef = useRef(null);
const isFullscreenRef = useRef(isFullscreen);
useEffect(() => {
isFullscreenRef.current = isFullscreen;
}, [isFullscreen]);
const ensurePortalHost = useCallback(() => { const ensurePortalHost = useCallback(() => {
if (!videoPortalHostRef.current && typeof document !== 'undefined') { if (!videoPortalHostRef.current && typeof document !== 'undefined') {
const host = document.createElement('div'); const host = document.createElement('div');
@@ -373,9 +378,18 @@ export function ProductVideoV2({
// FullScreen 모드에서의 MediaPlayer Click 핸들러 // FullScreen 모드에서의 MediaPlayer Click 핸들러
const handleVideoPlayerClick = useCallback( const handleVideoPlayerClick = useCallback(
(e) => { (e) => {
const fullscreenNow = isFullscreenRef.current;
console.log('🎥 [ProductVideoV2] handleVideoPlayerClick 감지됨', {
isPlaying,
isFullscreen: fullscreenNow,
eventType: e?.type,
target: e?.target?.className,
});
if (!isPlaying) return; if (!isPlaying) return;
if (!isFullscreen) { if (!fullscreenNow) {
e.preventDefault?.(); e.preventDefault?.();
e.stopPropagation?.(); e.stopPropagation?.();
toggleFullscreen(); toggleFullscreen();
@@ -627,6 +641,8 @@ export function ProductVideoV2({
// VideoContainer의 모든 클릭 감시 (Hooks는 early return 전에 정의되어야 함) // VideoContainer의 모든 클릭 감시 (Hooks는 early return 전에 정의되어야 함)
const handleVideoContainerClick = useCallback( const handleVideoContainerClick = useCallback(
(e) => { (e) => {
if (isFullscreen) return;
console.log('🎥 [ProductVideoV2] videoContainer onClick 감지됨', { console.log('🎥 [ProductVideoV2] videoContainer onClick 감지됨', {
isPlaying, isPlaying,
isFullscreen, isFullscreen,
@@ -638,7 +654,7 @@ export function ProductVideoV2({
timestamp: new Date().getTime(), timestamp: new Date().getTime(),
}); });
if (isPlaying && !isFullscreen) { if (isPlaying) {
console.log('🎥 [ProductVideoV2] videoContainer 클릭 → 직접 전체화면 토글 실행', { console.log('🎥 [ProductVideoV2] videoContainer 클릭 → 직접 전체화면 토글 실행', {
direct: true, direct: true,
}); });
@@ -651,20 +667,22 @@ export function ProductVideoV2({
); );
const containerProps = useMemo(() => { const containerProps = useMemo(() => {
const baseProps = { if (isPlaying && !isFullscreen) {
onClick: handleVideoContainerClick,
};
if (isPlaying) {
return { return {
...baseProps, onClick: handleVideoContainerClick,
spotlightId: 'product-video-v2-playing', spotlightId: 'product-video-v2-playing',
onKeyDown: handleContainerKeyDown, // 일반 모드: 컨테이너에서 직접 처리 onKeyDown: handleContainerKeyDown,
onSpotlightDown: handleSpotlightDown, // 일반 재생에서도 Down 키 동작 onSpotlightDown: handleSpotlightDown,
}; };
} else {
return baseProps;
} }
if (!isFullscreen) {
return {
onClick: handleVideoContainerClick,
};
}
return {};
}, [ }, [
isFullscreen, isFullscreen,
isPlaying, isPlaying,
@@ -676,6 +694,8 @@ export function ProductVideoV2({
// ⚠️ 간단한 해결책: 비디오 영역 클릭 시 직접 동작 실행 // ⚠️ 간단한 해결책: 비디오 영역 클릭 시 직접 동작 실행
const handleContainerClickFallback = useCallback( const handleContainerClickFallback = useCallback(
(e) => { (e) => {
if (isFullscreen) return;
const isThumbnailArea = const isThumbnailArea =
e.target?.closest('[class*="videoThumbnail"]') || e.target?.closest('[class*="videoThumbnail"]') ||
e.target?.closest('[class*="playButton"]'); e.target?.closest('[class*="playButton"]');
@@ -683,14 +703,12 @@ export function ProductVideoV2({
const isVideoPlayerArea = const isVideoPlayerArea =
e.target?.closest('[class*="videoPlayer"]') || e.target?.closest('[class*="VideoPlayer"]'); e.target?.closest('[class*="videoPlayer"]') || e.target?.closest('[class*="VideoPlayer"]');
// 썸네일 클릭: 비디오 재생 시작
if (isThumbnailArea && !isPlaying) { if (isThumbnailArea && !isPlaying) {
console.log('🎬 [handleContainerClickFallback] 썸네일 클릭 → 비디오 재생 시작'); console.log('🎬 [handleContainerClickFallback] 썸네일 클릭 → 비디오 재생 시작');
handleThumbnailClick(e); handleThumbnailClick(e);
} }
// 비디오 재생 중 클릭: 전체화면 토글 if (isVideoPlayerArea && isPlaying) {
if (isVideoPlayerArea && isPlaying && !isFullscreen) {
console.log('🎬 [handleContainerClickFallback] 비디오 클릭 → 전체화면 토글'); console.log('🎬 [handleContainerClickFallback] 비디오 클릭 → 전체화면 토글');
toggleFullscreen(); toggleFullscreen();
} }
@@ -705,6 +723,8 @@ export function ProductVideoV2({
// 비디오 영역인지 확인 // 비디오 영역인지 확인
const isVideoArea = e.currentTarget === containerRef.current; const isVideoArea = e.currentTarget === containerRef.current;
console.log('####[ProductVideoV2 Container] isFullscreen:', isFullscreen);
console.log('🔓 [ProductVideoV2 Container] onMouseDownCapture - TScrollerDetail 차단 우회', { console.log('🔓 [ProductVideoV2 Container] onMouseDownCapture - TScrollerDetail 차단 우회', {
isVideoArea, isVideoArea,
target: e.target?.className, target: e.target?.className,
@@ -773,7 +793,7 @@ export function ProductVideoV2({
onTouchMove={handleUserActivity} onTouchMove={handleUserActivity}
onWheel={handleUserActivity} onWheel={handleUserActivity}
onClick={(e) => { onClick={(e) => {
console.log('🎬 [videoPlayerWrapper] onClick 실행됨', { console.log('<<<<<<< [videoPlayerWrapper] onClick 실행됨', {
isFullscreen, isFullscreen,
isPlaying, isPlaying,
eventType: e.type, eventType: e.type,
@@ -813,6 +833,7 @@ export function ProductVideoV2({
onEnded={handleVideoEnded} onEnded={handleVideoEnded}
onBackButton={handleBackButton} onBackButton={handleBackButton}
onClick={handleVideoPlayerClick} onClick={handleVideoPlayerClick}
notifyOnClickWhenNotModal
noAutoPlay={false} noAutoPlay={false}
noAutoShowMediaControls={!isFullscreen} noAutoShowMediaControls={!isFullscreen}
autoCloseTimeout={5000} autoCloseTimeout={5000}
@@ -858,8 +879,8 @@ export function ProductVideoV2({
{...containerProps} {...containerProps}
ref={containerRef} ref={containerRef}
className={`${css.videoContainer} ${isFullscreen ? css.hideWhileFullscreen : ''}`} className={`${css.videoContainer} ${isFullscreen ? css.hideWhileFullscreen : ''}`}
onMouseDownCapture={handleContainerMouseDownCapture} onMouseDownCapture={!isFullscreen ? handleContainerMouseDownCapture : undefined}
onClick={handleContainerClickFallback} onClick={!isFullscreen ? handleContainerClickFallback : undefined}
> >
{!isPlaying ? ( {!isPlaying ? (
// 썸네일 + 재생 버튼 표시 // 썸네일 + 재생 버튼 표시
@@ -892,34 +913,44 @@ export function ProductVideoV2({
<div <div
ref={normalContainerRef} ref={normalContainerRef}
className={css.normalPlayerContainer} className={css.normalPlayerContainer}
onClick={(e) => { onClick={
console.log('🎬 [normalContainerRef] onClick 실행됨', { !isFullscreen
isPlaying, ? (e) => {
isFullscreen, console.log('🎬 [normalContainerRef] onClick 실행됨', {
eventType: e.type, isPlaying,
target: e.target?.className, isFullscreen,
}); eventType: e.type,
handleVideoPlayerClick(e); target: e.target?.className,
}} });
onMouseDownCapture={(e) => { handleVideoPlayerClick(e);
const isVideoPlayerArea = }
e.target?.closest('[class*="videoPlayer"]') || : undefined
e.target?.closest('[class*="VideoPlayer"]'); }
onMouseDownCapture={
!isFullscreen
? (e) => {
const isVideoPlayerArea =
e.target?.closest('[class*="videoPlayer"]') ||
e.target?.closest('[class*="VideoPlayer"]');
console.log('🎬 [normalContainerRef] onMouseDownCapture 실행됨', { console.log('🎬 [normalContainerRef] onMouseDownCapture 실행됨', {
isPlaying, isPlaying,
isFullscreen, isFullscreen,
isVideoPlayerArea, isVideoPlayerArea,
target: e.target?.className, target: e.target?.className,
}); });
if (!isVideoPlayerArea) { if (!isVideoPlayerArea) {
e.preventDefault(); e.preventDefault();
console.log('🛑 [normalContainerRef] preventDefault - 스크롤 영역에서의 클릭'); console.log(
} else { '🛑 [normalContainerRef] preventDefault - 스크롤 영역에서의 클릭'
console.log('✅ [normalContainerRef] 이벤트 전파 허용 - VideoPlayer 영역'); );
} } else {
}} console.log('✅ [normalContainerRef] 이벤트 전파 허용 - VideoPlayer 영역');
}
}
: undefined
}
/> />
)} )}
</ContainerComponent> </ContainerComponent>