[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:
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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로 복원
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user