[251115] fix: ProductVideo.v3.jsx 전체화면에서 모달복귀 포커스 문제

🕐 커밋 시간: 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: 코드 최적화로 성능 개선 기대
This commit is contained in:
2025-11-15 14:15:28 +09:00
parent d1f63ee402
commit e474ac3ef2
6 changed files with 91 additions and 131 deletions

View File

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

View File

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

View File

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

View File

@@ -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(() => {

View File

@@ -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로 복원

View File

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