From e474ac3ef266a9baf7954377f79236ba14f25fe4 Mon Sep 17 00:00:00 2001 From: optrader Date: Sat, 15 Nov 2025 14:15:28 +0900 Subject: [PATCH] =?UTF-8?q?[251115]=20fix:=20ProductVideo.v3.jsx=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=EB=B3=B5=EA=B7=80=20=ED=8F=AC=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=20=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🕐 커밋 시간: 2025. 11. 15. 14:15:28 📊 변경 통계: • 총 파일: 6개 • 추가: +90줄 • 삭제: -131줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/App/App.js ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less ~ 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/ProductContentSection/ProductVideo/ProductVideo.v3.jsx ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/App/App.js (javascript): ✅ Added: resolveSpotlightIdFromEvent() 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx (javascript): 🔄 Modified: extractProductMeta() 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v3.jsx (javascript): 🔄 Modified: Spottable() 📄 com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx (javascript): 🔄 Modified: normalizeModalStyle() 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • UI 컴포넌트 아키텍처 개선 Performance: 코드 최적화로 성능 개선 기대 --- com.twin.app.shoptime/src/App/App.js | 18 +++ .../VideoPlayer/VideoPlayer.v3.module.less | 4 +- .../src/views/DetailPanel/DetailPanel.jsx | 9 +- .../ProductAllSection/ProductAllSection.jsx | 109 ++++-------------- .../ProductVideo/ProductVideo.v3.jsx | 17 ++- .../src/views/MediaPanel/MediaPanel.v3.jsx | 65 +++++------ 6 files changed, 91 insertions(+), 131 deletions(-) diff --git a/com.twin.app.shoptime/src/App/App.js b/com.twin.app.shoptime/src/App/App.js index 7eb37315..01f13fdc 100644 --- a/com.twin.app.shoptime/src/App/App.js +++ b/com.twin.app.shoptime/src/App/App.js @@ -421,6 +421,24 @@ const resolveSpotlightIdFromEvent = (event) => { return undefined; }; +// Spotlight Focus 추적 로그 [251115] +// DOM 이벤트 리스너로 대체 + +document.addEventListener('focusin', (ev) => { + console.log('[SPOTLIGHT FOCUS-IN]', ev.target); +}); + +document.addEventListener('focusout', (ev) => { + console.log('[SPOTLIGHT FOCUS-OUT]', ev.target); +}); + +// Spotlight 커스텀 이벤트가 있다면 추가 +if (typeof Spotlight !== 'undefined' && Spotlight.addEventListener) { + Spotlight.addEventListener('focus', (ev) => { + console.log('[SPOTLIGHT: focus]', ev.target); + }); +} + function AppBase(props) { const dispatch = useDispatch(); const httpHeader = useSelector((state) => state.common.httpHeader); diff --git a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less index a74c8dab..9e2af48c 100644 --- a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less +++ b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less @@ -26,8 +26,8 @@ } .media { - height: calc(100% - 4px); - width: calc(100% - 4px); + height: var(--media-height, calc(100% - 4px)); + width: var(--media-width, calc(100% - 4px)); background: #000; &.mediaBackground { diff --git a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx index 9773a2c8..7b6f0a3e 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx @@ -720,16 +720,15 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { const topPanel = panels[panels.length - 1]; // MediaPanel이 modal=true로 복귀했을 때 포커스를 ProductVideo로 이동 + // 하지만 MediaPanel에서 이미 포커스를 시도하므로 여기서는 보조 역할만 함 if ( topPanel && topPanel.name === panel_names.MEDIA_PANEL && topPanel.panelInfo.modal === true ) { - console.log('[DetailPanel] MediaPanel modal=true detected - focusing ProductVideo'); - const focusTimer = setTimeout(() => { - Spotlight.focus('product-video-player'); - }, 2500); - return () => clearTimeout(focusTimer); + console.log('[DetailPanel] MediaPanel modal=true detected - will not interfere with focus'); + // MediaPanel의 포커스 이동을 방해하지 않기 위해 아무것도 하지 않음 + return; } }, [panels]); 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 dacf4177..23cd5437 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx @@ -341,60 +341,21 @@ export default function ProductAllSection({ dispatch(resetShowAllReviews()); }, []); // 빈 dependency array = 마운트 시에만 실행 - // 임시: 무조건 1.5초 후에 product-video-player에 포커스 - useEffect(() => { - console.log( - '[ProductAllSection] 포커스 시도 전 - hasVideo:', - hasVideo, - 'productVideoVersion:', - productVideoVersion - ); - const timer = setTimeout(() => { - console.log('[ProductAllSection] 포커스 호출 시도: product-video-player'); - - // DOM에 요소가 존재하는지 확인 - const element = - document.querySelector('[data-spotlight-id="product-video-player"]') || - document.getElementById('product-video-player') || - document.querySelector('[spotlight-id="product-video-player"]'); - - console.log('[ProductAllSection] DOM 요소 확인:', { - element: element, - elementExists: !!element, - elementTag: element?.tagName, - elementId: element?.id, - elementSpotlightId: - element?.getAttribute('data-spotlight-id') || element?.getAttribute('spotlight-id'), - }); - - try { - Spotlight.focus('product-video-player'); - console.log('[ProductAllSection] 포커스 호출 성공'); - - // 포커스 후 현재 포커스된 요소 확인 - setTimeout(() => { - const activeElement = document.activeElement; - console.log('[ProductAllSection] 포커스 후 activeElement:', { - activeElement: activeElement, - activeElementTag: activeElement?.tagName, - activeElementId: activeElement?.id, - activeElementSpotlightId: - activeElement?.getAttribute('data-spotlight-id') || - activeElement?.getAttribute('spotlight-id'), - }); - }, 100); - } catch (error) { - console.error('[ProductAllSection] 포커스 호출 실패:', error); - } - }, 1500); // 1.5초 = 1500ms - - return () => { - clearTimeout(timer); - }; - }, []); - // 이미 useReviews 훅에서 동일한 기능을 수행하고 있음 - // ProductAllSection에서 중복으로 호출하면 UserReviewPanel 진입 시 - // reviewListData가 반복적으로 초기화되어 Chrome에서 진입 불가 발생 + // [251115] 주석 처리: MediaPanel에서 이미 포커스 이동을 처리하므로 + // ProductAllSection의 자동 포커스는 포커스 탈취를 일으킬 수 있음 + // useEffect(() => { + // console.log( + // '[ProductAllSection] 포커스 시도 전 - hasVideo:', + // hasVideo, + // 'productVideoVersion:', + // productVideoVersion + // ); + // const timer = setTimeout(() => { + // console.log('[ProductAllSection] 포커스 호출 시도: product-video-player'); + // ... + // }, 1500); + // return () => clearTimeout(timer); + // }, []); // BUY NOW 버튼 클릭 핸들러 - Toast로 BuyOption 표시 const handleBuyNowClick = useCallback( @@ -897,37 +858,15 @@ export default function ProductAllSection({ // Redux에서 panels 상태 가져오기 const panels = useSelector((state) => state.panels.panels); - // MediaPanel의 전체화면 복귀 감지 및 포커스 복구 - useEffect(() => { - const topPanel = panels[panels.length - 1]; - const currentModalState = topPanel?.panelInfo?.modal; - - // 전체화면(false) → 모달(true)로 복귀하는 경우만 감지 - if ( - topPanel?.name === panel_names.MEDIA_PANEL && - currentModalState === true && - prevMediaPanelModalStateRef.current === false - ) { - console.log( - '[ProductAllSection] 🔄 MediaPanel이 전체화면에서 모달로 복귀 - ProductVideo로 포커스 복구 시도' - ); - const focusTimer = setTimeout(() => { - console.log('[ProductAllSection] MediaPanel 복귀 후 포커스 호출: product-video-player'); - try { - Spotlight.focus('product-video-player'); - console.log('[ProductAllSection] MediaPanel 복귀 후 포커스 호출 성공'); - } catch (error) { - console.error('[ProductAllSection] MediaPanel 복귀 후 포커스 호출 실패:', error); - } - }, 100); - return () => clearTimeout(focusTimer); - } - - // 현재 modal 상태 저장 - if (topPanel?.name === panel_names.MEDIA_PANEL) { - prevMediaPanelModalStateRef.current = currentModalState; - } - }, [panels]); + // [251115] 주석 처리: MediaPanel에서 이미 포커스 이동을 처리하므로 + // ProductAllSection의 자동 포커스는 포커스 탈취를 일으킬 수 있음 + // useEffect(() => { + // const topPanel = panels[panels.length - 1]; + // const currentModalState = topPanel?.panelInfo?.modal; + // if (topPanel?.name === panel_names.MEDIA_PANEL && ...) { + // Spotlight.focus('product-video-player'); + // } + // }, [panels]); // 컴포넌트 unmount 시 timer cleanup useEffect(() => { 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 2a60816f..bca55589 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 @@ -59,11 +59,20 @@ export default function ProductVideo({ prevModalStateRef.current === false ) { console.log('[ProductVideo] MediaPanel returned to modal - restoring focus to ProductVideo'); - const focusTimer = setTimeout(() => { - Spotlight.focus('product-video-player'); - prevModalStateRef.current = true; + + // VideoPlayer의 controlsHandleAbove가 자동으로 포커스를 빼앗지 않도록 + // 약간의 딜레이 후에 강제로 포커스 설정 + setTimeout(() => { + console.log('[ProductVideo] Forcing focus to product-video-player'); + const element = document.querySelector('[data-spotlight-id="product-video-player"]'); + if (element) { + // Spotlight 내부 포커스 강제 설정 + Spotlight.focus('product-video-player'); + console.log('[ProductVideo] Focus set to product-video-player'); + } }, 50); - return () => clearTimeout(focusTimer); + + prevModalStateRef.current = true; } // MediaPanel이 닫혔을 때 modalState를 true로 복원 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 532acc7f..b9f1186b 100644 --- a/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx +++ b/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx @@ -900,15 +900,8 @@ const MediaPanel = React.forwardRef( ); } - // 모달 복귀 시 ProductVideo로 포커스 이동 (ProductVideo.v3.jsx에서 처리) - console.log( - '[MediaPanel] Back button pressed - returning to modal, focus will be handled by ProductVideo' - ); - - setTimeout(() => { - console.log('[MediaPanel] focusPanel '); - dispatch(focusPanel('DETAIL_PANEL', 'product-video-player')); - }, 100); + // 모달 복귀 시 상태만 업데이트 - 포커스는 자연스럽게 ProductVideo로 + console.log('[MediaPanel] Back button pressed - returning to modal mode'); ev?.stopPropagation(); // ev?.preventDefault(); @@ -1688,19 +1681,19 @@ const MediaPanel = React.forwardRef( // MEDIA 타입일 때: panelInfo.showUrl 사용 if (panelInfo?.shptmBanrTpNm === 'MEDIA') { - console.log('[MediaPanel]-LoadingVideo 📺 MEDIA URL:', { - showUrl: panelInfo?.showUrl?.substring(0, 50), - prdtId: panelInfo?.prdtId, - }); + // console.log('[MediaPanel]-LoadingVideo 📺 MEDIA URL:', { + // showUrl: panelInfo?.showUrl?.substring(0, 50), + // prdtId: panelInfo?.prdtId, + // }); return panelInfo?.showUrl; } // 기타 타입: playListInfo 사용 const url = playListInfo && playListInfo[selectedIndex]?.showUrl; if (url) { - console.log('[MediaPanel]-LoadingVideo 🎬 PlayList URL:', { - url: url.substring(0, 50), - }); + // console.log('[MediaPanel]-LoadingVideo 🎬 PlayList URL:', { + // url: url.substring(0, 50), + // }); } return url; }, [playListInfo, selectedIndex, broadcast, panelInfo?.shptmBanrTpNm, panelInfo?.showUrl]); @@ -1722,17 +1715,17 @@ const MediaPanel = React.forwardRef( const isReadyToPlay = useMemo(() => { if (!currentPlayingUrl) { - console.log('[MediaPanel]-LoadingVideo ❌ isReadyToPlay = false (no URL)'); + // console.log('[MediaPanel]-LoadingVideo ❌ isReadyToPlay = false (no URL)'); return false; } if (!Config.DEBUG_VIDEO_SUBTITLE_TEST && currentSubtitleUrl && !currentSubtitleBlob) { - console.log('[MediaPanel]-LoadingVideo ❌ isReadyToPlay = false (subtitle not loaded):', { - currentSubtitleUrl, - currentSubtitleBlob: !!currentSubtitleBlob, - }); + // console.log('[MediaPanel]-LoadingVideo ❌ isReadyToPlay = false (subtitle not loaded):', { + // currentSubtitleUrl, + // currentSubtitleBlob: !!currentSubtitleBlob, + // }); return false; } - console.log('[MediaPanel]-LoadingVideo ✅ isReadyToPlay = true'); + // console.log('[MediaPanel]-LoadingVideo ✅ isReadyToPlay = true'); return true; }, [currentPlayingUrl, currentSubtitleUrl, currentSubtitleBlob, broadcast]); @@ -2213,18 +2206,18 @@ const MediaPanel = React.forwardRef( > {(() => { if (isReadyToPlay) { - console.log('[MediaPanel]-LoadingVideo 🎬 VideoPlayer 렌더링:', { - src: currentPlayingUrl?.substring(0, 50), - disabled: panelInfo.modal, - cannotPlay, - isYoutube, - videoComponent: - (typeof window === 'object' && !window.PalmSystem) || isYoutube - ? 'TReactPlayer' - : 'Media', - }); + // console.log('[MediaPanel]-LoadingVideo 🎬 VideoPlayer 렌더링:', { + // src: currentPlayingUrl?.substring(0, 50), + // disabled: panelInfo.modal, + // cannotPlay, + // isYoutube, + // videoComponent: + // (typeof window === 'object' && !window.PalmSystem) || isYoutube + // ? 'TReactPlayer' + // : 'Media', + // }); } else { - console.log('[MediaPanel]-LoadingVideo 🚫 VideoPlayer 렌더링 스킵됨'); + // console.log('[MediaPanel]-LoadingVideo 🚫 VideoPlayer 렌더링 스킵됨'); } return null; })()} @@ -2237,13 +2230,15 @@ const MediaPanel = React.forwardRef( noAutoPlay={false} autoCloseTimeout={3000} onBackButton={handleClickBack} - spotlightDisabled={false} + spotlightDisabled={panelInfo.modal} isYoutube={isYoutube} src={currentPlayingUrl} style={panelInfo.modal ? modalStyle : {}} modalScale={panelInfo.modal ? modalScale : 1} modalClassName={panelInfo.modal && panelInfo.modalClassName} - spotlightId={panelInfo.modalContainerId || spotlightId} + spotlightId={ + panelInfo.modal ? undefined : panelInfo.modalContainerId || spotlightId + } handleIndicatorDownClick={handleIndicatorDownClick} handleIndicatorUpClick={handleIndicatorUpClick} onError={mediainfoHandler}