[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:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user