import Spotlight from '@enact/spotlight'; import { URLS } from '../api/apiConfig'; import { TAxios } from '../api/TAxios'; import { panel_names } from '../utils/Config'; import { types } from './actionTypes'; import { popPanel, pushPanel, updatePanel } from './panelActions'; import { createDebugHelpers } from '../utils/debug'; // 디버그 헬퍼 설정 const DEBUG_MODE = false; const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE); // 🔽 [251116] 새로운 비디오 상태 관리 시스템 - 재생 상태 export const PLAYBACK_STATUS = { LOADING: 'loading', LOAD_SUCCESS: 'load_success', LOAD_ERROR: 'load_error', PLAYING: 'playing', NOT_PLAYING: 'not_playing', BUFFERING: 'buffering', }; // 🔽 [251116] 새로운 비디오 상태 관리 시스템 - 화면 상태 export const DISPLAY_STATUS = { HIDDEN: 'hidden', VISIBLE: 'visible', MINIMIZED: 'minimized', FULLSCREEN: 'fullscreen', }; //yhcho /* dispatch(startVideoPreview({ patnrId: randomData.patnrId, showId: randomData.showId, shptmBanrTpNm: randomData.shptmBanrTpNm, lgCatCd: randomData.lgCatCd, modal: true, modalContainerRef: videoModalContainerRef.current, //to calc width, height, left, top modalClassName:css.videoModal })); modalClassName: modal more class info and checking it's launched from preview mode */ let startVideoTimer = null; //start modal mode //start Full -> modal mode let startVideoFocusTimer = null; // 🔽 전역 타이머 정리 함수 - 메모리 누수 방지 export const clearAllVideoTimers = () => { if (startVideoFocusTimer) { clearTimeout(startVideoFocusTimer); startVideoFocusTimer = null; dlog('[playActions] startVideoFocusTimer cleared'); } if (startVideoTimer) { clearTimeout(startVideoTimer); startVideoTimer = null; dlog('[playActions] startVideoTimer cleared'); } }; export const startVideoPlayer = ({ modal, modalContainerId, modalClassName, spotlightDisable, useNewPlayer, videoId, showUrl, ...rest }) => (dispatch, getState) => { const caller = new Error().stack?.split('\n')[2]?.trim(); console.log('[PTRACE-SP] startVideoPlayer call', { modal, modalContainerId, modalClassName, videoId, showUrl, caller, }); dlog( '[startVideoPlayer] ✅ START - videoId:', videoId, ', showUrl:', showUrl, ', modal:', modal ); // 🔽 [251116] 즉시 로딩 상태 설정 const videoIdentifier = videoId || showUrl; if (videoIdentifier) { const displayMode = modal ? DISPLAY_STATUS.VISIBLE : DISPLAY_STATUS.FULLSCREEN; dlog( '[startVideoPlayer] 📌 Setting playback loading - identifier:', videoIdentifier, ', displayMode:', displayMode ); dispatch(setPlaybackLoading(videoIdentifier, displayMode)); } else { dlog( '[startVideoPlayer] ⚠️ No videoIdentifier provided (videoId and showUrl are both missing)' ); } const panels = getState().panels.panels; const existingPlayerPanel = panels.find((p) => p.name === panel_names.PLAYER_PANEL); let panelWorkingAction = pushPanel; // 기존 PlayerPanel이 어디든 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push if (existingPlayerPanel) { console.log('[PTRACE-SP] startVideoPlayer: popping existing player before push', { stack: panels.map((p) => p.name), }); dlog('[startVideoPlayer] 🔄 Resetting existing PLAYER_PANEL before start'); clearAllVideoTimers(); dispatch(popPanel(panel_names.PLAYER_PANEL)); } else { dlog( '[startVideoPlayer] 📊 No existing PLAYER_PANEL - panelsCount:', panels.length ); } dispatch( panelWorkingAction( { name: panel_names.PLAYER_PANEL, panelInfo: { modal, modalContainerId, modalClassName, videoId, // videoId 추가하여 PlayerPanel에서 사용 가능 showUrl, // showUrl 추가하여 PlayerPanel에서 사용 가능 ...rest, }, }, true ) ); dlog('[startVideoPlayer] ✨ Panel action dispatched'); // [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화 // if (modal && modalContainerId && !spotlightDisable) { // console.log('[startVideoPlayer] 🎯 Setting Spotlight focus - containerId:', modalContainerId); // Spotlight.setPointerMode(false); // startVideoFocusTimer = setTimeout(() => { // console.log('[startVideoPlayer] 🔍 Spotlight.focus called'); // Spotlight.focus(modalContainerId); // }, 0); // } else { // console.log('[startVideoPlayer] ⏭️ Spotlight focus skipped - modal:', modal, ', modalContainerId:', !!modalContainerId, ', spotlightDisable:', spotlightDisable); // } dlog('[startVideoPlayer] ✅ END'); }; // 중복 재생 방지: 정말 동일한 요청인지 확인 // 같은 배너 + 같은 modal 상태 + 같은 위치 = 정말 동일한 요청이므로 skip const shouldSkipVideoPlayback = ( currentPanelInfo, newModalState, newModalContainerId, newBannerId ) => { if (!currentPanelInfo) return false; const currentBannerId = currentPanelInfo.playerState?.currentBannerId; const currentModal = currentPanelInfo.modal; const currentModalContainerId = currentPanelInfo.modalContainerId; return ( currentBannerId === newBannerId && currentModal === newModalState && currentModalContainerId === newModalContainerId ); }; export const startVideoPlayerNew = ({ modal, modalContainerId, modalClassName, spotlightDisable, useNewPlayer, bannerId, videoId, showUrl, ...rest }) => (dispatch, getState) => { const caller = new Error().stack?.split('\n')[2]?.trim(); console.log('[PTRACE-SPN] startVideoPlayerNew call', { bannerId, modal, modalContainerId, modalClassName, videoId, showUrl, caller, }); dlog( '[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; dlog( '[startVideoPlayerNew] *** 📌 Setting playback loading - identifier:', videoIdentifier, ', displayMode:', displayMode ); dispatch(setPlaybackLoading(videoIdentifier, displayMode)); } else { dlog('[startVideoPlayerNew] *** ⚠️ No videoIdentifier provided'); } const panels = getState().panels.panels; const existingPlayerPanel = panels.find((p) => p.name === panel_names.PLAYER_PANEL); let panelWorkingAction = pushPanel; let shouldCheckDuplicate = true; // 기존 PlayerPanel이 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push if (existingPlayerPanel) { console.log('[PTRACE-SPN] popping existing player before push', { stack: panels.map((p) => p.name), }); dlog('[startVideoPlayerNew] *** 🔄 Resetting existing PLAYER_PANEL before start'); clearAllVideoTimers(); dispatch(popPanel(panel_names.PLAYER_PANEL)); shouldCheckDuplicate = false; } const topPanel = panels[panels.length - 1]; dlog( '[startVideoPlayerNew] *** 📊 Panel state - panelsCount:', panels.length, ', topPanelName:', topPanel?.name ); let currentPanelInfo = topPanel?.panelInfo || {}; let currentPlayerState = currentPanelInfo.playerState || {}; if (!existingPlayerPanel && topPanel && topPanel.name === panel_names.PLAYER_PANEL) { panelWorkingAction = updatePanel; dlog('[startVideoPlayerNew] *** 📋 Current PLAYER_PANEL panelInfo:', topPanel.panelInfo); } // 중복 실행 방지: 같은 배너 + 같은 modal 상태/컨테이너 + 같은 URL이면 skip if (shouldCheckDuplicate) { const isSameBanner = currentPlayerState.currentBannerId === bannerId; const isSameModalType = currentPanelInfo.modal === modal; const isSameContainer = currentPanelInfo.modalContainerId === modalContainerId; const isSameShowUrl = currentPanelInfo.showUrl === showUrl; const isSameVideoId = currentPanelInfo.videoId === videoId; dlog( '[startVideoPlayerNew] *** 🔍 Duplicate check - isSameBanner:', isSameBanner, ', isSameModalType:', isSameModalType, ', isSameContainer:', isSameContainer, ', isSameShowUrl:', isSameShowUrl, ', isSameVideoId:', isSameVideoId ); if (isSameBanner && isSameModalType && isSameContainer && isSameShowUrl && isSameVideoId) { dlog('[startVideoPlayerNew] *** ⏭️ SKIPPED - 동일한 요청', { bannerId, modal, modalContainerId, showUrl, videoId, }); return; } } else { // pop으로 초기화한 경우 중복 체크 스킵 currentPanelInfo = {}; currentPlayerState = {}; } const newPlayerState = { ...currentPlayerState, currentBannerId: bannerId, }; dlog('[startVideoPlayerNew] *** 🔄 Player state updated - currentBannerId:', bannerId); dispatch( panelWorkingAction( { name: panel_names.PLAYER_PANEL, panelInfo: { modal, modalContainerId, modalClassName, playerState: newPlayerState, videoId, // videoId 추가 showUrl, // showUrl 추가 bannerId, // bannerId 추가 ...rest, }, }, true ) ); dlog( '[startVideoPlayerNew] *** ✨ Panel action dispatched - action:', panelWorkingAction === updatePanel ? 'updatePanel' : 'pushPanel' ); // [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화 // if (modal && modalContainerId && !spotlightDisable) { // console.log('[startVideoPlayerNew] *** 🎯 Setting Spotlight focus - containerId:', modalContainerId); // Spotlight.setPointerMode(false); // startVideoFocusTimer = setTimeout(() => { // console.log('[startVideoPlayerNew] *** 🔍 Spotlight.focus called'); // Spotlight.focus(modalContainerId); // }, 0); // } else { // console.log('[startVideoPlayerNew] *** ⏭️ Spotlight focus skipped - modal:', modal, ', modalContainerId:', !!modalContainerId, ', spotlightDisable:', spotlightDisable); // } dlog('[startVideoPlayerNew] *** ✅ END'); }; export const finishVideoPreview = () => (dispatch, getState) => { if (DEBUG_MODE === true) { dlog('###-finishVideoPreview'); } const panels = getState().panels.panels; const topPanel = panels[panels.length - 1]; if (topPanel && topPanel.name === panel_names.PLAYER_PANEL && topPanel.panelInfo.modal) { console.log('[PANEL-TRACE] finishVideoPreview: popping modal player', { topPanelName: topPanel.name, modal: topPanel.panelInfo.modal, stack: panels.map((p) => p.name), panelInfo: topPanel.panelInfo, }); if (startVideoFocusTimer) { clearTimeout(startVideoFocusTimer); startVideoFocusTimer = null; } dispatch(popPanel()); } }; export const finishModalVideoForce = () => (dispatch, getState) => { const panels = getState().panels.panels; if (DEBUG_MODE === true) { dlog('###-finishModalVideoForce'); } // modal PlayerPanel이 존재하는지 확인 (스택 어디에 있든) const hasModalPlayerPanel = panels.some( (panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal ); if (hasModalPlayerPanel) { if (startVideoFocusTimer) { clearTimeout(startVideoFocusTimer); startVideoFocusTimer = null; } // panelName을 지정하면 스택 어디에 있든 해당 패널을 제거 dispatch(popPanel(panel_names.PLAYER_PANEL)); } }; // 모든 PlayerPanel을 강제 제거 (modal과 fullscreen 모두) export const finishAllVideoForce = () => (dispatch, getState) => { const panels = getState().panels.panels; if (DEBUG_MODE === true) { dlog('###-finishAllVideoForce'); } // 모든 PlayerPanel이 존재하는지 확인 (스택 어디에 있든) const hasPlayerPanel = panels.some((panel) => panel.name === panel_names.PLAYER_PANEL); if (hasPlayerPanel) { if (startVideoFocusTimer) { clearTimeout(startVideoFocusTimer); startVideoFocusTimer = null; } // panelName을 지정하면 스택 어디에 있든 해당 패널을 제거 dispatch(popPanel(panel_names.PLAYER_PANEL)); } }; // 모달 비디오를 일시정지 (패널은 유지) export const pauseModalVideo = () => (dispatch, getState) => { const panels = getState().panels.panels; if (DEBUG_MODE === true) { dlog('###-pauseModalVideo'); } // modal PlayerPanel 찾기 const modalPlayerPanel = panels.find( (panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal ); console.log('[Detail-BG] ⏸️ pauseModalVideo - Pausing modal video', { found: !!modalPlayerPanel, playerPanelModal: modalPlayerPanel?.panelInfo?.modal, currentIsPaused: modalPlayerPanel?.panelInfo?.isPaused, timestamp: Date.now(), }); if (modalPlayerPanel) { if (DEBUG_MODE === true) { dlog('[pauseModalVideo] Pausing modal video'); } dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { ...modalPlayerPanel.panelInfo, isPaused: true, // 일시정지 플래그 추가 }, }) ); console.log('[Detail-BG] ✅ pauseModalVideo - Modal video paused successfully', { timestamp: Date.now(), }); } else { console.log('[Detail-BG] ⚠️ pauseModalVideo - No modal PlayerPanel found', { timestamp: Date.now(), }); } }; // 모달 비디오를 재생 (일시정지 해제) 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 ); console.log('[Detail-BG] ▶️ resumeModalVideo - Resuming modal video', { found: !!modalPlayerPanel, playerPanelModal: modalPlayerPanel?.panelInfo?.modal, currentIsPaused: modalPlayerPanel?.panelInfo?.isPaused, timestamp: Date.now(), }); if (modalPlayerPanel && modalPlayerPanel.panelInfo?.isPaused) { if (DEBUG_MODE === true) { dlog('[resumeModalVideo] Resuming modal video'); } dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { ...modalPlayerPanel.panelInfo, isPaused: false, // 일시정지 해제 }, }) ); console.log('[Detail-BG] ✅ resumeModalVideo - Modal video resumed successfully', { timestamp: Date.now(), }); } else { console.log('[Detail-BG] ⚠️ resumeModalVideo - Modal video not paused or panel not found', { found: !!modalPlayerPanel, isPaused: modalPlayerPanel?.panelInfo?.isPaused, timestamp: Date.now(), }); } }; // 전체화면 비디오를 일시정지 (패널은 유지) export const pauseFullscreenVideo = () => (dispatch, getState) => { const panels = getState().panels.panels; // 전체화면 PlayerPanel 찾기 (modal이 false인 패널) const fullscreenPlayerPanel = panels.find( (panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal ); console.log('[Detail-BG] ⏸️ pauseFullscreenVideo - Pausing fullscreen video', { found: !!fullscreenPlayerPanel, playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal, currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused, timestamp: Date.now(), }); if (fullscreenPlayerPanel) { dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { ...fullscreenPlayerPanel.panelInfo, isPaused: true, // 일시정지 플래그 추가 }, }) ); console.log('[Detail-BG] ✅ pauseFullscreenVideo - Fullscreen video paused successfully', { timestamp: Date.now(), }); } else { console.log('[Detail-BG] ⚠️ pauseFullscreenVideo - No fullscreen PlayerPanel found', { timestamp: Date.now(), }); } }; // 전체화면 비디오를 재생 (일시정지 해제) export const resumeFullscreenVideo = () => (dispatch, getState) => { const panels = getState().panels.panels; // 전체화면 PlayerPanel 찾기 (modal이 false인 패널) const fullscreenPlayerPanel = panels.find( (panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal ); console.log('[Detail-BG] ▶️ resumeFullscreenVideo - Resuming fullscreen video', { found: !!fullscreenPlayerPanel, playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal, currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused, timestamp: Date.now(), }); if (fullscreenPlayerPanel && fullscreenPlayerPanel.panelInfo?.isPaused) { dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { ...fullscreenPlayerPanel.panelInfo, isPaused: false, // 일시정지 해제 }, }) ); console.log('[Detail-BG] ✅ resumeFullscreenVideo - Fullscreen video resumed successfully', { timestamp: Date.now(), }); } else { console.log('[Detail-BG] ⚠️ resumeFullscreenVideo - Fullscreen video not paused or panel not found', { found: !!fullscreenPlayerPanel, isPaused: fullscreenPlayerPanel?.panelInfo?.isPaused, timestamp: Date.now(), }); if (DEBUG_MODE === true) { dlog('[BgVideo] resumeFullscreenVideo - Not resuming (not found or not paused)'); } } }; // 모달 비디오를 1px로 축소 (배너 정보 저장) export const hideModalVideo = () => (dispatch, getState) => { const panels = getState().panels.panels; // modal PlayerPanel 찾기 const modalPlayerPanel = panels.find( (panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal ); if (modalPlayerPanel) { const panelInfo = modalPlayerPanel.panelInfo; // 축소 전 배너 정보를 playerState에 저장 const updatedPlayerState = { ...(panelInfo.playerState || {}), shrinkInfo: { // 복구 시 필요한 정보 modalContainerId: panelInfo.modalContainerId, modalClassName: panelInfo.modalClassName, modalStyle: panelInfo.modalStyle, modalScale: panelInfo.modalScale, // top, left는 따로 저장 (복구 시 재계산하지 않기 위함) top: panelInfo.modalStyle?.top, left: panelInfo.modalStyle?.left, currentBannerId: panelInfo.playerState?.currentBannerId, patnrId: panelInfo.patnrId, showId: panelInfo.showId, shptmBanrTpNm: panelInfo.shptmBanrTpNm, lgCatCd: panelInfo.lgCatCd, }, }; // console.log('[HomePanel] hideModalVideo: saving shrinkInfo', { // shrinkInfo: updatedPlayerState.shrinkInfo, // modalStyle: panelInfo.modalStyle, // }); dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { ...panelInfo, shouldShrinkTo1px: true, // 축소 플래그 설정 playerState: updatedPlayerState, }, }) ); } else { if (DEBUG_MODE === true) { dlog('[HomePanel] hideModalVideo: No modal PlayerPanel found', { panels: panels.map((p) => ({ name: p.name, modal: p.panelInfo?.modal, shouldShrinkTo1px: p.panelInfo?.shouldShrinkTo1px, })), }); } } }; // 축소된 모달 비디오를 원래 크기로 복구 export const showModalVideo = () => (dispatch, getState) => { if (DEBUG_MODE === true) { dlog('[showModalVideo] *** ✅ START'); } const panels = getState().panels.panels; if (DEBUG_MODE === true) { dlog('[showModalVideo] *** 📊 Total panels count:', panels.length); } // 축소된 modal PlayerPanel 찾기 const shrunkModalPlayerPanel = panels.find( (panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal && panel.panelInfo?.shouldShrinkTo1px ); if (DEBUG_MODE === true) { dlog('[showModalVideo] *** 🔍 Shrunk modal PlayerPanel found:', !!shrunkModalPlayerPanel); } if (shrunkModalPlayerPanel) { const panelInfo = shrunkModalPlayerPanel.panelInfo; const shrinkInfo = panelInfo.playerState?.shrinkInfo; if (DEBUG_MODE === true) { dlog('[showModalVideo] *** 📋 ShrinkInfo available:', !!shrinkInfo); } dlog('[showModalVideo] *** 📋 Current panelInfo state:', { shouldShrinkTo1px: panelInfo.shouldShrinkTo1px, modal: panelInfo.modal, modalContainerId: panelInfo.modalContainerId, hasModalStyle: !!panelInfo.modalStyle, }); const updatedPanelInfo = { ...panelInfo, shouldShrinkTo1px: false, // 축소 플래그 해제 // 저장된 정보로 복구 (하지만 DOM 재계산은 허용) ...(shrinkInfo && { modalContainerId: shrinkInfo.modalContainerId, modalClassName: shrinkInfo.modalClassName, modalStyle: shrinkInfo.modalStyle, modalScale: shrinkInfo.modalScale, }), skipModalStyleRecalculation: false, // 위치 변경 시 DOM 기준으로 다시 계산하도록 허용 }; dlog( '[showModalVideo] *** 🔄 Updated panelInfo - shouldShrinkTo1px:', updatedPanelInfo.shouldShrinkTo1px ); dlog('[showModalVideo] *** 📍 Restored modalStyle:', updatedPanelInfo.modalStyle); dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: updatedPanelInfo, }) ); dlog('[showModalVideo] *** ✨ updatePanel dispatched'); } else { dlog('[showModalVideo] *** ⚠️ No shrunk modal PlayerPanel found', { panels: panels.map((p) => ({ name: p.name, modal: p.panelInfo?.modal, shouldShrinkTo1px: p.panelInfo?.shouldShrinkTo1px, })), }); } dlog('[showModalVideo] *** ✅ END'); }; // 🔽 패널은 유지하고 비디오만 중지하는 함수들 /** * 패널을 닫지 않고(popPanel 하지 않고) 비디오만 중지합니다. * 모달 비디오의 재생을 중지하고 숨김 상태로 만듭니다. */ export const stopModalVideoWithoutClosingPanel = () => (dispatch, getState) => { const panels = getState().panels.panels; // modal PlayerPanel 찾기 const modalPlayerPanel = panels.find( (panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal ); if (modalPlayerPanel) { dlog('[stopModalVideoWithoutClosingPanel] Stopping modal video playback'); // 타이머 정리 if (startVideoFocusTimer) { clearTimeout(startVideoFocusTimer); startVideoFocusTimer = null; } // 패널은 유지하되, 비디오 중지 상태로 업데이트 dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { ...modalPlayerPanel.panelInfo, shouldStop: true, // 비디오 중지 플래그 isPaused: true, // 일시정지 상태 isHidden: true, // 화면에서 숨김 }, }) ); // Redux 상태도 중지로 업데이트 dispatch(setVideoStopped()); } }; /** * 패널을 닫지 않고 전체화면 비디오만 중지합니다. */ export const stopFullscreenVideoWithoutClosingPanel = () => (dispatch, getState) => { const panels = getState().panels.panels; // 전체화면 PlayerPanel 찾기 const fullscreenPlayerPanel = panels.find( (panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal ); if (fullscreenPlayerPanel) { dlog('[stopFullscreenVideoWithoutClosingPanel] Stopping fullscreen video playback'); // 타이머 정리 if (startVideoFocusTimer) { clearTimeout(startVideoFocusTimer); startVideoFocusTimer = null; } // 패널은 유지하되, 비디오 중지 상태로 업데이트 dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { ...fullscreenPlayerPanel.panelInfo, shouldStop: true, // 비디오 중지 플래그 isPaused: true, isHidden: true, }, }) ); // Redux 상태도 중지로 업데이트 dispatch(setVideoStopped()); } }; /** * 모든 비디오(모달+전체화면)를 패널 닫지 않고 중지합니다. */ export const stopAllVideosWithoutClosingPanel = () => (dispatch, getState) => { const panels = getState().panels.panels; // 모든 PlayerPanel 찾기 const playerPanels = panels.filter((panel) => panel.name === panel_names.PLAYER_PANEL); if (playerPanels.length > 0) { dlog('[stopAllVideosWithoutClosingPanel] Stopping all video playback'); // 타이머 정리 if (startVideoFocusTimer) { clearTimeout(startVideoFocusTimer); startVideoFocusTimer = null; } // 모든 PlayerPanel을 중지 상태로 업데이트 playerPanels.forEach((playerPanel) => { dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { ...playerPanel.panelInfo, shouldStop: true, isPaused: true, isHidden: true, }, }) ); }); // Redux 상태도 중지로 업데이트 dispatch(setVideoStopped()); } }; // 채팅 로그 가져오기 IF-LGSP-371 export const getChatLog = ({ patnrId, showId }) => (dispatch, getState) => { const onSuccess = (response) => { dlog('getChatLog onSuccess', response.data); dispatch({ type: types.GET_CHAT_LOG, payload: response.data.data, }); }; const onFail = (error) => { derror('getChatLog onFail', error); }; TAxios(dispatch, getState, 'get', URLS.CHAT_LOG, { patnrId, showId }, {}, onSuccess, onFail); }; // VOD 자막 가져오기 IF-LGSP-072 export const getSubTitle = ({ showSubtitleUrl }) => (dispatch, getState) => { const onSuccess = (response) => { dlog('getSubTitle onSuccess', response.data); dispatch({ type: types.GET_SUBTITLE, payload: { url: showSubtitleUrl, data: response.data.data }, }); }; const onFail = (error) => { derror('getSubTitle onFail', error); dispatch({ type: types.GET_SUBTITLE, payload: { url: showSubtitleUrl, data: 'Error' }, }); }; if (!getState().play.subTitleBlobs[showSubtitleUrl]) { TAxios(dispatch, getState, 'get', URLS.SUBTITLE, { showSubtitleUrl }, {}, onSuccess, onFail); } else { dlog("playActions getSubTitle no Nothing it's exist", showSubtitleUrl); } }; export const CLEAR_PLAYER_INFO = () => ({ type: types.CLEAR_PLAYER_INFO, }); // 특정 자막 Blob URL을 해제하는 액션 생성자 export const clearSubtitleBlob = (subtitleUrl) => (dispatch, getState) => { const currentBlobs = getState().play.subTitleBlobs; const blobUrl = currentBlobs[subtitleUrl]; // Blob URL 해제 if (blobUrl && blobUrl.startsWith('blob:')) { try { URL.revokeObjectURL(blobUrl); dlog('[clearSubtitleBlob] Revoked Blob URL:', subtitleUrl); } catch (error) { derror('[clearSubtitleBlob] Failed to revoke Blob URL:', error); } } // Redux 상태에서 제거 dispatch({ type: types.CLEAR_SUBTITLE_BLOB, payload: { subtitleUrl } }); }; /** * 비디오 재생 상태를 Redux에 업데이트합니다. * @param {Object} playState - 업데이트할 재생 상태 * @param {boolean} playState.isPlaying - 재생 중인지 여부 * @param {boolean} playState.isPaused - 일시정지 상태인지 여부 * @param {number} playState.currentTime - 현재 재생 시간(초) * @param {number} playState.duration - 전체 비디오 길이(초) * @param {number} playState.playbackRate - 재생 속도 */ 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) { dlog('🔄 [PlayerPanel] 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, }); }; /* 🔽 [추가] 새로운 '플레이 제어 매니저' 액션들 */ /** * 비디오 재생 제어권을 요청합니다. * 컴포넌트는 이 액션을 통해 중앙 매니저에게 재생을 '요청'합니다. * @param {string} ownerId - 제어권을 요청하는 컴포넌트의 고유 ID (예: 'banner0_persistent') * @param {object} videoInfo - 재생할 비디오 정보 (url, id 등) */ export const requestPlayControl = (ownerId, videoInfo) => (dispatch, getState) => { const { playerControl } = getState().home; const currentOwnerId = playerControl.ownerId; if (currentOwnerId === ownerId) return; // 이미 제어권 소유 if (currentOwnerId) { // 현재 제어중인 컴포넌트가 영구재생 배너이면 '일시정지' if (currentOwnerId === 'banner0_persistent') { dispatch(pausePlayerControl()); } else { // 다른 미리보기라면 완전히 숨김 dispatch(releasePlayControl(currentOwnerId, true)); } } // 1. 매니저 상태 업데이트 dispatch({ type: types.SET_PLAYER_CONTROL, payload: { ownerId } }); // 2. 공유 PlayerPanel의 상태 업데이트 dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { isHidden: false, modal: true, ...videoInfo, }, }) ); }; /** * 비디오 재생 제어권을 해제하고, 필요시 영구재생 비디오를 복원합니다. * @param {string} ownerId - 제어권을 해제하는 컴포넌트의 고유 ID * @param {boolean} fromPreemption - 다른 요청에 의해 강제로 중단되었는지 여부 */ export const releasePlayControl = (ownerId, fromPreemption = false) => (dispatch, getState) => { const { playerControl } = getState().home; if (fromPreemption || playerControl.ownerId === ownerId) { // 1. 공유 PlayerPanel을 다시 숨김 dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { isHidden: true, }, }) ); // 2. 매니저 상태 업데이트 (현재 소유주 없음) dispatch({ type: types.CLEAR_PLAYER_CONTROL }); // 3. 만약 '일시정지'된 영구재생 비디오가 있었다면, 제어권을 되돌려주고 다시 재생 if (playerControl.isPaused && playerControl.ownerId === 'banner0_persistent') { const persistentVideoInfo = { /* 영구 비디오 정보를 가져오는 로직 (필요시) */ }; dispatch(requestPlayControl('banner0_persistent', persistentVideoInfo)); } } }; /** * 현재 재생 중인 비디오를 '일시정지' 상태로 변경하는 액션. * 이 함수는 플레이어 패널을 닫지 않고, 단순히 비디오 재생을 멈추는 신호를 보냅니다. * * @param {string} ownerId - 비디오 제어권을 가진 컴포넌트의 고유 ID. */ // export const pausePlayerControl = (ownerId) => (dispatch, getState) => { // const { playerControl } = getState().home; // // 제어권을 가진 컴포넌트가 자신일 경우에만 일시정지 // if (playerControl.ownerId === ownerId) { // dispatch({ // type: types.PAUSE_PLAYER_CONTROL, // }); // } // }; /** * '일시정지' 상태의 비디오를 다시 재생하는 액션. * * @param {string} ownerId - 비디오 제어권을 가진 컴포넌트의 고유 ID. */ export const resumePlayerControl = (ownerId) => (dispatch, getState) => { const { playerControl } = getState().home; // 제어권을 가진 컴포넌트가 자신이고, 일시정지 상태일 때만 재개 if (playerControl.ownerId === ownerId && playerControl.isPaused) { dispatch({ type: types.RESUME_PLAYER_CONTROL, }); } }; /** * 공유 PlayerPanel을 전체화면 모드로 전환합니다. * 이 액션은 어떤 배너에서든 클릭 시 호출됩니다. */ export const goToFullScreen = () => (dispatch, getState) => { console.log('[Detail-BG] 🎬 goToFullScreen - Setting PlayerPanel to fullscreen mode', { targetModal: false, action: 'updatePanel', timestamp: Date.now(), }); // 공유 PlayerPanel의 'modal' 상태를 false로 변경하여 전체화면으로 전환 dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { modal: false, isHidden: false, // 혹시 숨겨져 있었다면 보이도록 }, }) ); console.log('[Detail-BG] ✅ goToFullScreen - PlayerPanel modal set to false (fullscreen)', { timestamp: Date.now(), }); }; /** * 영구재생 비디오를 일시정지 상태로 만듭니다. (내부 사용) */ export const pausePlayerControl = () => ({ type: types.PAUSE_PLAYER_CONTROL, }); /** * 전체화면 플레이어에서 미리보기 상태로 복귀할 때 호출됩니다. * 중앙 'playerControl' 상태를 확인하여 올바른 위치와 비디오로 복원합니다. */ export const returnToPreview = () => (dispatch, getState) => { const { playerControl } = getState().home; let targetOwnerId; let targetVideoInfo; // 만약 '일시정지'된 영구재생 비디오가 있다면, 무조건 그 비디오로 복귀하는 것이 최우선 if (playerControl.isPaused) { targetOwnerId = 'banner0_persistent'; // targetVideoInfo = ... (0번 배너의 비디오 정보를 가져오는 로직) } else { // 그렇지 않다면, 전체화면으로 가기 직전의 소유주(ownerId)에게로 복귀 targetOwnerId = playerControl.ownerId; // targetVideoInfo = ... (해당 ownerId의 비디오 정보를 가져오는 로직) } // 매니저에게 해당 타겟으로 재생을 다시 요청 if (targetOwnerId) { dispatch(requestPlayControl(targetOwnerId, targetVideoInfo)); } else { // 돌아갈 곳이 없으면 그냥 플레이어를 닫음 dispatch(finishVideoPreview()); } }; /* 🔽 [251116] 새로운 비디오 상태 관리 시스템 - 액션 함수들 */ /** * 비디오 로딩 시작 상태로 설정 * @param {string} videoId - 비디오 ID * @param {string} displayMode - 화면 모드 ('visible', 'fullscreen', 'minimized') */ export const setPlaybackLoading = (videoId, displayMode = DISPLAY_STATUS.VISIBLE) => ({ type: types.SET_VIDEO_LOADING, payload: { playback: PLAYBACK_STATUS.LOADING, display: displayMode, videoId, loadingProgress: 0, loadingError: null, lastUpdate: Date.now(), }, }); /** * 비디오 로딩 성공 상태로 설정 * @param {string} videoId - 비디오 ID * @param {string} displayMode - 화면 모드 */ export const setPlaybackSuccess = (videoId, displayMode = DISPLAY_STATUS.VISIBLE) => ({ type: types.SET_PLAYBACK_SUCCESS, payload: { playback: PLAYBACK_STATUS.LOAD_SUCCESS, display: displayMode, videoId, loadingProgress: 100, loadingError: null, lastUpdate: Date.now(), }, }); /** * 비디오 로딩 에러 상태로 설정 * @param {string} videoId - 비디오 ID * @param {object} error - 에러 정보 */ export const setPlaybackError = (videoId, error) => ({ type: types.SET_VIDEO_ERROR, payload: { playback: PLAYBACK_STATUS.LOAD_ERROR, display: DISPLAY_STATUS.VISIBLE, videoId, loadingProgress: 0, loadingError: error, lastUpdate: Date.now(), }, }); /** * 비디오 재생 상태로 설정 * @param {string} videoId - 비디오 ID * @param {string} displayMode - 화면 모드 */ export const setPlaybackPlaying = (videoId, displayMode = DISPLAY_STATUS.FULLSCREEN) => ({ type: types.SET_VIDEO_PLAYING, payload: { playback: PLAYBACK_STATUS.PLAYING, display: displayMode, videoId, loadingProgress: 100, loadingError: null, lastUpdate: Date.now(), }, }); /** * 비디오 정지 상태로 설정 */ export const setVideoStopped = () => ({ type: types.SET_VIDEO_STOPPED, payload: { playback: PLAYBACK_STATUS.NOT_PLAYING, display: DISPLAY_STATUS.HIDDEN, videoId: null, loadingProgress: 0, loadingError: null, lastUpdate: Date.now(), }, }); /** * 비디오 버퍼링 상태로 설정 * @param {string} videoId - 비디오 ID */ export const setPlaybackBuffering = (videoId) => ({ type: types.SET_PLAYBACK_BUFFERING, payload: { playback: PLAYBACK_STATUS.BUFFERING, videoId, lastUpdate: Date.now(), }, }); /** * 최소화된 상태로 비디오 재생 * @param {string} videoId - 비디오 ID */ export const setVideoMinimizedPlaying = (videoId) => ({ type: types.SET_VIDEO_MINIMIZED_PLAYING, payload: { playback: PLAYBACK_STATUS.PLAYING, display: DISPLAY_STATUS.MINIMIZED, videoId, loadingProgress: 100, loadingError: null, lastUpdate: Date.now(), }, }); /** * 화면 상태만 변경하는 액션들 */ export const setDisplayHidden = () => ({ type: types.SET_DISPLAY_HIDDEN, payload: { display: DISPLAY_STATUS.HIDDEN, lastUpdate: Date.now(), }, }); export const setDisplayVisible = () => ({ type: types.SET_DISPLAY_VISIBLE, payload: { display: DISPLAY_STATUS.VISIBLE, lastUpdate: Date.now(), }, }); export const setDisplayMinimized = () => ({ type: types.SET_DISPLAY_MINIMIZED, payload: { display: DISPLAY_STATUS.MINIMIZED, lastUpdate: Date.now(), }, }); export const setDisplayFullscreen = () => ({ type: types.SET_DISPLAY_FULLSCREEN, payload: { display: DISPLAY_STATUS.FULLSCREEN, lastUpdate: Date.now(), }, }); /** * 배너 비디오를 시작합니다. * @param {Object} videoInfo - 비디오 정보 * @param {string} videoInfo.bannerId - 배너 ID * @param {string} videoInfo.videoId - 비디오 ID * @param {string} videoInfo.showUrl - 비디오 URL * @param {boolean} videoInfo.modal - 모달 여부 * @param {string} videoInfo.modalContainerId - 모달 컨테이너 ID * @param {string} videoInfo.modalClassName - 모달 클래스 이름 */ export const startBannerVideo = (videoInfo) => (dispatch, getState) => { dlog('[startBannerVideo] ✅ START - videoInfo:', videoInfo); const { bannerId, videoId, showUrl, modal = true, modalContainerId, modalClassName, ...rest } = videoInfo; console.log('[Detail-BG] 🎥 startBannerVideo - Starting banner video', { modalStatus: modal, bannerId, displayMode: modal ? 'VISIBLE (modal=true)' : 'FULLSCREEN (modal=false)', videoId, timestamp: Date.now(), }); // 비디오 식별자 생성 const videoIdentifier = videoId || showUrl || bannerId; if (videoIdentifier) { const displayMode = modal ? DISPLAY_STATUS.VISIBLE : DISPLAY_STATUS.FULLSCREEN; dlog( '[startBannerVideo] 📌 Setting playback loading - identifier:', videoIdentifier, ', displayMode:', displayMode ); dispatch(setPlaybackLoading(videoIdentifier, displayMode)); } const panels = getState().panels.panels; const existingPlayerPanel = panels.find((p) => p.name === panel_names.PLAYER_PANEL); // 기존 PlayerPanel이 있으면 초기화 if (existingPlayerPanel) { dlog('[startBannerVideo] 🔄 Resetting existing PLAYER_PANEL before start'); console.log('[Detail-BG] 🔄 startBannerVideo - Clearing existing PlayerPanel', { existingModalStatus: existingPlayerPanel.panelInfo?.modal, timestamp: Date.now(), }); clearAllVideoTimers(); dispatch(popPanel(panel_names.PLAYER_PANEL)); } // 새로운 PlayerPanel push console.log('[Detail-BG] ➕ startBannerVideo - Pushing new PlayerPanel with modal status', { modal, modalContainerId, timestamp: Date.now(), }); dispatch( pushPanel( { name: panel_names.PLAYER_PANEL, panelInfo: { modal, modalContainerId, modalClassName, playerState: { currentBannerId: bannerId, }, videoId, showUrl, bannerId, ...rest, }, }, true ) ); console.log('[Detail-BG] ✅ startBannerVideo - PlayerPanel pushed with modal=' + modal, { timestamp: Date.now(), }); dlog('[startBannerVideo] ✨ Panel action dispatched'); }; /** * 비디오를 중지하고 화면에서 숨깁니다. * 패널을 닫지 않고 비디오 재생만 중지합니다. */ export const stopAndHideVideo = () => (dispatch, getState) => { const panels = getState().panels.panels; // 모든 PlayerPanel 찾기 const playerPanels = panels.filter((panel) => panel.name === panel_names.PLAYER_PANEL); if (playerPanels.length > 0) { dlog('[stopAndHideVideo] Stopping all video playback and hiding'); // 타이머 정리 if (startVideoFocusTimer) { clearTimeout(startVideoFocusTimer); startVideoFocusTimer = null; } // 모든 PlayerPanel을 중지 및 숨김 상태로 업데이트 playerPanels.forEach((playerPanel) => { dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { ...playerPanel.panelInfo, shouldStop: true, isPaused: true, isHidden: true, }, }) ); }); // Redux 상태도 중지로 업데이트 dispatch(setVideoStopped()); } };