diff --git a/com.twin.app.shoptime/src/utils/helperMethods.js b/com.twin.app.shoptime/src/utils/helperMethods.js index e8b5e80b..387cc054 100644 --- a/com.twin.app.shoptime/src/utils/helperMethods.js +++ b/com.twin.app.shoptime/src/utils/helperMethods.js @@ -397,6 +397,11 @@ export const getFormattingDate = (dateString) => { }; export const removeSpecificTags = (html) => { + // null 또는 undefined 체크 + if (!html) { + return html; + } + const tagPatterns = [ /]*>(.*?)<\/a>/gi, /]*>(.*?)<\/script>/gi, diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx index 3fba30d5..931b4873 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx @@ -151,6 +151,8 @@ export default function ProductAllSection({ // ProductVideo 버전 관리 (1: 기존 modal 방식, 2: 내장 방식 , 3: 비디오 생략) const [productVideoVersion, setProductVideoVersion] = useState(1); + // 비디오 재생 여부 flag (재생 전에는 minimize/restore 로직 비활성화) + const [isVideoPlaying, setIsVideoPlaying] = useState(false); // const [currentHeight, setCurrentHeight] = useState(0); //하단부분까지 갔을때 체크용 @@ -666,7 +668,8 @@ export default function ProductAllSection({ // 🔽 ProductVideo (v3.jsx)에만 HomePanel 스타일 즉각 스크롤 로직 적용 // ProductVideo.v3.jsx는 ProductVideo로 import되어 productVideoVersion === 1일 때 사용됨 - if (productVideoVersion === 1) { + // ⚠️ 비디오가 재생되었을 때만 minimize/restore 로직 실행 + if (productVideoVersion === 1 && isVideoPlaying) { const isScrollingDown = currentScrollTop > prevScrollTop; prevScrollTopRef.current = currentScrollTop; @@ -710,7 +713,7 @@ export default function ProductAllSection({ } // v2: onScrollStop에서 처리 (기존 로직 유지) }, - [documentHeight, isBottom, productVideoVersion, dispatch] + [documentHeight, isBottom, productVideoVersion, isVideoPlaying, dispatch] ); // 스크롤 멈추었을 때만 호출 (성능 최적화) @@ -1112,6 +1115,7 @@ export default function ProductAllSection({ thumbnailUrl={renderItems[0].thumbnail} autoPlay={true} continuousPlay={true} + onVideoPlaying={() => setIsVideoPlaying(true)} onScrollToImages={handleScrollToImagesV1} onFocus={() => console.log('[ProductVideo V1] Focused')} /> diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v3.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v3.jsx index f521321f..bf990476 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v3.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v3.jsx @@ -20,6 +20,7 @@ export default function ProductVideo({ videoUrl, thumbnailUrl, onScrollToImages, + onVideoPlaying = null, // 비디오 재생 시 호출되는 콜백 autoPlay = false, // 자동 재생 여부 continuousPlay = false, // 반복 재생 여부 onFocus = null, // 외부에서 전달된 포커스 핸들러 @@ -32,6 +33,7 @@ export default function ProductVideo({ const [focused, setFocused] = useState(false); const [modalState, setModalState] = useState(true); // 모달 상태 관리 추가 const [hasAutoPlayed, setHasAutoPlayed] = useState(false); // 자동 재생 완료 여부 + const [isVideoPlaying, setIsVideoPlaying] = useState(false); // 비디오 재생 여부 flag const topPanel = panels[panels.length - 1]; @@ -59,6 +61,10 @@ export default function ProductVideo({ // 짧은 딜레이 후 재생 시작 (컴포넌트 마운트 완료 후) setTimeout(() => { + setIsVideoPlaying(true); // 비디오 재생 flag 설정 + if (onVideoPlaying) { + onVideoPlaying(); // 부모 컴포넌트에 알림 + } dispatch( startMediaPlayer({ qrCurrentItem: productInfo, @@ -178,6 +184,10 @@ export default function ProductVideo({ console.log('[ProductVideo] *** Starting modal MediaPanel ***'); console.log('[ProductVideo] productInfo:', JSON.stringify(productInfo, null, 2)); // 처음 재생 시작 - modal=true로 시작 + setIsVideoPlaying(true); // 비디오 재생 flag 설정 + if (onVideoPlaying) { + onVideoPlaying(); // 부모 컴포넌트에 알림 + } dispatch( startMediaPlayer({ qrCurrentItem: productInfo, diff --git a/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx b/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx index 02bf1d49..8734455a 100644 --- a/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx +++ b/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx @@ -67,16 +67,7 @@ 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 '../PlayerPanel/PlayerItemCard/PlayerItemCard'; -import PlayerOverlayChat from '../PlayerPanel/PlayerOverlay/PlayerOverlayChat'; -import PlayerOverlayQRCode from '../PlayerPanel/PlayerOverlay/PlayerOverlayQRCode'; import css from './MediaPanel.v3.module.less'; -import PlayerTabButton from '../PlayerPanel/PlayerTabContents/TabButton/PlayerTabButton'; -import TabContainer from '../PlayerPanel/PlayerTabContents/TabContainer'; -import TabContainerV2 from '../PlayerPanel/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: 'default-element', preserveld: true }, @@ -168,12 +159,6 @@ const YOUTUBECONFIG = { }, }; -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; @@ -236,8 +221,6 @@ const MediaPanel = React.forwardRef( 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', @@ -261,8 +244,6 @@ const MediaPanel = React.forwardRef( 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 panels = USE_SELECTOR('panels', (state) => state.panels.panels); const chatData = USE_SELECTOR('chatData', (state) => state.play.chatData); @@ -890,30 +871,13 @@ const MediaPanel = React.forwardRef( const handleItemFocus = useCallback( (menu) => { dispatch(sendLogGNB(menu)); - - if (!videoVerticalVisible) { - resetTimer(REGULAR_TIMEOUT); - } }, - [dispatch, resetTimer, videoVerticalVisible] + [dispatch] ); 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( startMediaPlayer({ @@ -954,16 +918,7 @@ const MediaPanel = React.forwardRef( return; } }, - [ - dispatch, - panelInfo, - videoPlayer, - sideContentsVisible, - videoVerticalVisible, - backupInitialIndex, - panels, - tabContainerVersion, - ] + [dispatch, panelInfo, videoPlayer, videoVerticalVisible, backupInitialIndex, panels] ); useEffect(() => { @@ -1043,11 +998,7 @@ const MediaPanel = React.forwardRef( } 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'); - } + Spotlight.focus('below-tab-live-channel-button'); return; } //비디오 진입시 포커스 @@ -1063,7 +1014,6 @@ const MediaPanel = React.forwardRef( panelInfo.isUpdatedByClick, panelInfo.isIndicatorByClick, panelInfo.shptmBanrTpNm, - tabContainerVersion, ]); // 최상단 패널 정보 (여러 useMemo에서 공통으로 사용) @@ -1326,7 +1276,7 @@ const MediaPanel = React.forwardRef( if (playListInfo && playListInfo.length > 0) { videoInitialFocused(); } - }, [sideContentsVisible, panelInfo.modal]); + }, [panelInfo.modal]); // liveChannel initial selectedIndex useEffect(() => { @@ -1796,11 +1746,8 @@ const MediaPanel = React.forwardRef( }, [currentSubtitleBlob, isSubtitleActive]); const currentSideButtonStatus = useMemo(() => { - if (panelInfo?.shptmBanrTpNm !== 'MEDIA' && !panelInfo?.modal && sideContentsVisible) { - return true; - } return false; - }, [panelInfo, sideContentsVisible]); + }, []); const videoType = useMemo(() => { if (currentPlayingUrl) { @@ -1920,11 +1867,7 @@ const MediaPanel = React.forwardRef( ); } } - if (!sideContentsVisible) { - setPrevChannelIndex(selectedIndex); - } - setSideContentsVisible(true); - }, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter]); + }, [dispatch, playListInfo, selectedIndex, initialEnter]); const handleIndicatorUpClick = useCallback(() => { if (!initialEnter) { @@ -1967,11 +1910,7 @@ const MediaPanel = React.forwardRef( ); } } - if (!sideContentsVisible) { - setPrevChannelIndex(selectedIndex); - } - setSideContentsVisible(true); - }, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter]); + }, [dispatch, playListInfo, selectedIndex, initialEnter]); useEffect(() => { if (panelInfo.shptmBanrTpNm === 'VOD' && panelInfo.patnrId && panelInfo.showId) { @@ -2041,29 +1980,6 @@ const MediaPanel = React.forwardRef( }; const [initialEnter, setInitialEnter] = USE_STATE('initialEnter', true); - const [initialEnterV2, setInitialEnterV2] = USE_STATE('initialEnterV2', true); - const timerId = useRef(null); - const timerIdV2 = 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') { @@ -2096,61 +2012,11 @@ const MediaPanel = React.forwardRef( 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(() => { - clearTimeout(timerIdV2.current); - timerIdV2.current = null; - }, []); - - const resetTimerV2 = useCallback( - (timeout) => { - // console.log('[TabContainerV2] resetTimerV2 호출', timeout); - if (timerIdV2.current) { - // console.log('[TabContainerV2] 기존 타이머 클리어'); - clearTimerV2(); - } - - if (initialEnterV2) { - // console.log('[TabContainerV2] initialEnterV2 false로 변경'); - setInitialEnterV2(false); - } - - timerIdV2.current = setTimeout(() => { - // console.log('[TabContainerV2] 타이머 실행 - belowContentsVisible false로 변경'); - setBelowContentsVisible(false); - }, timeout); - }, - [clearTimerV2, initialEnterV2, setInitialEnterV2, setBelowContentsVisible] - ); // Redux로 오버레이 숨김 useEffect(() => { if (shouldHideOverlays) { console.log('[MediaPanel] shouldHideOverlays true - 오버레이 숨김'); - setSideContentsVisible(false); - setBelowContentsVisible(false); if (videoPlayer.current?.hideControls) { videoPlayer.current.hideControls(); @@ -2164,8 +2030,6 @@ const MediaPanel = React.forwardRef( useEffect(() => { if (shouldShowOverlays) { console.log('[MediaPanel] shouldShowOverlays true - 오버레이 표시'); - setSideContentsVisible(true); - setBelowContentsVisible(true); if (videoPlayer.current?.showControls) { videoPlayer.current.showControls(); @@ -2175,16 +2039,6 @@ const MediaPanel = React.forwardRef( } }, [shouldShowOverlays, dispatch]); - // MediaPanel이 최상단이 될 때 오버레이 표시 (DetailPanel에서 복귀) - useEffect(() => { - if (isOnTop && !panelInfo.modal && !videoVerticalVisible) { - console.log('[MediaPanel] isOnTop true - 오버레이 표시'); - setSideContentsVisible(true); - setBelowContentsVisible(true); - // VideoPlayer가 belowContentsVisible prop을 감지해서 자동으로 controls 표시함 - } - }, [isOnTop, panelInfo.modal, videoVerticalVisible]); - useEffect(() => { if (panelInfoRef.current?.modal && !panelInfo.modal && isOnTop && !videoVerticalVisible) { const focusTimer = setTimeout(() => { @@ -2197,104 +2051,6 @@ const MediaPanel = React.forwardRef( } }, [panelInfo.modal, isOnTop, videoVerticalVisible]); - 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 자동 닫기 - useEffect(() => { - // tabContainerVersion === 2일 때만 실행 - if (tabContainerVersion !== 2) return; - - // console.log('[TabContainerV2] useEffect 시작', { - // showBelowContents, - // videoVerticalVisible, - // initialEnterV2, - // }); - - const node = document.querySelector(`[data-spotlight-id=${TAB_CONTAINER_V2_SPOTLIGHT_ID}]`); - // console.log('[TabContainerV2] DOM node:', node); - - if (!showBelowContents || !node || videoVerticalVisible) { - // console.log('[TabContainerV2] early return'); - return; - } - - // NOTE 첫 진입 시에는 30초 후 탭이 닫히도록 설정 - if (initialEnterV2) { - // console.log('[TabContainerV2] 첫 진입 - 타이머 시작', INITIAL_TIMEOUT); - resetTimerV2(INITIAL_TIMEOUT); - } - - const handleEvent = (e) => { - // console.log('[TabContainerV2] 이벤트 발생:', e.type); - resetTimerV2(REGULAR_TIMEOUT); - }; - TARGET_EVENTS.forEach((event) => { - // console.log('[TabContainerV2] 이벤트 리스너 등록:', event); - node.addEventListener(event, handleEvent); - }); - - return () => { - // console.log('[TabContainerV2] cleanup'); - TARGET_EVENTS.forEach((event) => node.removeEventListener(event, handleEvent)); - - if (timerIdV2.current) { - clearTimerV2(); - } - }; - }, [ - showBelowContents, - videoVerticalVisible, - tabContainerVersion, - resetTimerV2, - initialEnterV2, - clearTimerV2, - ]); - useLayoutEffect(() => { const videoContainer = document.querySelector(`.${css.videoContainer}`); @@ -2466,16 +2222,10 @@ const MediaPanel = React.forwardRef( 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} > {typeof window === 'object' && window.PalmSystem && ( @@ -2501,91 +2251,6 @@ const MediaPanel = React.forwardRef( QRCodeUrl={playListInfo[selectedIndex]?.chatUrl} /> )} - - {tabContainerVersion === 1 && - currentSideButtonStatus && - !videoVerticalVisible && - playListInfo && ( - - )} - - {tabContainerVersion === 1 && showSideContents && ( - - )} - - {/* {shouldShowBelowTab && ( - <> - {belowTabMode === 'liveShow' && ( - setBelowTabMode('shopNowButton')} - tabTitle={[ - $L('SHOP NOW'), - panelInfo?.shptmBanrTpNm === 'LIVE' ? $L('LIVE CHANNEL') : $L('FEATURED SHOWS'), - ]} - selectedIndex={selectedIndex} - tabIndex={1} - /> - )} - {belowTabMode === 'shopNowButton' && ( - setBelowTabMode('shopNow')} /> - )} - {belowTabMode === 'shopNow' && ( - - )} - - )} */} - - {tabContainerVersion === 2 && showBelowContents && ( - setTabIndexV2(0)} - onLiveChannelButtonClick={() => setTabIndexV2(2)} - onTabClose={(newTabIndex) => setTabIndexV2(newTabIndex)} - tabVisible={belowContentsVisible} - /> - )} {activePopup === ACTIVE_POPUP.alertPopup && (