[251113] feat: MediaPanel ref Video Control

🕐 커밋 시간: 2025. 11. 13. 20:51:15

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +41줄
  • 삭제: -3줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx (javascript):
     Deleted: MediaPanel()
This commit is contained in:
2025-11-13 20:51:15 +09:00
parent 1bf490c46c
commit a33213fb8c

View File

@@ -1,4 +1,11 @@
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react'; import React, {
useCallback,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
} from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
@@ -56,498 +63,542 @@ const YOUTUBECONFIG = {
}, },
}; };
const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props }) => { const MediaPanel = React.forwardRef(
const dispatch = useDispatch(); ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props }, ref) => {
const dispatch = useDispatch();
const videoPlayer = useRef(null); const videoPlayer = useRef(null);
const onEndedTimerRef = useRef(null); // ✅ onEnded 타이머 관리 const onEndedTimerRef = useRef(null); // ✅ onEnded 타이머 관리
const mediaEventListenersRef = useRef([]); // ✅ 미디어 이벤트 리스너 추적 const mediaEventListenersRef = useRef([]); // ✅ 미디어 이벤트 리스너 추적
const [modalStyle, setModalStyle] = React.useState({}); const [modalStyle, setModalStyle] = React.useState({});
const [modalScale, setModalScale] = React.useState(1); const [modalScale, setModalScale] = React.useState(1);
const [currentTime, setCurrentTime] = React.useState(0); const [currentTime, setCurrentTime] = React.useState(0);
const [videoLoaded, setVideoLoaded] = React.useState(false); const [videoLoaded, setVideoLoaded] = React.useState(false);
const [isSubtitleActive, setIsSubtitleActive] = React.useState(true); const [isSubtitleActive, setIsSubtitleActive] = React.useState(true);
const panelInfoRef = usePrevious(panelInfo); const panelInfoRef = usePrevious(panelInfo);
// modal/full screen에 따른 일시정지/재생 처리 // modal/full screen에 따른 일시정지/재생 처리
useEffect(() => { useEffect(() => {
// debugLog('[MediaPanel] ========== isOnTop useEffect =========='); // debugLog('[MediaPanel] ========== isOnTop useEffect ==========');
// debugLog('[MediaPanel] isOnTop:', isOnTop); // debugLog('[MediaPanel] isOnTop:', isOnTop);
// debugLog('[MediaPanel] panelInfo:', JSON.stringify(panelInfo, null, 2)); // debugLog('[MediaPanel] panelInfo:', JSON.stringify(panelInfo, null, 2));
if (panelInfo && panelInfo.modal) { if (panelInfo && panelInfo.modal) {
if (!isOnTop) { if (!isOnTop) {
// debugLog('[MediaPanel] Not on top - pausing video'); // debugLog('[MediaPanel] Not on top - pausing video');
dispatch(pauseModalMedia()); dispatch(pauseModalMedia());
} else if (isOnTop && panelInfo.isPaused) { } else if (isOnTop && panelInfo.isPaused) {
// debugLog('[MediaPanel] Back on top - resuming video'); // debugLog('[MediaPanel] Back on top - resuming video');
dispatch(resumeModalMedia()); dispatch(resumeModalMedia());
}
}
}, [isOnTop, panelInfo, dispatch]);
// videoPlayer ref를 통한 직접 제어
useEffect(() => {
if (panelInfo?.modal && videoPlayer.current) {
if (panelInfo.isPaused) {
// debugLog('[MediaPanel] Executing pause via videoPlayer.current');
videoPlayer.current.pause();
} else if (panelInfo.isPaused === false) {
// debugLog('[MediaPanel] Executing play via videoPlayer.current');
videoPlayer.current.play();
}
}
}, [panelInfo?.isPaused, panelInfo?.modal]);
useEffect(() => {
if (!videoPlayer.current) return;
if (!isYoutube) return;
if (panelInfo?.modal) return;
videoPlayer.current.showControls?.();
}, [isYoutube, panelInfo?.modal]);
const getPlayer = useCallback((ref) => {
videoPlayer.current = ref;
}, []);
// ✅ 안전한 비디오 플레이어 메서드 호출
const safePlayerCall = useCallback((methodName, ...args) => {
if (videoPlayer.current && typeof videoPlayer.current[methodName] === 'function') {
try {
return videoPlayer.current[methodName](...args);
} catch (err) {
if (DEBUG_MODE) console.warn(`[MediaPanel] ${methodName} 호출 실패:`, err);
}
}
return null;
}, []);
// VideoPlayer가 MEDIA 타입에서 setIsVODPaused를 호출하므로 더미 함수 제공
const setIsVODPaused = useCallback(() => {
// MediaPanel에서는 VOD pause 상태 관리 불필요 (단순 재생만)
}, []);
// PlayerOverlayContents에서 필요한 더미 함수들
const setSideContentsVisible = useCallback(() => {
// MediaPanel에서는 사이드 컨텐츠 사용 안 함
}, []);
const handleIndicatorDownClick = useCallback(() => {
// MediaPanel에서는 indicator 사용 안 함
}, []);
const handleIndicatorUpClick = useCallback(() => {
// MediaPanel에서는 indicator 사용 안 함
}, []);
// ✅ modal 스타일 설정
useEffect(() => {
let resizeObserver = null;
if (panelInfo.modal && panelInfo.modalContainerId) {
// modal 모드: modalContainerId 기반으로 위치와 크기 계산
const node = document.querySelector(`[data-spotlight-id="${panelInfo.modalContainerId}"]`);
if (node) {
const { width, height, top, left } = node.getBoundingClientRect();
// ProductVideo의 padding(6px * 2)과 추가 여유를 고려하여 크기 조정
// 비디오가 오른쪽으로 넘치지 않도록 충분한 여유 확보
const paddingOffset = 6 * 2; // padding 양쪽
const extraMargin = 6 * 2; // 추가 여유 (포커스 테두리 + 비디오 비율 고려)
const totalOffset = paddingOffset + extraMargin; // 24px
const adjustedWidth = width - totalOffset;
const adjustedHeight = height - totalOffset;
const adjustedTop = top + totalOffset / 2;
const adjustedLeft = left + totalOffset / 2;
const style = {
width: adjustedWidth + 'px',
height: adjustedHeight + 'px',
maxWidth: adjustedWidth + 'px',
maxHeight: adjustedHeight + 'px',
top: adjustedTop + 'px',
left: adjustedLeft + 'px',
position: 'fixed',
overflow: 'hidden', // visible → hidden으로 변경하여 넘치는 부분 숨김
};
setModalStyle(style);
let scale = 1;
if (typeof window === 'object') {
scale = adjustedWidth / window.innerWidth;
setModalScale(scale);
} }
} else {
setModalStyle(panelInfo.modalStyle || {});
setModalScale(panelInfo.modalScale || 1);
} }
} else if (isOnTop && !panelInfo.modal && !panelInfo.isMinimized && videoPlayer.current) { }, [isOnTop, panelInfo, dispatch]);
// ✅ 안전한 메서드 호출로 null/undefined 체크
const mediaState = safePlayerCall('getMediaState'); // videoPlayer ref를 통한 직접 제어
if (mediaState?.paused) { useEffect(() => {
safePlayerCall('play'); if (panelInfo?.modal && videoPlayer.current) {
if (panelInfo.isPaused) {
// debugLog('[MediaPanel] Executing pause via videoPlayer.current');
videoPlayer.current.pause();
} else if (panelInfo.isPaused === false) {
// debugLog('[MediaPanel] Executing play via videoPlayer.current');
videoPlayer.current.play();
}
} }
}, [panelInfo?.isPaused, panelInfo?.modal]);
const isControlsHidden = useEffect(() => {
videoPlayer.current.areControlsVisible && !videoPlayer.current.areControlsVisible(); if (!videoPlayer.current) return;
if (isControlsHidden) { if (!isYoutube) return;
safePlayerCall('showControls'); if (panelInfo?.modal) return;
videoPlayer.current.showControls?.();
}, [isYoutube, panelInfo?.modal]);
const getPlayer = useCallback((ref) => {
videoPlayer.current = ref;
}, []);
// ✅ 안전한 비디오 플레이어 메서드 호출
const safePlayerCall = useCallback((methodName, ...args) => {
if (videoPlayer.current && typeof videoPlayer.current[methodName] === 'function') {
try {
return videoPlayer.current[methodName](...args);
} catch (err) {
if (DEBUG_MODE) console.warn(`[MediaPanel] ${methodName} 호출 실패:`, err);
}
} }
} return null;
}, []);
// ✅ cleanup: resize observer 정리 const playViaRef = useCallback(() => safePlayerCall('play'), [safePlayerCall]);
return () => { const pauseViaRef = useCallback(() => safePlayerCall('pause'), [safePlayerCall]);
if (resizeObserver) { const seekViaRef = useCallback(
resizeObserver.disconnect(); (seconds = 0) => safePlayerCall('seek', seconds),
} [safePlayerCall]
}; );
}, [panelInfo, isOnTop]); const showControlsViaRef = useCallback(() => safePlayerCall('showControls'), [safePlayerCall]);
const hideControlsViaRef = useCallback(() => safePlayerCall('hideControls'), [safePlayerCall]);
const toggleControlsViaRef = useCallback(
() => safePlayerCall('toggleControls'),
[safePlayerCall]
);
const getMediaStateViaRef = useCallback(
() => safePlayerCall('getMediaState'),
[safePlayerCall]
);
// ✅ 비디오 클릭 시 modal → fullscreen 전환 또는 controls 토글 useImperativeHandle(
const onVideoClick = useCallback(() => { ref,
if (panelInfo.modal) { () => ({
// debugLog('[MediaPanel] Video clicked - switching to fullscreen'); play: playViaRef,
dispatch(switchMediaToFullscreen()); pause: pauseViaRef,
} else { seek: seekViaRef,
// 비디오 클릭 시 controls 토글 showControls: showControlsViaRef,
safePlayerCall('toggleControls'); hideControls: hideControlsViaRef,
} toggleControls: toggleControlsViaRef,
}, [dispatch, panelInfo.modal, safePlayerCall]); getMediaState: getMediaStateViaRef,
getInternalPlayer: () => videoPlayer.current,
}),
[
playViaRef,
pauseViaRef,
seekViaRef,
showControlsViaRef,
hideControlsViaRef,
toggleControlsViaRef,
getMediaStateViaRef,
]
);
const onClickBack = useCallback( // VideoPlayer가 MEDIA 타입에서 setIsVODPaused를 호출하므로 더미 함수 제공
(ev) => { const setIsVODPaused = useCallback(() => {
// ✅ 뒤로가기 시 비디오 정지 // MediaPanel에서는 VOD pause 상태 관리 불필요 (단순 재생만)
safePlayerCall('pause'); }, []);
// modal에서 full로 전환된 경우 다시 modal로 돌아감 // PlayerOverlayContents에서 필요한 더미 함수들
if (panelInfo.modalContainerId && !panelInfo.modal) { const setSideContentsVisible = useCallback(() => {
// 다시 modal로 돌리는 로직은 startVideoPlayer 액션을 사용할 수도 있지만 // MediaPanel에서는 사이드 컨텐츠 사용 안 함
// 여기서는 단순히 패널을 pop }, []);
dispatch(PanelActions.popPanel());
ev?.stopPropagation();
return;
}
if (!panelInfo.modal) { const handleIndicatorDownClick = useCallback(() => {
dispatch(PanelActions.popPanel()); // MediaPanel에서는 indicator 사용 안 함
ev?.stopPropagation(); }, []);
return;
}
},
[dispatch, panelInfo, safePlayerCall]
);
const currentPlayingUrl = useMemo(() => { const handleIndicatorUpClick = useCallback(() => {
return panelInfo?.showUrl; // MediaPanel에서는 indicator 사용 안 함
}, [panelInfo?.showUrl]); }, []);
const isYoutube = useMemo(() => { // ✅ modal 스타일 설정
if (currentPlayingUrl && currentPlayingUrl.includes('youtu')) { useEffect(() => {
return true; let resizeObserver = null;
}
return false;
}, [currentPlayingUrl]);
const currentSubtitleUrl = useMemo(() => { if (panelInfo.modal && panelInfo.modalContainerId) {
return panelInfo?.subtitle; // modal 모드: modalContainerId 기반으로 위치와 크기 계산
}, [panelInfo?.subtitle]); const node = document.querySelector(`[data-spotlight-id="${panelInfo.modalContainerId}"]`);
if (node) {
const { width, height, top, left } = node.getBoundingClientRect();
const reactPlayerSubtitleConfig = useMemo(() => { // ProductVideo의 padding(6px * 2)과 추가 여유를 고려하여 크기 조정
if (isSubtitleActive && currentSubtitleUrl) { // 비디오가 오른쪽으로 넘치지 않도록 충분한 여유 확보
return { const paddingOffset = 6 * 2; // padding 양쪽
file: { const extraMargin = 6 * 2; // 추가 여유 (포커스 테두리 + 비디오 비율 고려)
attributes: { const totalOffset = paddingOffset + extraMargin; // 24px
crossOrigin: 'true',
},
tracks: [{ kind: 'subtitles', src: currentSubtitleUrl, default: true }],
},
youtube: YOUTUBECONFIG,
};
} else {
return {
youtube: YOUTUBECONFIG,
};
}
}, [currentSubtitleUrl, isSubtitleActive]);
const videoType = useMemo(() => { const adjustedWidth = width - totalOffset;
if (currentPlayingUrl) { const adjustedHeight = height - totalOffset;
if (currentPlayingUrl.toLowerCase().endsWith('.mp4')) { const adjustedTop = top + totalOffset / 2;
return 'video/mp4'; const adjustedLeft = left + totalOffset / 2;
} else if (currentPlayingUrl.toLowerCase().endsWith('.mpd')) {
return 'application/dash+xml';
} else if (currentPlayingUrl.toLowerCase().endsWith('.m3u8')) {
return 'application/mpegurl';
}
}
return 'application/mpegurl';
}, [currentPlayingUrl]);
const videoThumbnailUrl = useMemo(() => { const style = {
return panelInfo?.thumbnailUrl; width: adjustedWidth + 'px',
}, [panelInfo?.thumbnailUrl]); height: adjustedHeight + 'px',
maxWidth: adjustedWidth + 'px',
const mediainfoHandler = useCallback( maxHeight: adjustedHeight + 'px',
(ev) => { top: adjustedTop + 'px',
const type = ev.type; left: adjustedLeft + 'px',
if (type !== 'timeupdate' && type !== 'durationchange') { position: 'fixed',
debugLog('mediainfoHandler....', type, ev, videoPlayer.current?.getMediaState()); overflow: 'hidden', // visible → hidden으로 변경하여 넘치는 부분 숨김
} };
setModalStyle(style);
// ✅ hlsError 처리 강화 let scale = 1;
if (ev === 'hlsError') { if (typeof window === 'object') {
scale = adjustedWidth / window.innerWidth;
setModalScale(scale);
}
} else {
setModalStyle(panelInfo.modalStyle || {});
setModalScale(panelInfo.modalScale || 1);
}
} else if (isOnTop && !panelInfo.modal && !panelInfo.isMinimized && videoPlayer.current) {
// ✅ 안전한 메서드 호출로 null/undefined 체크
const mediaState = safePlayerCall('getMediaState'); const mediaState = safePlayerCall('getMediaState');
if (mediaState && isNaN(Number(mediaState.playbackRate))) { if (mediaState?.paused) {
dispatch( safePlayerCall('play');
sendBroadCast({ }
type: 'videoError',
moreInfo: { reason: 'hlsError' }, const isControlsHidden =
}) videoPlayer.current.areControlsVisible && !videoPlayer.current.areControlsVisible();
); if (isControlsHidden) {
safePlayerCall('showControls');
}
}
// ✅ cleanup: resize observer 정리
return () => {
if (resizeObserver) {
resizeObserver.disconnect();
}
};
}, [panelInfo, isOnTop]);
// ✅ 비디오 클릭 시 modal → fullscreen 전환 또는 controls 토글
const onVideoClick = useCallback(() => {
if (panelInfo.modal) {
// debugLog('[MediaPanel] Video clicked - switching to fullscreen');
dispatch(switchMediaToFullscreen());
} else {
// 비디오 클릭 시 controls 토글
safePlayerCall('toggleControls');
}
}, [dispatch, panelInfo.modal, safePlayerCall]);
const onClickBack = useCallback(
(ev) => {
// ✅ 뒤로가기 시 비디오 정지
safePlayerCall('pause');
// modal에서 full로 전환된 경우 다시 modal로 돌아감
if (panelInfo.modalContainerId && !panelInfo.modal) {
// 다시 modal로 돌리는 로직은 startVideoPlayer 액션을 사용할 수도 있지만
// 여기서는 단순히 패널을 pop
dispatch(PanelActions.popPanel());
ev?.stopPropagation();
return; return;
} }
}
switch (type) { if (!panelInfo.modal) {
case 'timeupdate': { dispatch(PanelActions.popPanel());
ev?.stopPropagation();
return;
}
},
[dispatch, panelInfo, safePlayerCall]
);
const currentPlayingUrl = useMemo(() => {
return panelInfo?.showUrl;
}, [panelInfo?.showUrl]);
const isYoutube = useMemo(() => {
if (currentPlayingUrl && currentPlayingUrl.includes('youtu')) {
return true;
}
return false;
}, [currentPlayingUrl]);
const currentSubtitleUrl = useMemo(() => {
return panelInfo?.subtitle;
}, [panelInfo?.subtitle]);
const reactPlayerSubtitleConfig = useMemo(() => {
if (isSubtitleActive && currentSubtitleUrl) {
return {
file: {
attributes: {
crossOrigin: 'true',
},
tracks: [{ kind: 'subtitles', src: currentSubtitleUrl, default: true }],
},
youtube: YOUTUBECONFIG,
};
} else {
return {
youtube: YOUTUBECONFIG,
};
}
}, [currentSubtitleUrl, isSubtitleActive]);
const videoType = useMemo(() => {
if (currentPlayingUrl) {
if (currentPlayingUrl.toLowerCase().endsWith('.mp4')) {
return 'video/mp4';
} else if (currentPlayingUrl.toLowerCase().endsWith('.mpd')) {
return 'application/dash+xml';
} else if (currentPlayingUrl.toLowerCase().endsWith('.m3u8')) {
return 'application/mpegurl';
}
}
return 'application/mpegurl';
}, [currentPlayingUrl]);
const videoThumbnailUrl = useMemo(() => {
return panelInfo?.thumbnailUrl;
}, [panelInfo?.thumbnailUrl]);
const mediainfoHandler = useCallback(
(ev) => {
const type = ev.type;
if (type !== 'timeupdate' && type !== 'durationchange') {
debugLog('mediainfoHandler....', type, ev, videoPlayer.current?.getMediaState());
}
// ✅ hlsError 처리 강화
if (ev === 'hlsError') {
const mediaState = safePlayerCall('getMediaState'); const mediaState = safePlayerCall('getMediaState');
if (mediaState) { if (mediaState && isNaN(Number(mediaState.playbackRate))) {
setCurrentTime(mediaState.currentTime || 0); dispatch(
sendBroadCast({
type: 'videoError',
moreInfo: { reason: 'hlsError' },
})
);
return;
} }
break;
} }
case 'error': {
const mediaState = safePlayerCall('getMediaState'); switch (type) {
const errorInfo = mediaState?.error || 'unknown'; case 'timeupdate': {
dispatch( const mediaState = safePlayerCall('getMediaState');
sendBroadCast({ if (mediaState) {
type: 'videoError', setCurrentTime(mediaState.currentTime || 0);
moreInfo: { reason: errorInfo }, }
}) break;
); }
break; case 'error': {
const mediaState = safePlayerCall('getMediaState');
const errorInfo = mediaState?.error || 'unknown';
dispatch(
sendBroadCast({
type: 'videoError',
moreInfo: { reason: errorInfo },
})
);
break;
}
case 'loadeddata': {
setVideoLoaded(true);
break;
}
default:
break;
} }
case 'loadeddata': { },
setVideoLoaded(true); [dispatch, safePlayerCall]
break; );
const onEnded = useCallback(
(e) => {
debugLog('[MediaPanel] Video ended');
// continuousPlay는 MediaPlayer(VideoPlayer) 컴포넌트 내부에서 loop 속성으로 처리
// onEnded가 호출되면 loop=false 인 경우이므로 패널을 닫음
Spotlight.pause();
// ✅ 이전 타이머가 있으면 정리
if (onEndedTimerRef.current) {
clearTimeout(onEndedTimerRef.current);
} }
default:
break;
}
},
[dispatch, safePlayerCall]
);
const onEnded = useCallback( // ✅ 새로운 타이머 저장 (cleanup 시 정리용)
(e) => { onEndedTimerRef.current = setTimeout(() => {
debugLog('[MediaPanel] Video ended'); Spotlight.resume();
// continuousPlay는 MediaPlayer(VideoPlayer) 컴포넌트 내부에서 loop 속성으로 처리 dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL));
// onEnded가 호출되면 loop=false 인 경우이므로 패널을 닫음 onEndedTimerRef.current = null;
Spotlight.pause(); }, 1500);
// ✅ 이전 타이머가 있으면 정리 e?.stopPropagation();
if (onEndedTimerRef.current) { e?.preventDefault();
clearTimeout(onEndedTimerRef.current); },
} [dispatch]
);
// ✅ 새로운 타이머 저장 (cleanup 시 정리용) // ✅ useLayoutEffect: DOM 스타일 설정 (메모리 누수 방지)
onEndedTimerRef.current = setTimeout(() => { useLayoutEffect(() => {
Spotlight.resume(); const videoContainer = document.querySelector(`.${css.videoContainer}`);
dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL)); if (videoContainer && panelInfo.thumbnailUrl && !videoLoaded) {
onEndedTimerRef.current = null;
}, 1500);
e?.stopPropagation();
e?.preventDefault();
},
[dispatch]
);
// ✅ useLayoutEffect: DOM 스타일 설정 (메모리 누수 방지)
useLayoutEffect(() => {
const videoContainer = document.querySelector(`.${css.videoContainer}`);
if (videoContainer && panelInfo.thumbnailUrl && !videoLoaded) {
try {
videoContainer.style.background = `url(${panelInfo.thumbnailUrl}) center center / contain no-repeat`;
videoContainer.style.backgroundColor = 'black';
} catch (err) {
if (DEBUG_MODE) console.warn('[MediaPanel] 썸네일 스타일 설정 실패:', err);
}
}
// ✅ cleanup: 컴포넌트 언마운트 시 DOM 스타일 초기화
return () => {
if (videoContainer) {
try { try {
videoContainer.style.background = ''; videoContainer.style.background = `url(${panelInfo.thumbnailUrl}) center center / contain no-repeat`;
videoContainer.style.backgroundColor = ''; videoContainer.style.backgroundColor = 'black';
} catch (err) { } catch (err) {
// 스타일 초기화 실패는 무시 if (DEBUG_MODE) console.warn('[MediaPanel] 썸네일 스타일 설정 실패:', err);
} }
} }
};
}, [panelInfo.thumbnailUrl, videoLoaded]);
useEffect(() => { // ✅ cleanup: 컴포넌트 언마운트 시 DOM 스타일 초기화
setVideoLoaded(false); return () => {
}, [currentPlayingUrl]); if (videoContainer) {
// ✅ 컴포넌트 언마운트 시 모든 타이머 및 리소스 정리
useEffect(() => {
return () => {
// ✅ onEnded 타이머 정리
if (onEndedTimerRef.current) {
clearTimeout(onEndedTimerRef.current);
onEndedTimerRef.current = null;
}
// ✅ 비디오 플레이어 정지 및 정리
if (videoPlayer.current) {
try {
// 재생 중이면 정지
safePlayerCall('pause');
// controls 타임아웃 정리
safePlayerCall('hideControls');
} catch (err) {
if (DEBUG_MODE) console.warn('[MediaPanel] 비디오 정지 실패:', err);
}
// ref 초기화
videoPlayer.current = null;
}
// ✅ 이벤트 리스너 정리
if (mediaEventListenersRef.current && mediaEventListenersRef.current.length > 0) {
mediaEventListenersRef.current.forEach(({ element, event, handler }) => {
try { try {
element?.removeEventListener?.(event, handler); videoContainer.style.background = '';
videoContainer.style.backgroundColor = '';
} catch (err) { } catch (err) {
// 리스너 제거 실패는 무시 // 스타일 초기화 실패는 무시
} }
}); }
mediaEventListenersRef.current = []; };
} }, [panelInfo.thumbnailUrl, videoLoaded]);
// ✅ Spotlight 상태 초기화 useEffect(() => {
try { setVideoLoaded(false);
Spotlight.resume?.(); }, [currentPlayingUrl]);
} catch (err) {
// Spotlight 초기화 실패는 무시
}
};
}, [dispatch, safePlayerCall]);
// debugLog('[MediaPanel] ========== Rendering =========='); // ✅ 컴포넌트 언마운트 시 모든 타이머 및 리소스 정리
// debugLog('[MediaPanel] isOnTop:', isOnTop); useEffect(() => {
// debugLog('[MediaPanel] panelInfo.modal:', panelInfo.modal); return () => {
// debugLog('[MediaPanel] panelInfo.isMinimized:', panelInfo.isMinimized); // ✅ onEnded 타이머 정리
// debugLog('[MediaPanel] panelInfo.isPaused:', panelInfo.isPaused); if (onEndedTimerRef.current) {
// debugLog('[MediaPanel] currentPlayingUrl:', currentPlayingUrl); clearTimeout(onEndedTimerRef.current);
// debugLog('[MediaPanel] hasVideoPlayer:', !!videoPlayer.current); onEndedTimerRef.current = null;
}
// classNames 적용 상태 확인 // ✅ 비디오 플레이어 정지 및 정리
// debugLog('[MediaPanel] ========== ClassNames Analysis =========='); if (videoPlayer.current) {
// debugLog('[MediaPanel] css.videoContainer:', css.videoContainer); try {
// debugLog('[MediaPanel] Condition [panelInfo.modal && !panelInfo.isMinimized]:', panelInfo.modal && !panelInfo.isMinimized); // 재생 중이면 정지
// debugLog('[MediaPanel] css.modal:', css.modal); safePlayerCall('pause');
// debugLog('[MediaPanel] Condition [panelInfo.isMinimized]:', panelInfo.isMinimized);
// debugLog('[MediaPanel] css["modal-minimized"]:', css['modal-minimized']);
// debugLog('[MediaPanel] Condition [!isOnTop]:', !isOnTop);
// debugLog('[MediaPanel] css.background:', css.background);
const appliedClassNames = classNames( // controls 타임아웃 정리
css.videoContainer, safePlayerCall('hideControls');
panelInfo.modal && !panelInfo.isMinimized && css.modal, } catch (err) {
panelInfo.isMinimized && css['modal-minimized'], if (DEBUG_MODE) console.warn('[MediaPanel] 비디오 정지 실패:', err);
!isOnTop && css.background }
);
// debugLog('[MediaPanel] Final Applied ClassNames:', appliedClassNames);
// debugLog('[MediaPanel] modalStyle:', modalStyle);
// debugLog('[MediaPanel] modalScale:', modalScale);
// debugLog('[MediaPanel] ===============================================');
// minimized 상태일 때는 spotlightRestrict 해제 (포커스 이동 허용) // ref 초기화
const containerSpotlightRestrict = panelInfo.isMinimized ? 'none' : 'self-only'; videoPlayer.current = null;
const shouldDisableIframeInteraction = isYoutube && !panelInfo.modal; }
return ( // ✅ 이벤트 리스너 정리
<TPanel if (mediaEventListenersRef.current && mediaEventListenersRef.current.length > 0) {
isTabActivated={false} mediaEventListenersRef.current.forEach(({ element, event, handler }) => {
{...props} try {
className={appliedClassNames} element?.removeEventListener?.(event, handler);
handleCancel={onClickBack} } catch (err) {
spotlightId={spotlightId} // 리스너 제거 실패는 무시
> }
<Container });
spotlightRestrict={containerSpotlightRestrict} mediaEventListenersRef.current = [];
spotlightId="spotlightId-media-video-container" }
// ✅ Spotlight 상태 초기화
try {
Spotlight.resume?.();
} catch (err) {
// Spotlight 초기화 실패는 무시
}
};
}, [dispatch, safePlayerCall]);
// debugLog('[MediaPanel] ========== Rendering ==========');
// debugLog('[MediaPanel] isOnTop:', isOnTop);
// debugLog('[MediaPanel] panelInfo.modal:', panelInfo.modal);
// debugLog('[MediaPanel] panelInfo.isMinimized:', panelInfo.isMinimized);
// debugLog('[MediaPanel] panelInfo.isPaused:', panelInfo.isPaused);
// debugLog('[MediaPanel] currentPlayingUrl:', currentPlayingUrl);
// debugLog('[MediaPanel] hasVideoPlayer:', !!videoPlayer.current);
// classNames 적용 상태 확인
// debugLog('[MediaPanel] ========== ClassNames Analysis ==========');
// debugLog('[MediaPanel] css.videoContainer:', css.videoContainer);
// debugLog('[MediaPanel] Condition [panelInfo.modal && !panelInfo.isMinimized]:', panelInfo.modal && !panelInfo.isMinimized);
// debugLog('[MediaPanel] css.modal:', css.modal);
// debugLog('[MediaPanel] Condition [panelInfo.isMinimized]:', panelInfo.isMinimized);
// debugLog('[MediaPanel] css["modal-minimized"]:', css['modal-minimized']);
// debugLog('[MediaPanel] Condition [!isOnTop]:', !isOnTop);
// debugLog('[MediaPanel] css.background:', css.background);
const appliedClassNames = classNames(
css.videoContainer,
panelInfo.modal && !panelInfo.isMinimized && css.modal,
panelInfo.isMinimized && css['modal-minimized'],
!isOnTop && css.background
);
// debugLog('[MediaPanel] Final Applied ClassNames:', appliedClassNames);
// debugLog('[MediaPanel] modalStyle:', modalStyle);
// debugLog('[MediaPanel] modalScale:', modalScale);
// debugLog('[MediaPanel] ===============================================');
// minimized 상태일 때는 spotlightRestrict 해제 (포커스 이동 허용)
const containerSpotlightRestrict = panelInfo.isMinimized ? 'none' : 'self-only';
const shouldDisableIframeInteraction = isYoutube && !panelInfo.modal;
return (
<TPanel
isTabActivated={false}
{...props}
className={appliedClassNames}
handleCancel={onClickBack}
spotlightId={spotlightId}
> >
{currentPlayingUrl && ( <Container
<div spotlightRestrict={containerSpotlightRestrict}
className={classNames( spotlightId="spotlightId-media-video-container"
css.videoPlayerWrapper, >
shouldDisableIframeInteraction && css.youtubeSafe {currentPlayingUrl && (
)} <div
> className={classNames(
<MediaPlayerV2 css.videoPlayerWrapper,
setApiProvider={getPlayer} shouldDisableIframeInteraction && css.youtubeSafe
disabled={panelInfo.modal}
onEnded={onEnded}
autoPlay={!panelInfo?.isPaused}
noAutoShowMediaControls={panelInfo.modal} // modal 모드에서는 자동으로 controls가 올라오지 않도록 설정
autoCloseTimeout={3000}
onBackButton={onClickBack}
onClick={onVideoClick}
spotlightDisabled={panelInfo.modal}
isYoutube={isYoutube}
src={currentPlayingUrl}
loop={panelInfo.continuousPlay || false}
style={panelInfo.modal ? modalStyle : {}}
modalScale={panelInfo.modal ? modalScale : 1}
modalClassName={panelInfo.modal ? panelInfo.modalClassName : undefined}
onError={mediainfoHandler}
onTimeUpdate={mediainfoHandler}
onLoadedData={mediainfoHandler}
onLoadedMetadata={mediainfoHandler}
onDurationChange={mediainfoHandler}
reactPlayerConfig={reactPlayerSubtitleConfig}
thumbnailUrl={videoLoaded ? '' : videoThumbnailUrl}
videoComponent={
(typeof window === 'object' && !window.PalmSystem) || isYoutube ? TReactPlayer : Media
}
// VideoOverlay props - 간소화
type="MEDIA"
panelInfo={panelInfo}
captionEnable={false}
setIsSubtitleActive={setIsSubtitleActive}
setCurrentTime={setCurrentTime}
setIsVODPaused={setIsVODPaused}
// PlayerOverlayContents props ( 배열로 전달하여 null 처리)
playListInfo={[]}
selectedIndex={0}
videoVerticalVisible={false}
sideContentsVisible={false}
setSideContentsVisible={setSideContentsVisible}
handleIndicatorDownClick={handleIndicatorDownClick}
handleIndicatorUpClick={handleIndicatorUpClick}
>
{typeof window === 'object' && window.PalmSystem && (
<source src={currentPlayingUrl} type={videoType} />
)} )}
{isSubtitleActive && >
!panelInfo.modal && <MediaPlayerV2
typeof window === 'object' && setApiProvider={getPlayer}
window.PalmSystem && disabled={panelInfo.modal}
currentSubtitleUrl && <track kind="subtitles" src={currentSubtitleUrl} default />} onEnded={onEnded}
</MediaPlayerV2> autoPlay={!panelInfo?.isPaused}
</div> noAutoShowMediaControls={panelInfo.modal} // modal 모드에서는 자동으로 controls가 올라오지 않도록 설정
)} autoCloseTimeout={3000}
</Container> onBackButton={onClickBack}
</TPanel> onClick={onVideoClick}
); spotlightDisabled={panelInfo.modal}
}; isYoutube={isYoutube}
src={currentPlayingUrl}
loop={panelInfo.continuousPlay || false}
style={panelInfo.modal ? modalStyle : {}}
modalScale={panelInfo.modal ? modalScale : 1}
modalClassName={panelInfo.modal ? panelInfo.modalClassName : undefined}
onError={mediainfoHandler}
onTimeUpdate={mediainfoHandler}
onLoadedData={mediainfoHandler}
onLoadedMetadata={mediainfoHandler}
onDurationChange={mediainfoHandler}
reactPlayerConfig={reactPlayerSubtitleConfig}
thumbnailUrl={videoLoaded ? '' : videoThumbnailUrl}
videoComponent={
(typeof window === 'object' && !window.PalmSystem) || isYoutube
? TReactPlayer
: Media
}
// VideoOverlay props - 간소화
type="MEDIA"
panelInfo={panelInfo}
captionEnable={false}
setIsSubtitleActive={setIsSubtitleActive}
setCurrentTime={setCurrentTime}
setIsVODPaused={setIsVODPaused}
// PlayerOverlayContents props ( 배열로 전달하여 null 처리)
playListInfo={[]}
selectedIndex={0}
videoVerticalVisible={false}
sideContentsVisible={false}
setSideContentsVisible={setSideContentsVisible}
handleIndicatorDownClick={handleIndicatorDownClick}
handleIndicatorUpClick={handleIndicatorUpClick}
>
{typeof window === 'object' && window.PalmSystem && (
<source src={currentPlayingUrl} type={videoType} />
)}
{isSubtitleActive &&
!panelInfo.modal &&
typeof window === 'object' &&
window.PalmSystem &&
currentSubtitleUrl && <track kind="subtitles" src={currentSubtitleUrl} default />}
</MediaPlayerV2>
</div>
)}
</Container>
</TPanel>
);
}
);
export default React.memo(MediaPanel); export default React.memo(MediaPanel);