Files
shoptime/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx
optrader fbd4f4024d [251202] fix: VOD-FeaturedShows
🕐 커밋 시간: 2025. 12. 02. 17:54:40

📊 변경 통계:
  • 총 파일: 4개
  • 추가: +48줄
  • 삭제: -15줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/FeaturedShowContents.v2.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/FeaturedShowContents.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
2025-12-02 17:54:41 +09:00

3125 lines
101 KiB
JavaScript

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) {
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) {
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(() => {
if (panelInfo.shptmBanrTpNm === 'VOD' && showDetailInfo && showDetailInfo.length > 0) {
// 현재 panelInfo의 showId와 showDetailInfo의 showId가 일치할 때만 처리
if (showDetailInfo[0]?.showId === panelInfo.showId) {
if (showDetailInfo[0]?.showCatCd && fullVideolgCatCd !== showDetailInfo[0]?.showCatCd) {
dispatch(
getHomeFullVideoInfo({
lgCatCd: showDetailInfo[0].showCatCd,
})
);
}
if (showDetailInfo[0].showId && showDetailInfo[0].patnrId) {
if (!featuredShowsInfos || Object.keys(featuredShowsInfos).length === 0) {
setPlayListInfo(showDetailInfo);
// VOD는 단일 비디오이므로 selectedIndex를 0으로 고정
setSelectedIndex(0);
}
setShopNowInfo(showDetailInfo[0].productInfos);
saveToLocalSettings(showDetailInfo[0].showId, showDetailInfo[0].patnrId);
}
} else {
// 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());
}
}
}, [
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(() => {
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 (
<TPanel
isTabActivated={false}
{...props}
className={containerClassName}
handleCancel={onClickBack}
spotlightId={spotlightId}
>
<Container
spotlightRestrict="self-only"
spotlightId="spotlightId-video-contaienr"
onKeyDown={onKeyDown}
// onClick={() => {
// if (!panelInfo?.modal && isOnTop && panelInfo?.shptmBanrTpNm !== 'MEDIA') {
// setBelowContentsVisible((prev) => !prev);
// }
// }}
>
<div
ref={focusReturnRef}
tabIndex={-1}
aria-hidden="true"
style={{ width: 1, height: 1, opacity: 0, position: 'absolute' }}
/>
{isReadyToPlay && (
<VideoPlayer
key={currentPlayingUrl}
setApiProvider={getPlayer}
disabled={panelInfo.modal}
onEnded={onEnded}
noAutoPlay={cannotPlay}
autoCloseTimeout={6000}
onBackButton={onClickBack}
spotlightDisabled={panelInfo.modal}
isYoutube={isYoutube}
src={currentPlayingUrl}
style={panelInfo.modal ? modalStyle : {}}
modalScale={panelInfo.modal ? modalScale : 1}
modalClassName={panelInfo.modal && panelInfo.modalClassName}
handleIndicatorDownClick={handleIndicatorDownClick}
handleIndicatorUpClick={handleIndicatorUpClick}
onError={mediainfoHandler}
onTimeUpdate={mediainfoHandler}
onLoadedData={mediainfoHandler}
onLoadedMetadata={mediainfoHandler}
onDurationChange={mediainfoHandler}
reactPlayerConfig={reactPlayerSubtitleConfig}
thumbnailUrl={videoLoaded ? '' : videoThumbnailUrl}
videoComponent={
(typeof window === 'object' && !window.PalmSystem) || isYoutube ? TReactPlayer : Media
}
liveTotalTime={liveTotalTime}
currentLiveTimeSeconds={currentLiveTimeSeconds}
countryCode={countryCode}
// VideoOverlay props
type={isShowType}
panelInfo={panelInfo}
captionEnable={captionEnable}
orderPhnNo={orderPhoneNumber}
disclaimer={shopNowInfo && shopNowInfo[smallestOffsetHourIndex]?.disclaimer}
themeProductInfos={themeProductInfos ? themeProductInfos : hotelInfos}
detailThemeProductImageLength={productImageLength}
playListInfo={playListInfo && playListInfo}
selectedIndex={selectedIndex}
qrCurrentItem={qrCurrentItem}
setIsSubtitleActive={setIsSubtitleActive}
setSideContentsVisible={setSideContentsVisible}
sideContentsVisible={sideContentsVisible}
setBelowContentsVisible={setBelowContentsVisible}
belowContentsVisible={belowContentsVisible}
videoVerticalVisible={videoVerticalVisible}
setCurrentTime={setCurrentTime}
setIsVODPaused={setIsVODPaused}
broadcast={broadcast}
tabContainerVersion={tabContainerVersion}
tabIndexV2={tabIndexV2}
dispatch={dispatch}
videoPlayState={videoPlayState}
>
{typeof window === 'object' && window.PalmSystem && (
<source src={currentPlayingUrl} type={videoType} />
)}
{isSubtitleActive &&
!panelInfo.modal &&
captionEnable &&
typeof window === 'object' &&
window.PalmSystem &&
currentSubtitleBlob && <track kind="subtitles" src={currentSubtitleBlob} default />}
</VideoPlayer>
)}
{chatVisible && (
<PlayerOverlayChat
currentTime={currentTime}
videoVerticalVisible={videoVerticalVisible}
imageQRCodeUrl={playListInfo[selectedIndex]?.chatQrImgUrl}
QRCodeUrl={playListInfo[selectedIndex]?.chatUrl}
/>
)}
{tabContainerVersion === 1 &&
currentSideButtonStatus &&
!videoVerticalVisible &&
playListInfo && (
<PlayerTabButton
setSideContentsVisible={setSideContentsVisible}
sideContentsVisible={sideContentsVisible}
videoType={isShowType}
/>
)}
{tabContainerVersion === 1 && showSideContents && (
<TabContainer
spotlightId={TAB_CONTAINER_SPOTLIGHT_ID}
panelInfo={panelInfo}
shopNowInfo={shopNowInfo}
playListInfo={playListInfo}
selectedIndex={selectedIndex}
setSelectedIndex={setSelectedIndex}
liveChannelInfos={liveChannelInfos || liveShowInfos}
videoVerticalVisible={videoVerticalVisible}
handleItemFocus={handleItemFocus}
prevChannelIndex={prevChannelIndex}
currentTime={currentTime}
/>
)}
{/* {shouldShowBelowTab && (
<>
{belowTabMode === 'liveShow' && (
<LiveShowContainer
panelInfo={panelInfo}
liveInfos={playListInfo}
currentTime={currentTime}
setSelectedIndex={setSelectedIndex}
videoVerticalVisible={videoVerticalVisible}
currentVideoShowId={playListInfo && playListInfo[selectedIndex]?.showId}
handleItemFocus={handleItemFocus}
onLiveChannelButtonClick={() => setBelowTabMode('shopNowButton')}
tabTitle={[
$L('SHOP NOW'),
panelInfo?.shptmBanrTpNm === 'LIVE' ? $L('LIVE CHANNEL') : $L('FEATURED SHOWS'),
]}
selectedIndex={selectedIndex}
tabIndex={1}
/>
)}
{belowTabMode === 'shopNowButton' && (
<ShopNowButton onClick={() => setBelowTabMode('shopNow')} />
)}
{belowTabMode === 'shopNow' && (
<ShopNowContainer
panelInfo={panelInfo}
liveInfos={playListInfo}
currentTime={currentTime}
setSelectedIndex={setSelectedIndex}
videoVerticalVisible={videoVerticalVisible}
currentVideoShowId={playListInfo && playListInfo[selectedIndex]?.showId}
handleItemFocus={handleItemFocus}
/>
)}
</>
)} */}
{tabContainerVersion === 2 && showBelowContents && (
<TabContainerV2
panelInfo={panelInfo}
playListInfo={playListInfo}
shopNowInfo={shopNowInfo}
selectedIndex={selectedIndex}
setSelectedIndex={setSelectedIndex}
liveChannelInfos={liveChannelInfos || liveShowInfos}
videoVerticalVisible={videoVerticalVisible}
handleItemFocus={handleItemFocus}
prevChannelIndex={prevChannelIndex}
currentTime={currentTime}
spotlightId={TAB_CONTAINER_V2_SPOTLIGHT_ID}
tabIndex={tabIndexV2}
onShopNowButtonClick={() => setTabIndexV2(0)}
onLiveChannelButtonClick={() => setTabIndexV2(2)}
onLiveNext={() => setTabIndexV2(1)}
onTabClose={(newTabIndex) => setTabIndexV2(newTabIndex)}
tabVisible={belowContentsVisible}
/>
)}
</Container>
{activePopup === ACTIVE_POPUP.alertPopup && (
<TPopUp
kind="textPopup"
hasText
hasButton
text={$L(
"To view the subtitles, please change the setting to 'On' in the system settings menu."
)}
button2Text={$L('OK')}
open={popupVisible}
onClose={handlePopupClose}
></TPopUp>
)}
</TPanel>
);
};
const propsAreEqual = (prev, next) => {
const keys = Object.keys(prev);
const nextKeys = Object.keys(next);
// if (!next.isOnTop) {
// //ignore event on background
// return true;
// }
if (keys.length !== nextKeys.length) {
return false;
}
for (let i = 0; i < keys.length; i++) {
if (prev[keys[i]] !== next[keys[i]]) {
if (JSON.stringify(prev[keys[i]]) === JSON.stringify(next[keys[i]])) {
continue;
}
return false;
}
}
return true;
};
export default React.memo(PlayerPanel, propsAreEqual);