diff --git a/com.twin.app.shoptime/src/actions/mainActions.js b/com.twin.app.shoptime/src/actions/mainActions.js index 9239497a..5167f09c 100644 --- a/com.twin.app.shoptime/src/actions/mainActions.js +++ b/com.twin.app.shoptime/src/actions/mainActions.js @@ -1,5 +1,5 @@ import { URLS } from '../api/apiConfig'; -import { TAxios } from '../api/TAxios'; +import { TAxios, TAxiosAdvancedPromise } from '../api/TAxios'; import { convertUtcToLocal } from '../components/MediaPlayer/util'; import { CATEGORY_DATA_MAX_RESULTS_LIMIT, LOG_CONTEXT_NAME, LOG_MESSAGE_ID } from '../utils/Config'; import * as HelperMethods from '../utils/helperMethods'; @@ -464,29 +464,29 @@ export const getHomeFullVideoInfo = export const getMainLiveShowNowProduct = ({ patnrId, showId, lstChgDt }) => (dispatch, getState) => { - const onSuccess = (response) => { - // console.log('getMainLiveShowNowProduct onSuccess', response.data); - - dispatch({ - type: types.GET_MAIN_LIVE_SHOW_NOW_PRODUCT, - payload: response.data.data, - }); - }; - - const onFail = (error) => { - console.error('getMainLiveShowNowProduct onFail', error); - }; - - TAxios( + return TAxiosAdvancedPromise( dispatch, getState, 'get', URLS.GET_MAIN_LIVE_SHOW_NOW_PRODUCT, { patnrId, showId, lstChgDt }, {}, - onSuccess, - onFail - ); + { + retries: 2, // 3회까지 재시도 (처음 시도 + 2회 재시도) + retryDelay: 500, // 500ms 간격으로 재시도 + throwOnError: false, // 에러를 throw하지 않고 객체로 반환 + } + ).then((result) => { + if (result.success && result.data?.data) { + dispatch({ + type: types.GET_MAIN_LIVE_SHOW_NOW_PRODUCT, + payload: result.data.data, + }); + } else { + console.error('getMainLiveShowNowProduct onFail', result.error); + } + return result; + }); }; export const clearShopNowInfo = () => { diff --git a/com.twin.app.shoptime/src/actions/playActions.js b/com.twin.app.shoptime/src/actions/playActions.js index 9e902a26..ca35d97c 100644 --- a/com.twin.app.shoptime/src/actions/playActions.js +++ b/com.twin.app.shoptime/src/actions/playActions.js @@ -68,24 +68,43 @@ export const startVideoPlayer = ...rest }) => (dispatch, getState) => { - console.log('[startVideoPlayer] ✅ START - videoId:', videoId, ', showUrl:', showUrl, ', modal:', modal); + console.log( + '[startVideoPlayer] ✅ START - videoId:', + videoId, + ', showUrl:', + showUrl, + ', modal:', + modal + ); // 🔽 [251116] 즉시 로딩 상태 설정 const videoIdentifier = videoId || showUrl; if (videoIdentifier) { const displayMode = modal ? DISPLAY_STATUS.VISIBLE : DISPLAY_STATUS.FULLSCREEN; - console.log('[startVideoPlayer] 📌 Setting playback loading - identifier:', videoIdentifier, ', displayMode:', displayMode); + console.log( + '[startVideoPlayer] 📌 Setting playback loading - identifier:', + videoIdentifier, + ', displayMode:', + displayMode + ); dispatch(setPlaybackLoading(videoIdentifier, displayMode)); } else { - console.log('[startVideoPlayer] ⚠️ No videoIdentifier provided (videoId and showUrl are both missing)'); + console.log( + '[startVideoPlayer] ⚠️ No videoIdentifier provided (videoId and showUrl are both missing)' + ); } const panels = getState().panels.panels; const topPanel = panels[panels.length - 1]; let panelWorkingAction = pushPanel; - + const panelName = panel_names.PLAYER_PANEL; - console.log('[startVideoPlayer] 📊 Panel state - panelsCount:', panels.length, ', topPanelName:', topPanel?.name); + console.log( + '[startVideoPlayer] 📊 Panel state - panelsCount:', + panels.length, + ', topPanelName:', + topPanel?.name + ); if (topPanel && topPanel.name === panelName) { panelWorkingAction = updatePanel; @@ -161,13 +180,27 @@ export const startVideoPlayerNew = ...rest }) => (dispatch, getState) => { - console.log('[startVideoPlayerNew] *** ✅ START - bannerId:', bannerId, ', videoId:', videoId, ', showUrl:', showUrl, ', modal:', modal); + console.log( + '[startVideoPlayerNew] *** ✅ START - bannerId:', + bannerId, + ', videoId:', + videoId, + ', showUrl:', + showUrl, + ', modal:', + modal + ); // 🔽 [251116] 즉시 로딩 상태 설정 const videoIdentifier = videoId || showUrl || bannerId; if (videoIdentifier) { const displayMode = modal ? DISPLAY_STATUS.VISIBLE : DISPLAY_STATUS.FULLSCREEN; - console.log('[startVideoPlayerNew] *** 📌 Setting playback loading - identifier:', videoIdentifier, ', displayMode:', displayMode); + console.log( + '[startVideoPlayerNew] *** 📌 Setting playback loading - identifier:', + videoIdentifier, + ', displayMode:', + displayMode + ); dispatch(setPlaybackLoading(videoIdentifier, displayMode)); } else { console.log('[startVideoPlayerNew] *** ⚠️ No videoIdentifier provided'); @@ -179,11 +212,19 @@ export const startVideoPlayerNew = // const panelName = useNewPlayer ? panel_names.PLAYER_PANEL_NEW : panel_names.PLAYER_PANEL; const panelName = panel_names.PLAYER_PANEL; - console.log('[startVideoPlayerNew] *** 📊 Panel state - panelsCount:', panels.length, ', topPanelName:', topPanel?.name); + console.log( + '[startVideoPlayerNew] *** 📊 Panel state - panelsCount:', + panels.length, + ', topPanelName:', + topPanel?.name + ); if (topPanel && topPanel.name === panelName) { panelWorkingAction = updatePanel; - console.log('[startVideoPlayerNew] *** 📋 Current PLAYER_PANEL panelInfo:', topPanel.panelInfo); + console.log( + '[startVideoPlayerNew] *** 📋 Current PLAYER_PANEL panelInfo:', + topPanel.panelInfo + ); } // 중복 실행 방지: 같은 배너 + 같은 modal 상태/컨테이너 + 같은 URL이면 skip @@ -195,7 +236,18 @@ export const startVideoPlayerNew = const isSameShowUrl = currentPanelInfo.showUrl === showUrl; const isSameVideoId = currentPanelInfo.videoId === videoId; - console.log('[startVideoPlayerNew] *** 🔍 Duplicate check - isSameBanner:', isSameBanner, ', isSameModalType:', isSameModalType, ', isSameContainer:', isSameContainer, ', isSameShowUrl:', isSameShowUrl, ', isSameVideoId:', isSameVideoId); + console.log( + '[startVideoPlayerNew] *** 🔍 Duplicate check - isSameBanner:', + isSameBanner, + ', isSameModalType:', + isSameModalType, + ', isSameContainer:', + isSameContainer, + ', isSameShowUrl:', + isSameShowUrl, + ', isSameVideoId:', + isSameVideoId + ); if (isSameBanner && isSameModalType && isSameContainer && isSameShowUrl && isSameVideoId) { console.log('[startVideoPlayerNew] *** ⏭️ SKIPPED - 동일한 요청', { @@ -232,7 +284,10 @@ export const startVideoPlayerNew = true ) ); - console.log('[startVideoPlayerNew] *** ✨ Panel action dispatched - action:', panelWorkingAction === updatePanel ? 'updatePanel' : 'pushPanel'); + console.log( + '[startVideoPlayerNew] *** ✨ Panel action dispatched - action:', + panelWorkingAction === updatePanel ? 'updatePanel' : 'pushPanel' + ); // [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화 // if (modal && modalContainerId && !spotlightDisable) { @@ -324,7 +379,7 @@ export const pauseModalVideo = () => (dispatch, getState) => { // 모달 비디오를 재생 (일시정지 해제) export const resumeModalVideo = () => (dispatch, getState) => { const panels = getState().panels.panels; - + // modal PlayerPanel 찾기 const modalPlayerPanel = panels.find( (panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal @@ -498,7 +553,10 @@ export const showModalVideo = () => (dispatch, getState) => { skipModalStyleRecalculation: false, // 위치 변경 시 DOM 기준으로 다시 계산하도록 허용 }; - console.log('[showModalVideo] *** 🔄 Updated panelInfo - shouldShrinkTo1px:', updatedPanelInfo.shouldShrinkTo1px); + console.log( + '[showModalVideo] *** 🔄 Updated panelInfo - shouldShrinkTo1px:', + updatedPanelInfo.shouldShrinkTo1px + ); console.log('[showModalVideo] *** 📍 Restored modalStyle:', updatedPanelInfo.modalStyle); dispatch( @@ -699,10 +757,27 @@ export const CLEAR_PLAYER_INFO = () => ({ * @param {number} playState.duration - 전체 비디오 길이(초) * @param {number} playState.playbackRate - 재생 속도 */ -export const updateVideoPlayState = (playState) => ({ - type: types.UPDATE_VIDEO_PLAY_STATE, - payload: playState, -}); +export const updateVideoPlayState = (playState) => (dispatch, getState) => { + const currentState = getState().play.videoPlayState; + + // 상태 변화 감지 + const hasChanges = Object.keys(playState).some((key) => { + return currentState[key] !== playState[key]; + }); + + if (hasChanges) { + console.log('🔄 [Redux] updateVideoPlayState action created', { + ...playState, + timestamp: new Date().toISOString(), + caller: new Error().stack?.split('\n')[2]?.trim() || 'unknown', + }); + } + + dispatch({ + type: types.UPDATE_VIDEO_PLAY_STATE, + payload: playState, + }); +}; /* 🔽 [추가] 새로운 '플레이 제어 매니저' 액션들 */ diff --git a/com.twin.app.shoptime/src/api/TAxios.js b/com.twin.app.shoptime/src/api/TAxios.js index 9cda4614..b7b68f47 100644 --- a/com.twin.app.shoptime/src/api/TAxios.js +++ b/com.twin.app.shoptime/src/api/TAxios.js @@ -29,7 +29,7 @@ export const setTokenRefreshing = (value) => { tokenRefreshing = value; }; export const runDelayedAction = (dispatch, getState) => { - console.log('runDelayedAction axiosQueue size', axiosQueue.length); + // console.log('runDelayedAction axiosQueue size', axiosQueue.length); while (axiosQueue.length > 0) { const requestConfig = axiosQueue.shift(); // queue에서 요청을 하나씩 shift TAxios( @@ -309,7 +309,7 @@ export const TAxiosAdvancedPromise = ( const attemptRequest = () => { attempts++; - console.log(`TAxiosPromise attempt ${attempts}/${maxAttempts} for ${baseUrl}`); + // console.log(`TAxiosPromise attempt ${attempts}/${maxAttempts} for ${baseUrl}`); const timeoutId = setTimeout(() => { const timeoutError = new Error(`Request timeout after ${timeout}ms for ${baseUrl}`); @@ -335,7 +335,7 @@ export const TAxiosAdvancedPromise = ( // onSuccess (response) => { clearTimeout(timeoutId); - console.log(`TAxiosPromise success on attempt ${attempts} for ${baseUrl}`); + // console.log(`TAxiosPromise success on attempt ${attempts} for ${baseUrl}`); resolve({ success: true, data: response.data, @@ -491,7 +491,7 @@ export const safeUsageExamples = { }); if (result.success) { - console.log('Success:', result.data); + // console.log('Success:', result.data); return result.data; } else { console.error('API call failed:', result.error); @@ -534,7 +534,7 @@ export const safeUsageExamples = { const result = await TAxiosAll(requests); if (result.success) { - console.log('All requests succeeded'); + // console.log('All requests succeeded'); return result.successResults.map((item) => item.result); } else { console.error('Some requests failed:', result.failedResults); @@ -562,7 +562,7 @@ export const ComponentUsageExample = () => { setLoading(false); if (result.success) { - console.log('Terms fetched successfully'); + // console.log('Terms fetched successfully'); // 성공 처리 (예: 성공 토스트 표시) } else { console.error('Failed to fetch terms:', result.message); diff --git a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js index c03e58ad..ef01c6c9 100644 --- a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js +++ b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js @@ -725,6 +725,7 @@ const VideoPlayerBase = class extends React.Component { tabContainerVersion: PropTypes.number, tabIndexV2: PropTypes.number, dispatch: PropTypes.func, + videoPlayState: PropTypes.object, }; static contextType = FloatingLayerContext; @@ -1546,17 +1547,101 @@ const VideoPlayerBase = class extends React.Component { } this.setState(updatedState); - // Redux에 비디오 재생 상태 업데이트 + // Redux에 비디오 재생 상태 업데이트 (기존 로직 유지) if (this.props.dispatch) { - this.props.dispatch( - updateVideoPlayState({ + // 🔥 onProgress 이벤트는 Redux 업데이트하지 않음 (빈번한 이벤트) + const shouldUpdateRedux = !['onProgress'].includes(ev.type); + + if (shouldUpdateRedux) { + const updateState = { isPlaying: !updatedState.paused, isPaused: updatedState.paused, currentTime: updatedState.currentTime, duration: updatedState.duration, playbackRate: updatedState.playbackRate, - }) - ); + }; + + // 가장 중요한 이벤트만 로그 + const shouldLogEvent = ['play', 'pause', 'ended'].includes(ev.type); + if (shouldLogEvent) { + console.log('🔄 [PlayerPanel][VideoPlayer] Event-driven Redux update', { + eventType: ev.type, + videoState: updatedState, + updateState, + timestamp: new Date().toISOString(), + }); + } + + // 🔍 Redux dispatch 확인 + console.log('📤 [PlayerPanel][VideoPlayer] Dispatching Redux update', { + eventType: ev.type, + updateState, + hasDispatch: !!this.props.dispatch, + propsVideoPlayState: this.props.videoPlayState, + }); + + this.props.dispatch(updateVideoPlayState(updateState)); + } + } else { + console.log('❌ [PlayerPanel][VideoPlayer] No dispatch prop available', { + props: Object.keys(this.props), + hasDispatch: !!this.props.dispatch, + hasVideoPlayState: !!this.props.videoPlayState, + }); + } + + // 🔹 [강화] 내부 상태와 Redux 상태 동기화 + // Redux 상태를 우선적으로 사용하여 내부 상태 일관성 확보 + if (this.props.videoPlayState && typeof this.props.videoPlayState === 'object') { + // Redux 상태 디버깅 (최소한의 중요 이벤트만) + if (ev.type === 'play' || ev.type === 'pause') { + console.log('🔍 [PlayerPanel][VideoPlayer] Redux state debug', { + videoPlayState: this.props.videoPlayState, + isPaused: this.props.videoPlayState?.isPaused, + isPlaying: this.props.videoPlayState?.isPlaying, + currentTime: this.props.videoPlayState?.currentTime, + eventType: ev.type, + timestamp: new Date().toISOString(), + }); + } + const { currentTime, paused, playbackRate } = this.props.videoPlayState; + + // Redux 상태와 현재 내부 상태가 크게 다를 경우 내부 상태 업데이트 + const timeDiff = Math.abs(currentTime - this.state.currentTime); + const shouldUpdateTime = timeDiff > 0.5; // 0.5초 이상 차이 시 업데이트 + + // 빈번한 이벤트는 로그에서 제외 + const isFrequentEvent = [ + 'onProgress', + 'onBuffer', + 'onBufferEnd', + 'onReady', + 'onDuration', + 'onStart', + ].includes(ev.type); + const hasSignificantChange = + shouldUpdateTime || (paused !== this.state.paused && !isFrequentEvent); + + // 중요한 상태 변화가 있고 빈번한 이벤트가 아닐 때만 로그 + if (hasSignificantChange && !isFrequentEvent) { + console.log('🔄 [PlayerPanel][VideoPlayer] Syncing internal state with Redux', { + timeDiff, + shouldUpdateTime, + pausedDiff: paused !== this.state.paused, + reduxPaused: paused, + internalPaused: this.state.paused, + eventType: ev.type, + timestamp: new Date().toISOString(), + }); + } + + if (hasSignificantChange) { + this.setState({ + currentTime: shouldUpdateTime ? currentTime : this.state.currentTime, + paused: paused !== undefined ? paused : this.state.paused, + playbackRate: playbackRate !== undefined ? playbackRate : this.state.playbackRate, + }); + } } }; @@ -1569,6 +1654,7 @@ const VideoPlayerBase = class extends React.Component { /** * Returns an object with the current state of the media including `currentTime`, `duration`, * `paused`, `playbackRate`, `proportionLoaded`, and `proportionPlayed`. + * Redux 상태와 내부 상태를 우선적으로 사용하여 일관성 보장 * * @function * @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype @@ -1576,13 +1662,19 @@ const VideoPlayerBase = class extends React.Component { * @public */ getMediaState = () => { + // Redux 상태를 우선적으로 사용하여 일관성 보장 + // Redux 상태가 없으면 내부 상태 사용 (fallback) + const reduxState = this.props.videoPlayState; + return { - currentTime: this.state.currentTime, - duration: this.state.duration, - paused: this.state.paused, - playbackRate: this.video?.playbackRate, + currentTime: reduxState?.currentTime ?? this.state.currentTime, + duration: reduxState?.duration ?? this.state.duration, + paused: reduxState?.isPaused ?? this.state.paused, + playbackRate: reduxState?.playbackRate ?? this.video?.playbackRate ?? this.state.playbackRate, proportionLoaded: this.state.proportionLoaded, proportionPlayed: this.state.proportionPlayed, + // Redux 상태 정보도 포함 + isPlaying: reduxState?.isPlaying ?? !this.state.paused, }; }; @@ -1611,7 +1703,16 @@ const VideoPlayerBase = class extends React.Component { * @public */ play = () => { + console.log('🟢 [PlayerPanel][VideoPlayer] play() called', { + currentTime: this.state.currentTime, + duration: this.state.duration, + paused: this.state.paused, + sourceUnavailable: this.state.sourceUnavailable, + prevCommand: this.prevCommand, + }); + if (this.state.sourceUnavailable) { + console.log('⚠️ [PlayerPanel][VideoPlayer] play() aborted - source unavailable'); return; } @@ -1623,6 +1724,19 @@ const VideoPlayerBase = class extends React.Component { this.send('play'); this.announce($L('Play')); this.startDelayedMiniFeedbackHide(5000); + + // Redux 상태 업데이트 - 재생 상태로 변경 + if (this.props.dispatch) { + this.props.dispatch( + updateVideoPlayState({ + isPlaying: true, + isPaused: false, + currentTime: this.state.currentTime, + duration: this.state.duration, + playbackRate: 1, + }) + ); + } }; /** @@ -1633,7 +1747,16 @@ const VideoPlayerBase = class extends React.Component { * @public */ pause = () => { + console.log('🔴 [VideoPlayer] pause() called', { + currentTime: this.state.currentTime, + duration: this.state.duration, + paused: this.state.paused, + sourceUnavailable: this.state.sourceUnavailable, + prevCommand: this.prevCommand, + }); + if (this.state.sourceUnavailable) { + console.log('⚠️ [VideoPlayer] pause() aborted - source unavailable'); return; } @@ -1645,6 +1768,22 @@ const VideoPlayerBase = class extends React.Component { this.send('pause'); this.announce($L('Pause')); this.stopDelayedMiniFeedbackHide(); + + // Redux 상태 업데이트 - 일시정지 상태로 변경 + if (this.props.dispatch) { + const pauseState = { + isPlaying: false, + isPaused: true, + currentTime: this.state.currentTime, + duration: this.state.duration, + playbackRate: 1, + }; + + console.log('📤 [VideoPlayer] Dispatching pause state', pauseState); + this.props.dispatch(updateVideoPlayState(pauseState)); + } else { + console.log('⚠️ [VideoPlayer] No dispatch prop available - Redux state not updated'); + } }; /** @@ -1656,6 +1795,15 @@ const VideoPlayerBase = class extends React.Component { * @public */ seek = (timeIndex) => { + console.log('⏩ [VideoPlayer] seek() called', { + timeIndex, + currentTime: this.state.currentTime, + duration: this.state.duration, + videoDuration: this.video?.duration, + seekDisabled: this.props.seekDisabled, + sourceUnavailable: this.state.sourceUnavailable, + }); + if (this.video) { if ( !this.props.seekDisabled && @@ -1663,14 +1811,37 @@ const VideoPlayerBase = class extends React.Component { !this.state.sourceUnavailable ) { // last time error - if (timeIndex >= this.video.duration) { - this.video.currentTime = this.video.duration - 1; + const actualSeekTime = + timeIndex >= this.video.duration ? this.video.duration - 1 : timeIndex; + this.video.currentTime = actualSeekTime; + + console.log('⏩ [VideoPlayer] Video seek completed', { + requestedTime: timeIndex, + actualTime: actualSeekTime, + videoDuration: this.video.duration, + }); + + // Redux 상태 업데이트 - 시간 이동 상태 반영 + if (this.props.dispatch) { + const seekState = { + isPlaying: !this.state.paused, + isPaused: this.state.paused, + currentTime: actualSeekTime, + duration: this.state.duration, + playbackRate: this.state.playbackRate, + }; + + console.log('📤 [VideoPlayer] Dispatching seek state', seekState); + this.props.dispatch(updateVideoPlayState(seekState)); } else { - this.video.currentTime = timeIndex; + console.log('⚠️ [VideoPlayer] No dispatch prop available - Redux state not updated'); } } else { + console.log('❌ [VideoPlayer] seek failed - disabled or source unavailable'); forward('onSeekFailed', {}, this.props); } + } else { + console.log('❌ [VideoPlayer] seek failed - no video element'); } }; diff --git a/com.twin.app.shoptime/src/reducers/playReducer.js b/com.twin.app.shoptime/src/reducers/playReducer.js index 62e62914..93c6aea7 100644 --- a/com.twin.app.shoptime/src/reducers/playReducer.js +++ b/com.twin.app.shoptime/src/reducers/playReducer.js @@ -4,21 +4,52 @@ import { PLAYBACK_STATUS, DISPLAY_STATUS } from '../actions/playActions'; const initialState = { subTitleBlobs: {}, chatData: null, - videoPlayState: { - // 기존 상태들 유지 + // 🔍 패널별 비디오 상태 분리 + playerPanelVideoState: { + // PlayerPanel 전용 상태 isPlaying: false, isPaused: true, currentTime: 0, duration: 0, playbackRate: 1, - // 🔽 [251116] 새로운 비디오 상태 관리 시스템 - playback: PLAYBACK_STATUS.NOT_PLAYING, // 재생 상태 - display: DISPLAY_STATUS.HIDDEN, // 화면 표시 상태 - videoId: null, // 현재 비디오 ID - loadingProgress: 0, // 로딩 진행률 (0-100) - loadingError: null, // 로딩 에러 정보 - lastUpdate: null, // 마지막 업데이트 시간 + // [251116] 새로운 비디오 상태 관리 시스템 + playback: PLAYBACK_STATUS.NOT_PLAYING, + display: DISPLAY_STATUS.HIDDEN, + videoId: null, + loadingProgress: 0, + loadingError: null, + lastUpdate: null, + }, + detailPanelVideoState: { + // DetailPanel 전용 상태 + isPlaying: false, + isPaused: true, + currentTime: 0, + duration: 0, + playbackRate: 1, + + // [251116] 새로운 비디오 상태 관리 시스템 + playback: PLAYBACK_STATUS.NOT_PLAYING, + display: DISPLAY_STATUS.HIDDEN, + videoId: null, + loadingProgress: 0, + loadingError: null, + lastUpdate: null, + }, + // 기존 videoPlayState는 하위 호환성을 위해 유지 + videoPlayState: { + isPlaying: false, + isPaused: true, + currentTime: 0, + duration: 0, + playbackRate: 1, + playback: PLAYBACK_STATUS.NOT_PLAYING, + display: DISPLAY_STATUS.HIDDEN, + videoId: null, + loadingProgress: 0, + loadingError: null, + lastUpdate: null, }, }; @@ -60,12 +91,35 @@ export const playReducer = (state = initialState, action) => { }; } case types.UPDATE_VIDEO_PLAY_STATE: { + const newState = { + ...state.videoPlayState, + ...action.payload, + }; + + // 🔍 실제 상태 변화 감지 - 중요한 변화만 로깅 + const importantKeys = ['isPaused', 'isPlaying', 'playback', 'display']; + const hasImportantChange = importantKeys.some((key) => { + return state.videoPlayState?.[key] !== newState[key]; + }); + + if (hasImportantChange) { + console.log('🔄 [Redux Reducer] VIDEO PLAY STATE CRITICAL CHANGE', { + previousPaused: state.videoPlayState?.isPaused, + newPaused: newState.isPaused, + previousPlaying: state.videoPlayState?.isPlaying, + newPlaying: newState.isPlaying, + previousPlayback: state.videoPlayState?.playback, + newPlayback: newState.playback, + previousDisplay: state.videoPlayState?.display, + newDisplay: newState.display, + currentTime: newState.currentTime, + timestamp: new Date().toISOString(), + }); + } + return { ...state, - videoPlayState: { - ...state.videoPlayState, - ...action.payload, - }, + videoPlayState: newState, }; } diff --git a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx index 5a6396a2..1238ae6e 100644 --- a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx +++ b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx @@ -20,7 +20,7 @@ import liveShow from '../../../../assets/images/tag-liveshow.png'; import { changeAppStatus } from '../../../actions/commonActions'; import { updateHomeInfo, setVideoTransitionLock } from '../../../actions/homeActions'; import { sendLogTopContents, sendLogTotalRecommend } from '../../../actions/logActions'; -import { pushPanel, navigateFromRandomUnit } from '../../../actions/panelActions'; +import { pushPanel, navigateFromRandomUnit, SOURCE_MENUS } from '../../../actions/panelActions'; import { finishVideoPreview, startVideoPlayer, @@ -451,6 +451,8 @@ export default function RandomUnit({ patnrId: randomData.patnrId, prdtId: randomData.prdtId, curationId: randomData.lnkCurationId, + sourcePanel: panel_names.HOME_PANEL, + sourceMenu: SOURCE_MENUS.HOME_RANDOM_UNIT, }, }; break; @@ -473,6 +475,8 @@ export default function RandomUnit({ curationId: randomData.lnkCurationId, prdtId: randomData.prdtId, type: 'theme', + sourcePanel: panel_names.HOME_PANEL, + sourceMenu: SOURCE_MENUS.HOME_RANDOM_UNIT, }, }; break; diff --git a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RollingUnit.jsx b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RollingUnit.jsx index f5884281..64022074 100644 --- a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RollingUnit.jsx +++ b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RollingUnit.jsx @@ -1,42 +1,22 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; -import { - useDispatch, - useSelector, -} from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import Spotlight from '@enact/spotlight'; -import SpotlightContainerDecorator - from '@enact/spotlight/SpotlightContainerDecorator'; +import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; import Spottable from '@enact/spotlight/Spottable'; import { getContainerId } from '@enact/spotlight/src/container'; import btnPlay from '../../../../assets/images/btn/btn-play-thumb-nor.png'; -import defaultLogoImg - from '../../../../assets/images/ic-tab-partners-default@3x.png'; -import emptyHorImage - from '../../../../assets/images/img-home-banner-empty-hor.png'; -import emptyVerImage - from '../../../../assets/images/img-home-banner-empty-ver.png'; -import defaultImageItem - from '../../../../assets/images/img-thumb-empty-product@3x.png'; +import defaultLogoImg from '../../../../assets/images/ic-tab-partners-default@3x.png'; +import emptyHorImage from '../../../../assets/images/img-home-banner-empty-hor.png'; +import emptyVerImage from '../../../../assets/images/img-home-banner-empty-ver.png'; +import defaultImageItem from '../../../../assets/images/img-thumb-empty-product@3x.png'; import liveShow from '../../../../assets/images/tag-liveshow.png'; -import { - setBannerIndex, - updateHomeInfo, -} from '../../../actions/homeActions'; -import { - sendLogTopContents, - sendLogTotalRecommend, -} from '../../../actions/logActions'; -import { pushPanel } from '../../../actions/panelActions'; +import { setBannerIndex, updateHomeInfo } from '../../../actions/homeActions'; +import { sendLogTopContents, sendLogTotalRecommend } from '../../../actions/logActions'; +import { pushPanel, SOURCE_MENUS } from '../../../actions/panelActions'; import { startVideoPlayer, finishVideoPreview } from '../../../actions/playActions'; import CustomImage from '../../../components/CustomImage/CustomImage'; import usePriceInfo from '../../../hooks/usePriceInfo'; @@ -47,31 +27,25 @@ import { LOG_TP_NO, panel_names, } from '../../../utils/Config'; -import { - $L, - formatGMTString, -} from '../../../utils/helperMethods'; +import { $L, formatGMTString } from '../../../utils/helperMethods'; import { TEMPLATE_CODE_CONF } from '../HomePanel'; import css from './RollingUnit.module.less'; -const SpottableComponent = Spottable("div"); +const SpottableComponent = Spottable('div'); -const Container = SpotlightContainerDecorator( - { enterTo: "last-focused", preserveId: true }, - "div" -); +const Container = SpotlightContainerDecorator({ enterTo: 'last-focused', preserveId: true }, 'div'); const LINK_TYPES = { - FEATURED_BRANDS: "DSP00501", - TRENDING_NOW: "DSP00502", - HOT_PICKS: "DSP00503", - ON_SALE: "DSP00504", - CATEGORY: "DSP00505", - PRODUCT_DETAIL: "DSP00506", - VOD: "DSP00507", - SHOW_DETAIL: "DSP00508", - THEME: "DSP00509", - JUSTFORYOU: "DSP00510", + FEATURED_BRANDS: 'DSP00501', + TRENDING_NOW: 'DSP00502', + HOT_PICKS: 'DSP00503', + ON_SALE: 'DSP00504', + CATEGORY: 'DSP00505', + PRODUCT_DETAIL: 'DSP00506', + VOD: 'DSP00507', + SHOW_DETAIL: 'DSP00508', + THEME: 'DSP00509', + JUSTFORYOU: 'DSP00510', }; const createPanelInfo = (data, categoryData = {}) => ({ @@ -101,17 +75,11 @@ export default function RollingUnit({ const { curationId, curationTitle } = useSelector((state) => state.home); const curtNm = useSelector((state) => state.home?.bannerData?.curtNm); - const shptmTmplCd = useSelector( - (state) => state.home?.bannerData?.shptmTmplCd - ); + const shptmTmplCd = useSelector((state) => state.home?.bannerData?.shptmTmplCd); const nowMenu = useSelector((state) => state.common.menu.nowMenu); const entryMenu = useSelector((state) => state.common.menu.entryMenu); - const introTermsAgree = useSelector( - (state) => state.common.optionalTermsAgree - ); - const homeCategory = useSelector( - (state) => state.home.menuData?.data?.homeCategory - ); + const introTermsAgree = useSelector((state) => state.common.optionalTermsAgree); + const homeCategory = useSelector((state) => state.home.menuData?.data?.homeCategory); const countryCode = useSelector((state) => state.common.httpHeader.cntry_cd); const { userNumber } = useSelector((state) => state.common.appStatus.loginUserData); @@ -127,15 +95,15 @@ export default function RollingUnit({ // 컴포넌트 상단에서 필터링 const filteredRollingData = useMemo(() => { return rollingData.filter( - (item) => (introTermsAgree === true && (userNumber !== undefined && userNumber !== '')) || item.shptmLnkTpCd !== "DSP00510" + (item) => + (introTermsAgree === true && userNumber !== undefined && userNumber !== '') || + item.shptmLnkTpCd !== 'DSP00510' ); }, [rollingData, introTermsAgree, userNumber]); // 이후 filteredRollingData 사용 const rollingDataLength = filteredRollingData.length; - const [startIndex, setStartIndex] = useState( - savedIndex !== undefined ? savedIndex : 0 - ); + const [startIndex, setStartIndex] = useState(savedIndex !== undefined ? savedIndex : 0); const lastIndexRef = useRef(rollingDataLength - 1); const doRollingRef = useRef(false); const [unitHasFocus, setUnitHasFocus] = useState(false); @@ -157,14 +125,14 @@ export default function RollingUnit({ switch (currentRollingData?.shptmBanrTpCd) { // case: "LIVE" or "VOD" - case "DSP00301": - case "DSP00302": + case 'DSP00301': + case 'DSP00302': contId = currentRollingData?.showId; contNm = currentRollingData?.showNm; break; // case: "Image Banner" - case "DSP00303": + case 'DSP00303': contId = currentRollingData?.shptmLnkTpCd; contNm = currentRollingData?.shptmLnkTpNm; break; @@ -177,27 +145,27 @@ export default function RollingUnit({ } if ( - currentRollingData?.shptmLnkTpCd === "DSP00503" || // "Hot Picks" - currentRollingData?.shptmLnkTpCd === "DSP00509" // "Theme" + currentRollingData?.shptmLnkTpCd === 'DSP00503' || // "Hot Picks" + currentRollingData?.shptmLnkTpCd === 'DSP00509' // "Theme" ) { - contNm = contNm + " | " + currentRollingData?.lnkCurationId; + contNm = contNm + ' | ' + currentRollingData?.lnkCurationId; } return { banrNo: `${currentRollingData?.banrDpOrd}`, banrTpNm: currentRollingData?.vtctpYn - ? currentRollingData.vtctpYn === "Y" - ? "Vertical" - : "Horizontal" - : "", + ? currentRollingData.vtctpYn === 'Y' + ? 'Vertical' + : 'Horizontal' + : '', contId, contNm, - contTpNm: currentRollingData?.shptmBanrTpNm ?? "", - dspyTpNm: bannerDataRef.current?.shptmDspyTpNm ?? "", - expsOrd: bannerDataRef.current?.banrLctnNo ?? "", - linkTpCd: "", - patncNm: currentRollingData?.patncNm ?? "", - patnrId: currentRollingData?.patnrId ?? "", + contTpNm: currentRollingData?.shptmBanrTpNm ?? '', + dspyTpNm: bannerDataRef.current?.shptmDspyTpNm ?? '', + expsOrd: bannerDataRef.current?.banrLctnNo ?? '', + linkTpCd: '', + patncNm: currentRollingData?.patncNm ?? '', + patnrId: currentRollingData?.patnrId ?? '', tmplCd: shptmTmplCd, }; } @@ -208,19 +176,17 @@ export default function RollingUnit({ (bannerClick) => { const data = rollingDataRef.current[startIndex]; const newParams = - bannerData.banrLctnNo === "2" + bannerData.banrLctnNo === '2' ? { - bannerType: "Horizontal", + bannerType: 'Horizontal', } : { - bannerType: "Vertical", + bannerType: 'Vertical', }; if (rollingDataRef.current && nowMenu === LOG_MENU.HOME_TOP) { const logParams = { contextName: LOG_CONTEXT_NAME.HOME, - messageId: bannerClick - ? LOG_MESSAGE_ID.BANNER_CLICK - : LOG_MESSAGE_ID.BANNER, + messageId: bannerClick ? LOG_MESSAGE_ID.BANNER_CLICK : LOG_MESSAGE_ID.BANNER, curationId: curationId, curationTitle: curationTitle, contentType: data.shptmBanrTpNm, @@ -228,7 +194,7 @@ export default function RollingUnit({ contentTitle: data.showNm, productId: data.prdtId, productTitle: data.prdtNm, - displayType: "button", + displayType: 'button', partner: data.patncNm, brand: data.brndNm, location: bannerData.banrLctnNo, @@ -251,16 +217,14 @@ export default function RollingUnit({ const deltaTime = time - previousTimeRef.current; if (deltaTime >= 10000 && doRollingRef.current) { - setStartIndex((prevIndex) => - prevIndex === lastIndexRef.current ? 0 : prevIndex + 1 - ); + setStartIndex((prevIndex) => (prevIndex === lastIndexRef.current ? 0 : prevIndex + 1)); previousTimeRef.current = time; } } else { previousTimeRef.current = time; } - if (typeof window === "object") { + if (typeof window === 'object') { requestRef.current = window.requestAnimationFrame(animate); } }, []); @@ -298,7 +262,7 @@ export default function RollingUnit({ // 인디케이터 아래키 누를시 [<] const prevKeyDown = (event) => { - if (event.key === "ArrowDown") { + if (event.key === 'ArrowDown') { setNextFocus(true); setContentsFocus(true); } @@ -306,7 +270,7 @@ export default function RollingUnit({ // 인디케이터 아래키 누를시 [>] const nextKeyDown = (event) => { - if (event.key === "ArrowDown") { + if (event.key === 'ArrowDown') { setPrevFocus(true); setContentsFocus(true); } @@ -364,7 +328,16 @@ export default function RollingUnit({ const handlePushPanel = useCallback( (name, panelInfo) => { - dispatch(pushPanel({ name, panelInfo })); + const isDetailPanel = name === panel_names.DETAIL_PANEL; + const enrichedPanelInfo = isDetailPanel + ? { + sourcePanel: panel_names.HOME_PANEL, + sourceMenu: SOURCE_MENUS.HOME_ROLLING_UNIT, + ...panelInfo, + } + : panelInfo; + + dispatch(pushPanel({ name, panelInfo: enrichedPanelInfo })); }, [dispatch] ); @@ -390,7 +363,7 @@ export default function RollingUnit({ dispatch( sendLogTopContents({ ...topContentsLogInfo, - inDt: formatGMTString(new Date()) ?? "", + inDt: formatGMTString(new Date()) ?? '', logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK, }) ); @@ -400,26 +373,20 @@ export default function RollingUnit({ switch (linkType) { case LINK_TYPES.FEATURED_BRANDS: handlePushPanel(panel_names.FEATURED_BRANDS_PANEL, { - from: "gnb", + from: 'gnb', patnrId: currentData.patnrId, }); break; case LINK_TYPES.TRENDING_NOW: - handlePushPanel( - panel_names.TRENDING_NOW_PANEL, - createPanelInfo(currentData) - ); + handlePushPanel(panel_names.TRENDING_NOW_PANEL, createPanelInfo(currentData)); break; case LINK_TYPES.HOT_PICKS: if (playerPanelInfo?.modal) { dispatch(finishVideoPreview()); } - handlePushPanel( - panel_names.HOT_PICKS_PANEL, - createPanelInfo(currentData) - ); + handlePushPanel(panel_names.HOT_PICKS_PANEL, createPanelInfo(currentData)); break; case LINK_TYPES.ON_SALE: @@ -430,10 +397,7 @@ export default function RollingUnit({ case LINK_TYPES.CATEGORY: if (Object.keys(categoryData).length > 0) { - handlePushPanel( - panel_names.CATEGORY_PANEL, - createPanelInfo(currentData, categoryData) - ); + handlePushPanel(panel_names.CATEGORY_PANEL, createPanelInfo(currentData, categoryData)); } break; @@ -454,10 +418,7 @@ export default function RollingUnit({ }); break; case LINK_TYPES.JUSTFORYOU: - handlePushPanel( - panel_names.JUST_FOR_YOU_TEST_PANEL, - createPanelInfo(currentData) - ); + handlePushPanel(panel_names.JUST_FOR_YOU_TEST_PANEL, createPanelInfo(currentData)); break; default: return; @@ -466,7 +427,7 @@ export default function RollingUnit({ dispatch( sendLogTopContents({ ...topContentsLogInfo, - inDt: formatGMTString(new Date()) ?? "", + inDt: formatGMTString(new Date()) ?? '', logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK, }) ); @@ -492,7 +453,7 @@ export default function RollingUnit({ panelInfo: { lastFocusedTargetId, focusedContainerId: TEMPLATE_CODE_CONF.TOP, - currentSpot: currentSpot?.getAttribute("data-spotlight-id"), + currentSpot: currentSpot?.getAttribute('data-spotlight-id'), }, }) ); @@ -508,7 +469,7 @@ export default function RollingUnit({ showUrl: currentData.showUrl, patnrId: currentData.patnrId, showId: currentData.showId, - shptmBanrTpNm: currentData.showId ? currentData.shptmBanrTpNm : "MEDIA", + shptmBanrTpNm: currentData.showId ? currentData.shptmBanrTpNm : 'MEDIA', lgCatCd: currentData.lgCatCd, chanId: currentData.brdcChnlId, modal: false, @@ -520,18 +481,11 @@ export default function RollingUnit({ dispatch( sendLogTopContents({ ...topContentsLogInfo, - inDt: formatGMTString(new Date()) ?? "", + inDt: formatGMTString(new Date()) ?? '', logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK, }) ); - }, [ - rollingData, - startIndex, - bannerId, - dispatch, - handleStartVideoPlayer, - topContentsLogInfo, - ]); + }, [rollingData, startIndex, bannerId, dispatch, handleStartVideoPlayer, topContentsLogInfo]); // 10초 롤링 useEffect(() => { @@ -540,20 +494,20 @@ export default function RollingUnit({ previousTimeRef.current = undefined; if (rollingDataLength <= 1 || unitHasFocus) { doRollingRef.current = false; - if (typeof window === "object") { + if (typeof window === 'object') { window.cancelAnimationFrame(requestRef.current); } return; } doRollingRef.current = true; - if (typeof window === "object") { + if (typeof window === 'object') { requestRef.current = window.requestAnimationFrame(animate); } return () => { doRollingRef.current = false; - if (typeof window === "object") { + if (typeof window === 'object') { window.cancelAnimationFrame(requestRef.current); } }; @@ -567,7 +521,7 @@ export default function RollingUnit({ const params = { ...topContentsLogInfo, entryMenu: _entryMenu, - inDt: formatGMTString(new Date()) ?? "", + inDt: formatGMTString(new Date()) ?? '', logTpNo: LOG_TP_NO.TOP_CONTENTS.VIEW, nowMenu: _nowMenu, }; @@ -593,10 +547,7 @@ export default function RollingUnit({ return ( @@ -606,15 +557,14 @@ export default function RollingUnit({ onClick={handlePrev} onFocus={indicatorFocus} onBlur={indicatorBlur} - spotlightId={spotlightId + "Prev"} + spotlightId={spotlightId + 'Prev'} spotlightDisabled={prevFocus} onKeyDown={prevKeyDown} aria-label="Move to left Button" /> ) : null} - {filteredRollingData && - filteredRollingData[startIndex].shptmBanrTpNm === "Image Banner" ? ( + {filteredRollingData && filteredRollingData[startIndex].shptmBanrTpNm === 'Image Banner' ? ( - ) : filteredRollingData[startIndex].shptmBanrTpNm === "LIVE" ? ( + ) : filteredRollingData[startIndex].shptmBanrTpNm === 'LIVE' ? (

- +

-
+
{filteredRollingData[startIndex].tmnlImgPath == null ? ( {filteredRollingData[startIndex].tmnlImgPath == null ? ( - "" + '' ) : ( - + )}
@@ -700,7 +634,7 @@ export default function RollingUnit({ />

- ) : filteredRollingData[startIndex].shptmBanrTpNm === "VOD" ? ( + ) : filteredRollingData[startIndex].shptmBanrTpNm === 'VOD' ? ( -
+
{filteredRollingData[startIndex].tmnlImgPath == null ? ( @@ -736,13 +663,9 @@ export default function RollingUnit({
{filteredRollingData[startIndex].tmnlImgPath == null ? ( - "" + '' ) : ( - + )}
@@ -764,8 +687,8 @@ export default function RollingUnit({ className={classNames( css.itemBox, css.todaysDeals, - countryCode === "RU" ? css.ru : "", - countryCode === "DE" ? css.de : "", + countryCode === 'RU' ? css.ru : '', + countryCode === 'DE' ? css.de : '', isHorizontal && css.isHorizontal )} onClick={imageBannerClick} @@ -788,7 +711,7 @@ export default function RollingUnit({ }} />
- {parseFloat(originalPrice?.replace("$", "")) === 0 + {parseFloat(originalPrice?.replace('$', '')) === 0 ? filteredRollingData[startIndex].offerInfo : discountRate ? discountedPrice @@ -797,10 +720,9 @@ export default function RollingUnit({ {originalPrice} )}
- {isHorizontal && - parseFloat(originalPrice?.replace("$", "")) !== 0 && ( - {originalPrice} - )} + {isHorizontal && parseFloat(originalPrice?.replace('$', '')) !== 0 && ( + {originalPrice} + )}
@@ -822,7 +744,7 @@ export default function RollingUnit({ onClick={handleNext} onFocus={indicatorFocus} onBlur={indicatorBlur} - spotlightId={spotlightId + "Next"} + spotlightId={spotlightId + 'Next'} spotlightDisabled={nextFocus} onKeyDown={nextKeyDown} aria-label="Move to right Button" diff --git a/com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx b/com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx index cc068871..5ef3e078 100644 --- a/com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx +++ b/com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx @@ -244,6 +244,31 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => { const focusedContainerIdRef = useRef(null); const prevIsOnTopRef = useRef(isOnTop); + // ✅ [251124] HomePanel을 벗어날 때(isOnTop: true -> false) 현재 포커스 저장 + useEffect(() => { + if (prevIsOnTopRef.current && !isOnTop) { + const current = Spotlight.getCurrent(); + const tBody = document.querySelector(`[data-spotlight-id="${SpotlightIds.HOME_TBODY}"]`); + + if (current && tBody && tBody.contains(current)) { + const targetId = current.getAttribute('data-spotlight-id'); + if (targetId) { + dlog('[HomePanel] Saving focus before leaving:', targetId); + lastFocusedTargetRef.current = targetId; + dispatch( + updateHomeInfo({ + name: panel_names.HOME_PANEL, + panelInfo: { + lastFocusedTargetId: targetId, + }, + }) + ); + } + } + } + prevIsOnTopRef.current = isOnTop; + }, [isOnTop, dispatch]); + // ✅ [251119] DetailPanelBackground 이미지 프리로딩 // HomePanel 마운트 시 백그라운드로 모든 파트너사 배경 이미지를 미리 로드하여 // DetailPanel 진입 시 로딩 지연을 방지함 diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx index f09d05b0..2818d20f 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx @@ -39,6 +39,7 @@ import { pauseModalVideo, resumeModalVideo, resumeFullscreenVideo, + updateVideoPlayState, } from '../../actions/playActions'; import { resetPlayerOverlays } from '../../actions/videoPlayActions'; import { convertUtcToLocal } from '../../components/MediaPlayer/util'; @@ -436,17 +437,153 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props } }, [isOnTop, panelInfo]); - // 새로운 useEffect 추가 (라인 328 이후) + // 이전 상태 저장을 위한 ref + const previousVideoPlayState = useRef(null); + const previousPanelInfo = useRef(null); + const previousVideoSource = useRef(null); + const previousIsOnTop = useRef(null); + + // Redux 상태 변화 모니터링 useEffect (중요 변화만) useEffect(() => { - // modal 여부와 관계없이 videoPlayer가 있고 isPaused 값이 명시적일 때 제어 - if (videoPlayer.current && panelInfo?.isPaused !== undefined) { - if (panelInfo.isPaused === true) { - videoPlayer.current.pause(); - } else if (panelInfo.isPaused === false) { - videoPlayer.current.play(); + // HomePanel이 최상위이고 videoPlayState가 실제로 변경되었을 때만 로그 + const isOnTop = panel_names.HOME_PANEL === topPanel?.name; + const hasSignificantChange = + previousVideoPlayState.current?.isPlaying !== videoPlayState?.isPlaying || + previousVideoPlayState.current?.isPaused !== videoPlayState?.isPaused || + Math.abs( + (previousVideoPlayState.current?.currentTime || 0) - (videoPlayState?.currentTime || 0) + ) > 1; + + // 🔍 DetailPanel으로 인한 일시정지 상태 변화도 모니터링 + const isDetailPanelOnTop = panel_names.DETAIL_PANEL === topPanel?.name; + const isPlayingChanged = + previousVideoPlayState.current?.isPlaying !== videoPlayState?.isPlaying; + const isPausedChanged = previousVideoPlayState.current?.isPaused !== videoPlayState?.isPaused; + + if ( + (isOnTop && videoPlayState && hasSignificantChange) || + (isDetailPanelOnTop && (isPlayingChanged || isPausedChanged)) + ) { + console.log('📊 [PlayerPanel] Significant videoPlayState change', { + previousState: previousVideoPlayState.current, + currentState: videoPlayState, + topPanelName: topPanel?.name, + isOnTop, + isDetailPanelOnTop, + videoPlayerExists: !!videoPlayer.current, + currentPlayingUrl, + changeReason: isDetailPanelOnTop ? 'DetailPanel transition' : 'HomePanel state change', + // 🔍 Redux paused 상태 특별 확인 + reduxIsPaused: videoPlayState?.isPaused, + reduxIsPlaying: videoPlayState?.isPlaying, + panelInfoIsPaused: panelInfo?.isPaused, + timestamp: new Date().toISOString(), + source: 'useEffect videoPlayState', + }); + } + + previousVideoPlayState.current = videoPlayState; + }, [videoPlayState, topPanel?.name, panelInfo?.isPaused]); + + // PanelInfo 상태 변화 모니터링 useEffect (isPaused가 실제로 변경될 때만) + useEffect(() => { + const isOnTop = panel_names.HOME_PANEL === topPanel?.name; + const isPausedChanged = previousPanelInfo.current?.isPaused !== panelInfo?.isPaused; + + if (isOnTop && panelInfo?.isPaused !== undefined && isPausedChanged) { + // 상태 변경 시에만 디버깅 로그 출력 + console.log('🔍 [PlayerPanel] PanelInfo isPaused changed', { + previousIsPaused: previousPanelInfo.current?.isPaused, + currentIsPaused: panelInfo.isPaused, + isOnTop, + videoPlayerExists: !!videoPlayer.current, + currentPlayingUrl, + timestamp: new Date().toISOString(), + }); + console.log('🎮 [PlayerPanel] PanelInfo isPaused changed', { + previousIsPaused: previousPanelInfo.current?.isPaused, + currentIsPaused: panelInfo.isPaused, + videoPlayerExists: !!videoPlayer.current, + currentPlayingUrl, + shptmBanrTpNm: panelInfo?.shptmBanrTpNm, + showId: panelInfo?.showId, + timestamp: new Date().toISOString(), + source: 'useEffect panelInfo.isPaused', + }); + + if (videoPlayer.current) { + if (panelInfo.isPaused === true) { + console.log('🔴 [PlayerPanel] Calling VideoPlayer.pause() due to PanelInfo change'); + videoPlayer.current.pause(); + } else if (panelInfo.isPaused === false) { + console.log('🟢 [PlayerPanel] Calling VideoPlayer.play() due to PanelInfo change'); + videoPlayer.current.play(); + } } } - }, [panelInfo?.isPaused]); + + previousPanelInfo.current = panelInfo; + }, [panelInfo?.isPaused, topPanel?.name, currentPlayingUrl]); + + // VideoPlayer 인스턴스 및 소스 변경 모니터링 (중요 변화만) + useEffect(() => { + const isOnTop = panel_names.HOME_PANEL === topPanel?.name; + const isDetailPanelOnTop = panel_names.DETAIL_PANEL === topPanel?.name; + const isVideoSourceChanged = previousVideoSource.current !== currentPlayingUrl; + const isOnTopChanged = previousIsOnTop.current !== isOnTop; + const isDetailPanelOnTopChanged = + previousIsOnTop.current === false && isDetailPanelOnTop === true; + const videoPlayerJustCreated = previousVideoSource.current === null && videoPlayer.current; + + if ( + (isVideoSourceChanged || + isOnTopChanged || + isDetailPanelOnTopChanged || + videoPlayerJustCreated) && + videoPlayer.current + ) { + const changeReason = isDetailPanelOnTopChanged + ? 'DetailPanel opened' + : isVideoSourceChanged + ? 'Video source changed' + : isOnTopChanged + ? 'Top panel changed' + : 'VideoPlayer created'; + + console.log('🎬 [PlayerPanel] VideoPlayer state change', { + hasVideoPlayer: !!videoPlayer.current, + currentPlayingUrl, + previousVideoSource: previousVideoSource.current, + topPanelName: topPanel?.name, + isOnTop, + isDetailPanelOnTop, + isVideoSourceChanged, + isOnTopChanged, + isDetailPanelOnTopChanged, + videoPlayerJustCreated, + changeReason, + timestamp: new Date().toISOString(), + source: 'useEffect videoPlayer.current', + }); + + // VideoPlayer 상태 확인 (소스 변경이나 PlayerPanel 생성 시에만) + if (isVideoSourceChanged || videoPlayerJustCreated || isDetailPanelOnTopChanged) { + const mediaState = videoPlayer.current.getMediaState?.(); + if (mediaState) { + console.log('📊 [PlayerPanel] VideoPlayer current state', { + mediaState, + videoPlayState, + changeReason, + timestamp: new Date().toISOString(), + source: 'useEffect getMediaState', + }); + } + } + } + + previousVideoSource.current = currentPlayingUrl; + previousIsOnTop.current = isOnTop || isDetailPanelOnTop; // 현재 최상위 패널 상태 저장 + }, [videoPlayer.current, currentPlayingUrl, topPanel?.name]); // Modal 상태 변화 감지 (true → false, false → true) useEffect(() => { @@ -1130,11 +1267,63 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props // 최상단 패널이 DetailPanel이고 PlayerPanel에서 진입했는지 확인 const isTopPanelDetailFromPlayer = useMemo(() => { - return ( + const result = topPanel?.name === panel_names.DETAIL_PANEL && - topPanel?.panelInfo?.launchedFromPlayer === true - ); - }, [topPanel]); + topPanel?.panelInfo?.launchedFromPlayer === true; + + // 🔍 DetailPanel 상태 변화 로깅 + if (result) { + console.log('🎬 [PlayerPanel] DetailPanel is now on top (from Player)', { + topPanelName: topPanel?.name, + launchedFromPlayer: topPanel?.panelInfo?.launchedFromPlayer, + modalPlayerPanelExists: panels.some( + (p) => p.name === panel_names.PLAYER_PANEL && p.panelInfo?.modal + ), + currentVideoState: { + isPlaying: videoPlayState?.isPlaying, + isPaused: videoPlayState?.isPaused, + currentTime: videoPlayState?.currentTime, + }, + timestamp: new Date().toISOString(), + }); + } + + return result; + }, [topPanel, panels, videoPlayState]); + + // 🔍 PlayerPanel이 밑에 깔렸을 때 자신의 상태를 업데이트하는 useEffect + useEffect(() => { + const isDetailPanelOnTop = topPanel?.name === panel_names.DETAIL_PANEL; + const isModalPlayerPanel = panelInfo?.modal === true; + const isCurrentPanelOnTop = topPanel?.name === panel_names.PLAYER_PANEL; + + // PlayerPanel이 modal이고 밑에 깔렸을 때 + if (isModalPlayerPanel && !isCurrentPanelOnTop && isDetailPanelOnTop) { + console.log('🔴 [PlayerPanel] Self-pausing due to DetailPanel on top', { + isDetailPanelOnTop, + isModalPlayerPanel, + isCurrentPanelOnTop, + currentReduxState: videoPlayState, + needsPause: videoPlayState?.isPlaying === true || videoPlayState?.isPaused === false, + timestamp: new Date().toISOString(), + }); + + // PlayerPanel 자신의 상태를 일시정지로 업데이트 + if (videoPlayState?.isPlaying === true || videoPlayState?.isPaused === false) { + console.log('🔄 [PlayerPanel] Dispatching self-pause to Redux'); + dispatch( + updateVideoPlayState({ + isPlaying: false, + isPaused: true, + currentTime: videoPlayState?.currentTime || 0, + duration: videoPlayState?.duration || 0, + playbackRate: videoPlayState?.playbackRate || 1, + source: 'PlayerPanel-self-pause', + }) + ); + } + } + }, [topPanel?.name, panelInfo?.modal, videoPlayState, dispatch]); const cannotPlay = useMemo(() => { return !isOnTop && topPanel?.name === panel_names.PLAYER_PANEL; @@ -2624,6 +2813,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props tabContainerVersion={tabContainerVersion} tabIndexV2={tabIndexV2} dispatch={dispatch} + videoPlayState={videoPlayState} > {typeof window === 'object' && window.PalmSystem && (