import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react'; import classNames from 'classnames'; import { useDispatch } from 'react-redux'; import { Job } from '@enact/core/util'; import Spotlight from '@enact/spotlight'; import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; import { setContainerLastFocusedElement } from '@enact/spotlight/src/container'; import dummyVtt from '../../../assets/mock/video.vtt'; import { createDebugHelpers } from '../../utils/debug'; import { changeAppStatus, changeLocalSettings, requestLiveSubtitle, sendBroadCast, setHidePopup, } from '../../actions/commonActions'; import { sendLogGNB, sendLogLive, sendLogTotalRecommend, sendLogVOD, } from '../../actions/logActions'; import { clearShopNowInfo, getHomeFullVideoInfo, getMainCategoryShowDetail, getMainLiveShow, getMainLiveShowNowProduct, } from '../../actions/mainActions'; import * as PanelActions from '../../actions/panelActions'; import { updatePanel } from '../../actions/panelActions'; import { CLEAR_PLAYER_INFO, clearSubtitleBlob, getChatLog, getSubTitle, startVideoPlayer, pauseModalVideo, resumeModalVideo, resumeFullscreenVideo, updateVideoPlayState, } from '../../actions/playActions'; import { resetPlayerOverlays } from '../../actions/videoPlayActions'; import { convertUtcToLocal } from '../../components/MediaPlayer/util'; import TPanel from '../../components/TPanel/TPanel'; import TPopUp from '../../components/TPopUp/TPopUp'; import Media from '../../components/VideoPlayer/Media'; import TReactPlayer from '../../components/VideoPlayer/TReactPlayer'; import { VideoPlayer } from '../../components/VideoPlayer/VideoPlayer'; import useWhyDidYouUpdate from '../../hooks/useWhyDidYouUpdate'; import * as Config from '../../utils/Config'; import { ACTIVE_POPUP, panel_names } from '../../utils/Config'; import { $L, formatGMTString } from '../../utils/helperMethods'; import { SpotlightIds } from '../../utils/SpotlightIds'; import { removeDotAndColon } from './PlayerItemCard/PlayerItemCard'; import PlayerOverlayChat from './PlayerOverlay/PlayerOverlayChat'; import PlayerOverlayQRCode from './PlayerOverlay/PlayerOverlayQRCode'; import css from './PlayerPanel.module.less'; import PlayerTabButton from './PlayerTabContents/TabButton/PlayerTabButton'; import TabContainer from './PlayerTabContents/TabContainer'; import TabContainerV2 from './PlayerTabContents/v2/TabContainer.v2'; // import LiveShowContainer from './PlayerTabContents/v2/LiveShowContainer'; // import ShopNowContainer from './PlayerTabContents/v2/ShopNowContainer'; // import ShopNowButton from './PlayerTabContents/v2/ShopNowButton'; const Container = SpotlightContainerDecorator({ enterTo: 'last-focused', preserveId: true }, 'div'); // 디버그 헬퍼 설정 const DEBUG_MODE = false; const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE); const findSelector = (selector, maxAttempts = 5, currentAttempts = 0) => { try { if (currentAttempts >= maxAttempts) { throw new Error('selector not found'); } const initialSelector = document.querySelector(selector); if (initialSelector) { return initialSelector; } else { return findSelector(selector, maxAttempts, currentAttempts + 1); } } catch (error) { // console.error(error.message); } }; const getLogTpNo = (type, nowMenu) => { if (type === 'LIVE') { switch (nowMenu) { case Config.LOG_MENU.HOME_TOP: return Config.LOG_TP_NO.LIVE.HOME; // case Config.LOG_MENU.FEATURED_BRANDS_LIVE_CHANNELS: // return Config.LOG_TP_NO.LIVE.FEATURED_BRANDS; case Config.LOG_MENU.FULL_SHOP_NOW: case Config.LOG_MENU.FULL_YOU_MAY_LIKE: case Config.LOG_MENU.FULL_LIVE_CHANNELS: return Config.LOG_TP_NO.LIVE.FULL; default: return Config.LOG_TP_NO.LIVE.FEATURED_BRANDS; } } else if (type === 'VOD') { switch (nowMenu) { case Config.LOG_MENU.HOME_TOP: return Config.LOG_TP_NO.VOD.HOME_VOD; // 153 case Config.LOG_MENU.TRENDING_NOW_POPULAR_SHOWS: return Config.LOG_TP_NO.VOD.POPULAR_SHOWS_AND_HOT_PICKS; // 151 case Config.LOG_MENU.FULL_SHOP_NOW: case Config.LOG_MENU.FULL_YOU_MAY_LIKE: case Config.LOG_MENU.FULL_FEATURED_SHOWS: return Config.LOG_TP_NO.VOD.FULL_VOD; // 150 default: return; } } else if (type === 'MEDIA') { switch (nowMenu) { case Config.LOG_MENU.HOME_TOP: return Config.LOG_TP_NO.VOD.HOME_VOD; // 153 case Config.LOG_MENU.HOT_PICKS: return Config.LOG_TP_NO.VOD.POPULAR_SHOWS_AND_HOT_PICKS; // 151 case Config.LOG_MENU.DETAIL_PAGE_BILLING_PRODUCT_DETAIL: case Config.LOG_MENU.DETAIL_PAGE_PRODUCT_DETAIL: case Config.LOG_MENU.DETAIL_PAGE_GROUP_DETAIL: case Config.LOG_MENU.DETAIL_PAGE_THEME_DETAIL: case Config.LOG_MENU.DETAIL_PAGE_TRAVEL_THEME_DETAIL: return Config.LOG_TP_NO.VOD.ITEM_DETAIL_MEDIA; // 156 case Config.LOG_MENU.FULL: return Config.LOG_TP_NO.VOD.FULL_MEDIA; // 155 default: return; } } }; const YOUTUBECONFIG = { playerVars: { controls: 0, // 플레이어 컨트롤 표시 autoplay: 1, disablekb: 1, enablejsapi: 1, listType: 'user_uploads', fs: 0, rel: 0, // 관련 동영상 표시 안 함 showinfo: 0, loop: 0, iv_load_policy: 3, modestbranding: 1, wmode: 'opaque', cc_lang_pref: 'en', cc_load_policy: 0, playsinline: 1, }, }; const INITIAL_TIMEOUT = 30000; const REGULAR_TIMEOUT = 30000; const TAB_CONTAINER_SPOTLIGHT_ID = 'tab-container-spotlight-id'; const TAB_CONTAINER_V2_SPOTLIGHT_ID = 'tab-container-v2-spotlight-id'; const TARGET_EVENTS = ['mousemove', 'keydown', 'click']; // last time error const VIDEO_END_ACTION_DELAY = 1500; const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props }) => { const dispatch = useDispatch(); const { USE_STATE, USE_SELECTOR } = useWhyDidYouUpdate(spotlightId, { isTabActivated, panelInfo, isOnTop, ...props, }); const videoPlayer = useRef(null); const focusReturnRef = useRef(null); const modalPrevRef = useRef(panelInfo?.modal); const prevIsTopPanelDetailFromPlayerRef = useRef(false); const [playListInfo, setPlayListInfo] = USE_STATE('playListInfo', ''); const [shopNowInfo, setShopNowInfo] = USE_STATE('shopNowInfo'); const [backupInitialIndex, setBackupInitialIndex] = USE_STATE('backupInitialIndex', 0); const [modalStyle, setModalStyle] = USE_STATE('modalStyle', {}); const [modalScale, setModalScale] = USE_STATE('modalScale', 1); const [mediaId, setMediaId] = USE_STATE('mediaId', null); const [currentLiveTimeSeconds, setCurrentLiveTimeSeconds] = USE_STATE( 'currentLiveTimeSeconds', 1 ); const [prevChannelIndex, setPrevChannelIndex] = USE_STATE('prevChannelIndex', 0); const [sideContentsVisible, setSideContentsVisible] = USE_STATE('sideContentsVisible', true); const [belowContentsVisible, setBelowContentsVisible] = USE_STATE('belowContentsVisible', true); const [currentTime, setCurrentTime] = USE_STATE('currentTime', 0); const [isInitialFocusOccurred, setIsInitialFocusOccurred] = USE_STATE( 'isInitialFocusOccurred', false ); const [selectedIndex, setSelectedIndex] = USE_STATE( 'selectedIndex', panelInfo.shptmBanrTpNm === 'LIVE' ? null : 0 ); const [isUpdate, setIsUpdate] = USE_STATE('isUpdate', false); const [isSubtitleActive, setIsSubtitleActive] = USE_STATE('isSubtitleActive', true); const [logStatus, setLogStatus] = USE_STATE('logStatus', { isModalLiveLogReady: false, isFullLiveLogReady: false, isDetailLiveLogReady: false, isModalVodLogReady: false, isFullVodLogReady: false, isDetailVodLogReady: false, isModalMediaLogReady: false, isFullMediaLogReady: false, isDetailMediaReady: false, }); const [isVODPaused, setIsVODPaused] = USE_STATE('isVODPaused', false); const [tabIndexV2, setTabIndexV2] = USE_STATE('tabIndexV2', 1); // 0: ShopNow, 1: LiveChannel, 2: ShopNowButton const [tabContainerVersion, setTabContainerVersion] = USE_STATE('tabContainerVersion', 2); // 1: TabContainer (우측), 2: TabContainerV2 (하단) const [isModalClosed, setIsModalClosed] = USE_STATE('isModalClosed', true); // 모달이 false 상태인지 나타내는 플래그 const panels = USE_SELECTOR('panels', (state) => state.panels.panels); const chatData = USE_SELECTOR('chatData', (state) => state.play.chatData); const shouldHideOverlays = USE_SELECTOR( 'shouldHideOverlays', (state) => state.videoPlay.shouldHideOverlays ); const shouldShowOverlays = USE_SELECTOR( 'shouldShowOverlays', (state) => state.videoPlay.shouldShowOverlays ); const popupVisible = USE_SELECTOR('popupVisible', (state) => state.common.popup.popupVisible); const activePopup = USE_SELECTOR('activePopup', (state) => state.common.popup.activePopup); const showDetailInfo = USE_SELECTOR('showDetailInfo', (state) => state.main.showDetailInfo); const productImageLength = USE_SELECTOR( 'productImageLength', (state) => state.product.productImageLength ); const themeProductInfos = USE_SELECTOR( 'themeProductInfos', (state) => state.home.themeCurationDetailInfoData ); const hotelInfos = USE_SELECTOR('hotelInfos', (state) => state.home.themeCurationHotelDetailData); const captionEnable = USE_SELECTOR( 'captionEnable', (state) => state.common.appStatus.captionEnable ); const fullVideolgCatCd = USE_SELECTOR('fullVideolgCatCd', (state) => state.main.fullVideolgCatCd); const featuredShowsInfos = USE_SELECTOR( 'featuredShowsInfos', (state) => state.main.featuredShowsInfos ); const localRecentItems = USE_SELECTOR( 'localRecentItems', (state) => state.localSettings?.recentItems ); const httpHeader = USE_SELECTOR('httpHeader', (state) => state.common?.httpHeader); const countryCode = USE_SELECTOR('countryCode', (state) => state.common.httpHeader?.cntry_cd); const liveChannelInfos = USE_SELECTOR('liveChannelInfos', (state) => state.main.liveChannelInfos); const showNowInfos = USE_SELECTOR('showNowInfos', (state) => state.main.showNowInfo); const liveShowInfos = USE_SELECTOR('liveShowInfos', (state) => state.main.liveShowInfos); const vodSubtitleData = USE_SELECTOR('vodSubtitleData', (state) => state.play.subTitleBlobs); const previousSubtitleBlobRef = useRef(null); const previousSubtitleUrlRef = useRef(null); const broadcast = USE_SELECTOR('broadcast', (state) => state.common.broadcast); const videoPlayState = USE_SELECTOR('videoPlayState', (state) => state.play.videoPlayState); const lastPanelAction = USE_SELECTOR('lastPanelAction', (state) => state.panels.lastPanelAction); const nowMenu = USE_SELECTOR('nowMenu', (state) => state.common.menu.nowMenu); const nowMenuRef = useRef(null); const entryMenu = USE_SELECTOR('entryMenu', (state) => state.common.menu.entryMenu); const [videoLoaded, setVideoLoaded] = USE_STATE('videoLoaded', false); const entryMenuRef = useRef(null); const panelInfoRef = useRef(null); const initialFocusTimeoutJob = useRef(new Job((func) => func(), 100)); const liveLogParamsRef = useRef(null); const vodLogParamsRef = useRef(null); const mediaLogParamsRef = useRef(null); const prevNowMenuRef = useRef(null); const watchIntervalLive = useRef(null); const watchIntervalVod = useRef(null); const watchIntervalMedia = useRef(null); const timeoutRef = useRef(null); // useEffect(() => { // console.log("###videoLoaded", videoLoaded); // if (nowMenu) { // } // }, [videoLoaded]); const currentLiveShowInfo = useMemo(() => { if (liveShowInfos && liveShowInfos.length > 0) { const panelInfoChanId = panelInfo?.chanId; const isLive = panelInfo?.shptmBanrTpNm === 'LIVE'; const isModal = panelInfo?.modal; if (isLive) { // live full 화면에서 modal 전환시 로그 전송 추가 if (isModal) { dispatch(sendLogGNB(Config.LOG_MENU.FULL)); prevNowMenuRef.current = nowMenuRef.current; return () => dispatch(sendLogGNB(prevNowMenuRef.current)); } const liveShowInfo = liveShowInfos // .find(({ chanId }) => panelInfoChanId === chanId); return liveShowInfo; } return {}; } return {}; }, [liveShowInfos, panelInfo?.chanId, panelInfo?.shptmBanrTpNm, panelInfo?.modal]); const currentVODShowInfo = useMemo(() => { if (showDetailInfo && showDetailInfo.length > 0) { const isVOD = panelInfo?.shptmBanrTpNm === 'VOD'; if (isVOD) { const vodShowInfo = showDetailInfo[0]; return vodShowInfo; } return {}; } return {}; }, [panelInfo?.shptmBanrTpNm, showDetailInfo]); useEffect(() => { if (!panelInfo?.modal && panelInfo?.shptmBanrTpNm === 'MEDIA') { dispatch(sendLogGNB(Config.LOG_MENU.FULL)); prevNowMenuRef.current = nowMenuRef.current; return () => dispatch(sendLogGNB(prevNowMenuRef.current)); } else if (panelInfo?.modal && panelInfo?.shptmBanrTpNm !== 'LIVE') { dispatch(sendLogGNB(entryMenu)); } }, [panelInfo?.modal, panelInfo?.shptmBanrTpNm]); // useEffect(()=>{ // console.log('[PlayerPanel] isOnTop:', { // isOnTop, // panelInfo // }); // if (panelInfo && panelInfo.modal) { // if (!isOnTop) { // console.log('[PlayerPanel] Not on top - pausing video'); // dispatch(pauseModalVideo()); // } else if (isOnTop && panelInfo.isPaused) { // console.log('[PlayerPanel] Back on top - resuming video'); // dispatch(resumeModalVideo()); // } // } // },[isOnTop, panelInfo]) // useRef들 업데이트 // prevIsTopPanelDetailFromPlayerRef 업데이트 useEffect(() => { prevIsTopPanelDetailFromPlayerRef.current = isTopPanelDetailFromPlayer; }, [isTopPanelDetailFromPlayer]); // nowMenuRef 업데이트 useEffect(() => { nowMenuRef.current = nowMenu; }, [nowMenu]); // entryMenuRef 업데이트 useEffect(() => { entryMenuRef.current = entryMenu; }, [entryMenu]); // panelInfoRef 업데이트 useEffect(() => { panelInfoRef.current = panelInfo; }, [panelInfo]); // PlayerPanel.jsx의 라인 313-327 useEffect 수정 - detailPanelClosed flag 감지 추가 useEffect(() => { dlog('[PlayerPanel] 🔍 isOnTop useEffect 호출:', { isOnTop, modal: panelInfo?.modal, isPaused: panelInfo?.isPaused, detailPanelClosed: panelInfo?.detailPanelClosed, }); if (isOnTop) { // 1. Resume Video if needed (isPaused or detailPanelClosed) if (panelInfo.isPaused || panelInfo.detailPanelClosed) { if (panelInfo.modal) { dlog('[PlayerPanel] ▶️ Back on top (Modal) - resuming video'); dispatch(resumeModalVideo()); } else { dlog('[PlayerPanel] ▶️ Back on top (Fullscreen) - resuming video'); dispatch(resumeFullscreenVideo()); } } // 2. Reset detailPanelClosed flag if (panelInfo.detailPanelClosed) { dlog('[PlayerPanel] 🔄 detailPanelClosed flag 초기화 시작'); dlog('[PlayerPanel] 🔙 DetailPanel에서 복귀 정보:', { detailPanelClosedAt: panelInfo.detailPanelClosedAt, detailPanelClosedFromSource: panelInfo.detailPanelClosedFromSource, lastFocusedTargetId: panelInfo.lastFocusedTargetId, }); // 포커스 복원 로직 추가 (1000ms 지연) if (panelInfo.lastFocusedTargetId) { dlog( '[PlayerPanel] 🎯 DetailPanel 복귀 후 1000ms 지연 포커스 복원 예약:', panelInfo.lastFocusedTargetId ); const focusTimeoutId = setTimeout(() => { dlog( '[PlayerPanel] 🔍 DetailPanel 복귀 후 포커스 복원 실행:', panelInfo.lastFocusedTargetId ); Spotlight.focus(panelInfo.lastFocusedTargetId); }, 1000); // cleanup 함수를 통해 컴포넌트 unmount 시 타이머 정리 return () => { if (focusTimeoutId) { clearTimeout(focusTimeoutId); dlog('[PlayerPanel] 🧹 포커스 복원 타이머 정리됨'); } }; } dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { detailPanelClosed: false, detailPanelClosedAt: undefined, detailPanelClosedFromSource: undefined, lastFocusedTargetId: undefined, // 포커스 복원 후 초기화 }, }) ); dlog('[PlayerPanel] 🏁 detailPanelClosed flag 초기화 완료'); } } else { // Not on top if (panelInfo && panelInfo.modal) { dlog('[PlayerPanel] ⏸️ Not on top (Modal) - pausing video logic commented out'); // dispatch(pauseModalVideo()); } } }, [isOnTop, panelInfo]); // 이전 상태 저장을 위한 ref const previousVideoPlayState = useRef(null); const previousPanelInfo = useRef(null); const previousVideoSource = useRef(null); const previousIsOnTop = useRef(null); // Redux 상태 변화 모니터링 useEffect (중요 변화만) useEffect(() => { // 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)) ) { dlog('📊 [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) { // 상태 변경 시에만 디버깅 로그 출력 dlog('🔍 [PlayerPanel] PanelInfo isPaused changed', { previousIsPaused: previousPanelInfo.current?.isPaused, currentIsPaused: panelInfo.isPaused, isOnTop, videoPlayerExists: !!videoPlayer.current, currentPlayingUrl, timestamp: new Date().toISOString(), }); dlog('🎮 [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) { dlog('🔴 [PlayerPanel] Calling VideoPlayer.pause() due to PanelInfo change'); videoPlayer.current.pause(); } else if (panelInfo.isPaused === false) { dlog('🟢 [PlayerPanel] Calling VideoPlayer.play() due to PanelInfo change'); videoPlayer.current.play(); } } } 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'; dlog('🎬 [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) { dlog('📊 [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(() => { const currentModalState = panelInfo?.modal; const prevModalState = modalPrevRef.current; dlog('[PlayerPanel] 🔍 Modal 상태 체크:', { prevModalState, currentModalState, shouldExecuteTrueToFalse: prevModalState === true && currentModalState === false, shouldExecuteFalseToTrue: prevModalState === false && currentModalState === true, }); // true → false 변화 감지 if (prevModalState === true && currentModalState === false) { dlog('[PlayerPanel] ▶️ Modal 상태 변화: true → false (전체화면 모드로 복귀)'); dlog( '[PlayerPanel] 🎯 포커스 복원 준비 - lastFocusedTargetId:', panelInfo?.lastFocusedTargetId ); setIsModalClosed(true); // 모달이 닫혔음을 표시 // DetailPanel에서 복귀 시 포커스 복원 const lastFocusedTargetId = panelInfo?.lastFocusedTargetId; if (lastFocusedTargetId) { // ShopNowContents가 렌더링될 때까지 대기 후 포커스 복원 timeoutRef.current = setTimeout(() => { dlog('[PlayerPanel] 🔍 800ms 후 포커스 복원 시도:', lastFocusedTargetId); Spotlight.focus(lastFocusedTargetId); }, 800); } } // false → true 변화 감지 (모달이 열림) if (prevModalState === false && currentModalState === true) { dlog('[PlayerPanel] 📱 Modal 상태 변화: false → true (모달 열림)'); setIsModalClosed(false); // 모달이 열렸음을 표시 } modalPrevRef.current = currentModalState; // 현재 modal 상태를 ref에 저장 }, [panelInfo?.modal, panelInfo?.lastFocusedTargetId]); // creating live log params useEffect(() => { if (currentLiveShowInfo && Object.keys(currentLiveShowInfo).length > 0) { if (currentLiveShowInfo.showId !== panelInfo?.showId) { dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { chanId: currentLiveShowInfo.chanId, modalContainerId: panelInfo?.modalContainerId, patnrId: currentLiveShowInfo.patnrId, showId: currentLiveShowInfo.showId, showUrl: currentLiveShowInfo.showUrl, }, }) ); return; } let logTemplateNo; if (isOnTop && panelInfo?.modal) { logTemplateNo = getLogTpNo('LIVE', nowMenu); setLogStatus((status) => ({ ...status, isModalLiveLogReady: true })); } // else if (isOnTop && !panelInfo?.modal) { logTemplateNo = Config.LOG_TP_NO.LIVE.FULL; setLogStatus((status) => ({ ...status, isFullLiveLogReady: true })); } // else if (!isOnTop && !panelInfo?.modal) { logTemplateNo = Config.LOG_TP_NO.LIVE.ITEM_DETAIL; setLogStatus((status) => ({ ...status, isDetailLiveLogReady: true })); } liveLogParamsRef.current = { entryMenu: entryMenuRef.current, lgCatCd: currentLiveShowInfo.catCd ?? '', lgCatNm: currentLiveShowInfo.catNm ?? '', linkTpCd: panelInfo?.linkTpCd ?? '', logTpNo: logTemplateNo, nowMenu: nowMenuRef.current, patncNm: currentLiveShowInfo.patncNm, patnrId: currentLiveShowInfo.patnrId, showId: currentLiveShowInfo.showId, showNm: currentLiveShowInfo.showNm, vdoTpNm: currentLiveShowInfo.vtctpYn ? currentLiveShowInfo.vtctpYn === 'Y' ? 'Vertical' : 'Horizontal' : '', }; } }, [ currentLiveShowInfo, isOnTop, nowMenu, panelInfo?.linkTpCd, panelInfo?.modal, panelInfo?.modalContainerId, panelInfo?.showId, setLogStatus, ]); // send live log useEffect(() => { if (broadcast && Object.keys(broadcast).length === 0) { // case: live, modal if ( logStatus.isModalLiveLogReady && isOnTop && panelInfo?.modal && liveLogParamsRef.current?.showId === panelInfo?.showId ) { dlog('[PlayerPanel] 📡 LIVE Modal Log Ready and Conditions Met:', { isModalLiveLogReady: logStatus.isModalLiveLogReady, isOnTop, isModal: panelInfo?.modal, showIdMatch: liveLogParamsRef.current?.showId === panelInfo?.showId, logParams: liveLogParamsRef.current, }); let watchStrtDt = formatGMTString(new Date()); watchIntervalLive.current = setInterval(() => { let watchEndDt = formatGMTString(new Date()); let watchRecord = { ...liveLogParamsRef.current, watchStrtDt, watchEndDt, }; dispatch(changeLocalSettings({ watchRecord })); }, 10000); return () => { setLogStatus((status) => ({ ...status, isModalLiveLogReady: false, })); clearInterval(watchIntervalLive.current); dlog('[PlayerPanel] 🚀 Dispatching LIVE Modal Log:', { logParams: liveLogParamsRef.current, watchStrtDt, }); dispatch( sendLogLive({ ...liveLogParamsRef.current, watchStrtDt }, () => dispatch(changeLocalSettings({ watchRecord: {} })) ) ); }; } // case: live, full if ( logStatus.isFullLiveLogReady && isOnTop && !panelInfo?.modal && liveLogParamsRef.current?.showId === panelInfo?.showId ) { let watchStrtDt = formatGMTString(new Date()); watchIntervalLive.current = setInterval(() => { let watchEndDt = formatGMTString(new Date()); let watchRecord = { ...liveLogParamsRef.current, watchStrtDt, watchEndDt, }; dispatch(changeLocalSettings({ watchRecord })); }, 10000); return () => { setLogStatus((status) => ({ ...status, isFullLiveLogReady: false, })); clearInterval(watchIntervalLive.current); dispatch( sendLogLive({ ...liveLogParamsRef.current, watchStrtDt }, () => dispatch(changeLocalSettings({ watchRecord: {} })) ) ); }; } // case: live, item detail if ( logStatus.isDetailLiveLogReady && !isOnTop && !panelInfo?.modal && liveLogParamsRef.current?.showId === panelInfo?.showId ) { let watchStrtDt = formatGMTString(new Date()); watchIntervalLive.current = setInterval(() => { let watchEndDt = formatGMTString(new Date()); let watchRecord = { ...liveLogParamsRef.current, watchStrtDt, watchEndDt, }; dispatch(changeLocalSettings({ watchRecord })); }, 10000); return () => { setLogStatus((status) => ({ ...status, isDetailLiveLogReady: false, })); clearInterval(watchIntervalLive.current); dispatch( sendLogLive({ ...liveLogParamsRef.current, watchStrtDt }, () => dispatch(changeLocalSettings({ watchRecord: {} })) ) ); }; } } }, [ broadcast, isOnTop, logStatus.isDetailLiveLogReady, logStatus.isFullLiveLogReady, logStatus.isModalLiveLogReady, panelInfo?.modal, panelInfo?.showId, setLogStatus, ]); // creating VOD log params useEffect(() => { if (currentVODShowInfo && Object.keys(currentVODShowInfo).length > 0 && !isVODPaused) { let logTemplateNo; if (isOnTop && panelInfo?.modal) { logTemplateNo = getLogTpNo('VOD', nowMenu); setLogStatus((status) => ({ ...status, isModalVodLogReady: true })); } // else if (isOnTop && !panelInfo?.modal) { logTemplateNo = Config.LOG_TP_NO.VOD.FULL_VOD; setLogStatus((status) => ({ ...status, isFullVodLogReady: true })); } // else if (!isOnTop && !panelInfo?.modal) { logTemplateNo = Config.LOG_TP_NO.VOD.ITEM_DETAIL_VOD; setLogStatus((status) => ({ ...status, isDetailVodLogReady: true })); } vodLogParamsRef.current = { entryMenu: entryMenuRef.current, lgCatCd: currentVODShowInfo?.showCatCd ?? '', lgCatNm: currentVODShowInfo?.showCatNm ?? '', logTpNo: logTemplateNo, linkTpCd: panelInfo?.linkTpCd ?? '', nowMenu: nowMenuRef.current, patncNm: currentVODShowInfo?.patncNm, patnrId: currentVODShowInfo?.patnrId, showId: currentVODShowInfo?.showId, showNm: currentVODShowInfo?.showNm, vdoTpNm: currentVODShowInfo?.vtctpYn ? currentVODShowInfo.vtctpYn === 'Y' ? 'Vertical' : 'Horizontal' : '', }; } }, [currentVODShowInfo, isOnTop, isVODPaused, nowMenu, panelInfo?.linkTpCd, panelInfo?.modal]); // send VOD log useEffect(() => { if (broadcast && Object.keys(broadcast).length === 0 && !isVODPaused) { // case: VOD, modal if ( logStatus.isModalVodLogReady && isOnTop && panelInfo?.modal && vodLogParamsRef.current?.showId === panelInfo?.showId ) { let watchStrtDt = formatGMTString(new Date()); watchIntervalVod.current = setInterval(() => { let watchEndDt = formatGMTString(new Date()); let watchRecord = { ...vodLogParamsRef.current, watchStrtDt, watchEndDt, }; dispatch(changeLocalSettings({ watchRecord })); }, 10000); return () => { setLogStatus((status) => ({ ...status, isModalVodLogReady: false, })); clearInterval(watchIntervalVod.current); dispatch( sendLogVOD({ ...vodLogParamsRef.current, watchStrtDt }, () => dispatch(changeLocalSettings({ watchRecord: {} })) ) ); }; } // case: VOD, full if ( logStatus.isFullVodLogReady && isOnTop && !panelInfo?.modal && vodLogParamsRef.current?.showId === panelInfo?.showId ) { let watchStrtDt = formatGMTString(new Date()); watchIntervalVod.current = setInterval(() => { let watchEndDt = formatGMTString(new Date()); let watchRecord = { ...vodLogParamsRef.current, watchStrtDt, watchEndDt, }; dispatch(changeLocalSettings({ watchRecord })); }, 10000); return () => { setLogStatus((status) => ({ ...status, isFullVodLogReady: false, })); clearInterval(watchIntervalVod.current); dispatch( sendLogVOD({ ...vodLogParamsRef.current, watchStrtDt }, () => dispatch(changeLocalSettings({ watchRecord: {} })) ) ); }; } // case: VOD, item detail if ( logStatus.isDetailVodLogReady && !isOnTop && !panelInfo?.modal && vodLogParamsRef.current?.showId === panelInfo?.showId ) { let watchStrtDt = formatGMTString(new Date()); watchIntervalVod.current = setInterval(() => { let watchEndDt = formatGMTString(new Date()); let watchRecord = { ...vodLogParamsRef.current, watchStrtDt, watchEndDt, }; dispatch(changeLocalSettings({ watchRecord })); }, 10000); return () => { setLogStatus((status) => ({ ...status, isDetailVodLogReady: false, })); clearInterval(watchIntervalVod.current); dispatch( sendLogVOD({ ...vodLogParamsRef.current, watchStrtDt }, () => dispatch(changeLocalSettings({ watchRecord: {} })) ) ); }; } } }, [ broadcast, isOnTop, isVODPaused, logStatus.isDetailVodLogReady, logStatus.isFullVodLogReady, logStatus.isModalVodLogReady, panelInfo?.modal, panelInfo?.showId, setLogStatus, ]); // creating media log params useEffect(() => { if (panelInfo?.shptmBanrTpNm === 'MEDIA' && isOnTop && !isVODPaused) { let logTemplateNo; if (panelInfo?.modal) { logTemplateNo = getLogTpNo('MEDIA', nowMenu); setLogStatus((status) => ({ ...status, isModalMediaLogReady: true })); } // else if (!panelInfo?.modal) { logTemplateNo = Config.LOG_TP_NO.VOD.FULL_MEDIA; setLogStatus((status) => ({ ...status, isFullMediaLogReady: true })); } mediaLogParamsRef.current = { entryMenu: entryMenuRef.current, lgCatCd: panelInfo?.lgCatCd ?? '', lgCatNm: panelInfo?.lgCatNm ?? '', logTpNo: logTemplateNo, linkTpCd: panelInfo?.linkTpCd ?? '', nowMenu: nowMenuRef.current, patncNm: panelInfo?.patncNm ?? '', patnrId: panelInfo?.patnrId ?? '', showId: panelInfo?.prdtId ?? panelInfo?.showId, showNm: panelInfo?.prdtNm ?? panelInfo?.showNm, vdoTpNm: 'Horizontal', }; } }, [ isOnTop, isVODPaused, nowMenu, panelInfo?.lgCatCd, panelInfo?.lgCatNm, panelInfo?.linkTpCd, panelInfo?.modal, panelInfo?.patncNm, panelInfo?.patnrId, panelInfo?.prdtId, panelInfo?.prdtNm, panelInfo?.showId, panelInfo?.showNm, panelInfo?.shptmBanrTpNm, setLogStatus, ]); // send log media useEffect(() => { if (broadcast && Object.keys(broadcast).length === 0 && !isVODPaused) { // case: media, modal if (logStatus.isModalMediaLogReady && panelInfo?.modal) { let watchStrtDt = formatGMTString(new Date()); watchIntervalMedia.current = setInterval(() => { let watchEndDt = formatGMTString(new Date()); let watchRecord = { ...mediaLogParamsRef.current, watchStrtDt, watchEndDt, }; dispatch(changeLocalSettings({ watchRecord })); }, 10000); return () => { setLogStatus((status) => ({ ...status, isModalMediaLogReady: false, })); clearInterval(watchIntervalMedia.current); dispatch( sendLogVOD({ ...mediaLogParamsRef.current, watchStrtDt }, () => dispatch(changeLocalSettings({ watchRecord: {} })) ) ); }; } } // case: media, full if (logStatus.isFullMediaLogReady && !panelInfo?.modal) { let watchStrtDt = formatGMTString(new Date()); watchIntervalMedia.current = setInterval(() => { let watchEndDt = formatGMTString(new Date()); let watchRecord = { ...mediaLogParamsRef.current, watchStrtDt, watchEndDt, }; dispatch(changeLocalSettings({ watchRecord })); }, 10000); return () => { setLogStatus((status) => ({ ...status, isFullMediaLogReady: false, })); clearInterval(watchIntervalMedia.current); dispatch( sendLogVOD({ ...mediaLogParamsRef.current, watchStrtDt }, () => dispatch(changeLocalSettings({ watchRecord: {} })) ) ); }; } }, [ broadcast, isVODPaused, logStatus.isFullMediaLogReady, logStatus.isModalMediaLogReady, panelInfo?.modal, setLogStatus, ]); const videoVerticalVisible = useMemo(() => { if (playListInfo && playListInfo[selectedIndex]?.vtctpYn === 'Y') { return true; } return false; }, [playListInfo, selectedIndex]); const handleItemFocus = useCallback( (menu) => { dispatch(sendLogGNB(menu)); if (!videoVerticalVisible) { resetTimer(REGULAR_TIMEOUT); } }, [dispatch, resetTimer, videoVerticalVisible] ); const onClickBack = useCallback( (ev, isEnd) => { //modal로부터 Full 전환된 경우 다시 preview 모드로 돌아감. // TabContainer(v1)만: Side Contents가 보이는 경우 먼저 숨기고 return if ( tabContainerVersion === 1 && sideContentsVisible && !videoVerticalVisible && panelInfo.shptmBanrTpNm !== 'MEDIA' ) { setSideContentsVisible(false); ev?.stopPropagation(); // ev?.preventDefault(); return; } if (panelInfo.modalContainerId && !panelInfo.modal) { dispatch( startVideoPlayer({ ...panelInfo, modal: true, modalClassName: '', }) ); videoPlayer.current?.hideControls(); setSelectedIndex(backupInitialIndex); if (panelInfo.shptmBanrTpNm === 'MEDIA') { dispatch( updatePanel({ name: panel_names.DETAIL_PANEL, panelInfo: { launchedFromPlayer: false, }, }) ); } ev?.stopPropagation(); // ev?.preventDefault(); return; } if (!panelInfo.modal) { console.log('[PlayerPanel] popPanel - closeButtonHandler'); dispatch(PanelActions.popPanel()); dispatch(changeAppStatus({ cursorVisible: false })); //딮링크로 플레이어 진입 후 이전버튼 클릭시 if (panels.length === 1) { timeoutRef.current = setTimeout(() => { Spotlight.focus(SpotlightIds.HOME_TBODY); }); } ev?.stopPropagation(); // ev?.preventDefault(); return; } }, [ dispatch, panelInfo, videoPlayer, sideContentsVisible, videoVerticalVisible, backupInitialIndex, panels, tabContainerVersion, ] ); useEffect(() => { //todo if(modal) return () => { // 패널이 2개 존재할때만 popPanel 진행 if (panelInfo.modal && !isOnTop) { console.log('[PlayerPanel] popPanel - useEffect cleanup'); dispatch(PanelActions.popPanel()); } else { Spotlight.focus('tbody'); } }; }, [panelInfo?.modal, isOnTop]); useEffect(() => { if (showNowInfos && panelInfo.shptmBanrTpNm === 'LIVE') { const period = showNowInfos.period !== undefined ? showNowInfos.period : 30; const periodInMilliseconds = period * 1000; const timer = setTimeout(() => { dispatch( getMainLiveShowNowProduct({ patnrId: panelInfo.patnrId ? panelInfo.patnrId : playListInfo[selectedIndex].patnrId, showId: panelInfo.showId ? panelInfo.showId : playListInfo[selectedIndex].showId, lstChgDt: showNowInfos.lstChgDt, }) ); }, periodInMilliseconds); return () => { clearTimeout(timer); }; } }, [showNowInfos, panelInfo]); const videoItemFocused = useCallback(() => { // 아이템클릭 진입시 포커스 let hasProperSpot = false; let targetId; if (!isInitialFocusOccurred) { targetId = panelInfo.targetId; initialFocusTimeoutJob.current.start(() => { const initialFocusTarget = findSelector(`[data-spotlight-id="${targetId}"]`); if (initialFocusTarget) { hasProperSpot = true; Spotlight.focus(initialFocusTarget); setIsInitialFocusOccurred(true); return; } }); } }, [isInitialFocusOccurred, panelInfo.targetId, panelInfo.modal, videoVerticalVisible]); const videoInitialFocused = useCallback(() => { if (panelInfo.isUpdatedByClick || !isOnTop) { return; } setContainerLastFocusedElement(null, ['playVideoShopNowBox']); // 세로형모드 포커스 if (videoVerticalVisible) { Spotlight.focus('tab-0'); return; } // 화살표버튼 포커스 const current = Spotlight.getCurrent(); let hasProperSpot = false; if (current) { const spotId = current.getAttribute('data-spotlight-id'); if (spotId && spotId.indexOf('tabChannel-video') >= 0) { hasProperSpot = true; } } if (!panelInfo.modal && !videoVerticalVisible && !hasProperSpot) { if (tabContainerVersion === 1) { Spotlight.focus(SpotlightIds.PLAYER_TAB_BUTTON); } else if (tabContainerVersion === 2) { Spotlight.focus('below-tab-live-channel-button'); } return; } //비디오 진입시 포커스 if (panelInfo.isIndicatorByClick && shopNowInfo?.length > 0) { Spotlight.focus('playVideoShopNowBox'); return; } }, [ shopNowInfo, videoVerticalVisible, isOnTop, panelInfo.modal, panelInfo.isUpdatedByClick, panelInfo.isIndicatorByClick, panelInfo.shptmBanrTpNm, tabContainerVersion, ]); // 최상단 패널 정보 (여러 useMemo에서 공통으로 사용) const topPanel = useMemo(() => { return panels[panels.length - 1]; }, [panels]); // 최상단 패널이 DetailPanel이고 PlayerPanel에서 진입했는지 확인 const isTopPanelDetailFromPlayer = useMemo(() => { const result = topPanel?.name === panel_names.DETAIL_PANEL && topPanel?.panelInfo?.launchedFromPlayer === true; // 🔍 DetailPanel 상태 변화 로깅 if (result) { dlog('🎬 [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) { dlog('🔴 [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) { dlog('🔄 [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; }, [topPanel, isOnTop]); const getPlayer = useCallback((ref) => { videoPlayer.current = ref; }, []); /** for VOD */ const addPanelInfoToPlayList = useCallback( (featuredShowsInfos) => { if (Array.isArray(featuredShowsInfos)) { const showId = showDetailInfo[0]?.showId; const index = featuredShowsInfos.findIndex((show) => show.showId === showId); let newArray = []; if (index > -1) { // 인덱스를 찾은 경우 그대로 newArray = [...featuredShowsInfos]; setBackupInitialIndex(index); setSelectedIndex(index); } else { // 인덱스를 찾지 못한 경우 showDetailInfo 를 제일 앞에 배치 newArray = [{ ...showDetailInfo[0] }, ...featuredShowsInfos]; setBackupInitialIndex(0); setSelectedIndex(0); } setPlayListInfo(newArray); } }, [showDetailInfo] ); useEffect(() => { if (panelInfo.shptmBanrTpNm === 'LIVE') { if (panelInfo.patnrId && panelInfo.showId) { dispatch( getMainCategoryShowDetail({ patnrId: panelInfo.patnrId, showId: panelInfo.showId, curationId: panelInfo.curationId, }) ); } dispatch(getMainLiveShow({ vodIncFlag: 'Y' })); } }, [ dispatch, panelInfo?.curationId, panelInfo?.lgCatCd, panelInfo?.patnrId, panelInfo?.showId, panelInfo?.shptmBanrTpNm, ]); useEffect(() => { // console.log('[PlayerPanel] VOD useEffect 진입', { // shptmBanrTpNm: panelInfo.shptmBanrTpNm, // panelInfoShowId: panelInfo.showId, // showDetailInfoLength: showDetailInfo?.length, // showDetailInfoId: showDetailInfo?.[0]?.showId, // }); if (panelInfo.shptmBanrTpNm === 'VOD' && showDetailInfo && showDetailInfo.length > 0) { // console.log('[PlayerPanel] VOD 조건 만족'); // 현재 panelInfo의 showId와 showDetailInfo의 showId가 일치할 때만 처리 if (showDetailInfo[0]?.showId === panelInfo.showId) { // console.log('[PlayerPanel] showId 일치! 동영상 설정 시작', { // showId: showDetailInfo[0]?.showId, // patnrId: showDetailInfo[0]?.patnrId, // }); if (showDetailInfo[0]?.showCatCd && fullVideolgCatCd !== showDetailInfo[0]?.showCatCd) { dispatch( getHomeFullVideoInfo({ lgCatCd: showDetailInfo[0].showCatCd, }) ); } if (showDetailInfo[0].showId && showDetailInfo[0].patnrId) { // console.log('[PlayerPanel] setPlayListInfo 호출'); // featuredShowsInfos가 있으면 addPanelInfoToPlayList로 여러 동영상 처리 if (featuredShowsInfos && featuredShowsInfos.length > 0) { // console.log('[PlayerPanel] addPanelInfoToPlayList 호출 (여러 배너)', { // featuredShowsInfosLength: featuredShowsInfos.length, // }); addPanelInfoToPlayList(featuredShowsInfos); } else { // featuredShowsInfos가 없으면 현재 showDetailInfo만 설정 // console.log('[PlayerPanel] setPlayListInfo 호출 (단일 배너만)'); setPlayListInfo(showDetailInfo); setSelectedIndex(0); } setShopNowInfo(showDetailInfo[0].productInfos); saveToLocalSettings(showDetailInfo[0].showId, showDetailInfo[0].patnrId); } } else { // showId가 일치하지 않으면 이전 상태를 재활용하지 않고 초기화 // console.log('[PlayerPanel] VOD showDetailInfo mismatch. Clearing playListInfo.', { // panelInfoShowId: panelInfo.showId, // showDetailInfoId: showDetailInfo[0]?.showId, // }); dlog('[PlayerPanel] VOD showDetailInfo mismatch. Clearing playListInfo.', { panelInfoShowId: panelInfo.showId, showDetailInfoId: showDetailInfo[0]?.showId, }); setPlayListInfo(null); setSelectedIndex(null); setShopNowInfo(null); } } }, [ showDetailInfo, panelInfo.showId, panelInfo.shptmBanrTpNm, fullVideolgCatCd, featuredShowsInfos, ]); //LIVE useEffect(() => { if (playListInfo && playListInfo.length > 0 && panelInfo.shptmBanrTpNm === 'LIVE') { if (playListInfo[selectedIndex]?.patnrId) { dispatch( getMainLiveShowNowProduct({ patnrId: playListInfo[selectedIndex]?.patnrId, showId: playListInfo[selectedIndex]?.showId, }) ); } if (playListInfo[selectedIndex]?.catCd) { dispatch( getHomeFullVideoInfo({ lgCatCd: playListInfo[selectedIndex]?.catCd, }) ); } } }, [selectedIndex]); useEffect(() => { if (showDetailInfo && showDetailInfo.length > 0) { dispatch(CLEAR_PLAYER_INFO()); if ( showDetailInfo[0]?.liveFlag === 'N' && showDetailInfo[0]?.chatLogFlag === 'Y' && panelInfo.shptmBanrTpNm === 'VOD' ) { dispatch(getChatLog({ patnrId: panelInfo.patnrId, showId: panelInfo.showId })); } } }, [showDetailInfo]); // videoClick focused useEffect(() => { if (playListInfo && playListInfo.length > 0) { if (panelInfo.targetId) { videoItemFocused(); } else { videoInitialFocused(); } } }, [playListInfo]); //10초 후 닫힐때 TabButton 포커스 useEffect(() => { if (playListInfo && playListInfo.length > 0) { videoInitialFocused(); } }, [sideContentsVisible, panelInfo.modal]); // liveChannel initial selectedIndex useEffect(() => { if (panelInfo?.shptmBanrTpNm === 'LIVE' && playListInfo?.length > 0) { const index = playListInfo.findIndex((item) => item.chanId === panelInfo.chanId); if (index !== -1 && !isUpdate) { setBackupInitialIndex(index); setSelectedIndex(index); setIsUpdate(true); } } }, [panelInfo?.shptmBanrTpNm, playListInfo]); // 컴포넌트 언마운트 시 Job 정리 useEffect(() => { return () => { initialFocusTimeoutJob.current?.stop?.(); }; }, []); // live subtitle Luna API useEffect(() => { if (currentSubtitleBlob) { return; } else if (isYoutube) { if (mediaId) { dispatch(requestLiveSubtitle({ mediaId, enable: false })); setMediaId(null); } return; //do caption action on VideoPlayer(componentDidUpdate) } else { if (mediaId && captionEnable && isSubtitleActive && !panelInfo?.modal) { dispatch(requestLiveSubtitle({ mediaId, enable: true })); } else { if (mediaId) { dispatch(requestLiveSubtitle({ mediaId, enable: false })); } } } }, [ mediaId, isYoutube, captionEnable, isSubtitleActive, currentSubtitleBlob, panelInfo.modal, panelInfo.shptmBanrTpNm, ]); // get PlayListInfo useEffect(() => { if (panelInfo?.shptmBanrTpNm === 'VOD') { if (showDetailInfo && showDetailInfo.length > 0) { if (featuredShowsInfos && featuredShowsInfos.length > 0) { addPanelInfoToPlayList(featuredShowsInfos); } } } }, [featuredShowsInfos]); // get PlayListInfo useEffect(() => { if (!panelInfo) return; switch (panelInfo.shptmBanrTpNm) { case 'LIVE': { const playlist = liveShowInfos ?? liveChannelInfos; if (!Array.isArray(playlist)) return; const modifiedList = []; playlist.forEach((item) => { if (item.showType === 'vod' && Array.isArray(item.vodInfos)) { const mergedVodInfos = item.vodInfos.map((vod) => ({ ...vod, patnrId: item.patnrId, patncNm: item.patncNm, patncLogoPath: item.patncLogoPath, showType: 'vod', })); modifiedList.push(...mergedVodInfos); } else { modifiedList.push(item); } }); setPlayListInfo(modifiedList); if (showNowInfos?.prdtChgYn === 'N') { return; } if (showNowInfos || showDetailInfo?.length > 0) { const productInfos = showNowInfos ? showNowInfos.productInfos : showDetailInfo[0]?.productInfos; setShopNowInfo(productInfos); } break; } case 'MEDIA': setPlayListInfo([panelInfo]); break; default: break; } }, [ panelInfo, showDetailInfo, featuredShowsInfos, liveChannelInfos, liveShowInfos, showNowInfos, dispatch, ]); const liveTotalTime = useMemo(() => { let liveTotalTime; if (liveShowInfos && panelInfo?.shptmBanrTpNm === 'LIVE') { const startDtMoment = new Date(liveShowInfos[selectedIndex]?.strtDt); const endDtMoment = new Date(liveShowInfos[selectedIndex]?.endDt); liveTotalTime = Math.floor((endDtMoment - startDtMoment) / 1000); return liveTotalTime; } }, [liveShowInfos, selectedIndex, panelInfo.shptmBanrTpNm]); useEffect(() => { const handleVisibilityChange = () => { if ( document.visibilityState === 'visible' && liveShowInfos && panelInfo?.shptmBanrTpNm === 'LIVE' ) { const localStartDt = convertUtcToLocal(liveShowInfos[selectedIndex]?.strtDt); const curDt = new Date(); const localStartSec = localStartDt?.getTime() / 1000; const curSec = curDt?.getTime() / 1000; const calculatedLiveTime = curSec - localStartSec; if (calculatedLiveTime >= liveTotalTime) { setCurrentLiveTimeSeconds(0); } else { setCurrentLiveTimeSeconds(calculatedLiveTime); } } }; document.addEventListener('visibilitychange', handleVisibilityChange); if (panelInfo.offsetHour) { setCurrentLiveTimeSeconds(parseInt(panelInfo.offsetHour)); } else if (liveShowInfos && panelInfo?.shptmBanrTpNm === 'LIVE') { const localStartDt = convertUtcToLocal(liveShowInfos[selectedIndex]?.strtDt); const curDt = new Date(); const localStartSec = localStartDt?.getTime() / 1000; const curSec = curDt?.getTime() / 1000; const calculatedLiveTime = curSec - localStartSec; if (calculatedLiveTime >= liveTotalTime) { setCurrentLiveTimeSeconds(0); } else { setCurrentLiveTimeSeconds(calculatedLiveTime); } } else { setCurrentLiveTimeSeconds(0); } return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, [ liveShowInfos, selectedIndex, panelInfo.offsetHour, panelInfo.shptmBanrTpNm, playListInfo, liveTotalTime, ]); useEffect(() => { if (panelInfo.shptmBanrTpNm == 'LIVE' && liveTotalTime > 0) { const interval = setInterval(() => { setCurrentLiveTimeSeconds((prev) => { if (prev >= liveTotalTime) { return 1; } return prev + 1; }); }, 1000); return () => clearInterval(interval); } }, [liveTotalTime]); useEffect(() => { if (currentLiveTimeSeconds > liveTotalTime) { timeoutRef.current = setTimeout(() => { dispatch(getMainLiveShow()); setShopNowInfo(''); dispatch( getHomeFullVideoInfo({ lgCatCd: playListInfo[selectedIndex].showCatCd, }) ); }, 3000); return () => { if (timeoutRef.current) clearTimeout(timeoutRef.current); }; } }, [currentLiveTimeSeconds, liveTotalTime, dispatch, playListInfo, selectedIndex]); useEffect(() => { return () => { if (timeoutRef.current) clearTimeout(timeoutRef.current); if (watchIntervalLive.current) clearInterval(watchIntervalLive.current); if (watchIntervalVod.current) clearInterval(watchIntervalVod.current); if (watchIntervalMedia.current) clearInterval(watchIntervalMedia.current); }; }, []); const mediainfoHandler = useCallback( (ev) => { const type = ev.type; if (type !== 'timeupdate' && type !== 'durationchange') { dlog('mediainfoHandler....', type, ev, videoPlayer.current?.getMediaState()); } if (ev === 'hlsError' && isNaN(Number(videoPlayer.current?.getMediaState().playbackRate))) { dispatch( sendBroadCast({ type: 'videoError', moreInfo: { reason: 'hlsError' }, }) ); return; } switch (type) { case 'timeupdate': { setCurrentTime(videoPlayer.current?.getMediaState()?.currentTime); break; } case 'error': { dispatch( sendBroadCast({ type: 'videoError', moreInfo: { reason: videoPlayer.current?.getMediaState().error }, }) ); break; } case 'loadeddata': { const mediaId = videoPlayer.current?.video?.media?.mediaId; setMediaId(mediaId); setVideoLoaded(true); dlog( '[PlayerPanel] 🎬 Video Loaded - shptmBanrTpNm:', panelInfoRef.current?.shptmBanrTpNm ); } } }, [dispatch] ); useEffect(() => { // case: video error when the video is in fullscreen mode if ( broadcast?.type === 'videoError' && isOnTop && !panelInfo?.modal && panelInfo?.modalContainerId ) { // case: Featured Brands // if (panelInfo?.sourcePanel === panel_names.FEATURED_BRANDS_PANEL) { // dispatch(PanelActions.popPanel()); // } console.log('[PlayerPanel] Condition 4: Handling video error in fullscreen mode'); } }, [ broadcast?.type, isOnTop, panelInfo?.modal, panelInfo?.modalContainerId, panelInfo?.sourcePanel, ]); useEffect(() => { // 복구 직후: skipModalStyleRecalculation이 true면 저장된 modalStyle 적용 if (panelInfo.skipModalStyleRecalculation && !panelInfo.shouldShrinkTo1px) { dlog('[PlayerPanel] Condition 2.5: Using saved modalStyle from expand'); const shrinkInfo = panelInfo.playerState?.shrinkInfo; // 저장된 modalStyle 사용 (top, left 포함) if (shrinkInfo?.modalStyle) { setModalStyle(shrinkInfo.modalStyle); setModalScale(panelInfo.modalScale || shrinkInfo.modalScale); } else { setModalStyle(panelInfo.modalStyle); setModalScale(panelInfo.modalScale); } // DOM 렌더링 후 플래그 제거 if (typeof window !== 'undefined') { window.requestAnimationFrame(() => { window.requestAnimationFrame(() => { dlog('[PlayerPanel] Condition 2.5: Removing skipFlag after DOM render'); dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { ...panelInfo, skipModalStyleRecalculation: false, }, }) ); }); }); } } if ( panelInfo.modal && !panelInfo.shouldShrinkTo1px && panelInfo.modalContainerId && (lastPanelAction === 'previewPush' || lastPanelAction === 'previewUpdate') ) { dlog('[PlayerPanel] Condition 1: Calculating modalStyle from DOM', { lastPanelAction, }); const node = document.querySelector(`[data-spotlight-id="${panelInfo.modalContainerId}"]`); if (node) { const { width, height, top, left } = node.getBoundingClientRect(); const modalStyle = { width: width + 'px', height: height + 'px', top: top + 'px', left: left + 'px', position: 'fixed', overflow: 'visible', }; setModalStyle(modalStyle); let scale = 1; if (typeof window === 'object') { scale = width / window.innerWidth; setModalScale(scale); } dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { modalStyle: modalStyle, modalScale: scale }, }) ); } else { dlog('[PlayerPanel] Condition 1: Node not found, using saved modalStyle'); setModalStyle(panelInfo.modalStyle); setModalScale(panelInfo.modalScale); } } else if (panelInfo.shouldShrinkTo1px) { dlog('[PlayerPanel] Condition 2: Shrinking - clearing modalStyle'); // 축소 상태: 인라인 스타일 제거 (CSS만 적용) setModalStyle({}); setModalScale(1); } else if (isOnTop && !panelInfo.modal && videoPlayer.current) { dlog('[PlayerPanel] Condition 3: Playing fullscreen video'); if (videoPlayer.current?.getMediaState()?.paused) { videoPlayer.current.play(); } if (videoPlayer.current.areControlsVisible && !videoPlayer.current.areControlsVisible()) { videoPlayer.current.showControls(); } } }, [panelInfo, isOnTop, dispatch]); const smallestOffsetHourIndex = useMemo(() => { if (shopNowInfo) { const filteredVideos = shopNowInfo.filter((video) => video.offsetHour >= currentTime); const newSmallestOffsetHour = Math.min(...filteredVideos.map((video) => video.offsetHour)); const newSmallestOffsetHourIndex = shopNowInfo.findIndex( (video) => video.offsetHour === newSmallestOffsetHour.toString() ); if (shopNowInfo.length === 1) { return 0; } if (newSmallestOffsetHourIndex >= 1) { return newSmallestOffsetHourIndex - 1; } return newSmallestOffsetHourIndex; } }, [shopNowInfo, currentTime]); const currentSubtitleUrl = useMemo(() => { if (panelInfo?.shptmBanrTpNm === 'MEDIA') { return panelInfo.subtitle; } return playListInfo && playListInfo[selectedIndex]?.showSubtitlUrl; }, [playListInfo, selectedIndex, panelInfo]); const currentPlayingUrl = useMemo(() => { // return "https://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_320x180.mp4"; if (broadcast.type === 'videoError') { return null; } return playListInfo && playListInfo[selectedIndex]?.showUrl; }, [playListInfo, selectedIndex, broadcast]); const isYoutube = useMemo(() => { if (currentPlayingUrl && currentPlayingUrl.includes('youtu')) { return true; } else { return false; } }, [currentPlayingUrl]); const currentSubtitleBlob = useMemo(() => { if (Config.DEBUG_VIDEO_SUBTITLE_TEST) { return dummyVtt; } return vodSubtitleData[currentSubtitleUrl]; }, [vodSubtitleData, currentSubtitleUrl]); // 자막 Blob URL 수명 관리: 이전 Blob을 해제해 메모리 누수 방지 useEffect(() => { const prevBlobUrl = previousSubtitleBlobRef.current; // 더 엄격한 타입과 형식 체크 if (prevBlobUrl && typeof prevBlobUrl === 'string' && prevBlobUrl.startsWith('blob:') && prevBlobUrl !== currentSubtitleBlob) { try { URL.revokeObjectURL(prevBlobUrl); dlog('[PlayerPanel] Previous subtitle Blob URL revoked:', prevBlobUrl); } catch (error) { derror('[PlayerPanel] Failed to revoke Blob URL:', error); } } previousSubtitleBlobRef.current = currentSubtitleBlob; return () => { const lastBlobUrl = previousSubtitleBlobRef.current; if (lastBlobUrl && typeof lastBlobUrl === 'string' && lastBlobUrl.startsWith('blob:')) { try { URL.revokeObjectURL(lastBlobUrl); dlog('[PlayerPanel] Final subtitle Blob URL revoked:', lastBlobUrl); } catch (error) { derror('[PlayerPanel] Failed to revoke final Blob URL:', error); } } previousSubtitleBlobRef.current = null; }; }, [currentSubtitleBlob]); const isReadyToPlay = useMemo(() => { if (!currentPlayingUrl) { return false; } if (!Config.DEBUG_VIDEO_SUBTITLE_TEST && currentSubtitleUrl && !currentSubtitleBlob) { return false; } return true; }, [currentPlayingUrl, currentSubtitleUrl, currentSubtitleBlob, broadcast]); const chatVisible = useMemo(() => { if ( playListInfo && chatData && !panelInfo.modal && isOnTop && panelInfo?.shptmBanrTpNm !== 'MEDIA' ) { return true; } return false; }, [playListInfo, chatData, panelInfo.modal, isOnTop]); useEffect(() => { if (currentSubtitleUrl) { dispatch(getSubTitle({ showSubtitleUrl: currentSubtitleUrl })); } // 이전 자막 URL 정리 (Redux 메모리 누수 방지) const prevSubtitleUrl = previousSubtitleUrlRef.current; if (prevSubtitleUrl && prevSubtitleUrl !== currentSubtitleUrl) { dispatch(clearSubtitleBlob(prevSubtitleUrl)); dlog('[PlayerPanel] Clearing previous subtitle URL:', prevSubtitleUrl); } previousSubtitleUrlRef.current = currentSubtitleUrl; // 컴포넌트 언마운트 시 마지막 자막 URL 정리 return () => { if (previousSubtitleUrlRef.current) { dispatch(clearSubtitleBlob(previousSubtitleUrlRef.current)); } }; }, [currentSubtitleUrl, dispatch]); useEffect(() => { setVideoLoaded(false); }, [currentPlayingUrl]); // 비디오가 새로 선택될 때 타이머 초기화 useEffect(() => { if (currentPlayingUrl) { dlog('[PlayerPanel] 🎬 비디오 선택됨 - tabIndexV2 타이머 초기화'); resetTimerTabAutoAdvance(10000); } }, [selectedIndex, resetTimerTabAutoAdvance]); const handlePopupClose = useCallback(() => { dispatch(setHidePopup()); timeoutRef.current = setTimeout(() => Spotlight.focus(SpotlightIds.PLAYER_SUBTITLE_BUTTON)); }, [dispatch]); const reactPlayerSubtitleConfig = useMemo(() => { if (isSubtitleActive && currentSubtitleBlob) { return { file: { attributes: { crossOrigin: 'true', preload: 'metadata', }, tracks: [{ kind: 'subtitles', src: currentSubtitleBlob, default: true }], hlsOptions: { // 버퍼 길이를 60초 기준으로 설정 maxBufferLength: 60, maxMaxBufferLength: 180, backBufferLength: 0, maxBufferSize: 100 * 1000 * 1000, // 최대 버퍼 크기 100MB liveSyncDuration: 16, liveMaxLatencyDuration: 32, }, }, youtube: YOUTUBECONFIG, }; } else { return { youtube: YOUTUBECONFIG, file: { attributes: { preload: 'metadata', }, hlsOptions: { maxBufferLength: 60, maxMaxBufferLength: 180, backBufferLength: 0, maxBufferSize: 100 * 1000 * 1000, liveSyncDuration: 16, liveMaxLatencyDuration: 32, }, }, }; } }, [currentSubtitleBlob, isSubtitleActive]); const currentSideButtonStatus = useMemo(() => { if (panelInfo?.shptmBanrTpNm !== 'MEDIA' && !panelInfo?.modal && sideContentsVisible) { return true; } return false; }, [panelInfo, sideContentsVisible]); const videoType = useMemo(() => { if (currentPlayingUrl) { if (currentPlayingUrl.toLowerCase().endsWith('.mp4')) { return 'video/mp4'; } else if (currentPlayingUrl.toLowerCase().endsWith('.mpd')) { return 'application/dash+xml'; } else if (currentPlayingUrl.toLowerCase().endsWith('.m3u8')) { return 'application/mpegurl'; } } return 'application/mpegurl'; }, [currentPlayingUrl]); const orderPhoneNumber = useMemo(() => { if (panelInfo?.shptmBanrTpNm !== 'MEDIA' && showDetailInfo) { return showDetailInfo[0]?.orderPhnNo; } else { return playListInfo[selectedIndex]?.orderPhnNo; } }, [panelInfo?.shptmBanrTpNm, showDetailInfo, playListInfo, selectedIndex]); const videoThumbnailUrl = useMemo(() => { let res = null; if (panelInfo.shptmBanrTpNm === 'MEDIA') { res = panelInfo?.thumbnailUrl; } else if (playListInfo && playListInfo.length > 0) { res = playListInfo[selectedIndex]?.thumbnailUrl; } else if (!res) { res = showDetailInfo[0]?.thumbnailUrl; } return res; }, [ showDetailInfo, playListInfo, selectedIndex, panelInfo.thumbnailUrl, panelInfo.shptmBanrTpNm, ]); const saveToLocalSettings = useCallback( (showId, patnrId) => { let recentItems = []; if (localRecentItems) { recentItems = [...localRecentItems]; } const currentDate = new Date(); const formattedDate = `${currentDate.getMonth() + 1}/${currentDate.getDate()}`; const existingProductIndex = recentItems.findIndex((item) => { if (item.showId) return item.showId === showId; }); if (existingProductIndex !== -1) { recentItems.splice(existingProductIndex, 1); } recentItems.push({ patnrId: patnrId, showId: showId, date: formattedDate, expireTime: currentDate.getTime() + 1000 * 60 * 60 * 24 * 14, cntryCd: httpHeader['X-Device-Country'], }); if (recentItems.length >= 51) { const data = [...recentItems]; dispatch(changeLocalSettings({ recentItems: data.slice(1) })); } else { dispatch(changeLocalSettings({ recentItems })); } }, [httpHeader, localRecentItems, dispatch] ); const handleIndicatorDownClick = useCallback(() => { if (!initialEnter) { setInitialEnter(true); } let newIndex = selectedIndex === playListInfo.length - 1 ? 0 : selectedIndex + 1; let initialIndex = newIndex; let attempts = 0; while (!playListInfo[newIndex]?.showId && attempts < playListInfo.length) { newIndex = newIndex === playListInfo.length - 1 ? 0 : newIndex + 1; attempts++; if (newIndex === initialIndex) break; } if (playListInfo[newIndex]?.showId) { setSelectedIndex(newIndex); if (panelInfo.shptmBanrTpNm === 'VOD') { dispatch( getMainCategoryShowDetail({ patnrId: playListInfo[newIndex]?.patnrId, showId: playListInfo[newIndex]?.showId, curationId: playListInfo[newIndex]?.curationId, }) ); Spotlight.focus('playVideoShopNowBox'); } else { dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { chanId: playListInfo[newIndex].chanId, patnrId: playListInfo[newIndex].patnrId, showId: playListInfo[newIndex].showId, shptmBanrTpNm: panelInfo?.shptmBanrTpNm, isIndicatorByClick: true, }, }) ); } } if (!sideContentsVisible) { setPrevChannelIndex(selectedIndex); } setSideContentsVisible(true); }, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter]); const handleIndicatorUpClick = useCallback(() => { if (!initialEnter) { setInitialEnter(true); } let newIndex = selectedIndex === 0 ? playListInfo.length - 1 : selectedIndex - 1; let initialIndex = newIndex; let attempts = 0; while (!playListInfo[newIndex]?.showId && attempts < playListInfo.length) { newIndex = newIndex === 0 ? playListInfo.length - 1 : newIndex - 1; attempts++; if (newIndex === initialIndex) break; } if (playListInfo[newIndex]?.showId) { setSelectedIndex(newIndex); if (panelInfo.shptmBanrTpNm === 'VOD') { dispatch( getMainCategoryShowDetail({ patnrId: playListInfo[newIndex]?.patnrId, showId: playListInfo[newIndex]?.showId, curationId: playListInfo[newIndex]?.curationId, }) ); Spotlight.focus('playVideoShopNowBox'); } else { dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { chanId: playListInfo[newIndex].chanId, patnrId: playListInfo[newIndex].patnrId, showId: playListInfo[newIndex].showId, shptmBanrTpNm: panelInfo?.shptmBanrTpNm, isIndicatorByClick: true, }, }) ); } } if (!sideContentsVisible) { setPrevChannelIndex(selectedIndex); } setSideContentsVisible(true); }, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter]); useEffect(() => { if (panelInfo.shptmBanrTpNm === 'VOD' && panelInfo.patnrId && panelInfo.showId) { //VOD의 panelInfo.showId 가 변경된 최초 한번만 호출하고, FearchedShow 항목에서 선택시 또는 상하 indicator 선택시 호출한다. dispatch( getMainCategoryShowDetail({ patnrId: panelInfo.patnrId, showId: panelInfo.showId, curationId: panelInfo.curationId, }) ); } }, [panelInfo.showId]); useEffect(() => { return () => { // console.log('[PlayerPanel] unmount cleanup start'); cleanupPlayerOnUnmount(); stopExternalPlayer(); dispatch(clearShopNowInfo()); dispatch(CLEAR_PLAYER_INFO()); setShopNowInfo([]); // console.log('[PlayerPanel] unmount cleanup done'); }; }, [cleanupPlayerOnUnmount, stopExternalPlayer, dispatch]); const focusBackToPanel = useCallback(() => { // 포커스를 PlayerPanel 쪽으로 강제 이동해 YouTube iframe이 포커스를 가져가는 것을 차단 if (focusReturnRef.current) { focusReturnRef.current.focus(); return true; } return false; }, []); const focusBackButtonOrFallback = useCallback(() => { // 종료 시 Back 아이콘으로 포커스를 이동시키되 실패하면 센티넬로 폴백 if (Spotlight.focus('player-back-button')) { return true; } return focusBackToPanel(); }, [focusBackToPanel]); const stopExternalPlayer = useCallback(() => { const playerInstance = videoPlayer.current; const media = playerInstance?.video; if (!media) { return; } // ReactPlayer의 YouTube 인스턴스 대응: 종료 직후 리플레이 오버레이가 뜨지 않도록 정지/초기화 시도 if (typeof media.stopVideo === 'function') { media.stopVideo(); } if (typeof media.seekTo === 'function') { media.seekTo(0); } if (typeof media.pause === 'function') { media.pause(); } }, []); // PlayerPanel 언마운트 시 비디오 자원 강제 해제 (popPanel 시점) const cleanupPlayerOnUnmount = useCallback(() => { const playerInstance = videoPlayer.current; const media = playerInstance?.video; try { playerInstance?.pause?.(); playerInstance?.stopVideo?.(); } catch (err) { // ignore } if (media) { try { media.pause?.(); media.stopVideo?.(); media.seekTo?.(0); if ('currentTime' in media) { media.currentTime = 0; } if ('src' in media) { media.src = ''; media.removeAttribute?.('src'); } if ('srcObject' in media) { media.srcObject = null; } media.load?.(); } catch (err) { // ignore } } }, []); const onEnded = useCallback( (e) => { if (panelInfoRef.current.shptmBanrTpNm === 'MEDIA') { dispatch( updatePanel({ name: panel_names.DETAIL_PANEL, panelInfo: { launchedFromPlayer: true, isPlayerFinished: true, }, }) ); Spotlight.pause(); timeoutRef.current = setTimeout(() => { console.log('[PlayerPanel] popPanel - VIDEO_END_ACTION_DELAY'); Spotlight.resume(); dispatch(PanelActions.popPanel()); }, VIDEO_END_ACTION_DELAY); return; } if (panelInfoRef.current.shptmBanrTpNm === 'VOD') { Spotlight.pause(); timeoutRef.current = setTimeout(() => { stopExternalPlayer(); if (panelInfoRef.current.modal) { // 모달 모드에서는 종료 후 화면을 유지하고 Back 아이콘으로 포커스 이동 videoPlayer.current?.showControls?.(); } else { // 전체화면 모드에서도 종료 후 즉시 닫지 않고 제어권 회수 videoPlayer.current?.showControls?.(); } Spotlight.resume(); focusBackButtonOrFallback(); }, VIDEO_END_ACTION_DELAY); e?.stopPropagation(); e?.preventDefault(); return; } }, [dispatch, focusBackButtonOrFallback, stopExternalPlayer] ); const onKeyDown = (ev) => { if (ev.keyCode === 34) { handleIndicatorDownClick(); ev.stopPropagation(); ev.preventDefault(); } else if (ev.keyCode === 33) { handleIndicatorUpClick(); ev.stopPropagation(); ev.preventDefault(); } }; const [initialEnter, setInitialEnter] = USE_STATE('initialEnter', true); const [initialEnterV2, setInitialEnterV2] = USE_STATE('initialEnterV2', true); const timerId = useRef(null); const timerIdV2 = useRef(null); const timerIdTabAutoAdvance = useRef(null); const prevTabIndexV2 = useRef(null); const showSideContents = useMemo(() => { return ( sideContentsVisible && playListInfo && panelInfo?.shptmBanrTpNm !== 'MEDIA' && !panelInfo?.modal && isOnTop ); }, [sideContentsVisible, playListInfo, panelInfo, isOnTop]); const showBelowContents = useMemo(() => { return ( belowContentsVisible && playListInfo && panelInfo?.shptmBanrTpNm !== 'MEDIA' && !panelInfo?.modal && isOnTop ); }, [belowContentsVisible, playListInfo, panelInfo, isOnTop]); const qrCurrentItem = useMemo(() => { if (shopNowInfo?.length && panelInfo?.shptmBanrTpNm === 'LIVE') { return shopNowInfo[shopNowInfo.length - 1]; } if ( shopNowInfo?.length && smallestOffsetHourIndex >= 0 && panelInfo?.shptmBanrTpNm !== 'LIVE' ) { return shopNowInfo[smallestOffsetHourIndex]; } if (panelInfo?.shptmBanrTpNm === 'MEDIA' && panelInfo?.qrCurrentItem) { return panelInfo.qrCurrentItem; } return null; }, [shopNowInfo, smallestOffsetHourIndex, panelInfo?.shptmBanrTpNm, panelInfo?.qrCurrentItem]); const isShowType = useMemo(() => { if (['VOD', 'MEDIA'].includes(panelInfo.shptmBanrTpNm)) { return panelInfo.shptmBanrTpNm; } const showType = playListInfo?.[selectedIndex]?.showType; if (showType === 'live') return panelInfo.shptmBanrTpNm; if (showType === 'vod') return 'VOD'; return panelInfo.shptmBanrTpNm; }, [panelInfo.shptmBanrTpNm, playListInfo, selectedIndex]); const clearTimer = useCallback(() => { clearTimeout(timerId.current); timerId.current = null; }, []); const resetTimer = useCallback( (timeout) => { if (timerId.current) { clearTimer(); } if (initialEnter) { setInitialEnter(false); } timerId.current = setTimeout(() => { setSideContentsVisible(false); // setBelowContentsVisible(false); }, timeout); }, [clearTimer, initialEnter, setInitialEnter, setSideContentsVisible] ); const clearTimerV2 = useCallback(() => { if (timerIdV2.current) { dlog('[clearTimerV2] 타이머 클리어됨'); const stack = new Error().stack; const lines = stack.split('\n').slice(1, 4).join(' → '); dlog('[clearTimerV2] 호출 스택:', lines); } clearTimeout(timerIdV2.current); timerIdV2.current = null; }, []); const resetTimerV2 = useCallback( (timeout) => { dlog('[TabContainerV2] resetTimerV2 호출', timeout); if (timerIdV2.current) { dlog('[TabContainerV2] 기존 타이머 클리어'); clearTimerV2(); } if (initialEnterV2) { dlog('[TabContainerV2] initialEnterV2 false로 변경'); setInitialEnterV2(false); } timerIdV2.current = setTimeout(() => { dlog('[TabContainerV2] 타이머 실행 - belowContentsVisible false로 변경 (30초 경과)'); setBelowContentsVisible(false); }, timeout); }, [clearTimerV2, initialEnterV2, setInitialEnterV2, setBelowContentsVisible] ); const clearTimerTabAutoAdvance = useCallback(() => { clearTimeout(timerIdTabAutoAdvance.current); timerIdTabAutoAdvance.current = null; }, []); const resetTimerTabAutoAdvance = useCallback( (timeout) => { if (timerIdTabAutoAdvance.current) { clearTimerTabAutoAdvance(); } timerIdTabAutoAdvance.current = setTimeout(() => { setTabIndexV2(2); }, timeout); }, [clearTimerTabAutoAdvance] ); // Redux로 오버레이 숨김 useEffect(() => { if (shouldHideOverlays) { dlog('[PlayerPanel] shouldHideOverlays true - 오버레이 숨김'); setSideContentsVisible(false); dlog('[setBelowContentsVisible] Redux로 오버레이 숨김 - false로 변경'); setBelowContentsVisible(false); if (videoPlayer.current?.hideControls) { videoPlayer.current.hideControls(); } // 모든 타이머 클리어 if (timerId.current) { clearTimer(); } if (timerIdV2.current) { clearTimerV2(); } if (timerIdTabAutoAdvance.current) { clearTimerTabAutoAdvance(); } dispatch(resetPlayerOverlays()); } }, [shouldHideOverlays, dispatch, clearTimer, clearTimerV2, clearTimerTabAutoAdvance]); // Redux로 오버레이 표시 useEffect(() => { if (shouldShowOverlays) { dlog('[PlayerPanel] shouldShowOverlays true - 오버레이 표시'); setSideContentsVisible(true); dlog('[setBelowContentsVisible] Redux로 오버레이 표시 - true로 변경'); setBelowContentsVisible(true); if (videoPlayer.current?.showControls) { videoPlayer.current.showControls(); } dispatch(resetPlayerOverlays()); } }, [shouldShowOverlays, dispatch]); // PlayerPanel이 최상단이 될 때 오버레이 표시 (DetailPanel에서 복귀) useEffect(() => { if (isOnTop && !panelInfo.modal && !videoVerticalVisible) { // 정확한 복귀 종류 구분: // 1. HomePanel 복귀: modalPrevRef.current === true && prevIsTopPanelDetailFromPlayerRef.current === false // 2. DetailPanel 복귀: prevIsTopPanelDetailFromPlayerRef.current === true const isHomePanelReturn = modalPrevRef.current === true && prevIsTopPanelDetailFromPlayerRef.current === false; const isDetailPanelReturn = prevIsTopPanelDetailFromPlayerRef.current === true; if (isDetailPanelReturn) { dlog('[PlayerPanel] ✅ PlayerPanel 내부 DetailPanel에서 복귀함! - 오버레이 표시'); } else if (isHomePanelReturn) { dlog('[PlayerPanel] 📺 HomePanel에서 복귀함 - 오버레이 표시'); // HomePanel에서 복귀 시 콘텐츠 타입에 따라 tabIndex 설정 dlog('[PlayerPanel] 🔄 HomePanel 복귀 - tabIndex를 콘텐츠 타입에 따라 설정'); if (tabContainerVersion === 2) { if (panelInfoRef.current.shptmBanrTpNm === 'VOD') { setTabIndexV2(1); dlog('[PlayerPanel] 📝 VOD 콘텐츠 - tabIndexV2를 1로 설정됨 (FeaturedShowContents 표시)'); } else { setTabIndexV2(1); dlog('[PlayerPanel] 📝 LIVE 콘텐츠 - tabIndexV2를 1로 설정됨 (LiveChannelContents 표시)'); } } } else { dlog('[PlayerPanel] 🔄 그 외 복귀 - 오버레이 표시'); } setSideContentsVisible(true); dlog('[setBelowContentsVisible] 복귀 - true로 변경'); setBelowContentsVisible(true); // VideoPlayer가 belowContentsVisible prop을 감지해서 자동으로 controls 표시함 // PlayerPanel 내부 DetailPanel에서 복귀 시에만 포커스 복원 시도 if (isDetailPanelReturn) { const lastFocusedTargetId = panelInfo?.lastFocusedTargetId; dlog( '[PlayerPanel] 🎯 PlayerPanel DetailPanel 복귀 - lastFocusedTargetId:', lastFocusedTargetId ); if (lastFocusedTargetId) { // ShopNowContents가 렌더링될 때까지 잠시 대기 후 포커스 복원 timeoutRef.current = setTimeout(() => { dlog('[PlayerPanel] 🔍 500ms 후 포커스 복원 시도:', lastFocusedTargetId); Spotlight.focus(lastFocusedTargetId); }, 500); } // 한 번 처리한 복귀 플래그는 즉시 해제해 중복 영향을 막는다. prevIsTopPanelDetailFromPlayerRef.current = false; } return () => { if (timeoutRef.current) clearTimeout(timeoutRef.current); }; } }, [ isOnTop, panelInfo.modal, videoVerticalVisible, panelInfo?.lastFocusedTargetId, tabContainerVersion, setTabIndexV2, ]); // PopularShow에서 처음 호출할 때처럼 modal이 false인 상태에서 VOD/LIVE 구분 useEffect(() => { if (isOnTop && !panelInfo.modal && !videoVerticalVisible && tabContainerVersion === 2) { // DetailPanel에서 막 복귀한 경우 탭 인덱스를 초기화하지 않는다. if (panelInfo?.detailPanelClosed) { return; } dlog( '[PlayerPanel] 📺 Modal false 상태 - shptmBanrTpNm:', panelInfoRef.current?.shptmBanrTpNm ); if (panelInfoRef.current?.shptmBanrTpNm === 'VOD') { setTabIndexV2(1); dlog('[PlayerPanel] 📝 VOD 콘텐츠 - tabIndexV2를 1로 설정됨 (FeaturedShowContents 표시)'); } else { setTabIndexV2(1); dlog('[PlayerPanel] 📝 LIVE 콘텐츠 - tabIndexV2를 1로 설정됨 (LiveChannelContents 표시)'); } } }, [isOnTop, panelInfo.modal, videoVerticalVisible, tabContainerVersion]); useEffect(() => { // tabContainerVersion === 1일 때만 실행 if (tabContainerVersion !== 1) return; const node = document.querySelector(`[data-spotlight-id=${TAB_CONTAINER_SPOTLIGHT_ID}]`); if (!showSideContents || !node || videoVerticalVisible) return; // NOTE 첫 진입 시에는 10초 후 탭이 닫히도록 설정 if (initialEnter) { resetTimer(INITIAL_TIMEOUT); } const handleEvent = () => resetTimer(REGULAR_TIMEOUT); TARGET_EVENTS.forEach((event) => node.addEventListener(event, handleEvent)); return () => { TARGET_EVENTS.forEach((event) => node.removeEventListener(event, handleEvent)); if (timerId.current) { clearTimer(); } }; }, [ showSideContents, videoVerticalVisible, tabContainerVersion, resetTimer, initialEnter, clearTimer, ]); useEffect(() => { if (initialEnter || !sideContentsVisible || videoVerticalVisible) return; // NOTE button을 통해 탭을 연 경우 5초 후 탭이 닫히도록 설정 if (sideContentsVisible) { resetTimer(REGULAR_TIMEOUT); } return () => { if (timerId.current) { clearTimer(); } }; }, [sideContentsVisible]); // TabContainerV2 자동 닫기 (tabIndex 1 → 2 변경 감지) useEffect(() => { // tabContainerVersion === 2일 때만 실행 if (tabContainerVersion !== 2) { prevTabIndexV2.current = tabIndexV2; return; } // modal === false일 때만 실행 if (panelInfo?.modal) { prevTabIndexV2.current = tabIndexV2; return; } // tabIndexV2가 1에서 2로 정확하게 변경되는 시점만 감지 const isTransitionedTo2 = prevTabIndexV2.current === 1 && tabIndexV2 === 2; prevTabIndexV2.current = tabIndexV2; if (!isTransitionedTo2) { if (timerIdV2.current) { dlog('[TabContainerV2] 타이머 클리어 - tabIndex가 2가 아님', tabIndexV2); clearTimerV2(); } return; } dlog('[TabContainerV2] tabIndex 1 → 2 감지, 타이머 시작'); if (!belowContentsVisible || videoVerticalVisible) { dlog( '[TabContainerV2] early return - belowContentsVisible 또는 videoVerticalVisible 조건 불만족' ); return; } // tabIndex 1 → 2로 변경된 정확한 시점에 30초 타이머 시작 dlog('[TabContainerV2] 30초 타이머 시작'); resetTimerV2(REGULAR_TIMEOUT); return () => { // cleanup: tabIndex가 2가 아니거나 오버레이가 사라질 때만 타이머 클리어 if (!belowContentsVisible || videoVerticalVisible || tabIndexV2 !== 2) { if (timerIdV2.current) { dlog('[TabContainerV2] cleanup - 타이머 클리어'); clearTimerV2(); } } }; }, [ tabContainerVersion, tabIndexV2, belowContentsVisible, videoVerticalVisible, panelInfo?.modal, ]); // TabIndex 1 자동 다음 단계로 이동 useEffect(() => { // tabIndex === 1일 때만 실행 if (tabIndexV2 !== 1 || !belowContentsVisible || videoVerticalVisible) { if (timerIdTabAutoAdvance.current) { clearTimerTabAutoAdvance(); } return; } // 10초 후 tabIndex를 2로 변경 resetTimerTabAutoAdvance(10000); return () => { if (timerIdTabAutoAdvance.current) { clearTimerTabAutoAdvance(); } }; }, [ tabIndexV2, belowContentsVisible, videoVerticalVisible, resetTimerTabAutoAdvance, clearTimerTabAutoAdvance, ]); useLayoutEffect(() => { const videoContainer = document.querySelector(`.${css.videoContainer}`); if (panelInfo.thumbnail && !videoVerticalVisible) { videoContainer.style.background = `url(${panelInfo.thumbnail}) center/contain no-repeat`; videoContainer.style.backgroundColor = 'black'; } if (broadcast.type === 'videoError' && videoThumbnailUrl) { videoContainer.style.background = `url(${videoThumbnailUrl}) center/contain no-repeat`; videoContainer.style.backgroundColor = 'black'; } }, [panelInfo.thumbnail, broadcast]); const isPlayer = useMemo(() => { if (!panelInfo?.modal) { return 'full player'; } switch (panels[0].name) { case 'categorypanel': return 'category'; case 'mypagepanel': return 'my page'; case 'searchpanel': return 'search'; case 'hotpickpanel': return 'hot picks'; case 'featuredbrandspanel': return 'featured brands'; case 'trendingnowpanel': return 'trending now'; case 'playerpanel': return 'home'; } }, [panelInfo.modal, panels]); const createLogParams = useCallback( (visible) => { if (videoLoaded && isShowType) { if (showDetailInfo?.[0]) { return { visible, showType: isShowType, player: isPlayer, category: showDetailInfo[0].showCatNm, showId: showDetailInfo[0].showId, showTitle: showDetailInfo[0].showNm, partner: showDetailInfo[0].patncNm, contextName: Config.LOG_CONTEXT_NAME.SHOW, messageId: Config.LOG_MESSAGE_ID.SHOWVIEW, }; } else if (playListInfo?.[selectedIndex]) { const currentItem = playListInfo[selectedIndex]; return { visible, showType: isShowType, player: isPlayer, category: currentItem.catNm, showId: currentItem.showId, contentTitle: currentItem.showNm, partner: currentItem.patncNm, contextName: Config.LOG_CONTEXT_NAME.SHOW, messageId: Config.LOG_MESSAGE_ID.SHOWVIEW, }; } } return null; }, [isShowType, videoLoaded, showDetailInfo?.[0]?.showId, playListInfo?.[selectedIndex]?.showId] ); // isVODPaused 상태 변경 시에만 로그를 보냄 useEffect(() => { if (showDetailInfo?.[0]) { const params = createLogParams(!isVODPaused); if (params) { dispatch(sendLogTotalRecommend(params)); } } else if (playListInfo?.[selectedIndex]) { const params = createLogParams(true); if (params) { dispatch(sendLogTotalRecommend(params)); } } }, [isVODPaused, createLogParams, showDetailInfo]); // 컴포넌트 언마운트 시에만 로그를 보냄 useEffect(() => { return () => { const params = createLogParams(false); if (params) { dispatch(sendLogTotalRecommend(params)); } }; }, [createLogParams, dispatch, showDetailInfo]); const containerClassName = classNames( css.videoContainer, panelInfo.modal && css.modal, panelInfo.shouldShrinkTo1px && css.shrinkTo1px, // PlayerPanel이 최상단 아니고, 최상단이 DetailPanel(from Player)이면 비디오 보이도록 !isOnTop && isTopPanelDetailFromPlayer && css['background-visible'], // PlayerPanel이 최상단 아니고, 위 조건 아니면 1px로 숨김 !isOnTop && !isTopPanelDetailFromPlayer && css.background, !captionEnable && css.hideSubtitle ); return ( { // if (!panelInfo?.modal && isOnTop && panelInfo?.shptmBanrTpNm !== 'MEDIA') { // setBelowContentsVisible((prev) => !prev); // } // }} >