[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:
@@ -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,
|
||||
});
|
||||
|
||||
@@ -2582,6 +2582,7 @@ const VideoPlayer = ApiDecorator(
|
||||
'showControls',
|
||||
'showFeedback',
|
||||
'toggleControls',
|
||||
'onVideoClick',
|
||||
],
|
||||
},
|
||||
I18nContextDecorator(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -462,7 +462,9 @@ export default function ProductAllSection({
|
||||
);
|
||||
|
||||
const handleButtonFocus = useCallback((buttonType) => {
|
||||
if (activeButton !== buttonType) {
|
||||
setActiveButton(buttonType);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleButtonBlur = useCallback(() => {
|
||||
|
||||
@@ -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
|
||||
{!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도 방향 그라데이션 (왼쪽→오른쪽, 투명→불투명) */}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}, [panelInfo.modal]);
|
||||
|
||||
dispatch(resetPlayerOverlays());
|
||||
}
|
||||
}, [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}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user