From 8644527502b3e608827fe973e14c3197193d5cde Mon Sep 17 00:00:00 2001 From: optrader Date: Thu, 20 Nov 2025 12:29:36 +0900 Subject: [PATCH] [251120] fix: PlayerPanel Return Video Playback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🕐 커밋 시간: 2025. 11. 20. 12:29:35 📊 변경 통계: • 총 파일: 5개 • 추가: +270줄 • 삭제: -97줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/actions/panelActions.js ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx ~ com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.jsx 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • 대규모 기능 개발 --- .../src/actions/panelActions.js | 24 ++ .../src/views/DetailPanel/DetailPanel.jsx | 247 +++++++++++++----- .../src/views/HomePanel/HomePanel.jsx | 28 +- .../src/views/PlayerPanel/PlayerPanel.jsx | 42 ++- .../TabContents/ShopNowContents.jsx | 22 +- 5 files changed, 268 insertions(+), 95 deletions(-) diff --git a/com.twin.app.shoptime/src/actions/panelActions.js b/com.twin.app.shoptime/src/actions/panelActions.js index 677d440f..14038a0a 100644 --- a/com.twin.app.shoptime/src/actions/panelActions.js +++ b/com.twin.app.shoptime/src/actions/panelActions.js @@ -17,6 +17,8 @@ export const SOURCE_MENUS = { HOME_GENERAL: 'home_general', THEMED_PRODUCT: 'themed_product', GENERAL_PRODUCT: 'general_product', + PLAYER_SHOP_NOW: 'player_shop_now', // PlayerPanel의 ShopNow에서 진입 + PLAYER_MEDIA: 'player_media', // PlayerPanel의 Media에서 진입 }; /* @@ -218,6 +220,7 @@ export const navigateToDetail = ({ }, }) ); + panelInfo.sourcePanel = panel_names.HOME_PANEL; // ✅ source panel 정보 panelInfo.fromHome = true; break; } @@ -236,14 +239,35 @@ export const navigateToDetail = ({ }) ); } + panelInfo.sourcePanel = panel_names.SEARCH_PANEL; // ✅ source panel 정보 panelInfo.fromSearch = true; panelInfo.searchQuery = additionalInfo.searchVal; break; case SOURCE_MENUS.THEMED_PRODUCT: // 테마 상품: 별도 처리 필요할 경우 + panelInfo.sourcePanel = panel_names.HOME_PANEL; // ✅ source panel 정보 (HOME으로 간주) break; + case SOURCE_MENUS.PLAYER_SHOP_NOW: + case SOURCE_MENUS.PLAYER_MEDIA: { + // PlayerPanel에서 온 경우 + const { hidePlayerOverlays } = require('./videoPlayActions'); + + // DetailPanel push 전에 VideoPlayer 오버레이 숨김 + dispatch(hidePlayerOverlays()); + + // 현재 포커스된 요소 저장 + if (Object.keys(focusSnapshot).length > 0) { + panelInfo.lastFocusedTargetId = focusSnapshot.lastFocusedTargetId; + } + + // PlayerPanel 정보 보존 (복귀 시 필요) + panelInfo.sourcePanel = panel_names.PLAYER_PANEL; // ✅ source panel 정보 + panelInfo.fromPlayer = true; + break; + } + case SOURCE_MENUS.GENERAL_PRODUCT: default: // 일반 상품: 기본 처리 diff --git a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx index f8479fdb..7a569cf3 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx @@ -31,6 +31,8 @@ import { finishVideoPreview, pauseFullscreenVideo, resumeFullscreenVideo, + pauseModalVideo, + resumeModalVideo, } from '../../actions/playActions'; import { clearProductDetail, @@ -159,62 +161,175 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { })); }, [dispatch]); - // ✅ [251118] DetailPanel이 사라질 때 HomePanel 활성화 + // ✅ [251120] DetailPanel이 사라질 때 처리 - sourcePanel에 따라 switch 문으로 처리 useEffect(() => { return () => { + const sourcePanel = panelInfo?.sourcePanel; + const sourceMenu = panelInfo?.sourceMenu; + // DetailPanel이 unmount되는 시점 - console.log('[DetailPanel] unmount - HomePanel 활성화 신호 전송'); + console.log('[DetailPanel] unmount:', { + sourcePanel, + sourceMenu, + timestamp: Date.now(), + }); - // HomePanel에서 비디오 재생을 다시 시작하도록 신호 보내기 - console.log('[TRACE-GRADIENT] 🔶 DetailPanel unmount - Creating new panelInfo'); - console.log('[TRACE-GRADIENT] 🔶 DetailPanel unmount - Existing panelInfo before update:', JSON.stringify(panelInfo)); - - dispatch(updateHomeInfo({ - name: panel_names.HOME_PANEL, - panelInfo: { - shouldResumeVideo: true, // ✅ 신호 - lastDetailPanelClosed: Date.now(), // ✅ 시점 기록 - showGradientBackground: false, // ✅ 명시적으로 그라데이션 끔기 + // sourcePanel에 따른 상태 업데이트 + switch (sourcePanel) { + case panel_names.PLAYER_PANEL: { + // PlayerPanel에서 온 경우: PlayerPanel에 detailPanelClosed flag 전달 + console.log('[DetailPanel] unmount - PlayerPanel에 detailPanelClosed flag 전달'); + dispatch(updatePanel({ + name: panel_names.PLAYER_PANEL, + panelInfo: { + detailPanelClosed: true, // ✅ flag + detailPanelClosedAt: Date.now(), // ✅ 시점 기록 + detailPanelClosedFromSource: sourceMenu, // ✅ 출처 + } + })); + break; } - })); + + case panel_names.HOME_PANEL: { + // HomePanel에서 온 경우: HomePanel에 detailPanelClosed flag 전달 + console.log('[DetailPanel] unmount - HomePanel에 detailPanelClosed flag 전달'); + console.log('[TRACE-GRADIENT] 🔶 DetailPanel unmount - HomePanel 복귀'); + + dispatch(updateHomeInfo({ + name: panel_names.HOME_PANEL, + panelInfo: { + detailPanelClosed: true, // ✅ flag + detailPanelClosedAt: Date.now(), // ✅ 시점 기록 + detailPanelClosedFromSource: sourceMenu, // ✅ 출처 + showGradientBackground: false, // ✅ 명시적으로 그라데이션 끔기 + } + })); + break; + } + + case panel_names.SEARCH_PANEL: { + // SearchPanel에서 온 경우: SearchPanel에 detailPanelClosed flag 전달 + console.log('[DetailPanel] unmount - SearchPanel에 detailPanelClosed flag 전달'); + dispatch( + updatePanel({ + name: panel_names.SEARCH_PANEL, + panelInfo: { + detailPanelClosed: true, // ✅ flag + detailPanelClosedAt: Date.now(), // ✅ 시점 기록 + detailPanelClosedFromSource: sourceMenu, // ✅ 출처 + }, + }) + ); + break; + } + + default: + console.warn('[DetailPanel] unmount - 처리되지 않은 sourcePanel:', sourcePanel); + break; + } }; - }, [dispatch]); + }, [dispatch, panelInfo?.sourcePanel]); const onBackClick = useCallback( (isCancelClick) => (ev) => { + const sourcePanel = panelInfo?.sourcePanel; + const sourceMenu = panelInfo?.sourceMenu; + fp.pipe( () => { dispatch(clearAllToasts()); // BuyOption Toast 포함 모든 토스트 제거 - dispatch(pauseFullscreenVideo()); // PLAYER_PANEL 비디오 중지 - dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료 - dispatch(finishVideoPreview()); + + // sourcePanel에 따른 사전 처리 + switch (sourcePanel) { + case panel_names.PLAYER_PANEL: + // PlayerPanel에서 온 경우: 플레이어 비디오는 그대로 두고 모달만 정리 + console.log('[DetailPanel] onBackClick - PlayerPanel 출신: 모달 정리만 수행'); + dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료 + dispatch(finishVideoPreview()); + break; + + case panel_names.HOME_PANEL: + case panel_names.SEARCH_PANEL: + default: + // HomePanel, SearchPanel 등에서 온 경우: 백그라운드 비디오 일시 중지 + console.log('[DetailPanel] onBackClick - source panel:', sourcePanel, '백그라운드 비디오 일시 중지'); + dispatch(pauseFullscreenVideo()); // PLAYER_PANEL 비디오 중지 + dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료 + dispatch(finishVideoPreview()); + break; + } + dispatch(popPanel(panel_names.DETAIL_PANEL)); }, () => { - // 패널 업데이트 조건 체크 - const shouldUpdatePanel = - fp.pipe( - () => panels, - fp.get('length'), - (length) => length === 4 - )() && - fp.pipe( - () => panels, - fp.get('1.name'), - (name) => name === panel_names.PLAYER_PANEL - )(); + // sourcePanel에 따른 상태 업데이트 + switch (sourcePanel) { + case panel_names.PLAYER_PANEL: { + // PlayerPanel에서 온 경우: PlayerPanel에 detailPanelClosed flag 전달 + const shouldUpdatePanel = + fp.pipe( + () => panels, + fp.get('length'), + (length) => length === 3 // PlayerPanel이 [1]에 있고 DetailPanel이 [2]에 있는 상태 + )() && + fp.pipe( + () => panels, + fp.get('1.name'), + (name) => name === panel_names.PLAYER_PANEL + )(); - if (shouldUpdatePanel) { - dispatch( - updatePanel({ - name: panel_names.PLAYER_PANEL, + if (shouldUpdatePanel) { + console.log('[DetailPanel] onBackClick - PlayerPanel에 detailPanelClosed flag 전달'); + dispatch( + updatePanel({ + name: panel_names.PLAYER_PANEL, + panelInfo: { + thumbnail: fp.pipe(() => panelInfo, fp.get('thumbnailUrl'))(), + detailPanelClosed: true, // ✅ flag + detailPanelClosedAt: Date.now(), // ✅ 시점 기록 + detailPanelClosedFromSource: sourceMenu, // ✅ 출처 + }, + }) + ); + } + break; + } + + case panel_names.HOME_PANEL: { + // HomePanel에서 온 경우: HomePanel에 detailPanelClosed flag 전달 + console.log('[DetailPanel] onBackClick - HomePanel에 detailPanelClosed flag 전달'); + dispatch(updateHomeInfo({ + name: panel_names.HOME_PANEL, panelInfo: { - thumbnail: fp.pipe(() => panelInfo, fp.get('thumbnailUrl'))(), - }, - }) - ); + detailPanelClosed: true, // ✅ flag + detailPanelClosedAt: Date.now(), // ✅ 시점 기록 + detailPanelClosedFromSource: sourceMenu, // ✅ 출처 + showGradientBackground: false, + } + })); + break; + } + + case panel_names.SEARCH_PANEL: { + // SearchPanel에서 온 경우: SearchPanel에 detailPanelClosed flag 전달 + console.log('[DetailPanel] onBackClick - SearchPanel에 detailPanelClosed flag 전달'); + dispatch( + updatePanel({ + name: panel_names.SEARCH_PANEL, + panelInfo: { + detailPanelClosed: true, // ✅ flag + detailPanelClosedAt: Date.now(), // ✅ 시점 기록 + detailPanelClosedFromSource: sourceMenu, // ✅ 출처 + }, + }) + ); + break; + } + + default: + console.warn('[DetailPanel] onBackClick - 처리되지 않은 sourcePanel:', sourcePanel); + break; } - // PlayerPanel의 isOnTop useEffect가 자동으로 오버레이 표시 } )(); @@ -725,46 +840,48 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { // 백그라운드 전체화면 비디오 제어: DetailPanel 진입/퇴장 시 useEffect(() => { - // console.log('[BgVideo] DetailPanel mounted - checking panels:', { - // panelsCount: panels?.length, - // panels: panels?.map(p => ({ name: p.name, modal: p.panelInfo?.modal })) - // }); - - // 전체화면 PlayerPanel(modal=false)이 존재하는지 확인 - const hasFullscreenPlayerPanel = fp.pipe( - () => panels, - (panelList) => - panelList.some( - (panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal - ) - )(); + // PlayerPanel이 존재하는지 확인 (Modal 또는 Fullscreen) + const playerPanel = panels.find( + (panel) => panel.name === panel_names.PLAYER_PANEL + ); + const hasPlayerPanel = !!playerPanel; + const isModal = playerPanel?.panelInfo?.modal; // ProductAllSection에 비디오가 있는지 확인 const hasProductVideo = fp.pipe(() => productData, fp.get('prdtMediaUrl'), fp.isNotNil)(); - // console.log('[BgVideo] hasFullscreenPlayerPanel:', hasFullscreenPlayerPanel); - // console.log('[BgVideo] hasProductVideo:', hasProductVideo); + console.log('[BgVideo] DetailPanel - Video Control Check:', { + hasPlayerPanel, + isModal, + hasProductVideo, + sourceMenu: panelInfo?.sourceMenu + }); - // 전체화면 PlayerPanel이 있고, 제품에 비디오가 있을 때만 백그라운드 비디오 멈춤 - if (hasFullscreenPlayerPanel && hasProductVideo) { - // console.log('[BgVideo] DetailPanel - Product has video, dispatching pauseFullscreenVideo()'); - dispatch(pauseFullscreenVideo()); + // PlayerPanel이 있고, 제품에 비디오가 있을 때만 비디오 멈춤 + if (hasPlayerPanel && hasProductVideo) { + console.log('[BgVideo] DetailPanel - Pausing video'); + if (isModal) { + dispatch(pauseModalVideo()); + } else { + dispatch(pauseFullscreenVideo()); + } } else { - console.log('[BgVideo] DetailPanel - Skipping pause:', { - reason: !hasFullscreenPlayerPanel ? 'no fullscreen PlayerPanel' : 'no product video', - }); + console.log('[BgVideo] DetailPanel - Skipping pause'); } return () => { // DetailPanel 언마운트 시: 비디오가 있었고 멈췄던 경우만 재생 재개 - // console.log('[BgVideo] DetailPanel unmounting'); - if (hasFullscreenPlayerPanel && hasProductVideo) { - // console.log('[BgVideo] DetailPanel - Product had video, dispatching resumeFullscreenVideo()'); - dispatch(resumeFullscreenVideo()); + if (hasPlayerPanel && hasProductVideo) { + console.log('[BgVideo] DetailPanel - Resuming video'); + if (isModal) { + dispatch(resumeModalVideo()); + } else { + dispatch(resumeFullscreenVideo()); + } } }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); // 마운트/언마운트 시에만 실행 + }, [panelInfo?.sourceMenu, productData?.prdtMediaUrl]); // MediaPanel modal 상태 변화 감지 -> ProductVideo로 포커스 이동 useEffect(() => { diff --git a/com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx b/com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx index d264ad25..4675a99a 100644 --- a/com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx +++ b/com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx @@ -809,17 +809,20 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => { console.log('[HomeActive] 재생 기록 업데이트:', bannerId); }, [isOnTop, dispatch]); - // ✅ [251118] DetailPanel 닫힘 감지 useEffect + // ✅ [251120] DetailPanel 닫힘 감지 useEffect - detailPanelClosed flag 사용 + const detailPanelClosed = useSelector( + (state) => state.home.homeInfo?.panelInfo?.detailPanelClosed + ); const detailPanelClosedTime = useSelector( - (state) => state.home.homeInfo?.panelInfo?.lastDetailPanelClosed + (state) => state.home.homeInfo?.panelInfo?.detailPanelClosedAt ); useEffect(() => { - if (detailPanelClosedTime && isOnTop) { - // if (isOnTop) { - console.log('[TRACE-GRADIENT] 🔄 lastDetailPanelClosed triggered - HomePanel reactivated'); + if (detailPanelClosed && isOnTop) { + console.log('[TRACE-GRADIENT] 🔄 detailPanelClosed flag triggered - HomePanel reactivated'); console.log('[HomePanel] *** ✅ HomePanel isOnTop = true'); - console.log('[HomePanel] *** lastDetailPanelClosed:', detailPanelClosedTime); + console.log('[HomePanel] *** detailPanelClosed:', detailPanelClosed); + console.log('[HomePanel] *** detailPanelClosedTime:', detailPanelClosedTime); console.log('[HomePanel] *** isOnTop:', isOnTop); console.log('[HomePanel] *** videoPlayIntentRef.current:', videoPlayIntentRef.current); console.log('[HomePanel] *** lastPlayedBannerIdRef.current:', lastPlayedBannerIdRef.current); @@ -931,9 +934,20 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => { // refs 초기화 videoPlayIntentRef.current = null; lastPlayedBannerIdRef.current = null; + + // detailPanelClosed 플래그 초기화 (다음 사이클에서 재사용 방지) + console.log('[HomePanel] *** detailPanelClosed flag 초기화'); + dispatch(updateHomeInfo({ + name: panel_names.HOME_PANEL, + panelInfo: { + detailPanelClosed: false, + detailPanelClosedAt: undefined, + detailPanelClosedFromSource: undefined, + } + })); } } - }, [detailPanelClosedTime, isOnTop, bannerDataList, dispatch]); + }, [detailPanelClosed, isOnTop, bannerDataList, dispatch]); useEffect(() => { return () => { diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx index ba7303b7..a2e162ce 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx @@ -38,6 +38,7 @@ import { startVideoPlayer, pauseModalVideo, resumeModalVideo, + resumeFullscreenVideo, } from '../../actions/playActions'; import { resetPlayerOverlays } from '../../actions/videoPlayActions'; import { convertUtcToLocal } from '../../components/MediaPlayer/util'; @@ -340,21 +341,46 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props // } // },[isOnTop, panelInfo]) - // PlayerPanel.jsx의 라인 313-327 useEffect 수정 + // PlayerPanel.jsx의 라인 313-327 useEffect 수정 - detailPanelClosed flag 감지 추가 useEffect(() => { console.log('[PlayerPanel] isOnTop useEffect:', { isOnTop, modal: panelInfo?.modal, isPaused: panelInfo?.isPaused, + detailPanelClosed: panelInfo?.detailPanelClosed, }); - 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 ← 이곳에서 resumeModalVideo 호출!'); - dispatch(resumeModalVideo()); + if (isOnTop) { + // 1. Resume Video if needed (isPaused or detailPanelClosed) + if (panelInfo.isPaused || panelInfo.detailPanelClosed) { + if (panelInfo.modal) { + console.log('[PlayerPanel] Back on top (Modal) - resuming video'); + dispatch(resumeModalVideo()); + } else { + console.log('[PlayerPanel] Back on top (Fullscreen) - resuming video'); + dispatch(resumeFullscreenVideo()); + } + } + + // 2. Reset detailPanelClosed flag + if (panelInfo.detailPanelClosed) { + console.log('[PlayerPanel] detailPanelClosed flag 초기화'); + dispatch( + updatePanel({ + name: panel_names.PLAYER_PANEL, + panelInfo: { + detailPanelClosed: false, + detailPanelClosedAt: undefined, + detailPanelClosedFromSource: undefined, + }, + }) + ); + } + } else { + // Not on top + if (panelInfo && panelInfo.modal) { + // console.log('[PlayerPanel] Not on top - pausing video'); + // dispatch(pauseModalVideo()); } } }, [isOnTop, panelInfo]); 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 c0bb83a3..4ac2b85b 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 @@ -9,7 +9,7 @@ import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDeco import { getContainerNode, setContainerLastFocusedElement } from '@enact/spotlight/src/container'; import { sendLogTotalRecommend } from '../../../../actions/logActions'; -import { pushPanel } from '../../../../actions/panelActions'; +import { navigateToDetail, SOURCE_MENUS, pushPanel } from '../../../../actions/panelActions'; import { hidePlayerOverlays } from '../../../../actions/videoPlayActions'; import TItemCard, { TYPES } from '../../../../components/TItemCard/TItemCard'; import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList'; @@ -151,26 +151,18 @@ export default function ShopNowContents({ const isYouMayLikeStart = shopNowInfo && index === shopNowInfo.length; const handleItemClick = () => { - // 현재 포커스된 요소의 spotlightId 저장 - const currentFocusedElement = Spotlight.getCurrent(); - const currentSpotlightId = currentFocusedElement?.getAttribute('data-spotlight-id'); - console.log('[ShopNowContents] 현재 포커스된 spotlightId:', currentSpotlightId); - - // DetailPanel push 전에 VideoPlayer 오버레이 숨김 - dispatch(hidePlayerOverlays()); + console.log('[ShopNowContents] DetailPanel 진입 - sourceMenu:', SOURCE_MENUS.PLAYER_SHOP_NOW); dispatch( - pushPanel({ - name: panel_names.DETAIL_PANEL, - panelInfo: { + navigateToDetail({ + patnrId, + prdtId, + sourceMenu: SOURCE_MENUS.PLAYER_SHOP_NOW, + additionalInfo: { showNm: playListInfo?.showNm, showId: playListInfo?.showId, liveFlag: playListInfo?.liveFlag, thumbnailUrl: playListInfo?.thumbnailUrl, - patnrId, - prdtId, - launchedFromPlayer: true, - lastFocusedTargetId: currentSpotlightId, // 현재 포커스된 spotlightId 저장 }, }) );