diff --git a/com.twin.app.shoptime/src/actions/videoPlayActions.js b/com.twin.app.shoptime/src/actions/videoPlayActions.js index 33177fbc..02d38683 100644 --- a/com.twin.app.shoptime/src/actions/videoPlayActions.js +++ b/com.twin.app.shoptime/src/actions/videoPlayActions.js @@ -9,6 +9,9 @@ export const VIDEO_PLAY_ACTIONS = { SET_VIDEO_BANNER: 'SET_VIDEO_BANNER', SET_VIDEO_FULLSCREEN: 'SET_VIDEO_FULLSCREEN', SET_VIDEO_MINIMIZED: 'SET_VIDEO_MINIMIZED', + HIDE_PLAYER_OVERLAYS: 'HIDE_PLAYER_OVERLAYS', + SHOW_PLAYER_OVERLAYS: 'SHOW_PLAYER_OVERLAYS', + RESET_PLAYER_OVERLAYS: 'RESET_PLAYER_OVERLAYS', }; // Video Play States @@ -92,3 +95,24 @@ export const setVideoMinimized = curry((videoInfo) => ({ timestamp: Date.now(), }, })); + +/** + * PlayerPanel 오버레이를 숨김 (DetailPanel 진입 시) + */ +export const hidePlayerOverlays = () => ({ + type: VIDEO_PLAY_ACTIONS.HIDE_PLAYER_OVERLAYS, +}); + +/** + * PlayerPanel 오버레이를 표시 (DetailPanel에서 복귀 시) + */ +export const showPlayerOverlays = () => ({ + type: VIDEO_PLAY_ACTIONS.SHOW_PLAYER_OVERLAYS, +}); + +/** + * 오버레이 상태를 리셋 + */ +export const resetPlayerOverlays = () => ({ + type: VIDEO_PLAY_ACTIONS.RESET_PLAYER_OVERLAYS, +}); diff --git a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js index 3b5c00ed..d9d87287 100644 --- a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js +++ b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js @@ -2582,6 +2582,7 @@ const VideoPlayer = ApiDecorator( 'showControls', 'showFeedback', 'toggleControls', + 'onVideoClick', ], }, I18nContextDecorator( diff --git a/com.twin.app.shoptime/src/reducers/videoPlayReducer.js b/com.twin.app.shoptime/src/reducers/videoPlayReducer.js index 9f07a4e1..30bbfc46 100644 --- a/com.twin.app.shoptime/src/reducers/videoPlayReducer.js +++ b/com.twin.app.shoptime/src/reducers/videoPlayReducer.js @@ -5,6 +5,8 @@ const initialState = { state: VIDEO_STATES.STOPPED, // 'stopped', 'banner', 'fullscreen', 'minimized' videoInfo: {}, // 비디오 관련 정보 (showUrl, thumbnail, modalContainerId 등) timestamp: null, // 마지막 상태 변경 시간 + shouldHideOverlays: false, // 오버레이 숨김 플래그 + shouldShowOverlays: false, // 오버레이 표시 플래그 }; // FP handlers (curried) with immutable updates only @@ -53,15 +55,30 @@ const handleSetVideoMinimized = curry((state, action) => { ); }); +const handleHidePlayerOverlays = curry((state) => { + return set('shouldHideOverlays', true, set('shouldShowOverlays', false, state)); +}); + +const handleShowPlayerOverlays = curry((state) => { + return set('shouldShowOverlays', true, set('shouldHideOverlays', false, state)); +}); + +const handleResetPlayerOverlays = curry((state) => { + return set('shouldHideOverlays', false, set('shouldShowOverlays', false, state)); +}); + const handlers = { [VIDEO_PLAY_ACTIONS.UPDATE_VIDEO_STATE]: handleUpdateVideoState, [VIDEO_PLAY_ACTIONS.SET_VIDEO_STOPPED]: handleSetVideoStopped, [VIDEO_PLAY_ACTIONS.SET_VIDEO_BANNER]: handleSetVideoBanner, [VIDEO_PLAY_ACTIONS.SET_VIDEO_FULLSCREEN]: handleSetVideoFullscreen, [VIDEO_PLAY_ACTIONS.SET_VIDEO_MINIMIZED]: handleSetVideoMinimized, + [VIDEO_PLAY_ACTIONS.HIDE_PLAYER_OVERLAYS]: handleHidePlayerOverlays, + [VIDEO_PLAY_ACTIONS.SHOW_PLAYER_OVERLAYS]: handleShowPlayerOverlays, + [VIDEO_PLAY_ACTIONS.RESET_PLAYER_OVERLAYS]: handleResetPlayerOverlays, }; -export default function videoPlayReducer(state = initialState, action = {}) { +export function videoPlayReducer(state = initialState, action = {}) { const type = get('type', action); const handler = handlers[type]; return handler ? handler(state, action) : state; diff --git a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx index 9764f561..f6e7a0f1 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx @@ -69,6 +69,11 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { [panelInfo] ); const panelBgImgNo = useMemo(() => fp.pipe(() => panelInfo, fp.get('bgImgNo'))(), [panelInfo]); + // PlayerPanel에서 진입했는지 여부를 panelInfo에서 추출 + const panelLaunchedFromPlayer = useMemo( + () => fp.pipe(() => panelInfo, fp.get('launchedFromPlayer'))(), + [panelInfo] + ); const productPmtSuptYn = useMemo( () => fp.pipe(() => productData, fp.get('pmtSuptYn'))(), [productData] @@ -142,6 +147,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { }) ); } + // PlayerPanel의 isOnTop useEffect가 자동으로 오버레이 표시 } )(); @@ -603,7 +609,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { }, [imageUrl]); */ - console.log('productDataSource :', productDataSource); + // console.log('productDataSource :', productDataSource); // 언마운트 시 인덱스 초기화가 필요하면: // useEffect(() => () => setSelectedIndex(0), []) @@ -620,7 +626,8 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { return (
{/* 배경 이미지 및 그라데이션 컴포넌트 - 모든 콘텐츠 뒤에 렌더링 */} - + {/* launchedFromPlayer: PlayerPanel에서 진입 시 true, 다른 패널에서 진입 시 false/undefined */} + { - setActiveButton(buttonType); + if (activeButton !== buttonType) { + setActiveButton(buttonType); + } }, []); const handleButtonBlur = useCallback(() => { diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.jsx b/com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.jsx index 7ac6749c..1fc97a43 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.jsx @@ -6,23 +6,31 @@ import detailPanelBg from '../../../../../assets/images/detailpanel/detailpanel- /** * DetailPanel의 배경 이미지와 그라데이션을 렌더링하는 컴포넌트 * CSS 변수 대신 실제 DOM 요소로 구현하여 webOS TV 호환성 확보 + * + * @param {boolean} launchedFromPlayer - PlayerPanel에서 진입했는지 여부 + * - true: PlayerPanel의 MEDIA 재생 완료 후 진입 (updatePanel로 전달됨) + * - false/undefined: 다른 패널(Shop Now, You May Like 등)에서 진입 + * - 이 값에 따라 배경 UI를 다르게 표시할 수 있음 */ -export default function DetailPanelBackground() { +export default function DetailPanelBackground({ launchedFromPlayer = false }) { useEffect(() => { console.log('[DetailPanelBackground] 배경 이미지 경로:', detailPanelBg); - }, []); + console.log('[DetailPanelBackground] launchedFromPlayer:', launchedFromPlayer); + }, [launchedFromPlayer]); return (
{/* 실제 배경 이미지 */} - {/* console.log('[DetailPanelBackground] 이미지 로드 완료')} - onError={(e) => console.error('[DetailPanelBackground] 이미지 로드 실패:', e)} - /> */} + {!launchedFromPlayer && ( + console.log('[DetailPanelBackground] 이미지 로드 완료')} + onError={(e) => console.error('[DetailPanelBackground] 이미지 로드 실패:', e)} + /> + )} {/* 그라데이션 레이어들 - CSS의 linear-gradient를 div로 구현 */} {/* 1. 270도 방향 그라데이션 (왼쪽→오른쪽, 투명→불투명) */} diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx index 736a1c00..cf9679a0 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx @@ -39,6 +39,7 @@ import { pauseModalVideo, resumeModalVideo, } 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'; @@ -212,6 +213,14 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props 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); @@ -981,10 +990,22 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props panelInfo.shptmBanrTpNm, ]); + // 최상단 패널 정보 (여러 useMemo에서 공통으로 사용) + const topPanel = useMemo(() => { + return panels[panels.length - 1]; + }, [panels]); + + // 최상단 패널이 DetailPanel이고 PlayerPanel에서 진입했는지 확인 + const isTopPanelDetailFromPlayer = useMemo(() => { + return ( + topPanel?.name === panel_names.DETAIL_PANEL && + topPanel?.panelInfo?.launchedFromPlayer === true + ); + }, [topPanel]); + const cannotPlay = useMemo(() => { - const topPanel = panels[panels.length - 1]; return !isOnTop && topPanel?.name === panel_names.PLAYER_PANEL; - }, [panels, isOnTop]); + }, [topPanel, isOnTop]); const getPlayer = useCallback((ref) => { videoPlayer.current = ref; @@ -1886,12 +1907,48 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props }, timeout); }, []); + // Redux로 오버레이 숨김 useEffect(() => { - if (isOnTop && !panelInfo.modal && !videoVerticalVisible) { + if (shouldHideOverlays) { + console.log('[PlayerPanel] shouldHideOverlays true - 오버레이 숨김'); + setSideContentsVisible(false); + setBelowContentsVisible(false); + + if (videoPlayer.current?.hideControls) { + videoPlayer.current.hideControls(); + } + + dispatch(resetPlayerOverlays()); + } + }, [shouldHideOverlays, dispatch]); + + // Redux로 오버레이 표시 + useEffect(() => { + if (shouldShowOverlays) { + console.log('[PlayerPanel] shouldShowOverlays true - 오버레이 표시'); setSideContentsVisible(true); setBelowContentsVisible(true); + + if (videoPlayer.current?.showControls) { + videoPlayer.current.showControls(); + } + + dispatch(resetPlayerOverlays()); } - }, [panelInfo.modal]); + }, [shouldShowOverlays, dispatch]); + + // PlayerPanel이 최상단이 될 때 오버레이 표시 (DetailPanel에서 복귀) + useEffect(() => { + if (isOnTop && !panelInfo.modal && !videoVerticalVisible) { + console.log('[PlayerPanel] isOnTop true - 오버레이 표시'); + setSideContentsVisible(true); + setBelowContentsVisible(true); + + if (videoPlayer.current?.showControls) { + videoPlayer.current.showControls(); + } + } + }, [isOnTop, panelInfo.modal, videoVerticalVisible]); useEffect(() => { // tabContainerVersion === 1일 때만 실행 @@ -2081,7 +2138,10 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props className={classNames( css.videoContainer, panelInfo.modal && css.modal, - !isOnTop && css.background, + // PlayerPanel이 최상단 아니고, 최상단이 DetailPanel(from Player)이면 비디오 보이도록 + !isOnTop && isTopPanelDetailFromPlayer && css['background-visible'], + // PlayerPanel이 최상단 아니고, 위 조건 아니면 1px로 숨김 + !isOnTop && !isTopPanelDetailFromPlayer && css.background, !captionEnable && css.hideSubtitle )} handleCancel={onClickBack} diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.jsx index eecff434..77dea663 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.jsx +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.jsx @@ -1,59 +1,36 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { - useDispatch, - useSelector, -} from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { Job } from '@enact/core/util'; -import SpotlightContainerDecorator - from '@enact/spotlight/SpotlightContainerDecorator'; -import { - getContainerNode, - setContainerLastFocusedElement, -} from '@enact/spotlight/src/container'; +import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; +import { getContainerNode, setContainerLastFocusedElement } from '@enact/spotlight/src/container'; import { sendLogTotalRecommend } from '../../../../actions/logActions'; import { pushPanel } from '../../../../actions/panelActions'; +import { hidePlayerOverlays } from '../../../../actions/videoPlayActions'; import TItemCard, { TYPES } from '../../../../components/TItemCard/TItemCard'; -import TVirtualGridList - from '../../../../components/TVirtualGridList/TVirtualGridList'; +import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList'; import useScrollTo from '../../../../hooks/useScrollTo'; -import { - LOG_CONTEXT_NAME, - LOG_MENU, - LOG_MESSAGE_ID, - panel_names, -} from '../../../../utils/Config'; +import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../../../utils/Config'; import { scaleH } from '../../../../utils/helperMethods'; -import ListEmptyContents - from '../TabContents/ListEmptyContents/ListEmptyContents'; +import ListEmptyContents from '../TabContents/ListEmptyContents/ListEmptyContents'; import css1 from './ShopNowContents.module.less'; import cssV2 from './ShopNowContents.v2.module.less'; const extractPriceInfo = (priceInfo) => { - if (!priceInfo) - return { originalPrice: "", discountedPrice: "", discountRate: "" }; + if (!priceInfo) return { originalPrice: '', discountedPrice: '', discountRate: '' }; - const parts = priceInfo.split("|").map((part) => part.trim()); + const parts = priceInfo.split('|').map((part) => part.trim()); return { - originalPrice: parts[0] || "", - discountedPrice: parts[1] || "", - discountRate: parts[4] || "", + originalPrice: parts[0] || '', + discountedPrice: parts[1] || '', + discountRate: parts[4] || '', }; }; -const Container = SpotlightContainerDecorator( - { enterTo: "last-focused" }, - "div" -); +const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); export default function ShopNowContents({ shopNowInfo, videoVerticalVisible, @@ -63,7 +40,7 @@ export default function ShopNowContents({ panelInfo, tabTitle, version = 1, - direction = "vertical", + direction = 'vertical', }) { const css = version === 2 ? cssV2 : css1; const { getScrollTo, scrollTop } = useScrollTo(); @@ -88,7 +65,7 @@ export default function ShopNowContents({ useEffect(() => { return () => { - const gridListId = "playVideoShopNowBox"; + const gridListId = 'playVideoShopNowBox'; const girdList = getContainerNode(gridListId); if (girdList) setContainerLastFocusedElement(null, [gridListId]); @@ -141,8 +118,7 @@ export default function ShopNowContents({ } = shopNowInfo[index]; // 미리 계산된 가격 정보를 사용 - const { originalPrice, discountedPrice, discountRate } = - priceInfoMap[index] || {}; + const { originalPrice, discountedPrice, discountRate } = priceInfoMap[index] || {}; const handleItemClick = () => { const params = { @@ -160,6 +136,9 @@ export default function ShopNowContents({ }; dispatch(sendLogTotalRecommend(params)); + // DetailPanel push 전에 VideoPlayer 오버레이 숨김 + dispatch(hidePlayerOverlays()); + dispatch( pushPanel({ name: panel_names.DETAIL_PANEL, @@ -168,7 +147,7 @@ export default function ShopNowContents({ showId: playListInfo?.showId, liveFlag: playListInfo?.liveFlag, thumbnailUrl: playListInfo?.thumbnailUrl, - liveReqFlag: panelInfo?.shptmBanrTpNm === "LIVE" && "Y", + liveReqFlag: panelInfo?.shptmBanrTpNm === 'LIVE' && 'Y', patnrId, prdtId, launchedFromPlayer: true, @@ -221,9 +200,7 @@ export default function ShopNowContents({ itemWidth={version === 2 ? 310 : videoVerticalVisible ? 540 : 600} itemHeight={version === 2 ? 445 : 236} spacing={version === 2 ? 30 : 12} - className={ - videoVerticalVisible ? css.verticalItemList : css.itemList - } + className={videoVerticalVisible ? css.verticalItemList : css.itemList} noScrollByWheel={false} spotlightId="playVideoShopNowBox" />