[251114] fix: ProductAllSection ProductVideo.v3

🕐 커밋 시간: 2025. 11. 14. 15:36:07

📊 변경 통계:
  • 총 파일: 4개
  • 추가: +27줄
  • 삭제: -333줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/utils/helperMethods.js
  ~ 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/utils/helperMethods.js (javascript):
     Added: getFormattingDate()
  📄 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):
     Added: Spottable()
  📄 com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx (javascript):
    🔄 Modified: normalizeModalStyle()
     Deleted: handleEvent()

🔧 주요 변경 내용:
  • 공통 유틸리티 함수 최적화

Performance: 코드 최적화로 성능 개선 기대
This commit is contained in:
2025-11-14 15:36:07 +09:00
parent 5d587dbdeb
commit c9c6fc07a9
4 changed files with 28 additions and 344 deletions

View File

@@ -397,6 +397,11 @@ export const getFormattingDate = (dateString) => {
};
export const removeSpecificTags = (html) => {
// null 또는 undefined 체크
if (!html) {
return html;
}
const tagPatterns = [
/<a\b[^>]*>(.*?)<\/a>/gi,
/<script\b[^>]*>(.*?)<\/script>/gi,

View File

@@ -151,6 +151,8 @@ export default function ProductAllSection({
// ProductVideo 버전 관리 (1: 기존 modal 방식, 2: 내장 방식 , 3: 비디오 생략)
const [productVideoVersion, setProductVideoVersion] = useState(1);
// 비디오 재생 여부 flag (재생 전에는 minimize/restore 로직 비활성화)
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
// const [currentHeight, setCurrentHeight] = useState(0);
//하단부분까지 갔을때 체크용
@@ -666,7 +668,8 @@ export default function ProductAllSection({
// 🔽 ProductVideo (v3.jsx)에만 HomePanel 스타일 즉각 스크롤 로직 적용
// ProductVideo.v3.jsx는 ProductVideo로 import되어 productVideoVersion === 1일 때 사용됨
if (productVideoVersion === 1) {
// ⚠️ 비디오가 재생되었을 때만 minimize/restore 로직 실행
if (productVideoVersion === 1 && isVideoPlaying) {
const isScrollingDown = currentScrollTop > prevScrollTop;
prevScrollTopRef.current = currentScrollTop;
@@ -710,7 +713,7 @@ export default function ProductAllSection({
}
// v2: onScrollStop에서 처리 (기존 로직 유지)
},
[documentHeight, isBottom, productVideoVersion, dispatch]
[documentHeight, isBottom, productVideoVersion, isVideoPlaying, dispatch]
);
// 스크롤 멈추었을 때만 호출 (성능 최적화)
@@ -1112,6 +1115,7 @@ export default function ProductAllSection({
thumbnailUrl={renderItems[0].thumbnail}
autoPlay={true}
continuousPlay={true}
onVideoPlaying={() => setIsVideoPlaying(true)}
onScrollToImages={handleScrollToImagesV1}
onFocus={() => console.log('[ProductVideo V1] Focused')}
/>

View File

@@ -20,6 +20,7 @@ export default function ProductVideo({
videoUrl,
thumbnailUrl,
onScrollToImages,
onVideoPlaying = null, // 비디오 재생 시 호출되는 콜백
autoPlay = false, // 자동 재생 여부
continuousPlay = false, // 반복 재생 여부
onFocus = null, // 외부에서 전달된 포커스 핸들러
@@ -32,6 +33,7 @@ export default function ProductVideo({
const [focused, setFocused] = useState(false);
const [modalState, setModalState] = useState(true); // 모달 상태 관리 추가
const [hasAutoPlayed, setHasAutoPlayed] = useState(false); // 자동 재생 완료 여부
const [isVideoPlaying, setIsVideoPlaying] = useState(false); // 비디오 재생 여부 flag
const topPanel = panels[panels.length - 1];
@@ -59,6 +61,10 @@ export default function ProductVideo({
// 짧은 딜레이 후 재생 시작 (컴포넌트 마운트 완료 후)
setTimeout(() => {
setIsVideoPlaying(true); // 비디오 재생 flag 설정
if (onVideoPlaying) {
onVideoPlaying(); // 부모 컴포넌트에 알림
}
dispatch(
startMediaPlayer({
qrCurrentItem: productInfo,
@@ -178,6 +184,10 @@ export default function ProductVideo({
console.log('[ProductVideo] *** Starting modal MediaPanel ***');
console.log('[ProductVideo] productInfo:', JSON.stringify(productInfo, null, 2));
// 처음 재생 시작 - modal=true로 시작
setIsVideoPlaying(true); // 비디오 재생 flag 설정
if (onVideoPlaying) {
onVideoPlaying(); // 부모 컴포넌트에 알림
}
dispatch(
startMediaPlayer({
qrCurrentItem: productInfo,

View File

@@ -67,16 +67,7 @@ import * as Config from '../../utils/Config';
import { ACTIVE_POPUP, panel_names } from '../../utils/Config';
import { $L, formatGMTString } from '../../utils/helperMethods';
import { SpotlightIds } from '../../utils/SpotlightIds';
import { removeDotAndColon } from '../PlayerPanel/PlayerItemCard/PlayerItemCard';
import PlayerOverlayChat from '../PlayerPanel/PlayerOverlay/PlayerOverlayChat';
import PlayerOverlayQRCode from '../PlayerPanel/PlayerOverlay/PlayerOverlayQRCode';
import css from './MediaPanel.v3.module.less';
import PlayerTabButton from '../PlayerPanel/PlayerTabContents/TabButton/PlayerTabButton';
import TabContainer from '../PlayerPanel/PlayerTabContents/TabContainer';
import TabContainerV2 from '../PlayerPanel/PlayerTabContents/v2/TabContainer.v2';
// import LiveShowContainer from './PlayerTabContents/v2/LiveShowContainer';
// import ShopNowContainer from './PlayerTabContents/v2/ShopNowContainer';
// import ShopNowButton from './PlayerTabContents/v2/ShopNowButton';
const Container = SpotlightContainerDecorator(
{ enterTo: 'default-element', preserveld: true },
@@ -168,12 +159,6 @@ const YOUTUBECONFIG = {
},
};
const INITIAL_TIMEOUT = 30000;
const REGULAR_TIMEOUT = 30000;
const TAB_CONTAINER_SPOTLIGHT_ID = 'tab-container-spotlight-id';
const TAB_CONTAINER_V2_SPOTLIGHT_ID = 'tab-container-v2-spotlight-id';
const TARGET_EVENTS = ['mousemove', 'keydown', 'click'];
// last time error
const VIDEO_END_ACTION_DELAY = 1500;
@@ -236,8 +221,6 @@ const MediaPanel = React.forwardRef(
1
);
const [prevChannelIndex, setPrevChannelIndex] = USE_STATE('prevChannelIndex', 0);
const [sideContentsVisible, setSideContentsVisible] = USE_STATE('sideContentsVisible', true);
const [belowContentsVisible, setBelowContentsVisible] = USE_STATE('belowContentsVisible', true);
const [currentTime, setCurrentTime] = USE_STATE('currentTime', 0);
const [isInitialFocusOccurred, setIsInitialFocusOccurred] = USE_STATE(
'isInitialFocusOccurred',
@@ -261,8 +244,6 @@ const MediaPanel = React.forwardRef(
isDetailMediaReady: false,
});
const [isVODPaused, setIsVODPaused] = USE_STATE('isVODPaused', false);
const [tabIndexV2, setTabIndexV2] = USE_STATE('tabIndexV2', 1); // 0: ShopNow, 1: LiveChannel, 2: ShopNowButton
const [tabContainerVersion, setTabContainerVersion] = USE_STATE('tabContainerVersion', 2); // 1: TabContainer (우측), 2: TabContainerV2 (하단)
const panels = USE_SELECTOR('panels', (state) => state.panels.panels);
const chatData = USE_SELECTOR('chatData', (state) => state.play.chatData);
@@ -890,30 +871,13 @@ const MediaPanel = React.forwardRef(
const handleItemFocus = useCallback(
(menu) => {
dispatch(sendLogGNB(menu));
if (!videoVerticalVisible) {
resetTimer(REGULAR_TIMEOUT);
}
},
[dispatch, resetTimer, videoVerticalVisible]
[dispatch]
);
const onClickBack = useCallback(
(ev, isEnd) => {
//modal로부터 Full 전환된 경우 다시 preview 모드로 돌아감.
// TabContainer(v1)만: Side Contents가 보이는 경우 먼저 숨기고 return
if (
tabContainerVersion === 1 &&
sideContentsVisible &&
!videoVerticalVisible &&
panelInfo.shptmBanrTpNm !== 'MEDIA'
) {
setSideContentsVisible(false);
ev?.stopPropagation();
// ev?.preventDefault();
return;
}
if (panelInfo.modalContainerId && !panelInfo.modal) {
dispatch(
startMediaPlayer({
@@ -954,16 +918,7 @@ const MediaPanel = React.forwardRef(
return;
}
},
[
dispatch,
panelInfo,
videoPlayer,
sideContentsVisible,
videoVerticalVisible,
backupInitialIndex,
panels,
tabContainerVersion,
]
[dispatch, panelInfo, videoPlayer, videoVerticalVisible, backupInitialIndex, panels]
);
useEffect(() => {
@@ -1043,11 +998,7 @@ const MediaPanel = React.forwardRef(
}
if (!panelInfo.modal && !videoVerticalVisible && !hasProperSpot) {
if (tabContainerVersion === 1) {
Spotlight.focus(SpotlightIds.PLAYER_TAB_BUTTON);
} else if (tabContainerVersion === 2) {
Spotlight.focus('below-tab-live-channel-button');
}
return;
}
//비디오 진입시 포커스
@@ -1063,7 +1014,6 @@ const MediaPanel = React.forwardRef(
panelInfo.isUpdatedByClick,
panelInfo.isIndicatorByClick,
panelInfo.shptmBanrTpNm,
tabContainerVersion,
]);
// 최상단 패널 정보 (여러 useMemo에서 공통으로 사용)
@@ -1326,7 +1276,7 @@ const MediaPanel = React.forwardRef(
if (playListInfo && playListInfo.length > 0) {
videoInitialFocused();
}
}, [sideContentsVisible, panelInfo.modal]);
}, [panelInfo.modal]);
// liveChannel initial selectedIndex
useEffect(() => {
@@ -1796,11 +1746,8 @@ const MediaPanel = React.forwardRef(
}, [currentSubtitleBlob, isSubtitleActive]);
const currentSideButtonStatus = useMemo(() => {
if (panelInfo?.shptmBanrTpNm !== 'MEDIA' && !panelInfo?.modal && sideContentsVisible) {
return true;
}
return false;
}, [panelInfo, sideContentsVisible]);
}, []);
const videoType = useMemo(() => {
if (currentPlayingUrl) {
@@ -1920,11 +1867,7 @@ const MediaPanel = React.forwardRef(
);
}
}
if (!sideContentsVisible) {
setPrevChannelIndex(selectedIndex);
}
setSideContentsVisible(true);
}, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter]);
}, [dispatch, playListInfo, selectedIndex, initialEnter]);
const handleIndicatorUpClick = useCallback(() => {
if (!initialEnter) {
@@ -1967,11 +1910,7 @@ const MediaPanel = React.forwardRef(
);
}
}
if (!sideContentsVisible) {
setPrevChannelIndex(selectedIndex);
}
setSideContentsVisible(true);
}, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter]);
}, [dispatch, playListInfo, selectedIndex, initialEnter]);
useEffect(() => {
if (panelInfo.shptmBanrTpNm === 'VOD' && panelInfo.patnrId && panelInfo.showId) {
@@ -2041,29 +1980,6 @@ const MediaPanel = React.forwardRef(
};
const [initialEnter, setInitialEnter] = USE_STATE('initialEnter', true);
const [initialEnterV2, setInitialEnterV2] = USE_STATE('initialEnterV2', true);
const timerId = useRef(null);
const timerIdV2 = useRef(null);
const showSideContents = useMemo(() => {
return (
sideContentsVisible &&
playListInfo &&
panelInfo?.shptmBanrTpNm !== 'MEDIA' &&
!panelInfo?.modal &&
isOnTop
);
}, [sideContentsVisible, playListInfo, panelInfo, isOnTop]);
const showBelowContents = useMemo(() => {
return (
belowContentsVisible &&
playListInfo &&
panelInfo?.shptmBanrTpNm !== 'MEDIA' &&
!panelInfo?.modal &&
isOnTop
);
}, [belowContentsVisible, playListInfo, panelInfo, isOnTop]);
const qrCurrentItem = useMemo(() => {
if (shopNowInfo?.length && panelInfo?.shptmBanrTpNm === 'LIVE') {
@@ -2096,61 +2012,11 @@ const MediaPanel = React.forwardRef(
return panelInfo.shptmBanrTpNm;
}, [panelInfo.shptmBanrTpNm, playListInfo, selectedIndex]);
const clearTimer = useCallback(() => {
clearTimeout(timerId.current);
timerId.current = null;
}, []);
const resetTimer = useCallback(
(timeout) => {
if (timerId.current) {
clearTimer();
}
if (initialEnter) {
setInitialEnter(false);
}
timerId.current = setTimeout(() => {
setSideContentsVisible(false);
// setBelowContentsVisible(false);
}, timeout);
},
[clearTimer, initialEnter, setInitialEnter, setSideContentsVisible]
);
const clearTimerV2 = useCallback(() => {
clearTimeout(timerIdV2.current);
timerIdV2.current = null;
}, []);
const resetTimerV2 = useCallback(
(timeout) => {
// console.log('[TabContainerV2] resetTimerV2 호출', timeout);
if (timerIdV2.current) {
// console.log('[TabContainerV2] 기존 타이머 클리어');
clearTimerV2();
}
if (initialEnterV2) {
// console.log('[TabContainerV2] initialEnterV2 false로 변경');
setInitialEnterV2(false);
}
timerIdV2.current = setTimeout(() => {
// console.log('[TabContainerV2] 타이머 실행 - belowContentsVisible false로 변경');
setBelowContentsVisible(false);
}, timeout);
},
[clearTimerV2, initialEnterV2, setInitialEnterV2, setBelowContentsVisible]
);
// Redux로 오버레이 숨김
useEffect(() => {
if (shouldHideOverlays) {
console.log('[MediaPanel] shouldHideOverlays true - 오버레이 숨김');
setSideContentsVisible(false);
setBelowContentsVisible(false);
if (videoPlayer.current?.hideControls) {
videoPlayer.current.hideControls();
@@ -2164,8 +2030,6 @@ const MediaPanel = React.forwardRef(
useEffect(() => {
if (shouldShowOverlays) {
console.log('[MediaPanel] shouldShowOverlays true - 오버레이 표시');
setSideContentsVisible(true);
setBelowContentsVisible(true);
if (videoPlayer.current?.showControls) {
videoPlayer.current.showControls();
@@ -2175,16 +2039,6 @@ const MediaPanel = React.forwardRef(
}
}, [shouldShowOverlays, dispatch]);
// MediaPanel이 최상단이 될 때 오버레이 표시 (DetailPanel에서 복귀)
useEffect(() => {
if (isOnTop && !panelInfo.modal && !videoVerticalVisible) {
console.log('[MediaPanel] isOnTop true - 오버레이 표시');
setSideContentsVisible(true);
setBelowContentsVisible(true);
// VideoPlayer가 belowContentsVisible prop을 감지해서 자동으로 controls 표시함
}
}, [isOnTop, panelInfo.modal, videoVerticalVisible]);
useEffect(() => {
if (panelInfoRef.current?.modal && !panelInfo.modal && isOnTop && !videoVerticalVisible) {
const focusTimer = setTimeout(() => {
@@ -2197,104 +2051,6 @@ const MediaPanel = React.forwardRef(
}
}, [panelInfo.modal, isOnTop, videoVerticalVisible]);
useEffect(() => {
// tabContainerVersion === 1일 때만 실행
if (tabContainerVersion !== 1) return;
const node = document.querySelector(`[data-spotlight-id=${TAB_CONTAINER_SPOTLIGHT_ID}]`);
if (!showSideContents || !node || videoVerticalVisible) return;
// NOTE 첫 진입 시에는 10초 후 탭이 닫히도록 설정
if (initialEnter) {
resetTimer(INITIAL_TIMEOUT);
}
const handleEvent = () => resetTimer(REGULAR_TIMEOUT);
TARGET_EVENTS.forEach((event) => node.addEventListener(event, handleEvent));
return () => {
TARGET_EVENTS.forEach((event) => node.removeEventListener(event, handleEvent));
if (timerId.current) {
clearTimer();
}
};
}, [
showSideContents,
videoVerticalVisible,
tabContainerVersion,
resetTimer,
initialEnter,
clearTimer,
]);
useEffect(() => {
if (initialEnter || !sideContentsVisible || videoVerticalVisible) return;
// NOTE button을 통해 탭을 연 경우 5초 후 탭이 닫히도록 설정
if (sideContentsVisible) {
resetTimer(REGULAR_TIMEOUT);
}
return () => {
if (timerId.current) {
clearTimer();
}
};
}, [sideContentsVisible]);
// TabContainerV2 자동 닫기
useEffect(() => {
// tabContainerVersion === 2일 때만 실행
if (tabContainerVersion !== 2) return;
// console.log('[TabContainerV2] useEffect 시작', {
// showBelowContents,
// videoVerticalVisible,
// initialEnterV2,
// });
const node = document.querySelector(`[data-spotlight-id=${TAB_CONTAINER_V2_SPOTLIGHT_ID}]`);
// console.log('[TabContainerV2] DOM node:', node);
if (!showBelowContents || !node || videoVerticalVisible) {
// console.log('[TabContainerV2] early return');
return;
}
// NOTE 첫 진입 시에는 30초 후 탭이 닫히도록 설정
if (initialEnterV2) {
// console.log('[TabContainerV2] 첫 진입 - 타이머 시작', INITIAL_TIMEOUT);
resetTimerV2(INITIAL_TIMEOUT);
}
const handleEvent = (e) => {
// console.log('[TabContainerV2] 이벤트 발생:', e.type);
resetTimerV2(REGULAR_TIMEOUT);
};
TARGET_EVENTS.forEach((event) => {
// console.log('[TabContainerV2] 이벤트 리스너 등록:', event);
node.addEventListener(event, handleEvent);
});
return () => {
// console.log('[TabContainerV2] cleanup');
TARGET_EVENTS.forEach((event) => node.removeEventListener(event, handleEvent));
if (timerIdV2.current) {
clearTimerV2();
}
};
}, [
showBelowContents,
videoVerticalVisible,
tabContainerVersion,
resetTimerV2,
initialEnterV2,
clearTimerV2,
]);
useLayoutEffect(() => {
const videoContainer = document.querySelector(`.${css.videoContainer}`);
@@ -2466,16 +2222,10 @@ const MediaPanel = React.forwardRef(
selectedIndex={selectedIndex}
qrCurrentItem={qrCurrentItem}
setIsSubtitleActive={setIsSubtitleActive}
setSideContentsVisible={setSideContentsVisible}
sideContentsVisible={sideContentsVisible}
setBelowContentsVisible={setBelowContentsVisible}
belowContentsVisible={belowContentsVisible}
videoVerticalVisible={videoVerticalVisible}
setCurrentTime={setCurrentTime}
setIsVODPaused={setIsVODPaused}
broadcast={broadcast}
tabContainerVersion={tabContainerVersion}
tabIndexV2={tabIndexV2}
dispatch={dispatch}
>
{typeof window === 'object' && window.PalmSystem && (
@@ -2501,91 +2251,6 @@ const MediaPanel = React.forwardRef(
QRCodeUrl={playListInfo[selectedIndex]?.chatUrl}
/>
)}
{tabContainerVersion === 1 &&
currentSideButtonStatus &&
!videoVerticalVisible &&
playListInfo && (
<PlayerTabButton
setSideContentsVisible={setSideContentsVisible}
sideContentsVisible={sideContentsVisible}
videoType={isShowType}
/>
)}
{tabContainerVersion === 1 && showSideContents && (
<TabContainer
spotlightId={TAB_CONTAINER_SPOTLIGHT_ID}
panelInfo={panelInfo}
shopNowInfo={shopNowInfo}
playListInfo={playListInfo}
selectedIndex={selectedIndex}
setSelectedIndex={setSelectedIndex}
liveChannelInfos={liveChannelInfos || liveShowInfos}
videoVerticalVisible={videoVerticalVisible}
handleItemFocus={handleItemFocus}
prevChannelIndex={prevChannelIndex}
currentTime={currentTime}
/>
)}
{/* {shouldShowBelowTab && (
<>
{belowTabMode === 'liveShow' && (
<LiveShowContainer
panelInfo={panelInfo}
liveInfos={playListInfo}
currentTime={currentTime}
setSelectedIndex={setSelectedIndex}
videoVerticalVisible={videoVerticalVisible}
currentVideoShowId={playListInfo && playListInfo[selectedIndex]?.showId}
handleItemFocus={handleItemFocus}
onLiveChannelButtonClick={() => setBelowTabMode('shopNowButton')}
tabTitle={[
$L('SHOP NOW'),
panelInfo?.shptmBanrTpNm === 'LIVE' ? $L('LIVE CHANNEL') : $L('FEATURED SHOWS'),
]}
selectedIndex={selectedIndex}
tabIndex={1}
/>
)}
{belowTabMode === 'shopNowButton' && (
<ShopNowButton onClick={() => setBelowTabMode('shopNow')} />
)}
{belowTabMode === 'shopNow' && (
<ShopNowContainer
panelInfo={panelInfo}
liveInfos={playListInfo}
currentTime={currentTime}
setSelectedIndex={setSelectedIndex}
videoVerticalVisible={videoVerticalVisible}
currentVideoShowId={playListInfo && playListInfo[selectedIndex]?.showId}
handleItemFocus={handleItemFocus}
/>
)}
</>
)} */}
{tabContainerVersion === 2 && showBelowContents && (
<TabContainerV2
panelInfo={panelInfo}
playListInfo={playListInfo}
shopNowInfo={shopNowInfo}
selectedIndex={selectedIndex}
setSelectedIndex={setSelectedIndex}
liveChannelInfos={liveChannelInfos || liveShowInfos}
videoVerticalVisible={videoVerticalVisible}
handleItemFocus={handleItemFocus}
prevChannelIndex={prevChannelIndex}
currentTime={currentTime}
spotlightId={TAB_CONTAINER_V2_SPOTLIGHT_ID}
tabIndex={tabIndexV2}
onShopNowButtonClick={() => setTabIndexV2(0)}
onLiveChannelButtonClick={() => setTabIndexV2(2)}
onTabClose={(newTabIndex) => setTabIndexV2(newTabIndex)}
tabVisible={belowContentsVisible}
/>
)}
</Container>
{activePopup === ACTIVE_POPUP.alertPopup && (