[251112] feat: ProductVideoV2,MediaPanel cleanup

🕐 커밋 시간: 2025. 11. 12. 19:55:49

📊 변경 통계:
  • 총 파일: 5개
  • 추가: +205줄
  • 삭제: -114줄

📁 추가된 파일:
  + com.twin.app.shoptime/DEBUG_MODE_IMPLEMENTATION.md
  + com.twin.app.shoptime/MEDIAPANEL_CLEANUP_IMPROVEMENTS.md

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

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v2.jsx (javascript):
     Added: debugLog()
  📄 com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx (javascript):
     Added: debugLog()
  📄 com.twin.app.shoptime/DEBUG_MODE_IMPLEMENTATION.md (md파일):
     Added: Before(), After()
  📄 com.twin.app.shoptime/MEDIAPANEL_CLEANUP_IMPROVEMENTS.md (md파일):
     Added: useCallback(), showControls(), areControlsVisible(), toggleControls(), useLayoutEffect(), useEffect(), clearTimeout(), dispatch(), forEach(), getVideoNode(), addEventListener()

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 개발 문서 및 가이드 개선
This commit is contained in:
2025-11-12 19:55:51 +09:00
parent ee4bb17ed7
commit 743e250030
5 changed files with 882 additions and 140 deletions

View File

