[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:
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 키 핸들러 - 비디오 다음 이미지로 스크롤
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user