[251113] fix: ProductAllSection ProductVideo version=1

🕐 커밋 시간: 2025. 11. 13. 20:45:31

📊 변경 통계:
  • 총 파일: 8개
  • 추가: +110줄
  • 삭제: -39줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/mediaActions.js
  ~ com.twin.app.shoptime/src/actions/playActions.js
  ~ com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.v2.jsx
  ~ 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.jsx
  ~ com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • UI 컴포넌트 아키텍처 개선
  • 중간 규모 기능 개선
  • 모듈 구조 개선
This commit is contained in:
2025-11-13 20:45:32 +09:00
parent 63ab5e2015
commit 1bf490c46c
8 changed files with 110 additions and 39 deletions

View File

@@ -90,18 +90,18 @@ export const finishMediaPreview = () => (dispatch, getState) => {
};
/**
* 강제로 modal MediaPanel을 종료합니다 (스택 어디에 있든)
* 강제로 DetailPanel ProductVideo MediaPanel을 종료합니다 (modal/fullscreen 모두)
*/
export const finishModalMediaForce = () => (dispatch, getState) => {
const panels = getState().panels.panels;
const hasModalMediaPanel = panels.some(
(panel) => panel.name === panel_names.MEDIA_PANEL && panel.panelInfo?.modal
const hasProductVideoPanel = panels.some(
(panel) =>
panel.name === panel_names.MEDIA_PANEL &&
(panel.panelInfo?.modal || panel.panelInfo?.modalContainerId === 'product-video-player')
);
if (hasModalMediaPanel) {
// console.log('[finishModalMediaForce] Force closing modal MediaPanel');
if (hasProductVideoPanel) {
if (startMediaFocusTimer) {
clearTimeout(startMediaFocusTimer);
startMediaFocusTimer = null;

View File

@@ -326,10 +326,10 @@ export const shrinkVideoTo1px = () => (dispatch, getState) => {
},
};
console.log('[HomePanel] shrinkVideoTo1px: saving shrinkInfo', {
shrinkInfo: updatedPlayerState.shrinkInfo,
modalStyle: panelInfo.modalStyle,
});
// console.log('[HomePanel] shrinkVideoTo1px: saving shrinkInfo', {
// shrinkInfo: updatedPlayerState.shrinkInfo,
// modalStyle: panelInfo.modalStyle,
// });
dispatch(
updatePanel({
@@ -359,11 +359,11 @@ export const expandVideoFrom1px = () => (dispatch, getState) => {
const panelInfo = shrunkModalPlayerPanel.panelInfo;
const shrinkInfo = panelInfo.playerState?.shrinkInfo;
console.log('[HomePanel] expandVideoFrom1px: expanding video', {
hasShrinkInfo: !!shrinkInfo,
hasModalStyle: !!shrinkInfo?.modalStyle,
hasModalContainerId: !!shrinkInfo?.modalContainerId,
});
// console.log('[HomePanel] expandVideoFrom1px: expanding video', {
// hasShrinkInfo: !!shrinkInfo,
// hasModalStyle: !!shrinkInfo?.modalStyle,
// hasModalContainerId: !!shrinkInfo?.modalContainerId,
// });
const updatedPanelInfo = {
...panelInfo,
@@ -378,7 +378,7 @@ export const expandVideoFrom1px = () => (dispatch, getState) => {
}),
};
console.log('[HomePanel] expandVideoFrom1px: updated panelInfo shouldShrinkTo1px=false, modalStyle restored, skipModalStyleRecalculation=true');
// console.log('[HomePanel] expandVideoFrom1px: updated panelInfo shouldShrinkTo1px=false, modalStyle restored, skipModalStyleRecalculation=true');
dispatch(
updatePanel({

View File

@@ -112,6 +112,7 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
onLoadedData,
onLoadedMetadata,
onDurationChange,
setApiProvider,
// Spotlight
spotlightId = 'mediaPlayerV2',
@@ -470,6 +471,23 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
getVideoNode: () => videoRef.current,
}), [play, pause, seek, getMediaState, showControls, hideControls, toggleControls, controlsVisible]);
// ========== setApiProvider 호출 ==========
useEffect(() => {
if (setApiProvider && typeof setApiProvider === 'function') {
setApiProvider({
play,
pause,
seek,
getMediaState,
showControls,
hideControls,
toggleControls,
areControlsVisible: () => controlsVisible,
getVideoNode: () => videoRef.current,
});
}
}, [setApiProvider, play, pause, seek, getMediaState, showControls, hideControls, toggleControls]);
// ========== Video Props ==========
const videoProps = useMemo(() => {
const baseProps = {
@@ -671,11 +689,13 @@ MediaPlayerV2.propTypes = {
style: PropTypes.object,
modalClassName: PropTypes.string,
modalScale: PropTypes.number,
setApiProvider: PropTypes.func,
// 패널 정보
panelInfo: PropTypes.shape({
modal: PropTypes.bool,
modalContainerId: PropTypes.string,
modalClassName: PropTypes.string,
isPaused: PropTypes.bool,
showUrl: PropTypes.string,
thumbnailUrl: PropTypes.string,

View File

@@ -22,6 +22,7 @@ import {
getMainCategoryDetail,
getMainYouMayLike,
} from '../../actions/mainActions';
import { finishModalMediaForce } from '../../actions/mediaActions';
import {
popPanel,
updatePanel,
@@ -141,11 +142,19 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
Spotlight.focus('spotlightId_backBtn');
}, []);
useEffect(() => {
return () => {
dispatch(finishModalMediaForce());
};
}, [dispatch]);
const onClick = useCallback(
(isCancelClick) => (ev) => {
fp.pipe(
() => {
dispatch(clearAllToasts()); // BuyOption Toast 포함 모든 토스트 제거
dispatch(pauseFullscreenVideo()); // PLAYER_PANEL 비디오 중지
dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료
dispatch(finishVideoPreview());
dispatch(popPanel(panel_names.DETAIL_PANEL));
},

View File

@@ -27,7 +27,7 @@ import arrowDown
import indicatorDefaultImage
from '../../../../assets/images/img-thumb-empty-144@3x.png';
// import { pushPanel } from '../../../actions/panelActions';
import { minimizeModalMedia } from '../../../actions/mediaActions';
import { minimizeModalMedia, restoreModalMedia } from '../../../actions/mediaActions';
import { pauseFullscreenVideo } from '../../../actions/playActions';
import { resetShowAllReviews } from '../../../actions/productActions';
import {
@@ -197,6 +197,9 @@ export default function ProductAllSection({
// ProductAllSection 초기 로딩 시 Skeleton 표시를 위한 상태
const [isInitialLoading, setIsInitialLoading] = useState(true);
// 스크롤 위치에 따른 MediaPanel 제어 상태
const [shouldMinimizeMedia, setShouldMinimizeMedia] = useState(false);
useEffect(() => {
const toggleQRCode = () => {
if (isShowQRCode) {
@@ -498,7 +501,7 @@ export default function ProductAllSection({
// ProductVideo V1 전용 - MediaPanel minimize 포함
const handleScrollToImagesV1 = useCallback(() => {
// 1. MediaPanel을 1px로 축소하여 포커스 충돌 방지
dispatch(minimizeModalMedia());
// dispatch(minimizeModalMedia());
// 2. 스크롤 이동
scrollToSection('scroll-marker-after-video');
@@ -657,6 +660,7 @@ export default function ProductAllSection({
[scrollToSection]
);
const scrollPositionRef = useRef(0);
const prevScrollPositionRef = useRef(0); // 이전 스크롤 위치 추적
const handleArrowClickAlternative = useCallback(() => {
const currentHeight = scrollPositionRef.current;
@@ -677,17 +681,40 @@ export default function ProductAllSection({
}
}, [documentHeight, scrollTop]);
const handleScroll = useCallback(
// const handleScroll = useCallback(
// (e) => {
// scrollPositionRef.current = e.scrollTop;
// if (documentHeight) {
// const isAtBottom =
// scrollPositionRef.current + 944 >=
// documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
// if (isAtBottom !== isBottom) {
// setIsBottom(isAtBottom);
// }
// }
// },
// [documentHeight, isBottom]
// );
// 스크롤 멈추었을 때만 호출 (성능 최적화)
const handleScrollStop = useCallback(
(e) => {
scrollPositionRef.current = e.scrollTop;
const currentScrollTop = e.scrollTop;
scrollPositionRef.current = currentScrollTop;
if (documentHeight) {
const isAtBottom =
scrollPositionRef.current + 944 >=
currentScrollTop + 944 >=
documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
if (isAtBottom !== isBottom) {
setIsBottom(isAtBottom);
}
}
console.log('📍 [ProductAllSection] 스크롤 멈춤 - 위치:', currentScrollTop);
const shouldMinimize = currentScrollTop > 0;
console.log('📍 [ProductAllSection] setShouldMinimizeMedia 호출:', shouldMinimize);
setShouldMinimizeMedia(shouldMinimize); // state 업데이트만
},
[documentHeight, isBottom]
);
@@ -710,6 +737,18 @@ export default function ProductAllSection({
);
}, [hasReviews, hasYouMayAlsoLike]);
// 스크롤 위치에 따른 MediaPanel 제어 (비디오 재생 중에는 자동 제어 안함 - unmount 시에만 정리)
// useEffect(() => {
// console.log('📍 [ProductAllSection] useEffect 실행 - shouldMinimizeMedia:', shouldMinimizeMedia);
// if (shouldMinimizeMedia) {
// console.log('📍 [ProductAllSection] MediaPanel minimize 호출 (true)');
// dispatch(minimizeModalMedia());
// } else {
// console.log('📍 [ProductAllSection] MediaPanel restore 호출 (false)');
// dispatch(restoreModalMedia());
// }
// }, [shouldMinimizeMedia, dispatch]);
//spot관련
useEffect(() => {
if (onReady && isOnRender) {
@@ -768,9 +807,11 @@ export default function ProductAllSection({
// };
// }, []);
// 컴포넌트 unmount 시 모든 timer cleanup
// 컴포넌트 unmount 시 timer cleanup
useEffect(() => {
return () => {
console.log('[ProductAllSection] unmount - cleanup 시작');
// QR code timer cleanup
if (timerRef.current) {
clearTimeout(timerRef.current);
@@ -788,7 +829,7 @@ export default function ProductAllSection({
scrollToImagesTimeoutRef.current = null;
}
// console.log('[ProductAllSection] All timers cleaned up on unmount');
console.log('[ProductAllSection] cleanup 완료 on unmount');
};
}, []);
@@ -1019,7 +1060,8 @@ export default function ProductAllSection({
spotlightId="main-content-scroller"
spotlightDisabled={false}
spotlightRestrict="none"
onScroll={handleScroll}
// onScroll={handleScroll}
onScrollStop={handleScrollStop}
onClick={handleScrollContainerClick}
>
<div className={css.productDetail}>

View File

@@ -114,17 +114,17 @@ export default function ProductVideo({
setFocused(true);
console.log('[ProductVideo] Calling restoreModalMedia');
// ProductVideo에 포커스가 돌아오면 비디오 복원
dispatch(restoreModalMedia());
// dispatch(restoreModalMedia());
}
}, [canPlayVideo, dispatch]);
const videoContainerOnBlur = useCallback(() => {
console.log('[ProductVideo] onBlur called - canPlayVideo:', canPlayVideo);
if (canPlayVideo) {
setFocused(false);
// 포커스를 잃으면 모달 MediaPanel을 최소화하여 1px 상태로 유지
dispatch(minimizeModalMedia());
}
// if (canPlayVideo) {
// setFocused(false);
// // 포커스를 잃으면 모달 MediaPanel을 최소화하여 1px 상태로 유지
// dispatch(minimizeModalMedia());
// }
}, [canPlayVideo, dispatch]);
// Spotlight Down 키 핸들러 - 비디오 다음 이미지로 스크롤

View File

@@ -466,7 +466,7 @@ const HomePanel = ({ isOnTop }) => {
// shouldShrinkTo1px가 false가 되면 interval 즉시 정리
if (!playerPanelShouldShrink && expandIntervalRef.current) {
console.log('[HomePanel] shouldShrinkTo1px=false - clearing expand interval');
// console.log('[HomePanel] shouldShrinkTo1px=false - clearing expand interval');
clearInterval(expandIntervalRef.current);
expandIntervalRef.current = null;
expandAttemptRef.current = 0;
@@ -489,20 +489,20 @@ const HomePanel = ({ isOnTop }) => {
// ✅ 최상단 도달: 1px 비디오 복구 시도 (shouldShrinkRef.current로 읽음)
if (currentScrollTop <= 1) {
if (shouldShrinkRef.current && !expandIntervalRef.current) {
console.log('[HomePanel] At top (scrollTop <= 1) - starting video expansion');
// console.log('[HomePanel] At top (scrollTop <= 1) - starting video expansion');
expandAttemptRef.current = 0;
// Interval 시작: 200ms마다 복구 시도
expandIntervalRef.current = setInterval(() => {
// 종료 조건: 최대 3회 시도
if (expandAttemptRef.current >= 3) {
console.log('[HomePanel] Max expansion attempts reached (3)');
// console.log('[HomePanel] Max expansion attempts reached (3)');
clearInterval(expandIntervalRef.current);
expandIntervalRef.current = null;
return;
}
console.log('[HomePanel] Expansion attempt', expandAttemptRef.current + 1);
// console.log('[HomePanel] Expansion attempt', expandAttemptRef.current + 1);
dispatch(expandVideoFrom1px());
expandAttemptRef.current++;
}, 200);
@@ -510,7 +510,7 @@ const HomePanel = ({ isOnTop }) => {
}
// 최상단 벗어남: interval 정리
else if (currentScrollTop > 1 && expandIntervalRef.current) {
console.log('[HomePanel] Left top - clearing expand interval');
// console.log('[HomePanel] Left top - clearing expand interval');
clearInterval(expandIntervalRef.current);
expandIntervalRef.current = null;
}
@@ -518,7 +518,7 @@ const HomePanel = ({ isOnTop }) => {
// 기존 로직: 아래로 스크롤
if (currentScrollTop > prevScrollTop) {
// 아래로 스크롤: 비디오를 1px로 축소
console.log('[HomePanel] Scrolling down - shrinking video');
// console.log('[HomePanel] Scrolling down - shrinking video');
dispatch(shrinkVideoTo1px());
// 기존 타이머 취소
if (scrollExpandTimerRef.current) {
@@ -529,14 +529,14 @@ const HomePanel = ({ isOnTop }) => {
// 기존 로직: 위로 스크롤 (0이 아닌)
else if (currentScrollTop < prevScrollTop && currentScrollTop > 1) {
// 위로 스크롤 (최상단 아님): 1초 후 복구
console.log('[HomePanel] Scrolling up - will expand after 1s');
// console.log('[HomePanel] Scrolling up - will expand after 1s');
// 기존 타이머 취소
if (scrollExpandTimerRef.current) {
clearTimeout(scrollExpandTimerRef.current);
}
// 1초 후 자동으로 크기 조정
scrollExpandTimerRef.current = setTimeout(() => {
console.log('[HomePanel] 1s passed - auto expanding video');
// console.log('[HomePanel] 1s passed - auto expanding video');
dispatch(expandVideoFrom1px());
scrollExpandTimerRef.current = null;
}, 1000);

View File

@@ -507,7 +507,7 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
loop={panelInfo.continuousPlay || false}
style={panelInfo.modal ? modalStyle : {}}
modalScale={panelInfo.modal ? modalScale : 1}
modalClassName={panelInfo.modal && panelInfo.modalClassName}
modalClassName={panelInfo.modal ? panelInfo.modalClassName : undefined}
onError={mediainfoHandler}
onTimeUpdate={mediainfoHandler}
onLoadedData={mediainfoHandler}