@@ -21,6 +21,16 @@ import usePrevious from '../../hooks/usePrevious';
import { panel_names } from '../../utils/Config';
import css from './MediaPanel.module.less';
// ✅ DEBUG 모드 설정
const DEBUG_MODE = false;
// ✅ DEBUG_MODE 기반 console 래퍼
const debugLog = (...args) => {
if (DEBUG_MODE) {
console.log(...args);
}
};
const Container = SpotlightContainerDecorator(
{ enterTo: 'default-element', preserveld: true },
'div'
@@ -51,6 +61,7 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
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);
@@ -61,16 +72,16 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
// modal/full screen에 따른 일시정지/재생 처리
useEffect(() => {
// console.log('[MediaPanel] ========== isOnTop useEffect ==========');
// console.log('[MediaPanel] isOnTop:', isOnTop);
// console.log('[MediaPanel] panelInfo:', JSON.stringify(panelInfo, null, 2));
// debugLog('[MediaPanel] ========== isOnTop useEffect ==========');
// debugLog('[MediaPanel] isOnTop:', isOnTop);
// debugLog('[MediaPanel] panelInfo:', JSON.stringify(panelInfo, null, 2));
if (panelInfo && panelInfo.modal) {
if (!isOnTop) {
// console.log('[MediaPanel] Not on top - pausing video');
// debugLog('[MediaPanel] Not on top - pausing video');
dispatch(pauseModalMedia());
} else if (isOnTop && panelInfo.isPaused) {
// console.log('[MediaPanel] Back on top - resuming video');
// debugLog('[MediaPanel] Back on top - resuming video');
dispatch(resumeModalMedia());
}
}
@@ -80,10 +91,10 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
useEffect(() => {
if (panelInfo?.modal && videoPlayer.current) {
if (panelInfo.isPaused) {
// console.log('[MediaPanel] Executing pause via videoPlayer.current');
// debugLog('[MediaPanel] Executing pause via videoPlayer.current');
videoPlayer.current.pause();
} else if (panelInfo.isPaused === false) {
// console.log('[MediaPanel] Executing play via videoPlayer.current');
// debugLog('[MediaPanel] Executing play via videoPlayer.current');
videoPlayer.current.play();
}
}
@@ -93,6 +104,18 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
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 상태 관리 불필요 (단순 재생만)
@@ -153,12 +176,16 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
setModalScale(panelInfo.modalScale || 1);
}
} else if (isOnTop && !panelInfo.modal && !panelInfo.isMinimized && videoPlayer.current) {
if (videoPlayer.current?.getMediaState()?.paused) {
videoPlayer.current.play();
// ✅ 안전한 메서드 호출로 null/undefined 체크
const mediaState = safePlayerCall('getMediaState');
if (mediaState?.paused) {
safePlayerCall('play');
}
if (videoPlayer.current.areControlsVisible && !videoPlayer.current.areControlsVisible()) {
videoPlayer.current.showControls();
const isControlsHidden =
videoPlayer.current.areControlsVisible && !videoPlayer.current.areControlsVisible();
if (isControlsHidden) {
safePlayerCall('showControls');
}
}
@@ -170,21 +197,22 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
};
}, [panelInfo, isOnTop]);
// 비디오 클릭 시 modal → fullscreen 전환 또는 controls 토글
// 비디오 클릭 시 modal → fullscreen 전환 또는 controls 토글
const onVideoClick = useCallback(() => {
if (panelInfo.modal) {
// console.log('[MediaPanel] Video clicked - switching to fullscreen');
// debugLog('[MediaPanel] Video clicked - switching to fullscreen');
dispatch(switchMediaToFullscreen());
} else {
// 비디오 클릭 시 controls 숨기기 (overlay들이 사라지도록)
if (videoPlayer.current && typeof videoPlayer.current.toggleControls === 'function') {
videoPlayer.current.toggleControls();
}
// 비디오 클릭 시 controls 토글
safePlayerCall('toggleControls');
}
}, [dispatch, panelInfo.modal, videoPlayer]);
}, [dispatch, panelInfo.modal, safePlayerCall]);
const onClickBack = useCallback(
(ev) => {
// ✅ 뒤로가기 시 비디오 정지
safePlayerCall('pause');
// modal에서 full로 전환된 경우 다시 modal로 돌아감
if (panelInfo.modalContainerId && !panelInfo.modal) {
// 다시 modal로 돌리는 로직은 startVideoPlayer 액션을 사용할 수도 있지만
@@ -200,7 +228,7 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
return;
}
},
[dispatch, panelInfo]
[dispatch, panelInfo, safePlayerCall]
);
const currentPlayingUrl = useMemo(() => {
@@ -253,47 +281,60 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
return panelInfo?.thumbnailUrl;
}, [panelInfo?.thumbnailUrl]);
const mediainfoHandler = useCallback((ev) => {
const type = ev.type;
if (type !== 'timeupdate' && type !== 'durationchange') {
console.log('mediainfoHandler....', type, ev, videoPlayer.current?.getMediaState());
}
if (ev === 'hlsError' && isNaN(Number(videoPlayer.current?.getMediaState().playbackRate))) {
dispatch(
sendBroadCast({
type: 'videoError',
moreInfo: { reason: 'hlsError' },
})
);
return;
}
const mediainfoHandler = useCallback(
(ev) => {
const type = ev.type;
if (type !== 'timeupdate' && type !== 'durationchange') {
debugLog('mediainfoHandler....', type, ev, videoPlayer.current?.getMediaState());
}
switch (type) {
case 'timeupdate': {
setCurrentTime(videoPlayer.current?.getMediaState()?.currentTime);
break;
// ✅ hlsError 처리 강화
if (ev === 'hlsError') {
const mediaState = safePlayerCall('getMediaState');
if (mediaState && isNaN(Number(mediaState.playbackRate))) {
dispatch(
sendBroadCast({
type: 'videoError',
moreInfo: { reason: 'hlsError' },
})
);
return;
}
}
case 'error': {
dispatch(
sendBroadCast({
type: 'videoError',
moreInfo: { reason: videoPlayer.current?.getMediaState().error },
})
);
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;
}
default:
break;
}
}, []);
},
[dispatch, safePlayerCall]
);
const onEnded = useCallback(
(e) => {
console.log('[MediaPanel] Video ended');
debugLog('[MediaPanel] Video ended');
// continuousPlay는 MediaPlayer(VideoPlayer) 컴포넌트 내부에서 loop 속성으로 처리
// onEnded가 호출되면 loop=false 인 경우이므로 패널을 닫음
Spotlight.pause();
@@ -316,55 +357,98 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
[dispatch]
);
// ✅ useLayoutEffect: DOM 스타일 설정 (메모리 누수 방지)
useLayoutEffect(() => {
const videoContainer = document.querySelector(`.${css.videoContainer}`);
if (videoContainer && panelInfo.thumbnailUrl && !videoLoaded) {
videoContainer.style.background = `url(${panelInfo.thumbnailUrl}) center center / contain no-repeat`;
videoContainer.style.backgroundColor = 'black';
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 {
videoContainer.style.background = '';
videoContainer.style.backgroundColor = '';
} catch (err) {
// 스타일 초기화 실패는 무시
}
}
};
}, [panelInfo.thumbnailUrl, videoLoaded]);
useEffect(() => {
setVideoLoaded(false);
}, [currentPlayingUrl]);
// ✅ 컴포넌트 언마운트 시 모든 타이머 정리
// ✅ 컴포넌트 언마운트 시 모든 타이머 및 리소스 정리
useEffect(() => {
return () => {
// onEnded 타이머 정리
// onEnded 타이머 정리
if (onEndedTimerRef.current) {
clearTimeout(onEndedTimerRef.current);
onEndedTimerRef.current = null;
}
// ✅ 비디오 플레이어 정지
// ✅ 비디오 플레이어 정지 및 정리
if (videoPlayer.current) {
try {
videoPlayer.current.pause?.();
// 재생 중이면 정지
safePlayerCall('pause');
// controls 타임아웃 정리
safePlayerCall('hideControls');
} catch (err) {
console.warn('[MediaPanel] 비디오 정지 실패:', 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 {
element?.removeEventListener?.(event, handler);
} catch (err) {
// 리스너 제거 실패는 무시
}
});
mediaEventListenersRef.current = [];
}
// ✅ Spotlight 상태 초기화
try {
Spotlight.resume?.();
} catch (err) {
// Spotlight 초기화 실패는 무시
}
};
}, []);
}, [dispatch, safePlayerCall]);
// console.log('[MediaPanel] ========== Rendering ==========');
// console.log('[MediaPanel] isOnTop:', isOnTop);
// console.log('[MediaPanel] panelInfo.modal:', panelInfo.modal);
// console.log('[MediaPanel] panelInfo.isMinimized:', panelInfo.isMinimized);
// console.log('[MediaPanel] panelInfo.isPaused:', panelInfo.isPaused);
// console.log('[MediaPanel] currentPlayingUrl:', currentPlayingUrl);
// console.log('[MediaPanel] hasVideoPlayer:', !!videoPlayer.current);
// 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 적용 상태 확인
// console.log('[MediaPanel] ========== ClassNames Analysis ==========');
// console.log('[MediaPanel] css.videoContainer:', css.videoContainer);
// console.log('[MediaPanel] Condition [panelInfo.modal && !panelInfo.isMinimized]:', panelInfo.modal && !panelInfo.isMinimized);
// console.log('[MediaPanel] css.modal:', css.modal);
// console.log('[MediaPanel] Condition [panelInfo.isMinimized]:', panelInfo.isMinimized);
// console.log('[MediaPanel] css["modal-minimized"]:', css['modal-minimized']);
// console.log('[MediaPanel] Condition [!isOnTop]:', !isOnTop);
// console.log('[MediaPanel] css.background:', css.background);
// 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,
@@ -372,10 +456,10 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
panelInfo.isMinimized && css['modal-minimized'],
!isOnTop && css.background
);
// console.log('[MediaPanel] Final Applied ClassNames:', appliedClassNames);
// console.log('[MediaPanel] modalStyle:', modalStyle);
// console.log('[MediaPanel] modalScale:', modalScale);
// console.log('[MediaPanel] ===============================================');
// 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';