diff --git a/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx b/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx
index 17ce6b55..7935dd60 100644
--- a/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx
+++ b/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx
@@ -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 { useDispatch } from 'react-redux';
@@ -56,498 +63,542 @@ const YOUTUBECONFIG = {
},
};
-const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props }) => {
- const dispatch = useDispatch();
+const MediaPanel = React.forwardRef(
+ ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props }, ref) => {
+ const dispatch = useDispatch();
- const videoPlayer = useRef(null);
- const onEndedTimerRef = useRef(null); // ✅ onEnded 타이머 관리
- const mediaEventListenersRef = useRef([]); // ✅ 미디어 이벤트 리스너 추적
- const [modalStyle, setModalStyle] = React.useState({});
- const [modalScale, setModalScale] = React.useState(1);
- const [currentTime, setCurrentTime] = React.useState(0);
- const [videoLoaded, setVideoLoaded] = React.useState(false);
- const [isSubtitleActive, setIsSubtitleActive] = React.useState(true);
+ const videoPlayer = useRef(null);
+ const onEndedTimerRef = useRef(null); // ✅ onEnded 타이머 관리
+ const mediaEventListenersRef = useRef([]); // ✅ 미디어 이벤트 리스너 추적
+ const [modalStyle, setModalStyle] = React.useState({});
+ const [modalScale, setModalScale] = React.useState(1);
+ const [currentTime, setCurrentTime] = React.useState(0);
+ const [videoLoaded, setVideoLoaded] = React.useState(false);
+ const [isSubtitleActive, setIsSubtitleActive] = React.useState(true);
- const panelInfoRef = usePrevious(panelInfo);
+ const panelInfoRef = usePrevious(panelInfo);
- // modal/full screen에 따른 일시정지/재생 처리
- useEffect(() => {
- // debugLog('[MediaPanel] ========== isOnTop useEffect ==========');
- // debugLog('[MediaPanel] isOnTop:', isOnTop);
- // debugLog('[MediaPanel] panelInfo:', JSON.stringify(panelInfo, null, 2));
+ // modal/full screen에 따른 일시정지/재생 처리
+ useEffect(() => {
+ // debugLog('[MediaPanel] ========== isOnTop useEffect ==========');
+ // debugLog('[MediaPanel] isOnTop:', isOnTop);
+ // debugLog('[MediaPanel] panelInfo:', JSON.stringify(panelInfo, null, 2));
- if (panelInfo && panelInfo.modal) {
- if (!isOnTop) {
- // debugLog('[MediaPanel] Not on top - pausing video');
- dispatch(pauseModalMedia());
- } else if (isOnTop && panelInfo.isPaused) {
- // debugLog('[MediaPanel] Back on top - resuming video');
- 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);
+ if (panelInfo && panelInfo.modal) {
+ if (!isOnTop) {
+ // debugLog('[MediaPanel] Not on top - pausing video');
+ dispatch(pauseModalMedia());
+ } else if (isOnTop && panelInfo.isPaused) {
+ // debugLog('[MediaPanel] Back on top - resuming video');
+ dispatch(resumeModalMedia());
}
- } else {
- setModalStyle(panelInfo.modalStyle || {});
- setModalScale(panelInfo.modalScale || 1);
}
- } else if (isOnTop && !panelInfo.modal && !panelInfo.isMinimized && videoPlayer.current) {
- // ✅ 안전한 메서드 호출로 null/undefined 체크
- const mediaState = safePlayerCall('getMediaState');
- if (mediaState?.paused) {
- safePlayerCall('play');
+ }, [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]);
- const isControlsHidden =
- videoPlayer.current.areControlsVisible && !videoPlayer.current.areControlsVisible();
- if (isControlsHidden) {
- safePlayerCall('showControls');
+ 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;
+ }, []);
- // ✅ cleanup: resize observer 정리
- return () => {
- if (resizeObserver) {
- resizeObserver.disconnect();
- }
- };
- }, [panelInfo, isOnTop]);
+ const playViaRef = useCallback(() => safePlayerCall('play'), [safePlayerCall]);
+ const pauseViaRef = useCallback(() => safePlayerCall('pause'), [safePlayerCall]);
+ const seekViaRef = useCallback(
+ (seconds = 0) => safePlayerCall('seek', seconds),
+ [safePlayerCall]
+ );
+ 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 토글
- const onVideoClick = useCallback(() => {
- if (panelInfo.modal) {
- // debugLog('[MediaPanel] Video clicked - switching to fullscreen');
- dispatch(switchMediaToFullscreen());
- } else {
- // 비디오 클릭 시 controls 토글
- safePlayerCall('toggleControls');
- }
- }, [dispatch, panelInfo.modal, safePlayerCall]);
+ useImperativeHandle(
+ ref,
+ () => ({
+ play: playViaRef,
+ pause: pauseViaRef,
+ seek: seekViaRef,
+ showControls: showControlsViaRef,
+ hideControls: hideControlsViaRef,
+ toggleControls: toggleControlsViaRef,
+ getMediaState: getMediaStateViaRef,
+ getInternalPlayer: () => videoPlayer.current,
+ }),
+ [
+ playViaRef,
+ pauseViaRef,
+ seekViaRef,
+ showControlsViaRef,
+ hideControlsViaRef,
+ toggleControlsViaRef,
+ getMediaStateViaRef,
+ ]
+ );
- const onClickBack = useCallback(
- (ev) => {
- // ✅ 뒤로가기 시 비디오 정지
- safePlayerCall('pause');
+ // VideoPlayer가 MEDIA 타입에서 setIsVODPaused를 호출하므로 더미 함수 제공
+ const setIsVODPaused = useCallback(() => {
+ // MediaPanel에서는 VOD pause 상태 관리 불필요 (단순 재생만)
+ }, []);
- // modal에서 full로 전환된 경우 다시 modal로 돌아감
- if (panelInfo.modalContainerId && !panelInfo.modal) {
- // 다시 modal로 돌리는 로직은 startVideoPlayer 액션을 사용할 수도 있지만
- // 여기서는 단순히 패널을 pop
- dispatch(PanelActions.popPanel());
- ev?.stopPropagation();
- return;
- }
+ // PlayerOverlayContents에서 필요한 더미 함수들
+ const setSideContentsVisible = useCallback(() => {
+ // MediaPanel에서는 사이드 컨텐츠 사용 안 함
+ }, []);
- if (!panelInfo.modal) {
- dispatch(PanelActions.popPanel());
- ev?.stopPropagation();
- return;
- }
- },
- [dispatch, panelInfo, safePlayerCall]
- );
+ const handleIndicatorDownClick = useCallback(() => {
+ // MediaPanel에서는 indicator 사용 안 함
+ }, []);
- const currentPlayingUrl = useMemo(() => {
- return panelInfo?.showUrl;
- }, [panelInfo?.showUrl]);
+ const handleIndicatorUpClick = useCallback(() => {
+ // MediaPanel에서는 indicator 사용 안 함
+ }, []);
- const isYoutube = useMemo(() => {
- if (currentPlayingUrl && currentPlayingUrl.includes('youtu')) {
- return true;
- }
- return false;
- }, [currentPlayingUrl]);
+ // ✅ modal 스타일 설정
+ useEffect(() => {
+ let resizeObserver = null;
- const currentSubtitleUrl = useMemo(() => {
- return panelInfo?.subtitle;
- }, [panelInfo?.subtitle]);
+ 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();
- 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]);
+ // ProductVideo의 padding(6px * 2)과 추가 여유를 고려하여 크기 조정
+ // 비디오가 오른쪽으로 넘치지 않도록 충분한 여유 확보
+ const paddingOffset = 6 * 2; // padding 양쪽
+ const extraMargin = 6 * 2; // 추가 여유 (포커스 테두리 + 비디오 비율 고려)
+ const totalOffset = paddingOffset + extraMargin; // 24px
- 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 adjustedWidth = width - totalOffset;
+ const adjustedHeight = height - totalOffset;
+ const adjustedTop = top + totalOffset / 2;
+ const adjustedLeft = left + totalOffset / 2;
- 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 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) {
+ // ✅ 안전한 메서드 호출로 null/undefined 체크
const mediaState = safePlayerCall('getMediaState');
- if (mediaState && isNaN(Number(mediaState.playbackRate))) {
- dispatch(
- sendBroadCast({
- type: 'videoError',
- moreInfo: { reason: 'hlsError' },
- })
- );
+ if (mediaState?.paused) {
+ safePlayerCall('play');
+ }
+
+ 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;
}
- }
- switch (type) {
- case 'timeupdate': {
+ if (!panelInfo.modal) {
+ 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');
- if (mediaState) {
- setCurrentTime(mediaState.currentTime || 0);
+ if (mediaState && isNaN(Number(mediaState.playbackRate))) {
+ dispatch(
+ sendBroadCast({
+ type: 'videoError',
+ moreInfo: { reason: 'hlsError' },
+ })
+ );
+ return;
}
- break;
}
- case 'error': {
- const mediaState = safePlayerCall('getMediaState');
- const errorInfo = mediaState?.error || 'unknown';
- dispatch(
- sendBroadCast({
- type: 'videoError',
- moreInfo: { reason: errorInfo },
- })
- );
- break;
+
+ switch (type) {
+ case 'timeupdate': {
+ const mediaState = safePlayerCall('getMediaState');
+ if (mediaState) {
+ setCurrentTime(mediaState.currentTime || 0);
+ }
+ 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);
- break;
+ },
+ [dispatch, safePlayerCall]
+ );
+
+ 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(
- (e) => {
- debugLog('[MediaPanel] Video ended');
- // continuousPlay는 MediaPlayer(VideoPlayer) 컴포넌트 내부에서 loop 속성으로 처리
- // onEnded가 호출되면 loop=false 인 경우이므로 패널을 닫음
- Spotlight.pause();
+ // ✅ 새로운 타이머 저장 (cleanup 시 정리용)
+ onEndedTimerRef.current = setTimeout(() => {
+ Spotlight.resume();
+ dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL));
+ onEndedTimerRef.current = null;
+ }, 1500);
- // ✅ 이전 타이머가 있으면 정리
- if (onEndedTimerRef.current) {
- clearTimeout(onEndedTimerRef.current);
- }
+ e?.stopPropagation();
+ e?.preventDefault();
+ },
+ [dispatch]
+ );
- // ✅ 새로운 타이머 저장 (cleanup 시 정리용)
- onEndedTimerRef.current = setTimeout(() => {
- Spotlight.resume();
- dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL));
- 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) {
+ // ✅ useLayoutEffect: DOM 스타일 설정 (메모리 누수 방지)
+ useLayoutEffect(() => {
+ const videoContainer = document.querySelector(`.${css.videoContainer}`);
+ if (videoContainer && panelInfo.thumbnailUrl && !videoLoaded) {
try {
- videoContainer.style.background = '';
- videoContainer.style.backgroundColor = '';
+ videoContainer.style.background = `url(${panelInfo.thumbnailUrl}) center center / contain no-repeat`;
+ videoContainer.style.backgroundColor = 'black';
} catch (err) {
- // 스타일 초기화 실패는 무시
+ if (DEBUG_MODE) console.warn('[MediaPanel] 썸네일 스타일 설정 실패:', err);
}
}
- };
- }, [panelInfo.thumbnailUrl, videoLoaded]);
- useEffect(() => {
- setVideoLoaded(false);
- }, [currentPlayingUrl]);
-
- // ✅ 컴포넌트 언마운트 시 모든 타이머 및 리소스 정리
- 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 }) => {
+ // ✅ cleanup: 컴포넌트 언마운트 시 DOM 스타일 초기화
+ return () => {
+ if (videoContainer) {
try {
- element?.removeEventListener?.(event, handler);
+ videoContainer.style.background = '';
+ videoContainer.style.backgroundColor = '';
} catch (err) {
- // 리스너 제거 실패는 무시
+ // 스타일 초기화 실패는 무시
}
- });
- mediaEventListenersRef.current = [];
- }
+ }
+ };
+ }, [panelInfo.thumbnailUrl, videoLoaded]);
- // ✅ Spotlight 상태 초기화
- try {
- Spotlight.resume?.();
- } catch (err) {
- // Spotlight 초기화 실패는 무시
- }
- };
- }, [dispatch, safePlayerCall]);
+ useEffect(() => {
+ setVideoLoaded(false);
+ }, [currentPlayingUrl]);
- // 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);
+ // ✅ 컴포넌트 언마운트 시 모든 타이머 및 리소스 정리
+ useEffect(() => {
+ return () => {
+ // ✅ onEnded 타이머 정리
+ if (onEndedTimerRef.current) {
+ clearTimeout(onEndedTimerRef.current);
+ onEndedTimerRef.current = null;
+ }
- // 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);
+ // ✅ 비디오 플레이어 정지 및 정리
+ if (videoPlayer.current) {
+ try {
+ // 재생 중이면 정지
+ safePlayerCall('pause');
- 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] ===============================================');
+ // controls 타임아웃 정리
+ safePlayerCall('hideControls');
+ } catch (err) {
+ if (DEBUG_MODE) console.warn('[MediaPanel] 비디오 정지 실패:', err);
+ }
- // minimized 상태일 때는 spotlightRestrict 해제 (포커스 이동 허용)
- const containerSpotlightRestrict = panelInfo.isMinimized ? 'none' : 'self-only';
- const shouldDisableIframeInteraction = isYoutube && !panelInfo.modal;
+ // ref 초기화
+ videoPlayer.current = null;
+ }
- return (
-
- 0) {
+ mediaEventListenersRef.current.forEach(({ element, event, handler }) => {
+ try {
+ element?.removeEventListener?.(event, handler);
+ } catch (err) {
+ // 리스너 제거 실패는 무시
+ }
+ });
+ mediaEventListenersRef.current = [];
+ }
+
+ // ✅ 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 (
+
- {currentPlayingUrl && (
-
-
- {typeof window === 'object' && window.PalmSystem && (
-
+
+ {currentPlayingUrl && (
+ }
-
-
- )}
-
-
- );
-};
+ >
+
+ {typeof window === 'object' && window.PalmSystem && (
+
+ )}
+ {isSubtitleActive &&
+ !panelInfo.modal &&
+ typeof window === 'object' &&
+ window.PalmSystem &&
+ currentSubtitleUrl && }
+
+
+ )}
+
+
+ );
+ }
+);
export default React.memo(MediaPanel);