[251012] fix: DetailPanel Background Video Play

🕐 커밋 시간: 2025. 10. 12. 15:20:05

📊 변경 통계:
  • 총 파일: 8개
  • 추가: +142줄
  • 삭제: -18줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/videoPlayActions.js
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js
  ~ com.twin.app.shoptime/src/reducers/videoPlayReducer.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/actions/videoPlayActions.js (javascript):
     Added: curry()
  📄 com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js (javascript):
    🔄 Modified: extends()
  📄 com.twin.app.shoptime/src/reducers/videoPlayReducer.js (javascript):
     Added: curry(), videoPlayReducer()
  📄 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx (javascript):
    🔄 Modified: extractProductMeta()

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • UI 컴포넌트 아키텍처 개선
This commit is contained in:
2025-10-12 15:20:08 +09:00
parent 7989e1c14e
commit 7f1f3100d8
8 changed files with 160 additions and 64 deletions

View File

@@ -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,
});

View File

@@ -2582,6 +2582,7 @@ const VideoPlayer = ApiDecorator(
'showControls',
'showFeedback',
'toggleControls',
'onVideoClick',
],
},
I18nContextDecorator(

View File

@@ -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;

View File

@@ -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 (
<div ref={containerRef}>
{/* 배경 이미지 및 그라데이션 컴포넌트 - 모든 콘텐츠 뒤에 렌더링 */}
<DetailPanelBackground />
{/* launchedFromPlayer: PlayerPanel에서 진입 시 true, 다른 패널에서 진입 시 false/undefined */}
<DetailPanelBackground launchedFromPlayer={panelLaunchedFromPlayer} />
<TPanel
isTabActivated={false}

View File

@@ -462,7 +462,9 @@ export default function ProductAllSection({
);
const handleButtonFocus = useCallback((buttonType) => {
setActiveButton(buttonType);
if (activeButton !== buttonType) {
setActiveButton(buttonType);
}
}, []);
const handleButtonBlur = useCallback(() => {

View File

@@ -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 (
<div className={css.backgroundContainer}>
{/* 실제 배경 이미지 */}
{/* <img
src={detailPanelBg}
alt=""
className={css.backgroundImage}
aria-hidden="true"
onLoad={() => console.log('[DetailPanelBackground] 이미지 로드 완료')}
onError={(e) => console.error('[DetailPanelBackground] 이미지 로드 실패:', e)}
/> */}
{!launchedFromPlayer && (
<img
src={detailPanelBg}
alt=""
className={css.backgroundImage}
aria-hidden="true"
onLoad={() => console.log('[DetailPanelBackground] 이미지 로드 완료')}
onError={(e) => console.error('[DetailPanelBackground] 이미지 로드 실패:', e)}
/>
)}
{/* 그라데이션 레이어들 - CSS의 linear-gradient를 div로 구현 */}
{/* 1. 270도 방향 그라데이션 (왼쪽→오른쪽, 투명→불투명) */}

View File

@@ -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}

View File

@@ -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"
/>