[251011] fix: MediaPlayer작업-2

🕐 커밋 시간: 2025. 10. 11. 23:11:17

📊 변경 통계:
  • 총 파일: 6개
  • 추가: +118줄
  • 삭제: -84줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.jsx

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/mediaActions.js
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/CustomerImages/CustomerImages.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/actions/mediaActions.js (javascript):
     Added: resumeModalMedia()
  📄 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.jsx (javascript):
    🔄 Modified: Spottable()
  📄 com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.jsx (javascript):
     Added: getControlsHandleAboveHoldConfig(), shouldJump(), calcNumberValueOfPlaybackRate(), getDurFmt(), onSpotlightFocus(), getVideoPhoneNumberClassNames()

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • UI 컴포넌트 아키텍처 개선
This commit is contained in:
2025-10-11 23:11:19 +09:00
parent c8416b90f3
commit 587406ffbb
6 changed files with 2756 additions and 125 deletions

View File

@@ -23,31 +23,39 @@ export const startMediaPlayer =
const topPanel = panels[panels.length - 1];
let panelWorkingAction = pushPanel;
console.log('[startMediaPlayer] ========== Called ==========');
console.log('[startMediaPlayer] Current panels:', JSON.stringify(panels, null, 2));
console.log('[startMediaPlayer] topPanel:', JSON.stringify(topPanel, null, 2));
if (topPanel && topPanel.name === panel_names.MEDIA_PANEL) {
panelWorkingAction = updatePanel;
console.log('[startMediaPlayer] Using updatePanel (existing MediaPanel)');
} else {
console.log('[startMediaPanel] Using pushPanel (new MediaPanel)');
}
console.log('[startMediaPlayer] Starting MediaPanel:', {
const allParams = {
modal,
modalContainerId,
showUrl: rest.showUrl,
});
modalClassName,
spotlightDisable,
...rest,
};
console.log('[startMediaPlayer] All parameters:', JSON.stringify(allParams, null, 2));
dispatch(
panelWorkingAction(
{
name: panel_names.MEDIA_PANEL,
panelInfo: {
modal,
modalContainerId,
modalClassName,
...rest,
},
panelInfo: allParams,
},
true
)
);
console.log('[startMediaPlayer] Panel action dispatched');
if (modal && modalContainerId && !spotlightDisable) {
Spotlight.setPointerMode(false);
startMediaFocusTimer = setTimeout(() => {
@@ -63,6 +71,10 @@ export const finishMediaPreview = () => (dispatch, getState) => {
const panels = getState().panels.panels;
const topPanel = panels[panels.length - 1];
console.log('[finishMediaPreview] ========== Called ==========');
console.log('[finishMediaPreview] Current panels:', JSON.stringify(panels, null, 2));
console.log('[finishMediaPreview] topPanel:', JSON.stringify(topPanel, null, 2));
if (topPanel && topPanel.name === panel_names.MEDIA_PANEL && topPanel.panelInfo.modal) {
console.log('[finishMediaPreview] Closing modal MediaPanel');
@@ -71,6 +83,9 @@ export const finishMediaPreview = () => (dispatch, getState) => {
startMediaFocusTimer = null;
}
dispatch(popPanel());
console.log('[finishMediaPreview] popPanel dispatched');
} else {
console.log('[finishMediaPreview] Not closing - no modal MediaPanel on top');
}
};
@@ -149,21 +164,46 @@ export const resumeModalMedia = () => (dispatch, getState) => {
export const switchMediaToFullscreen = () => (dispatch, getState) => {
const panels = getState().panels.panels;
console.log('[switchMediaToFullscreen] ========== Called ==========');
console.log('[switchMediaToFullscreen] Current panels:', JSON.stringify(panels, null, 2));
const modalMediaPanel = panels.find(
(panel) => panel.name === panel_names.MEDIA_PANEL && panel.panelInfo?.modal
);
console.log(
'[switchMediaToFullscreen] modalMediaPanel found:',
JSON.stringify(modalMediaPanel, null, 2)
);
if (modalMediaPanel) {
console.log('[switchMediaToFullscreen] Switching to fullscreen');
console.log('[switchMediaToFullscreen] Switching to fullscreen - updating modal to false');
console.log(
'[switchMediaToFullscreen] Existing panelInfo:',
JSON.stringify(modalMediaPanel.panelInfo, null, 2)
);
const newPanelInfo = {
...modalMediaPanel.panelInfo,
modal: false,
};
console.log(
'[switchMediaToFullscreen] New panelInfo to dispatch:',
JSON.stringify(newPanelInfo, null, 2)
);
dispatch(
updatePanel({
name: panel_names.MEDIA_PANEL,
panelInfo: {
...modalMediaPanel.panelInfo,
modal: false,
},
panelInfo: newPanelInfo,
})
);
console.log('[switchMediaToFullscreen] updatePanel dispatched');
} else {
console.log(
'[switchMediaToFullscreen] No modal MediaPanel found - cannot switch to fullscreen'
);
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -1940,6 +1940,13 @@ const VideoPlayerBase = class extends React.Component {
onVideoClick = () => {
console.log('[VideoPlayer] onVideoClick 호출');
// modal 상태일 때 외부 onClick 핸들러가 있으면 호출
if (this.props.panelInfo?.modal && this.props.onClick) {
console.log('[VideoPlayer] modal에서 onClick 호출');
this.props.onClick();
return;
}
// TabContainerV2 토글
if (this.props.setBelowContentsVisible && this.props.belowContentsVisible !== undefined) {
console.log('[VideoPlayer] belowContentsVisible 토글:', !this.props.belowContentsVisible);

View File

@@ -1,7 +1,11 @@
import React, { useCallback, useMemo, useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Spottable from '@enact/spotlight/Spottable';
import { startMediaPlayer, finishMediaPreview } from '../../../../actions/mediaActions';
import {
startMediaPlayer,
finishMediaPreview,
switchMediaToFullscreen,
} from '../../../../actions/mediaActions';
import CustomImage from '../../../../components/CustomImage/CustomImage';
import { panel_names } from '../../../../utils/Config';
import playImg from '../../../../../assets/images/btn/btn-play-thumb-nor.png';
@@ -57,34 +61,22 @@ export default function ProductVideo({ productInfo, videoUrl, thumbnailUrl }) {
}, [canPlayVideo]);
const videoContainerOnBlur = useCallback(() => {
console.log('[ProductVideo] onBlur called - canPlayVideo:', canPlayVideo);
if (canPlayVideo) {
setFocused(false);
// MediaPanel이 modal로 열려있지 않을 때만 종료
// (열려있을 때는 MediaPanel 자체에서 생명주기 관리)
const currentTopPanel = panels[panels.length - 1];
const isMediaPanelModalOpen =
currentTopPanel &&
currentTopPanel.name === panel_names.MEDIA_PANEL &&
currentTopPanel.panelInfo.modal === true;
console.log('[ProductVideo] onBlur:', {
isMediaPanelModalOpen,
currentTopPanelName: currentTopPanel?.name,
willClosePanel: !isMediaPanelModalOpen,
});
if (!isMediaPanelModalOpen) {
console.log('[ProductVideo] Closing MediaPanel from onBlur');
dispatch(finishMediaPreview());
} else {
console.log('[ProductVideo] MediaPanel is open, skipping finishMediaPreview');
}
console.log('[ProductVideo] Calling finishMediaPreview');
// ProductVideo에서 포커스가 벗어나면 비디오 재생 종료
dispatch(finishMediaPreview());
}
}, [canPlayVideo, dispatch, panels]);
}, [canPlayVideo, dispatch]);
// MediaPanel 비디오 클릭 핸들러 + 모달 토글 기능
const handleVideoClick = useCallback(() => {
console.log('[ProductVideo] ========== handleVideoClick 호출 ==========');
console.log('[ProductVideo] canPlayVideo:', canPlayVideo);
console.log('[ProductVideo] panels.length:', panels.length);
console.log('[ProductVideo] All panels:', JSON.stringify(panels, null, 2));
if (canPlayVideo) {
const currentTopPanel = panels[panels.length - 1];
@@ -94,37 +86,44 @@ export default function ProductVideo({ productInfo, videoUrl, thumbnailUrl }) {
currentTopPanel.name === panel_names.MEDIA_PANEL &&
currentTopPanel.panelInfo.modal === true;
// modal=true로 재생 중이면 modal=false(전체화면)로 변경
const newModalState = isCurrentlyPlayingModal ? false : modalState;
console.log('[ProductVideo] currentTopPanel:', JSON.stringify(currentTopPanel, null, 2));
console.log('[ProductVideo] isCurrentlyPlayingModal:', isCurrentlyPlayingModal);
dispatch(
startMediaPlayer({
qrCurrentItem: productInfo,
showUrl: productInfo?.prdtMediaUrl,
showNm: productInfo?.prdtNm,
patnrNm: productInfo?.patncNm,
patncLogoPath: productInfo?.patncLogoPath,
orderPhnNo: productInfo?.orderPhnNo,
disclaimer: productInfo?.disclaimer,
subtitle: productInfo?.prdtMediaSubtitlUrl,
lgCatCd: productInfo?.catCd,
patnrId: productInfo?.patnrId,
lgCatNm: productInfo?.catNm,
prdtId: productInfo?.prdtId,
patncNm: productInfo?.patncNm,
prdtNm: productInfo?.prdtNm,
thumbnailUrl: productInfo?.thumbnailUrl960,
shptmBanrTpNm: 'MEDIA',
modal: newModalState,
modalContainerId: 'product-video-player',
modalClassName: modalClassNameChange(),
spotlightDisable: true,
})
);
// 모달 상태가 변경된 경우 상태 업데이트
// modal로 재생 중이면 전체화면으로 전환
if (isCurrentlyPlayingModal) {
console.log(
'[ProductVideo] *** Switching to fullscreen mode via switchMediaToFullscreen ***'
);
dispatch(switchMediaToFullscreen());
setModalState(false);
} else {
console.log('[ProductVideo] *** Starting modal MediaPanel ***');
console.log('[ProductVideo] productInfo:', JSON.stringify(productInfo, null, 2));
// 처음 재생 시작 - modal=true로 시작
dispatch(
startMediaPlayer({
qrCurrentItem: productInfo,
showUrl: productInfo?.prdtMediaUrl,
showNm: productInfo?.prdtNm,
patnrNm: productInfo?.patncNm,
patncLogoPath: productInfo?.patncLogoPath,
orderPhnNo: productInfo?.orderPhnNo,
disclaimer: productInfo?.disclaimer,
subtitle: productInfo?.prdtMediaSubtitlUrl,
lgCatCd: productInfo?.catCd,
patnrId: productInfo?.patnrId,
lgCatNm: productInfo?.catNm,
prdtId: productInfo?.prdtId,
patncNm: productInfo?.patncNm,
prdtNm: productInfo?.prdtNm,
thumbnailUrl: productInfo?.thumbnailUrl960,
shptmBanrTpNm: 'MEDIA',
modal: true,
modalContainerId: 'product-video-player',
modalClassName: modalClassNameChange(),
spotlightDisable: true,
})
);
}
}

View File

@@ -1,14 +1,9 @@
import React, {
useCallback,
useEffect,
useState,
} from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import classNames from 'classnames';
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import THeader from '../../../../../components/THeader/THeader';
@@ -17,22 +12,18 @@ import css from './CustomerImages.module.less';
const Container = SpotlightContainerDecorator(
{
enterTo: "default-element",
enterTo: 'default-element',
preserveld: true,
leaveFor: {
left: "spotlight-product-info-section-container",
left: 'spotlight-product-info-section-container',
},
},
"div"
'div'
);
const SpottableComponent = Spottable("div");
const SpottableComponent = Spottable('div');
export default function CustomerImages({
onImageClick,
onViewMoreClick,
imageData,
}) {
export default function CustomerImages({ onImageClick, onViewMoreClick, imageData }) {
// Props로 전달받은 imageData 사용 (useReviews Hook에서 추출된 이미지 데이터)
const [selectedIndex, setSelectedIndex] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
@@ -77,15 +68,15 @@ export default function CustomerImages({
// 왼쪽 화살표 키
ev.preventDefault();
ev.stopPropagation();
console.log(
"[CustomerImages] Left arrow pressed, focusing product-details-button"
);
Spotlight.focus("product-details-button");
// console.log(
// "[CustomerImages] Left arrow pressed, focusing product-details-button"
// );
Spotlight.focus('product-details-button');
} else if (ev.keyCode === 13) {
// Enter 키
ev.preventDefault();
ev.stopPropagation();
console.log("[CustomerImages] Enter pressed on image:", index);
// console.log("[CustomerImages] Enter pressed on image:", index);
handleReviewImageClick(index);
}
},
@@ -98,7 +89,7 @@ export default function CustomerImages({
<Container className={css.container}>
<THeader
className={classNames(css.tHeader, css.customTHeader)}
title={$L("Customer Images")}
title={$L('Customer Images')}
/>
{imageData && imageData.length > 0 ? (
<div className={css.wrapper}>
@@ -108,15 +99,15 @@ export default function CustomerImages({
const displayImages = imageData.slice(startIndex, endIndex);
const hasMoreImages = imageData.length > endIndex;
console.log("[CustomerImages] Pagination debug:", {
currentPage,
IMAGES_PER_PAGE,
totalImages: imageData.length,
startIndex,
endIndex,
displayImagesCount: displayImages.length,
hasMoreImages,
});
// console.log("[CustomerImages] Pagination debug:", {
// currentPage,
// IMAGES_PER_PAGE,
// totalImages: imageData.length,
// startIndex,
// endIndex,
// displayImagesCount: displayImages.length,
// hasMoreImages,
// });
return (
<>
@@ -128,18 +119,14 @@ export default function CustomerImages({
<SpottableComponent
className={classNames(
css.reviewCard,
selectedIndex === actualIndex
? css.selectedReviewImage
: null,
focusIdx === displayIndex ? css.focused : ""
selectedIndex === actualIndex ? css.selectedReviewImage : null,
focusIdx === displayIndex ? css.focused : ''
)}
key={`review-image-${imgId}-${reviewId}`}
onClick={() => handleReviewImageClick(actualIndex)}
onKeyDown={(ev) => handleKeyDown(ev, actualIndex)}
spotlightId={`customer-image-${actualIndex}`}
onFocus={() =>
displayIndex < 5 ? setFocusIdx(displayIndex) : ""
}
onFocus={() => (displayIndex < 5 ? setFocusIdx(displayIndex) : '')}
>
<img
className={css.reviewImg}
@@ -153,16 +140,13 @@ export default function CustomerImages({
}); */
}}
onError={(e) => {
console.error(
`[CustomerImages] Image load failed:`,
{
index: actualIndex,
imgUrl,
imgId,
error: e.target.error,
}
);
e.target.style.display = "none";
console.error(`[CustomerImages] Image load failed:`, {
index: actualIndex,
imgUrl,
imgId,
error: e.target.error,
});
e.target.style.display = 'none';
}}
/>
</SpottableComponent>
@@ -195,15 +179,13 @@ export default function CustomerImages({
<div className={css.wrapper}>
<div
style={{
color: "rgba(255, 255, 255, 0.7)",
textAlign: "center",
padding: "20px",
fontSize: "16px",
color: 'rgba(255, 255, 255, 0.7)',
textAlign: 'center',
padding: '20px',
fontSize: '16px',
}}
>
{imageData
? "No customer images available"
: "Loading customer images..."}
{imageData ? 'No customer images available' : 'Loading customer images...'}
</div>
</div>
)}

View File

@@ -16,7 +16,7 @@ import * as PanelActions from '../../actions/panelActions';
import TPanel from '../../components/TPanel/TPanel';
import Media from '../../components/VideoPlayer/Media';
import TReactPlayer from '../../components/VideoPlayer/TReactPlayer';
import { VideoPlayer } from '../../components/VideoPlayer/VideoPlayer';
import { VideoPlayer } from '../../components/VideoPlayer/MediaPlayer';
import usePrevious from '../../hooks/usePrevious';
import { panel_names } from '../../utils/Config';
import css from './MediaPanel.module.less';
@@ -60,10 +60,9 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
// modal/full screen에 따른 일시정지/재생 처리
useEffect(() => {
console.log('[MediaPanel] isOnTop:', {
isOnTop,
panelInfo,
});
console.log('[MediaPanel] ========== isOnTop useEffect ==========');
console.log('[MediaPanel] isOnTop:', isOnTop);
console.log('[MediaPanel] panelInfo:', JSON.stringify(panelInfo, null, 2));
if (panelInfo && panelInfo.modal) {
if (!isOnTop) {
@@ -276,6 +275,12 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
setVideoLoaded(false);
}, [currentPlayingUrl]);
console.log('[MediaPanel] ========== Rendering ==========');
console.log('[MediaPanel] isOnTop:', isOnTop);
console.log('[MediaPanel] panelInfo:', JSON.stringify(panelInfo, null, 2));
console.log('[MediaPanel] currentPlayingUrl:', currentPlayingUrl);
console.log('[MediaPanel] hasVideoPlayer:', !!videoPlayer.current);
return (
<TPanel
isTabActivated={false}