1 Commits

Author SHA1 Message Date
0cd22f4989 [251216] fix: panelHistoryMiddleware log
🕐 커밋 시간: 2025. 12. 16. 10:09:15

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/middleware/panelHistoryMiddleware.js

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 코드 정리 및 최적화
2025-12-16 10:09:15 +09:00
47 changed files with 1059 additions and 2140 deletions

View File

@@ -85,7 +85,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, _getState) => {
patnrId: patnrId, patnrId: patnrId,
chanId: chanId, chanId: chanId,
shptmBanrTpNm: "LIVE", shptmBanrTpNm: "LIVE",
modal: false, // DeepLink 진입 시 fullscreen으로 재생
// expsOrd: expsOrd, // expsOrd: expsOrd,
}; };
break; break;
@@ -102,7 +101,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, _getState) => {
patnrId: patnrId, patnrId: patnrId,
showId: showId, showId: showId,
shptmBanrTpNm: "VOD", shptmBanrTpNm: "VOD",
modal: false, // DeepLink 진입 시 fullscreen으로 재생
// expsOrd: expsOrd, // expsOrd: expsOrd,
}; };
break; break;
@@ -276,18 +274,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, _getState) => {
const action = const action =
panelName === panel_names.HOME_PANEL ? updateHomeInfo : pushPanel; panelName === panel_names.HOME_PANEL ? updateHomeInfo : pushPanel;
// 🔽 LS(Live Show) 또는 VS(VOD Show)인 경우 DeepLink 진입 플래그 설정
if ((type === 'LS' || type === 'VS') && action === pushPanel) {
dispatch(
updateHomeInfo({
name: panel_names.HOME_PANEL,
panelInfo: {
isDeepLinkEntry: true, // DeepLink PlayerPanel 진입 플래그
},
})
);
}
dispatch( dispatch(
action({ action({
name: panelName, name: panelName,

View File

@@ -99,25 +99,17 @@ export const finishMediaPreview = () => (dispatch, getState) => {
export const finishModalMediaForce = () => (dispatch, getState) => { export const finishModalMediaForce = () => (dispatch, getState) => {
const panels = getState().panels.panels; const panels = getState().panels.panels;
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] finishModalMediaForce called', {
// panelCount: panels.length,
// panelNames: panels.map((p) => p.name),
// });
const hasProductVideoPanel = panels.some( const hasProductVideoPanel = panels.some(
(panel) => (panel) =>
panel.name === panel_names.MEDIA_PANEL && panel.name === panel_names.MEDIA_PANEL &&
(panel.panelInfo?.modal || panel.panelInfo?.modalContainerId === 'product-video-player') (panel.panelInfo?.modal || panel.panelInfo?.modalContainerId === 'product-video-player')
); );
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] hasProductVideoPanel:', hasProductVideoPanel);
if (hasProductVideoPanel) { if (hasProductVideoPanel) {
if (startMediaFocusTimer) { if (startMediaFocusTimer) {
clearTimeout(startMediaFocusTimer); clearTimeout(startMediaFocusTimer);
startMediaFocusTimer = null; startMediaFocusTimer = null;
} }
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] Calling popPanel(panel_names.MEDIA_PANEL)');
dispatch(popPanel(panel_names.MEDIA_PANEL)); dispatch(popPanel(panel_names.MEDIA_PANEL));
} }
}; };

View File

@@ -6,7 +6,7 @@ import { updateHomeInfo } from './homeActions';
import { createDebugHelpers } from '../utils/debug'; import { createDebugHelpers } from '../utils/debug';
// 디버그 헬퍼 설정 // 디버그 헬퍼 설정
const DEBUG_MODE = false; const DEBUG_MODE = true;
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE); const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
// 시작 메뉴 추적을 위한 상수 // 시작 메뉴 추적을 위한 상수
@@ -40,20 +40,12 @@ export const pushPanel = (panel, duplicatable = false) => ({
}); });
export const popPanel = (panelName) => { export const popPanel = (panelName) => {
const stack = new Error().stack;
const stackLines = stack?.split('\n') || [];
// console.log('[💜UNIQUE_PANEL_STACK💜] popPanel action dispatcher - REMOVING PANEL:', {
// panelName,
// timestamp: Date.now(),
// fullStack: stackLines.slice(1, 6).map((line) => line.trim()),
// });
if (DEBUG_MODE) { if (DEBUG_MODE) {
console.log('[💜UNIQUE_PANEL_STACK💜] popPanel action creator stack:', { console.log('[PANEL-TRACE] popPanel action creator', {
panelName, panelName,
caller: stackLines[2]?.trim(), caller: new Error().stack?.split('\n')[2]?.trim(),
}); });
console.trace('[PANEL-TRACE] popPanel stack trace');
} }
return { return {
type: types.POP_PANEL, type: types.POP_PANEL,

View File

@@ -74,7 +74,7 @@ export const startVideoPlayer =
}) => }) =>
(dispatch, getState) => { (dispatch, getState) => {
const caller = new Error().stack?.split('\n')[2]?.trim(); const caller = new Error().stack?.split('\n')[2]?.trim();
dlog('[PTRACE-SP] startVideoPlayer call', { console.log('[PTRACE-SP] startVideoPlayer call', {
modal, modal,
modalContainerId, modalContainerId,
modalClassName, modalClassName,
@@ -115,7 +115,7 @@ export const startVideoPlayer =
// 기존 PlayerPanel이 어디든 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push // 기존 PlayerPanel이 어디든 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push
if (existingPlayerPanel) { if (existingPlayerPanel) {
dlog('[PTRACE-SP] startVideoPlayer: popping existing player before push', { console.log('[PTRACE-SP] startVideoPlayer: popping existing player before push', {
stack: panels.map((p) => p.name), stack: panels.map((p) => p.name),
}); });
dlog('[startVideoPlayer] 🔄 Resetting existing PLAYER_PANEL before start'); dlog('[startVideoPlayer] 🔄 Resetting existing PLAYER_PANEL before start');
@@ -148,14 +148,14 @@ export const startVideoPlayer =
// [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화 // [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화
// if (modal && modalContainerId && !spotlightDisable) { // if (modal && modalContainerId && !spotlightDisable) {
// dlog('[startVideoPlayer] 🎯 Setting Spotlight focus - containerId:', modalContainerId); // console.log('[startVideoPlayer] 🎯 Setting Spotlight focus - containerId:', modalContainerId);
// Spotlight.setPointerMode(false); // Spotlight.setPointerMode(false);
// startVideoFocusTimer = setTimeout(() => { // startVideoFocusTimer = setTimeout(() => {
// dlog('[startVideoPlayer] 🔍 Spotlight.focus called'); // console.log('[startVideoPlayer] 🔍 Spotlight.focus called');
// Spotlight.focus(modalContainerId); // Spotlight.focus(modalContainerId);
// }, 0); // }, 0);
// } else { // } else {
// dlog('[startVideoPlayer] ⏭️ Spotlight focus skipped - modal:', modal, ', modalContainerId:', !!modalContainerId, ', spotlightDisable:', spotlightDisable); // console.log('[startVideoPlayer] ⏭️ Spotlight focus skipped - modal:', modal, ', modalContainerId:', !!modalContainerId, ', spotlightDisable:', spotlightDisable);
// } // }
dlog('[startVideoPlayer] ✅ END'); dlog('[startVideoPlayer] ✅ END');
@@ -196,7 +196,7 @@ export const startVideoPlayerNew =
}) => }) =>
(dispatch, getState) => { (dispatch, getState) => {
const caller = new Error().stack?.split('\n')[2]?.trim(); const caller = new Error().stack?.split('\n')[2]?.trim();
dlog('[PTRACE-SPN] startVideoPlayerNew call', { console.log('[PTRACE-SPN] startVideoPlayerNew call', {
bannerId, bannerId,
modal, modal,
modalContainerId, modalContainerId,
@@ -239,7 +239,7 @@ export const startVideoPlayerNew =
// 기존 PlayerPanel이 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push // 기존 PlayerPanel이 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push
if (existingPlayerPanel) { if (existingPlayerPanel) {
dlog('[PTRACE-SPN] popping existing player before push', { console.log('[PTRACE-SPN] popping existing player before push', {
stack: panels.map((p) => p.name), stack: panels.map((p) => p.name),
}); });
dlog('[startVideoPlayerNew] *** 🔄 Resetting existing PLAYER_PANEL before start'); dlog('[startVideoPlayerNew] *** 🔄 Resetting existing PLAYER_PANEL before start');
@@ -332,14 +332,14 @@ export const startVideoPlayerNew =
// [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화 // [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화
// if (modal && modalContainerId && !spotlightDisable) { // if (modal && modalContainerId && !spotlightDisable) {
// dlog('[startVideoPlayerNew] *** 🎯 Setting Spotlight focus - containerId:', modalContainerId); // console.log('[startVideoPlayerNew] *** 🎯 Setting Spotlight focus - containerId:', modalContainerId);
// Spotlight.setPointerMode(false); // Spotlight.setPointerMode(false);
// startVideoFocusTimer = setTimeout(() => { // startVideoFocusTimer = setTimeout(() => {
// dlog('[startVideoPlayerNew] *** 🔍 Spotlight.focus called'); // console.log('[startVideoPlayerNew] *** 🔍 Spotlight.focus called');
// Spotlight.focus(modalContainerId); // Spotlight.focus(modalContainerId);
// }, 0); // }, 0);
// } else { // } else {
// dlog('[startVideoPlayerNew] *** ⏭️ Spotlight focus skipped - modal:', modal, ', modalContainerId:', !!modalContainerId, ', spotlightDisable:', spotlightDisable); // console.log('[startVideoPlayerNew] *** ⏭️ Spotlight focus skipped - modal:', modal, ', modalContainerId:', !!modalContainerId, ', spotlightDisable:', spotlightDisable);
// } // }
dlog('[startVideoPlayerNew] *** ✅ END'); dlog('[startVideoPlayerNew] *** ✅ END');
@@ -352,7 +352,7 @@ export const finishVideoPreview = () => (dispatch, getState) => {
const panels = getState().panels.panels; const panels = getState().panels.panels;
const topPanel = panels[panels.length - 1]; const topPanel = panels[panels.length - 1];
if (topPanel && topPanel.name === panel_names.PLAYER_PANEL && topPanel.panelInfo.modal) { if (topPanel && topPanel.name === panel_names.PLAYER_PANEL && topPanel.panelInfo.modal) {
dlog('[PANEL-TRACE] finishVideoPreview: popping modal player', { console.log('[PANEL-TRACE] finishVideoPreview: popping modal player', {
topPanelName: topPanel.name, topPanelName: topPanel.name,
modal: topPanel.panelInfo.modal, modal: topPanel.panelInfo.modal,
stack: panels.map((p) => p.name), stack: panels.map((p) => p.name),
@@ -417,7 +417,7 @@ export const pauseModalVideo = () => (dispatch, getState) => {
(panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal (panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal
); );
dlog('[Detail-BG] ⏸️ pauseModalVideo - Pausing modal video', { console.log('[Detail-BG] ⏸️ pauseModalVideo - Pausing modal video', {
found: !!modalPlayerPanel, found: !!modalPlayerPanel,
playerPanelModal: modalPlayerPanel?.panelInfo?.modal, playerPanelModal: modalPlayerPanel?.panelInfo?.modal,
currentIsPaused: modalPlayerPanel?.panelInfo?.isPaused, currentIsPaused: modalPlayerPanel?.panelInfo?.isPaused,
@@ -438,11 +438,11 @@ export const pauseModalVideo = () => (dispatch, getState) => {
}) })
); );
dlog('[Detail-BG] ✅ pauseModalVideo - Modal video paused successfully', { console.log('[Detail-BG] ✅ pauseModalVideo - Modal video paused successfully', {
timestamp: Date.now(), timestamp: Date.now(),
}); });
} else { } else {
dlog('[Detail-BG] ⚠️ pauseModalVideo - No modal PlayerPanel found', { console.log('[Detail-BG] ⚠️ pauseModalVideo - No modal PlayerPanel found', {
timestamp: Date.now(), timestamp: Date.now(),
}); });
} }
@@ -457,7 +457,7 @@ export const resumeModalVideo = () => (dispatch, getState) => {
(panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal (panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal
); );
dlog('[Detail-BG] ▶️ resumeModalVideo - Resuming modal video', { console.log('[Detail-BG] ▶️ resumeModalVideo - Resuming modal video', {
found: !!modalPlayerPanel, found: !!modalPlayerPanel,
playerPanelModal: modalPlayerPanel?.panelInfo?.modal, playerPanelModal: modalPlayerPanel?.panelInfo?.modal,
currentIsPaused: modalPlayerPanel?.panelInfo?.isPaused, currentIsPaused: modalPlayerPanel?.panelInfo?.isPaused,
@@ -478,11 +478,11 @@ export const resumeModalVideo = () => (dispatch, getState) => {
}) })
); );
dlog('[Detail-BG] ✅ resumeModalVideo - Modal video resumed successfully', { console.log('[Detail-BG] ✅ resumeModalVideo - Modal video resumed successfully', {
timestamp: Date.now(), timestamp: Date.now(),
}); });
} else { } else {
dlog('[Detail-BG] ⚠️ resumeModalVideo - Modal video not paused or panel not found', { console.log('[Detail-BG] ⚠️ resumeModalVideo - Modal video not paused or panel not found', {
found: !!modalPlayerPanel, found: !!modalPlayerPanel,
isPaused: modalPlayerPanel?.panelInfo?.isPaused, isPaused: modalPlayerPanel?.panelInfo?.isPaused,
timestamp: Date.now(), timestamp: Date.now(),
@@ -499,7 +499,7 @@ export const pauseFullscreenVideo = () => (dispatch, getState) => {
(panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal (panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal
); );
dlog('[Detail-BG] ⏸️ pauseFullscreenVideo - Pausing fullscreen video', { console.log('[Detail-BG] ⏸️ pauseFullscreenVideo - Pausing fullscreen video', {
found: !!fullscreenPlayerPanel, found: !!fullscreenPlayerPanel,
playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal, playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal,
currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused, currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused,
@@ -517,11 +517,11 @@ export const pauseFullscreenVideo = () => (dispatch, getState) => {
}) })
); );
dlog('[Detail-BG] ✅ pauseFullscreenVideo - Fullscreen video paused successfully', { console.log('[Detail-BG] ✅ pauseFullscreenVideo - Fullscreen video paused successfully', {
timestamp: Date.now(), timestamp: Date.now(),
}); });
} else { } else {
dlog('[Detail-BG] ⚠️ pauseFullscreenVideo - No fullscreen PlayerPanel found', { console.log('[Detail-BG] ⚠️ pauseFullscreenVideo - No fullscreen PlayerPanel found', {
timestamp: Date.now(), timestamp: Date.now(),
}); });
} }
@@ -536,7 +536,7 @@ export const resumeFullscreenVideo = () => (dispatch, getState) => {
(panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal (panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal
); );
dlog('[Detail-BG] ▶️ resumeFullscreenVideo - Resuming fullscreen video', { console.log('[Detail-BG] ▶️ resumeFullscreenVideo - Resuming fullscreen video', {
found: !!fullscreenPlayerPanel, found: !!fullscreenPlayerPanel,
playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal, playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal,
currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused, currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused,
@@ -554,11 +554,11 @@ export const resumeFullscreenVideo = () => (dispatch, getState) => {
}) })
); );
dlog('[Detail-BG] ✅ resumeFullscreenVideo - Fullscreen video resumed successfully', { console.log('[Detail-BG] ✅ resumeFullscreenVideo - Fullscreen video resumed successfully', {
timestamp: Date.now(), timestamp: Date.now(),
}); });
} else { } else {
dlog('[Detail-BG] ⚠️ resumeFullscreenVideo - Fullscreen video not paused or panel not found', { console.log('[Detail-BG] ⚠️ resumeFullscreenVideo - Fullscreen video not paused or panel not found', {
found: !!fullscreenPlayerPanel, found: !!fullscreenPlayerPanel,
isPaused: fullscreenPlayerPanel?.panelInfo?.isPaused, isPaused: fullscreenPlayerPanel?.panelInfo?.isPaused,
timestamp: Date.now(), timestamp: Date.now(),
@@ -601,7 +601,7 @@ export const hideModalVideo = () => (dispatch, getState) => {
}, },
}; };
// dlog('[HomePanel] hideModalVideo: saving shrinkInfo', { // console.log('[HomePanel] hideModalVideo: saving shrinkInfo', {
// shrinkInfo: updatedPlayerState.shrinkInfo, // shrinkInfo: updatedPlayerState.shrinkInfo,
// modalStyle: panelInfo.modalStyle, // modalStyle: panelInfo.modalStyle,
// }); // });
@@ -1038,7 +1038,7 @@ export const resumePlayerControl = (ownerId) => (dispatch, getState) => {
* 이 액션은 어떤 배너에서든 클릭 시 호출됩니다. * 이 액션은 어떤 배너에서든 클릭 시 호출됩니다.
*/ */
export const goToFullScreen = () => (dispatch, getState) => { export const goToFullScreen = () => (dispatch, getState) => {
dlog('[Detail-BG] 🎬 goToFullScreen - Setting PlayerPanel to fullscreen mode', { console.log('[Detail-BG] 🎬 goToFullScreen - Setting PlayerPanel to fullscreen mode', {
targetModal: false, targetModal: false,
action: 'updatePanel', action: 'updatePanel',
timestamp: Date.now(), timestamp: Date.now(),
@@ -1055,7 +1055,7 @@ export const goToFullScreen = () => (dispatch, getState) => {
}) })
); );
dlog('[Detail-BG] ✅ goToFullScreen - PlayerPanel modal set to false (fullscreen)', { console.log('[Detail-BG] ✅ goToFullScreen - PlayerPanel modal set to false (fullscreen)', {
timestamp: Date.now(), timestamp: Date.now(),
}); });
}; };
@@ -1268,7 +1268,7 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
...rest ...rest
} = videoInfo; } = videoInfo;
dlog('[Detail-BG] 🎥 startBannerVideo - Starting banner video', { console.log('[Detail-BG] 🎥 startBannerVideo - Starting banner video', {
modalStatus: modal, modalStatus: modal,
bannerId, bannerId,
displayMode: modal ? 'VISIBLE (modal=true)' : 'FULLSCREEN (modal=false)', displayMode: modal ? 'VISIBLE (modal=true)' : 'FULLSCREEN (modal=false)',
@@ -1295,7 +1295,7 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
// 기존 PlayerPanel이 있으면 초기화 // 기존 PlayerPanel이 있으면 초기화
if (existingPlayerPanel) { if (existingPlayerPanel) {
dlog('[startBannerVideo] 🔄 Resetting existing PLAYER_PANEL before start'); dlog('[startBannerVideo] 🔄 Resetting existing PLAYER_PANEL before start');
dlog('[Detail-BG] 🔄 startBannerVideo - Clearing existing PlayerPanel', { console.log('[Detail-BG] 🔄 startBannerVideo - Clearing existing PlayerPanel', {
existingModalStatus: existingPlayerPanel.panelInfo?.modal, existingModalStatus: existingPlayerPanel.panelInfo?.modal,
timestamp: Date.now(), timestamp: Date.now(),
}); });
@@ -1304,7 +1304,7 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
} }
// 새로운 PlayerPanel push // 새로운 PlayerPanel push
dlog('[Detail-BG] startBannerVideo - Pushing new PlayerPanel with modal status', { console.log('[Detail-BG] startBannerVideo - Pushing new PlayerPanel with modal status', {
modal, modal,
modalContainerId, modalContainerId,
timestamp: Date.now(), timestamp: Date.now(),
@@ -1331,7 +1331,7 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
) )
); );
dlog('[Detail-BG] ✅ startBannerVideo - PlayerPanel pushed with modal=' + modal, { console.log('[Detail-BG] ✅ startBannerVideo - PlayerPanel pushed with modal=' + modal, {
timestamp: Date.now(), timestamp: Date.now(),
}); });

View File

@@ -34,7 +34,7 @@
padding: @slider-padding-v 0; padding: @slider-padding-v 0;
height: @sand-mediaplayer-slider-height; height: @sand-mediaplayer-slider-height;
right: 154px; right: 154px;
width: 1558px; width: 1466px;
// Add a tap area that extends to the edges of the screen, to make the slider more accessible // Add a tap area that extends to the edges of the screen, to make the slider more accessible
&::before { &::before {
content: ""; content: "";

View File

@@ -7,7 +7,7 @@
position: absolute; position: absolute;
font-family: @baseFont; font-family: @baseFont;
width: 100%; width: 100%;
right: 20px; right: 90px;
bottom: -5px; bottom: -5px;
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
@@ -16,12 +16,12 @@
letter-spacing: -1px; letter-spacing: -1px;
.separator { .separator {
position: absolute; position: absolute;
right: 95px; right: 105px;
bottom: -5px; bottom: -5px;
} }
.currentTime { .currentTime {
position: absolute; position: absolute;
right: 120px; right: 130px;
bottom: -5px; bottom: -5px;
} }
.totalTime { .totalTime {

View File

@@ -483,7 +483,7 @@
.default-style(); .default-style();
.scrollInfo { .scrollInfo {
width: 850px; width: 900px;
background-color: @BG_COLOR_01; background-color: @BG_COLOR_01;
color: @COLOR_GRAY03; color: @COLOR_GRAY03;
display: flex; display: flex;

View File

@@ -840,11 +840,9 @@ const VideoPlayerBase = class extends React.Component {
this.state.mediaSliderVisible === nextState.mediaSliderVisible && this.state.mediaSliderVisible === nextState.mediaSliderVisible &&
this.state.loading === nextState.loading && this.state.loading === nextState.loading &&
this.props.loading === nextProps.loading && this.props.loading === nextProps.loading &&
this.state.currentTime === nextState.currentTime && (this.state.currentTime !== nextState.currentTime ||
this.state.proportionPlayed === nextState.proportionPlayed && this.state.proportionPlayed !== nextState.proportionPlayed ||
this.state.sliderTooltipTime === nextState.sliderTooltipTime && this.state.sliderTooltipTime !== nextState.sliderTooltipTime)
this.state.mediaControlsVisible === nextState.mediaControlsVisible &&
this.state.bottomControlsRendered === nextState.bottomControlsRendered
) { ) {
return false; return false;
} }
@@ -1281,12 +1279,15 @@ const VideoPlayerBase = class extends React.Component {
sourceUnavailable: true, sourceUnavailable: true,
proportionPlayed: 0, proportionPlayed: 0,
proportionLoaded: 0, proportionLoaded: 0,
bottomControlsRendered: true,
}); });
if (!this.props.noAutoShowMediaControls) { if (!this.props.noAutoShowMediaControls) {
if (!this.state.bottomControlsRendered) {
this.renderBottomControl.idle();
} else {
this.showControls(); this.showControls();
} }
}
}; };
handlePlay = this.handle(forwardPlay, this.shouldShowMiniFeedback, () => this.play()); handlePlay = this.handle(forwardPlay, this.shouldShowMiniFeedback, () => this.play());

View File

@@ -855,11 +855,9 @@ const VideoPlayerBase = class extends React.Component {
this.state.mediaSliderVisible === nextState.mediaSliderVisible && this.state.mediaSliderVisible === nextState.mediaSliderVisible &&
this.state.loading === nextState.loading && this.state.loading === nextState.loading &&
this.props.loading === nextProps.loading && this.props.loading === nextProps.loading &&
this.state.currentTime === nextState.currentTime && (this.state.currentTime !== nextState.currentTime ||
this.state.proportionPlayed === nextState.proportionPlayed && this.state.proportionPlayed !== nextState.proportionPlayed ||
this.state.sliderTooltipTime === nextState.sliderTooltipTime && this.state.sliderTooltipTime !== nextState.sliderTooltipTime)
this.state.mediaControlsVisible === nextState.mediaControlsVisible &&
this.state.bottomControlsRendered === nextState.bottomControlsRendered
) { ) {
return false; return false;
} }
@@ -1425,12 +1423,15 @@ const VideoPlayerBase = class extends React.Component {
sourceUnavailable: true, sourceUnavailable: true,
proportionPlayed: 0, proportionPlayed: 0,
proportionLoaded: 0, proportionLoaded: 0,
bottomControlsRendered: true,
}); });
if (!this.props.noAutoShowMediaControls) { if (!this.props.noAutoShowMediaControls) {
if (!this.state.bottomControlsRendered) {
this.renderBottomControl.idle();
} else {
this.showControls(); this.showControls();
} }
}
}; };
handlePlay = this.handle(forwardPlay, this.shouldShowMiniFeedback, () => this.play()); handlePlay = this.handle(forwardPlay, this.shouldShowMiniFeedback, () => this.play());
@@ -1635,6 +1636,103 @@ const VideoPlayerBase = class extends React.Component {
updatedState.thumbnailUrl = null; updatedState.thumbnailUrl = null;
} }
this.setState(updatedState); this.setState(updatedState);
// Redux에 비디오 재생 상태 업데이트 (기존 로직 유지)
if (this.props.dispatch) {
// 🔥 onProgress 이벤트는 Redux 업데이트하지 않음 (빈번한 이벤트)
const shouldUpdateRedux = !['onProgress'].includes(ev.type);
if (shouldUpdateRedux) {
const updateState = {
isPlaying: !updatedState.paused,
isPaused: updatedState.paused,
currentTime: updatedState.currentTime,
duration: updatedState.duration,
playbackRate: updatedState.playbackRate,
};
// 가장 중요한 이벤트만 로그
const shouldLogEvent = ['play', 'pause', 'ended'].includes(ev.type);
if (shouldLogEvent) {
dlog('🔄 [PlayerPanel][VideoPlayer] Event-driven Redux update', {
eventType: ev.type,
videoState: updatedState,
updateState,
timestamp: new Date().toISOString(),
});
}
// 🔍 Redux dispatch 확인
dlog('📤 [PlayerPanel][VideoPlayer] Dispatching Redux update', {
eventType: ev.type,
updateState,
hasDispatch: !!this.props.dispatch,
propsVideoPlayState: this.props.videoPlayState,
});
this.props.dispatch(updateVideoPlayState(updateState));
}
} else {
derror('❌ [PlayerPanel][VideoPlayer] No dispatch prop available', {
props: Object.keys(this.props),
hasDispatch: !!this.props.dispatch,
hasVideoPlayState: !!this.props.videoPlayState,
});
}
// 🔹 [강화] 내부 상태와 Redux 상태 동기화
// Redux 상태를 우선적으로 사용하여 내부 상태 일관성 확보
if (this.props.videoPlayState && typeof this.props.videoPlayState === 'object') {
// Redux 상태 디버깅 (최소한의 중요 이벤트만)
if (ev.type === 'play' || ev.type === 'pause') {
dlog('🔍 [PlayerPanel][VideoPlayer] Redux state debug', {
videoPlayState: this.props.videoPlayState,
isPaused: this.props.videoPlayState?.isPaused,
isPlaying: this.props.videoPlayState?.isPlaying,
currentTime: this.props.videoPlayState?.currentTime,
eventType: ev.type,
timestamp: new Date().toISOString(),
});
}
const { currentTime, paused, playbackRate } = this.props.videoPlayState;
// Redux 상태와 현재 내부 상태가 크게 다를 경우 내부 상태 업데이트
const timeDiff = Math.abs(currentTime - this.state.currentTime);
const shouldUpdateTime = timeDiff > 0.5; // 0.5초 이상 차이 시 업데이트
// 빈번한 이벤트는 로그에서 제외
const isFrequentEvent = [
'onProgress',
'onBuffer',
'onBufferEnd',
'onReady',
'onDuration',
'onStart',
].includes(ev.type);
const hasSignificantChange =
shouldUpdateTime || (paused !== this.state.paused && !isFrequentEvent);
// 중요한 상태 변화가 있고 빈번한 이벤트가 아닐 때만 로그
if (hasSignificantChange && !isFrequentEvent) {
dlog('🔄 [PlayerPanel][VideoPlayer] Syncing internal state with Redux', {
timeDiff,
shouldUpdateTime,
pausedDiff: paused !== this.state.paused,
reduxPaused: paused,
internalPaused: this.state.paused,
eventType: ev.type,
timestamp: new Date().toISOString(),
});
}
if (hasSignificantChange) {
this.setState({
currentTime: shouldUpdateTime ? currentTime : this.state.currentTime,
paused: paused !== undefined ? paused : this.state.paused,
playbackRate: playbackRate !== undefined ? playbackRate : this.state.playbackRate,
});
}
}
}; };
renderBottomControl = new Job(() => { renderBottomControl = new Job(() => {
@@ -1646,6 +1744,7 @@ const VideoPlayerBase = class extends React.Component {
/** /**
* Returns an object with the current state of the media including `currentTime`, `duration`, * Returns an object with the current state of the media including `currentTime`, `duration`,
* `paused`, `playbackRate`, `proportionLoaded`, and `proportionPlayed`. * `paused`, `playbackRate`, `proportionLoaded`, and `proportionPlayed`.
* Redux 상태와 내부 상태를 우선적으로 사용하여 일관성 보장
* *
* @function * @function
* @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype * @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
@@ -1653,13 +1752,19 @@ const VideoPlayerBase = class extends React.Component {
* @public * @public
*/ */
getMediaState = () => { getMediaState = () => {
// Redux 상태를 우선적으로 사용하여 일관성 보장
// Redux 상태가 없으면 내부 상태 사용 (fallback)
const reduxState = this.props.videoPlayState;
return { return {
currentTime: this.state.currentTime, currentTime: reduxState?.currentTime ?? this.state.currentTime,
duration: this.state.duration, duration: reduxState?.duration ?? this.state.duration,
paused: this.state.paused, paused: reduxState?.isPaused ?? this.state.paused,
playbackRate: this.video?.playbackRate, playbackRate: reduxState?.playbackRate ?? this.video?.playbackRate ?? this.state.playbackRate,
proportionLoaded: this.state.proportionLoaded, proportionLoaded: this.state.proportionLoaded,
proportionPlayed: this.state.proportionPlayed, proportionPlayed: this.state.proportionPlayed,
// Redux 상태 정보도 포함
isPlaying: reduxState?.isPlaying ?? !this.state.paused,
}; };
}; };
@@ -1688,7 +1793,16 @@ const VideoPlayerBase = class extends React.Component {
* @public * @public
*/ */
play = () => { play = () => {
dlog('🟢 [PlayerPanel][VideoPlayer] play() called', {
currentTime: this.state.currentTime,
duration: this.state.duration,
paused: this.state.paused,
sourceUnavailable: this.state.sourceUnavailable,
prevCommand: this.prevCommand,
});
if (this.state.sourceUnavailable) { if (this.state.sourceUnavailable) {
dwarn('⚠️ [PlayerPanel][VideoPlayer] play() aborted - source unavailable');
return; return;
} }
@@ -1700,6 +1814,19 @@ const VideoPlayerBase = class extends React.Component {
this.send('play'); this.send('play');
this.announce($L('Play')); this.announce($L('Play'));
this.startDelayedMiniFeedbackHide(5000); this.startDelayedMiniFeedbackHide(5000);
// Redux 상태 업데이트 - 재생 상태로 변경
if (this.props.dispatch) {
this.props.dispatch(
updateVideoPlayState({
isPlaying: true,
isPaused: false,
currentTime: this.state.currentTime,
duration: this.state.duration,
playbackRate: 1,
})
);
}
}; };
/** /**
@@ -1710,7 +1837,16 @@ const VideoPlayerBase = class extends React.Component {
* @public * @public
*/ */
pause = () => { pause = () => {
dlog('🔴 [VideoPlayer] pause() called', {
currentTime: this.state.currentTime,
duration: this.state.duration,
paused: this.state.paused,
sourceUnavailable: this.state.sourceUnavailable,
prevCommand: this.prevCommand,
});
if (this.state.sourceUnavailable) { if (this.state.sourceUnavailable) {
dwarn('⚠️ [VideoPlayer] pause() aborted - source unavailable');
return; return;
} }
@@ -1722,6 +1858,22 @@ const VideoPlayerBase = class extends React.Component {
this.send('pause'); this.send('pause');
this.announce($L('Pause')); this.announce($L('Pause'));
this.stopDelayedMiniFeedbackHide(); this.stopDelayedMiniFeedbackHide();
// Redux 상태 업데이트 - 일시정지 상태로 변경
if (this.props.dispatch) {
const pauseState = {
isPlaying: false,
isPaused: true,
currentTime: this.state.currentTime,
duration: this.state.duration,
playbackRate: 1,
};
dlog('📤 [VideoPlayer] Dispatching pause state', pauseState);
this.props.dispatch(updateVideoPlayState(pauseState));
} else {
dwarn('⚠️ [VideoPlayer] No dispatch prop available - Redux state not updated');
}
}; };
/** /**
@@ -1733,6 +1885,15 @@ const VideoPlayerBase = class extends React.Component {
* @public * @public
*/ */
seek = (timeIndex) => { seek = (timeIndex) => {
dlog('⏩ [VideoPlayer] seek() called', {
timeIndex,
currentTime: this.state.currentTime,
duration: this.state.duration,
videoDuration: this.video?.duration,
seekDisabled: this.props.seekDisabled,
sourceUnavailable: this.state.sourceUnavailable,
});
if (this.video) { if (this.video) {
if ( if (
!this.props.seekDisabled && !this.props.seekDisabled &&
@@ -1743,9 +1904,34 @@ const VideoPlayerBase = class extends React.Component {
const actualSeekTime = const actualSeekTime =
timeIndex >= this.video.duration ? this.video.duration - 1 : timeIndex; timeIndex >= this.video.duration ? this.video.duration - 1 : timeIndex;
this.video.currentTime = actualSeekTime; this.video.currentTime = actualSeekTime;
dlog('⏩ [VideoPlayer] Video seek completed', {
requestedTime: timeIndex,
actualTime: actualSeekTime,
videoDuration: this.video.duration,
});
// Redux 상태 업데이트 - 시간 이동 상태 반영
if (this.props.dispatch) {
const seekState = {
isPlaying: !this.state.paused,
isPaused: this.state.paused,
currentTime: actualSeekTime,
duration: this.state.duration,
playbackRate: this.state.playbackRate,
};
dlog('📤 [VideoPlayer] Dispatching seek state', seekState);
this.props.dispatch(updateVideoPlayState(seekState));
} else { } else {
dwarn('⚠️ [VideoPlayer] No dispatch prop available - Redux state not updated');
}
} else {
derror('❌ [VideoPlayer] seek failed - disabled or source unavailable');
forward('onSeekFailed', {}, this.props); forward('onSeekFailed', {}, this.props);
} }
} else {
derror('❌ [VideoPlayer] seek failed - no video element');
} }
}; };

View File

@@ -16,7 +16,7 @@ import { calculateIsPanelOnTop } from '../utils/panelUtils'; // 🎯 isOnTop 유
// DEBUG_MODE - true인 경우에만 로그 출력 // DEBUG_MODE - true인 경우에만 로그 출력
// ⚠️ [251122] panelHistory 로그 비활성화 - 로그 생성 차단 // ⚠️ [251122] panelHistory 로그 비활성화 - 로그 생성 차단
const DEBUG_MODE = false; const DEBUG_MODE = true;
/** /**
* Panel history middleware * Panel history middleware
@@ -33,8 +33,8 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
(action.type.includes('PANEL') || action.type === 'CLEAR_PANEL_HISTORY') (action.type.includes('PANEL') || action.type === 'CLEAR_PANEL_HISTORY')
) { ) {
const caller = new Error().stack.split('\n')[1]?.trim(); const caller = new Error().stack.split('\n')[1]?.trim();
// console.log(`[PANEL DEBUG] ${action.type} from: ${caller}`); console.log(`[PANEL DEBUG] ${action.type} from: ${caller}`);
// console.log(' Payload:', action.payload); console.log(' Payload:', action.payload);
} }
// GNB 호출 식별을 위한 helper 함수 // GNB 호출 식별을 위한 helper 함수
@@ -81,7 +81,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const isGNB = isGNBCall(); const isGNB = isGNBCall();
const isOnTop = calculateIsOnTop(panelName); // 🎯 isOnTop 계산 const isOnTop = calculateIsOnTop(panelName); // 🎯 isOnTop 계산
if (DEBUG_MODE) if (DEBUG_MODE)
console.log(`[PANEL] PUSH_PANEL: ${panelName}`, { console.log('[PANEL] PUSH_PANEL:', {
panelName, panelName,
panelInfo, panelInfo,
isGNB, isGNB,
@@ -126,7 +126,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
if (panels.length > 0) { if (panels.length > 0) {
const topPanel = panels[panels.length - 1]; const topPanel = panels[panels.length - 1];
if (DEBUG_MODE) { if (DEBUG_MODE) {
console.log(`[PANEL-TRACE] POP_PANEL middleware stack: ${topPanel?.name}`, { console.log('[PANEL-TRACE] POP_PANEL middleware stack', {
stack: panels.map((p) => p.name), stack: panels.map((p) => p.name),
topPanel: topPanel?.name, topPanel: topPanel?.name,
payload: action.payload, payload: action.payload,
@@ -137,7 +137,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const isGNB = isGNBCall(); const isGNB = isGNBCall();
const isOnTop = calculateIsOnTop(topPanel.name); // 🎯 isOnTop 계산 const isOnTop = calculateIsOnTop(topPanel.name); // 🎯 isOnTop 계산
if (DEBUG_MODE) if (DEBUG_MODE)
console.log(`[PANEL] POP_PANEL: ${topPanel.name}`, { console.log('[PANEL] POP_PANEL:', {
panelName: topPanel.name, panelName: topPanel.name,
panelInfo: topPanel.panelInfo || {}, panelInfo: topPanel.panelInfo || {},
isGNB, isGNB,
@@ -185,7 +185,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const isGNB = isGNBCall(); const isGNB = isGNBCall();
const isOnTop = calculateIsOnTop(panelName); // 🎯 isOnTop 계산 const isOnTop = calculateIsOnTop(panelName); // 🎯 isOnTop 계산
if (DEBUG_MODE) if (DEBUG_MODE)
console.log(`[PANEL] UPDATE_PANEL: ${panelName}`, { console.log('[PANEL] UPDATE_PANEL:', {
panelName, panelName,
panelInfo, panelInfo,
isGNB, isGNB,
@@ -226,15 +226,11 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
// RESET_PANELS: GNB 네비게이션 또는 완전 초기화 // RESET_PANELS: GNB 네비게이션 또는 완전 초기화
case types.RESET_PANELS: { case types.RESET_PANELS: {
if (DEBUG_MODE) { if (DEBUG_MODE)
const resetPanelNameForLog = (action.payload && action.payload.length > 0) console.log('[PANEL] RESET_PANELS:', {
? action.payload[0].name
: 'homepanel';
console.log(`[PANEL] RESET_PANELS: ${resetPanelNameForLog}`, {
payload: action.payload, payload: action.payload,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
}
if (DEBUG_MODE) if (DEBUG_MODE)
console.log('[PANEL_HISTORY] Before RESET_PANELS:', store.getState().panelHistory); console.log('[PANEL_HISTORY] Before RESET_PANELS:', store.getState().panelHistory);

View File

@@ -94,12 +94,6 @@ export const panelsReducer = (state = initialState, action) => {
} }
case types.POP_PANEL: { case types.POP_PANEL: {
// console.log('[💜UNIQUE_PANEL_STACK💜] POP_PANEL reducer START', {
// targetPanel: action.payload || 'last_panel',
// currentPanels: state.panels.map((p) => p.name),
// timestamp: Date.now(),
// });
dlog('[panelReducer] 🔴 POP_PANEL START', { dlog('[panelReducer] 🔴 POP_PANEL START', {
targetPanel: action.payload || 'last_panel', targetPanel: action.payload || 'last_panel',
currentPanels: state.panels.map((p) => p.name), currentPanels: state.panels.map((p) => p.name),
@@ -124,13 +118,6 @@ export const panelsReducer = (state = initialState, action) => {
resultPanels = state.panels.slice(0, state.panels.length - 1); resultPanels = state.panels.slice(0, state.panels.length - 1);
} }
// console.log('[💜UNIQUE_PANEL_STACK💜] POP_PANEL reducer END', {
// resultPanels: resultPanels.map((p) => p.name),
// panelCount: resultPanels.length,
// lastAction,
// timestamp: Date.now(),
// });
dlog('[panelReducer] 🔴 POP_PANEL END', { dlog('[panelReducer] 🔴 POP_PANEL END', {
resultPanels: resultPanels.map((p) => p.name), resultPanels: resultPanels.map((p) => p.name),
lastAction, lastAction,

View File

@@ -114,7 +114,6 @@ export const ACTIVE_POPUP = {
optionalConfirm: 'optionalConfirm', optionalConfirm: 'optionalConfirm',
energyPopup: 'energyPopup', energyPopup: 'energyPopup',
addCartPopup: 'addCartPopup', addCartPopup: 'addCartPopup',
scrollPopup: 'scrollPopup',
}; };
export const DEBUG_VIDEO_SUBTITLE_TEST = false; export const DEBUG_VIDEO_SUBTITLE_TEST = false;
export const AUTO_SCROLL_DELAY = 600; export const AUTO_SCROLL_DELAY = 600;

View File

@@ -458,7 +458,7 @@ const tap = fp.curry((fn, value) => {
* @param {*} value 대상 값 * @param {*} value 대상 값
*/ */
const trace = fp.curry((label, value) => { const trace = fp.curry((label, value) => {
// console.log(label, value); console.log(label, value);
return value; return value;
}); });

View File

@@ -280,7 +280,6 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
useEffect(() => { useEffect(() => {
return () => { return () => {
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] DetailPanel cleanup - calling finishModalMediaForce');
dispatch(finishModalMediaForce()); dispatch(finishModalMediaForce());
}; };
}, [dispatch]); }, [dispatch]);
@@ -304,25 +303,26 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
const sourcePanel = panelInfo?.sourcePanel; const sourcePanel = panelInfo?.sourcePanel;
const sourceMenu = panelInfo?.sourceMenu; const sourceMenu = panelInfo?.sourceMenu;
// console.log('[🔴UNIQUE_DETAIL_UNMOUNT🔴] DetailPanel cleanup/unmount triggered', { console.log('[DP-TRACE] Detail unmount start', {
// sourcePanel, sourcePanel,
// sourceMenu, sourceMenu,
// panelsSnapshot: panels.map((p) => p.name), panelsSnapshot: panels.map((p) => p.name),
// timestamp: Date.now(), });
// });
console.log('[Detail-BG] 306-line sourcePanel:', sourcePanel, 'sourceMenu:', sourceMenu);
// DetailPanel이 unmount되는 시점 // DetailPanel이 unmount되는 시점
// console.log('[🔴UNIQUE_DETAIL_UNMOUNT🔴] DetailPanel unmount details:', { console.log('[DetailPanel] unmount:', {
// sourcePanel, sourcePanel,
// sourceMenu, sourceMenu,
// timestamp: Date.now(), timestamp: Date.now(),
// }); });
// sourcePanel에 따른 상태 업데이트 // sourcePanel에 따른 상태 업데이트
switch (sourcePanel) { switch (sourcePanel) {
case panel_names.PLAYER_PANEL: { case panel_names.PLAYER_PANEL: {
// PlayerPanel에서 온 경우: PlayerPanel에 detailPanelClosed flag 전달 // PlayerPanel에서 온 경우: PlayerPanel에 detailPanelClosed flag 전달
console.log('[PANEL][DetailPanel] unmount - PlayerPanel에 detailPanelClosed flag 전달'); console.log('[DetailPanel] unmount - PlayerPanel에 detailPanelClosed flag 전달');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, name: panel_names.PLAYER_PANEL,
@@ -385,14 +385,6 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
const sourcePanel = panelInfo?.sourcePanel; const sourcePanel = panelInfo?.sourcePanel;
const sourceMenu = panelInfo?.sourceMenu; const sourceMenu = panelInfo?.sourceMenu;
// console.log('[🟠UNIQUE_DETAIL_BACK🟠] onBackClick triggered', {
// sourcePanel,
// sourceMenu,
// isCancelClick,
// currentPanels: panels.map((p) => p.name),
// timestamp: Date.now(),
// });
fp.pipe( fp.pipe(
() => { () => {
dispatch(clearAllToasts()); // BuyOption Toast 포함 모든 토스트 제거 dispatch(clearAllToasts()); // BuyOption Toast 포함 모든 토스트 제거
@@ -401,7 +393,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
switch (sourcePanel) { switch (sourcePanel) {
case panel_names.PLAYER_PANEL: case panel_names.PLAYER_PANEL:
// PlayerPanel에서 온 경우: 플레이어 비디오는 그대로 두고 모달만 정리 // PlayerPanel에서 온 경우: 플레이어 비디오는 그대로 두고 모달만 정리
console.log('[🟠UNIQUE_DETAIL_BACK🟠] PlayerPanel 출신: 모달 정리만 수행'); console.log('[DetailPanel] onBackClick - PlayerPanel 출신: 모달 정리만 수행');
dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료 dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료
dispatch(finishVideoPreview()); dispatch(finishVideoPreview());
break; break;
@@ -410,18 +402,17 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
case panel_names.SEARCH_PANEL: case panel_names.SEARCH_PANEL:
default: default:
// HomePanel, SearchPanel 등에서 온 경우: 백그라운드 비디오 일시 중지 // HomePanel, SearchPanel 등에서 온 경우: 백그라운드 비디오 일시 중지
// console.log( console.log(
// '[🟠UNIQUE_DETAIL_BACK🟠] source panel:', '[DetailPanel] onBackClick - source panel:',
// sourcePanel, sourcePanel,
// '백그라운드 비디오 일시 중지' '백그라운드 비디오 일시 중지'
// ); );
dispatch(pauseFullscreenVideo()); // PLAYER_PANEL 비디오 중지 dispatch(pauseFullscreenVideo()); // PLAYER_PANEL 비디오 중지
dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료 dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료
dispatch(finishVideoPreview()); dispatch(finishVideoPreview());
break; break;
} }
// console.log('[🟠UNIQUE_DETAIL_BACK🟠] Calling popPanel(DETAIL_PANEL)');
dispatch(popPanel(panel_names.DETAIL_PANEL)); dispatch(popPanel(panel_names.DETAIL_PANEL));
}, },
() => { () => {
@@ -443,7 +434,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
if (shouldUpdatePanel) { if (shouldUpdatePanel) {
console.log( console.log(
'[PANEL][DetailPanel] onBackClick - PlayerPanel에 detailPanelClosed flag 전달' '[DetailPanel] onBackClick - PlayerPanel에 detailPanelClosed flag 전달'
); );
dispatch( dispatch(
updatePanel({ updatePanel({

View File

@@ -9,21 +9,30 @@ import React, {
import classNames from 'classnames'; import classNames from 'classnames';
// import { throttle } from 'lodash'; // import { throttle } from 'lodash';
import { PropTypes } from 'prop-types'; import { PropTypes } from 'prop-types';
import { useDispatch, useSelector } from 'react-redux'; import {
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
/* eslint-disable react/jsx-no-bind */ /* eslint-disable react/jsx-no-bind */
// src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx // src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable'; import Spottable from '@enact/spotlight/Spottable';
import couponImg from '../../../../assets/images/icons/coupon.png'; import couponImg from '../../../../assets/images/icons/coupon.png';
import arrowDownIcon from '../../../../assets/images/icons/ic-arrow-down.svg'; import arrowDownIcon from '../../../../assets/images/icons/ic-arrow-down.svg';
// import Spottable from '@enact/spotlight/Spottable'; // import Spottable from '@enact/spotlight/Spottable';
//image //image
import arrowDown from '../../../../assets/images/icons/ic_arrow_down_3x_new.png'; import arrowDown
import indicatorDefaultImage from '../../../../assets/images/img-thumb-empty-144@3x.png'; from '../../../../assets/images/icons/ic_arrow_down_3x_new.png';
import { setHidePopup, setShowPopup } from '../../../actions/commonActions.js'; import indicatorDefaultImage
from '../../../../assets/images/img-thumb-empty-144@3x.png';
import {
setHidePopup,
setShowPopup,
} from '../../../actions/commonActions.js';
import { import {
getProductCouponDownload, getProductCouponDownload,
getProductCouponSearch, getProductCouponSearch,
@@ -53,7 +62,8 @@ import CustomImage from '../../../components/CustomImage/CustomImage.jsx';
// ProductInfoSection imports // ProductInfoSection imports
import TButton, { TYPES } from '../../../components/TButton/TButton'; import TButton, { TYPES } from '../../../components/TButton/TButton';
import TPopUp from '../../../components/TPopUp/TPopUp.jsx'; import TPopUp from '../../../components/TPopUp/TPopUp.jsx';
import TVirtualGridList from '../../../components/TVirtualGridList/TVirtualGridList.jsx'; import TVirtualGridList
from '../../../components/TVirtualGridList/TVirtualGridList.jsx';
import useReviews from '../../../hooks/useReviews/useReviews'; import useReviews from '../../../hooks/useReviews/useReviews';
import useScrollTo from '../../../hooks/useScrollTo'; import useScrollTo from '../../../hooks/useScrollTo';
import { BUYNOW_CONFIG } from '../../../utils/BuyNowConfig'; import { BUYNOW_CONFIG } from '../../../utils/BuyNowConfig';
@@ -79,7 +89,10 @@ import {
tap, tap,
when, when,
} from '../../../utils/fp'; } from '../../../utils/fp';
import { $L, formatGMTString } from '../../../utils/helperMethods'; import {
$L,
formatGMTString,
} from '../../../utils/helperMethods';
import { SpotlightIds } from '../../../utils/SpotlightIds'; import { SpotlightIds } from '../../../utils/SpotlightIds';
import ShowUserReviews from '../../UserReview/ShowUserReviews'; import ShowUserReviews from '../../UserReview/ShowUserReviews';
// import CustomScrollbar from '../components/CustomScrollbar/CustomScrollbar'; // import CustomScrollbar from '../components/CustomScrollbar/CustomScrollbar';
@@ -90,14 +103,21 @@ import StarRating from '../components/StarRating';
// ProductContentSection imports // ProductContentSection imports
import TScrollerDetail from '../components/TScroller/TScrollerDetail'; import TScrollerDetail from '../components/TScroller/TScrollerDetail';
import DetailPanelSkeleton from '../DetailPanelSkeleton/DetailPanelSkeleton'; import DetailPanelSkeleton from '../DetailPanelSkeleton/DetailPanelSkeleton';
import ProductDescription from '../ProductContentSection/ProductDescription/ProductDescription'; import ProductDescription
import ProductDetail from '../ProductContentSection/ProductDetail/ProductDetail.new'; from '../ProductContentSection/ProductDescription/ProductDescription';
import { ProductVideoV2 } from '../ProductContentSection/ProductVideo/ProductVideo.v2.jsx'; import ProductDetail
import ProductVideo from '../ProductContentSection/ProductVideo/ProductVideo.v3'; from '../ProductContentSection/ProductDetail/ProductDetail.new';
import SeeMoreProducts from '../ProductContentSection/SeeMoreProducts/SeeMoreProducts'; import {
ProductVideoV2,
} from '../ProductContentSection/ProductVideo/ProductVideo.v2.jsx';
import ProductVideo
from '../ProductContentSection/ProductVideo/ProductVideo.v3';
import SeeMoreProducts
from '../ProductContentSection/SeeMoreProducts/SeeMoreProducts';
import UserReviews from '../ProductContentSection/UserReviews/UserReviews'; import UserReviews from '../ProductContentSection/UserReviews/UserReviews';
// import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton'; // import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton';
import YouMayAlsoLike from '../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike'; import YouMayAlsoLike
from '../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike';
import QRCode from '../ProductInfoSection/QRCode/QRCode'; import QRCode from '../ProductInfoSection/QRCode/QRCode';
import ProductOverview from '../ProductOverview/ProductOverview'; import ProductOverview from '../ProductOverview/ProductOverview';
// CSS imports // CSS imports
@@ -146,8 +166,7 @@ const ShopByMobileContainer = SpotlightContainerDecorator(
spotlightDirection: 'horizontal', spotlightDirection: 'horizontal',
enterTo: 'last-focused', enterTo: 'last-focused',
restrict: 'self-only', restrict: 'self-only',
defaultElement: defaultElement: SpotlightIds?.DETAIL_SHOPBYMOBILE || 'detail_shop_by_mobile',
SpotlightIds?.DETAIL_SHOPBYMOBILE || 'detail_shop_by_mobile',
}, },
'div' 'div'
); );
@@ -239,27 +258,17 @@ export default function ProductAllSection({
const dispatch = useDispatch(); const dispatch = useDispatch();
// Redux 상태 // Redux 상태
const webOSVersion = useSelector( const webOSVersion = useSelector((state) => state.common.appStatus.webOSVersion);
(state) => state.common.appStatus.webOSVersion
);
const groupInfos = useSelector((state) => state.product.groupInfo); const groupInfos = useSelector((state) => state.product.groupInfo);
const nowMenu = useSelector((state) => state.common.menu.nowMenu); const nowMenu = useSelector((state) => state.common.menu.nowMenu);
// YouMayLike 데이터는 API 응답 시간이 걸리므로 직접 구독 // YouMayLike 데이터는 API 응답 시간이 걸리므로 직접 구독
const youmaylikeData = useSelector((state) => state.main.youmaylikeData); const youmaylikeData = useSelector((state) => state.main.youmaylikeData);
//coupon //coupon
const { partnerCoupon } = useSelector( const { partnerCoupon } = useSelector((state) => state.coupon.productCouponSearchData);
(state) => state.coupon.productCouponSearchData const { userNumber } = useSelector((state) => state.common.appStatus.loginUserData);
); const { popupVisible, activePopup } = useSelector((state) => state.common.popup);
const { userNumber } = useSelector( const cursorVisible = useSelector((state) => state.common.appStatus.cursorVisible);
(state) => state.common.appStatus.loginUserData
);
const { popupVisible, activePopup } = useSelector(
(state) => state.common.popup
);
const cursorVisible = useSelector(
(state) => state.common.appStatus.cursorVisible
);
// 🆕 [251210] patnrId=21 카테고리 그룹 데이터 // 🆕 [251210] patnrId=21 카테고리 그룹 데이터
const brandShopByShowCategoryGroups = useSelector( const brandShopByShowCategoryGroups = useSelector(
(state) => state.brand.brandShopByShowCategoryGroups (state) => state.brand.brandShopByShowCategoryGroups
@@ -284,15 +293,12 @@ export default function ProductAllSection({
// 출처 정보 통합 (향후 확장성 대비) // 출처 정보 통합 (향후 확장성 대비)
// YouMayLike 상품이 아닐 경우 fromPanel을 초기화하여 오기 방지 // YouMayLike 상품이 아닐 경우 fromPanel을 초기화하여 오기 방지
const fromPanel = useMemo( const fromPanel = useMemo(() => ({
() => ({
fromYouMayLike: panelInfo?.fromPanel?.fromYouMayLike || false, fromYouMayLike: panelInfo?.fromPanel?.fromYouMayLike || false,
// 향후 다른 출처 플래그들 추가 가능 // 향후 다른 출처 플래그들 추가 가능
// fromRecommendation: panelInfo?.fromPanel?.fromRecommendation || false, // fromRecommendation: panelInfo?.fromPanel?.fromRecommendation || false,
// fromSearch: panelInfo?.fromPanel?.fromSearch || false, // fromSearch: panelInfo?.fromPanel?.fromSearch || false,
}), }), [panelInfo?.fromPanel?.fromYouMayLike]);
[panelInfo?.fromPanel?.fromYouMayLike]
);
//구매 하단 토스트 노출 확인을 위한 용도 //구매 하단 토스트 노출 확인을 위한 용도
const [openToast, setOpenToast] = useState(false); const [openToast, setOpenToast] = useState(false);
@@ -326,13 +332,16 @@ export default function ProductAllSection({
scrollTop({ y: 0, animate: true }); scrollTop({ y: 0, animate: true });
setTimeout(()=>{ setTimeout(()=>{
if(hasVideo){ if(hasVideo){
Spotlight.focus('product-video-player'); Spotlight.focus("product-video-player");
} else { } else {
Spotlight.focus('product-detail-container-0'); Spotlight.focus("product-detail-container-0");
} }
},100); },100);
} }
}, [scrollTop, hasVideo]); }, [
scrollTop,
hasVideo
]);
const fetchCouponData = useCallback(() => { const fetchCouponData = useCallback(() => {
dispatch( dispatch(
@@ -394,9 +403,7 @@ export default function ProductAllSection({
const handleCouponTotDownload = useCallback(() => { const handleCouponTotDownload = useCallback(() => {
if (selectedCoupon && userNumber) { if (selectedCoupon && userNumber) {
const couponCodesArray = couponCodes const couponCodesArray = couponCodes.split(',').map((code) => parseInt(code.trim()));
.split(',')
.map((code) => parseInt(code.trim()));
setDownloadCouponArr((prevArr) => [...prevArr, ...couponCodesArray]); setDownloadCouponArr((prevArr) => [...prevArr, ...couponCodesArray]);
dispatch( dispatch(
@@ -467,9 +474,7 @@ export default function ProductAllSection({
return; return;
} }
setDownloadCouponArr((prevArr) => [...prevArr, cpnSno]); setDownloadCouponArr((prevArr) => [...prevArr, cpnSno]);
dispatch( dispatch(getProductCouponDownload({ mbrNo: userNumber, cpnSno: cpnSno }));
getProductCouponDownload({ mbrNo: userNumber, cpnSno: cpnSno })
);
dispatch(showToast({ message: 'Your coupon download is complete.' })); dispatch(showToast({ message: 'Your coupon download is complete.' }));
}; };
@@ -484,13 +489,7 @@ export default function ProductAllSection({
> >
<div <div
className={css.couponItem} className={css.couponItem}
aria-label={ aria-label={'Purchase over ' + cpnAplyMinPurcAmt + 'up to ' + cpnAplyMaxDcAmt + ' off'}
'Purchase over ' +
cpnAplyMinPurcAmt +
'up to ' +
cpnAplyMaxDcAmt +
' off'
}
> >
<div className={css.couponTopContents}> <div className={css.couponTopContents}>
{shptmDcTpCd === 'CPN00401' && ( {shptmDcTpCd === 'CPN00401' && (
@@ -506,9 +505,7 @@ export default function ProductAllSection({
<div className={css.couponMiddleContents}> <div className={css.couponMiddleContents}>
<span> <span>
{$L( {$L('Purchase over ${cpnAplyMinPurcAmt} (up to ${cpnAplyMaxDcAmt} off)')
'Purchase over ${cpnAplyMinPurcAmt} (up to ${cpnAplyMaxDcAmt} off)'
)
.replace('{cpnAplyMinPurcAmt}', cpnAplyMinPurcAmt) .replace('{cpnAplyMinPurcAmt}', cpnAplyMinPurcAmt)
.replace('{cpnAplyMaxDcAmt}', cpnAplyMaxDcAmt)} .replace('{cpnAplyMaxDcAmt}', cpnAplyMaxDcAmt)}
</span> </span>
@@ -521,9 +518,7 @@ export default function ProductAllSection({
<div <div
className={classNames( className={classNames(
css.couponBottomButton, css.couponBottomButton,
downloadCouponArr.length > 0 && downloadCouponArr.length > 0 && downloadCouponArr.includes(cpnSno) && css.disable,
downloadCouponArr.includes(cpnSno) &&
css.disable,
duplDwldYn === 'N' && downloadYn === 'Y' && css.disable, duplDwldYn === 'N' && downloadYn === 'Y' && css.disable,
!downloadCouponArr.includes(cpnSno) && !downloadCouponArr.includes(cpnSno) &&
index === selectedCouponIndex && index === selectedCouponIndex &&
@@ -532,9 +527,7 @@ export default function ProductAllSection({
)} )}
aria-label="Download Button" aria-label="Download Button"
> >
{downloadCouponArr.length > 0 && {downloadCouponArr.length > 0 && downloadCouponArr.includes(cpnSno) && duplDwldYn === 'N'
downloadCouponArr.includes(cpnSno) &&
duplDwldYn === 'N'
? $L('DOWNLOAD COMPLETED') ? $L('DOWNLOAD COMPLETED')
: $L('DOWNLOAD')} : $L('DOWNLOAD')}
</div> </div>
@@ -573,25 +566,13 @@ export default function ProductAllSection({
const [activeButton, setActiveButton] = useState(null); const [activeButton, setActiveButton] = useState(null);
const productData = useMemo( const productData = useMemo(
() => () => getProductData(productType, themeProductInfo, themeProducts, selectedIndex, productInfo),
getProductData(
productType,
themeProductInfo,
themeProducts,
selectedIndex,
productInfo
),
[productType, themeProductInfo, themeProducts, selectedIndex, productInfo] [productType, themeProductInfo, themeProducts, selectedIndex, productInfo]
); );
// ProductDescription 데이터 유무 확인
const hasProductDescription = useMemo(() => {
return !!productData?.prdtDesc;
}, [productData?.prdtDesc]);
// 🆕 [251211] patnrId=21인 경우 QR 데이터 확인 // 🆕 [251211] patnrId=21인 경우 QR 데이터 확인
useEffect(() => { useEffect(() => {
if (productData?.patnrId === 21 || productData?.patnrId === '21') { if (productData?.patnrId === 21 || productData?.patnrId === "21") {
console.log('[QR-Data] patnrId=21 QR 데이터 확인:', { console.log('[QR-Data] patnrId=21 QR 데이터 확인:', {
patnrId: productData?.patnrId, patnrId: productData?.patnrId,
prdtId: productData?.prdtId, prdtId: productData?.prdtId,
@@ -648,9 +629,7 @@ export default function ProductAllSection({
if (webOSVersion < '6.0') { if (webOSVersion < '6.0') {
return ( return (
productData?.pmtSuptYn === 'N' || productData?.pmtSuptYn === 'N' ||
(productData?.pmtSuptYn === 'Y' && (productData?.pmtSuptYn === 'Y' && productData?.grPrdtProcYn === 'N' && panelInfo?.prdtId)
productData?.grPrdtProcYn === 'N' &&
panelInfo?.prdtId)
); );
} }
@@ -671,10 +650,7 @@ export default function ProductAllSection({
// 여행/테마 상품 - DetailPanel.backup.jsx와 동일한 로직 // 여행/테마 상품 - DetailPanel.backup.jsx와 동일한 로직
const isTravelProductVisible = useMemo(() => { const isTravelProductVisible = useMemo(() => {
return ( return panelInfo?.curationId && (panelInfo?.type === 'theme' || panelInfo?.type === 'hotel');
panelInfo?.curationId &&
(panelInfo?.type === 'theme' || panelInfo?.type === 'hotel')
);
}, [panelInfo]); }, [panelInfo]);
// useReviews Hook 사용 - 모든 리뷰 관련 로직을 담당 // useReviews Hook 사용 - 모든 리뷰 관련 로직을 담당
@@ -746,13 +722,13 @@ export default function ProductAllSection({
if (productData && Object.keys(productData).length > 0) { if (productData && Object.keys(productData).length > 0) {
// sendLogDetail - 제품 상세 버튼 클릭 로깅 (Source와 동일) // sendLogDetail - 제품 상세 버튼 클릭 로깅 (Source와 동일)
const detailLogParams = { const detailLogParams = {
curationId: productData?.curationId ?? '', curationId: productData?.curationId ?? "",
curationNm: productData?.curationNm ?? '', curationNm: productData?.curationNm ?? "",
inDt: '', inDt: "",
linkTpCd: panelInfo?.linkTpCd ?? '', linkTpCd: panelInfo?.linkTpCd ?? "",
logTpNo: LOG_TP_NO.DETAIL.DETAIL_BUTTON_CLICK, logTpNo: LOG_TP_NO.DETAIL.DETAIL_BUTTON_CLICK,
patncNm: productData?.patncNm ?? '', patncNm: productData?.patncNm ?? "",
patnrId: productData?.patnrId ?? '', patnrId: productData?.patnrId ?? "",
}; };
dispatch(sendLogDetail(detailLogParams)); dispatch(sendLogDetail(detailLogParams));
@@ -769,22 +745,14 @@ export default function ProductAllSection({
menuType = Config.LOG_MENU.DETAIL_PAGE_PRODUCT_DETAIL; menuType = Config.LOG_MENU.DETAIL_PAGE_PRODUCT_DETAIL;
} }
dispatch( dispatch(sendLogTotalRecommend({
sendLogTotalRecommend({
menu: menuType, menu: menuType,
buttonTitle: 'DESCRIPTION', buttonTitle: "DESCRIPTION",
contextName: LOG_CONTEXT_NAME.DETAILPAGE, contextName: LOG_CONTEXT_NAME.DETAILPAGE,
messageId: LOG_MESSAGE_ID.BUTTONCLICK, messageId: LOG_MESSAGE_ID.BUTTONCLICK,
}) }));
);
} }
}, [ }, [productData, panelInfo, isBillingProductVisible, isGroupProductVisible, isTravelProductVisible]);
productData,
panelInfo,
isBillingProductVisible,
isGroupProductVisible,
isTravelProductVisible,
]);
// sendLogGNB 로깅 - Source의 DetailPanel 컴포넌트들과 동일한 패턴 // sendLogGNB 로깅 - Source의 DetailPanel 컴포넌트들과 동일한 패턴
useEffect(() => { useEffect(() => {
@@ -806,9 +774,7 @@ export default function ProductAllSection({
} }
// YouMayLike에서 상품 선택 시 메뉴 변경 (Source의 isYouMayLikeOpened와 동일 패턴) // YouMayLike에서 상품 선택 시 메뉴 변경 (Source의 isYouMayLikeOpened와 동일 패턴)
const menu = const menu = (fromPanel?.fromYouMayLike !== undefined && fromPanel?.fromYouMayLike === true)
fromPanel?.fromYouMayLike !== undefined &&
fromPanel?.fromYouMayLike === true
? `${baseMenu}/${Config.LOG_MENU.DETAIL_PAGE_YOU_MAY_LIKE}` ? `${baseMenu}/${Config.LOG_MENU.DETAIL_PAGE_YOU_MAY_LIKE}`
: baseMenu; : baseMenu;
@@ -832,17 +798,15 @@ export default function ProductAllSection({
// sendLogGNB 전송 후 플래그 초기화 (1회 사용 후 비활성화) // sendLogGNB 전송 후 플래그 초기화 (1회 사용 후 비활성화)
useEffect(() => { useEffect(() => {
if (fromPanel?.fromYouMayLike === true) { if (fromPanel?.fromYouMayLike === true) {
dispatch( dispatch(updatePanel({
updatePanel({
name: panel_names.DETAIL_PANEL, name: panel_names.DETAIL_PANEL,
panelInfo: { panelInfo: {
...panelInfo, ...panelInfo,
fromPanel: { fromPanel: {
fromYouMayLike: false, // 플래그 초기화 fromYouMayLike: false // 플래그 초기화
}, }
}, }
}) }));
);
} }
}, [fromPanel?.fromYouMayLike, dispatch, panelInfo]); }, [fromPanel?.fromYouMayLike, dispatch, panelInfo]);
@@ -867,24 +831,24 @@ export default function ProductAllSection({
lastProductDetailLogKeyRef.current = logKey; lastProductDetailLogKeyRef.current = logKey;
const params = { const params = {
befPrice: productData?.priceInfo?.split('|')[0], befPrice: productData?.priceInfo?.split("|")[0],
curationId: productData?.curationId ?? '', curationId: productData?.curationId ?? "",
curationNm: productData?.curationNm ?? '', curationNm: productData?.curationNm ?? "",
entryMenu: entryMenuRef.current, entryMenu: entryMenuRef.current,
expsOrd: '1', expsOrd: "1",
inDt: formatGMTString(new Date()), inDt: formatGMTString(new Date()),
lastPrice: productData?.priceInfo?.split('|')[1], lastPrice: productData?.priceInfo?.split("|")[1],
lgCatCd: productData?.catCd ?? '', lgCatCd: productData?.catCd ?? "",
lgCatNm: productData?.catNm ?? '', lgCatNm: productData?.catNm ?? "",
linkTpCd: panelInfo?.linkTpCd ?? '', linkTpCd: panelInfo?.linkTpCd ?? "",
logTpNo, logTpNo,
patncNm: productData?.patncNm ?? '', patncNm: productData?.patncNm ?? "",
patnrId: productData?.patnrId ?? '', patnrId: productData?.patnrId ?? "",
prdtId: productData?.prdtId ?? '', prdtId: productData?.prdtId ?? "",
prdtNm: productData?.prdtNm ?? '', prdtNm: productData?.prdtNm ?? "",
revwGrd: productData?.revwGrd ?? '', revwGrd: productData?.revwGrd ?? "",
rewdAplyFlag: productData.priceInfo?.split('|')[2], rewdAplyFlag: productData.priceInfo?.split("|")[2],
tsvFlag: productData?.todaySpclFlag ?? '', tsvFlag: productData?.todaySpclFlag ?? "",
}; };
dispatch(sendLogProductDetail(params)); dispatch(sendLogProductDetail(params));
@@ -921,22 +885,13 @@ export default function ProductAllSection({
// 🚀 SingleOption.jsx의 sendLogTotalRecommend 로직 추가 // 🚀 SingleOption.jsx의 sendLogTotalRecommend 로직 추가
if (productData && Object.keys(productData).length > 0) { if (productData && Object.keys(productData).length > 0) {
const { const { priceInfo, patncNm, prdtId, prdtNm, brndNm, catNm, showId, showNm } = productData;
priceInfo, const regularPrice = priceInfo?.split("|")[0];
patncNm, const discountPrice = priceInfo?.split("|")[1];
prdtId, const discountRate = priceInfo?.split("|")[4];
prdtNm,
brndNm,
catNm,
showId,
showNm,
} = productData;
const regularPrice = priceInfo?.split('|')[0];
const discountPrice = priceInfo?.split('|')[1];
const discountRate = priceInfo?.split('|')[4];
// Option 정보는 현재 선택된 옵션이 없으므로 기본값 사용 // Option 정보는 현재 선택된 옵션이 없으므로 기본값 사용
const prodOptCval = ''; // 실제로는 선택된 옵션 값이 들어가야 함 const prodOptCval = ""; // 실제로는 선택된 옵션 값이 들어가야 함
dispatch( dispatch(
sendLogTotalRecommend({ sendLogTotalRecommend({
@@ -951,8 +906,8 @@ export default function ProductAllSection({
category: catNm, category: catNm,
contextName: Config.LOG_CONTEXT_NAME.DETAILPAGE, contextName: Config.LOG_CONTEXT_NAME.DETAILPAGE,
messageId: Config.LOG_MESSAGE_ID.BUY_NOW, messageId: Config.LOG_MESSAGE_ID.BUY_NOW,
showId: showId ?? '', showId: showId ?? "",
showNm: showNm ?? '', showNm: showNm ?? "",
}) })
); );
} }
@@ -992,29 +947,23 @@ export default function ProductAllSection({
); );
//닫히도록 //닫히도록
const handleCloseToast = useCallback( const handleCloseToast = useCallback((e) => {
(e) => {
// 팝업이 열려있으면 닫지 않음 // 팝업이 열려있으면 닫지 않음
if (popupVisible) { if (popupVisible) {
return; // 팝업이 활성이면 무시 return; // 팝업이 활성이면 무시
} }
dispatch(clearAllToasts()); dispatch(clearAllToasts());
setOpenToast(false); setOpenToast(false);
}, }, [dispatch, popupVisible]);
[dispatch, popupVisible]
);
const handleFocus = useCallback( const handleFocus = useCallback((e)=>{
(e) => {
// 팝업이 열려있으면 닫지 않음 // 팝업이 열려있으면 닫지 않음
if (popupVisible && cursorVisible) { if (popupVisible && cursorVisible) {
return; // 팝업이 활성이면 무시 return; // 팝업이 활성이면 무시
} }
dispatch(clearAllToasts()); dispatch(clearAllToasts());
setOpenToast(false); setOpenToast(false);
}, },[dispatch, popupVisible, cursorVisible])
[dispatch, popupVisible, cursorVisible]
);
// 스크롤 컨테이너의 클릭 이벤트 추적용 로깅 // 스크롤 컨테이너의 클릭 이벤트 추적용 로깅
const handleScrollContainerClick = useCallback((e) => { const handleScrollContainerClick = useCallback((e) => {
@@ -1061,10 +1010,7 @@ export default function ProductAllSection({
// TODO: 장바구니 추가 로직 구현 // TODO: 장바구니 추가 로직 구현
}, []); }, []);
const { revwGrd, orderPhnNo } = useMemo( const { revwGrd, orderPhnNo } = useMemo(() => extractProductMeta(productInfo), [productInfo]);
() => extractProductMeta(productInfo),
[productInfo]
);
const [favoriteOverride, setFavoriteOverride] = useState(null); const [favoriteOverride, setFavoriteOverride] = useState(null);
const favoriteFlag = useMemo( const favoriteFlag = useMemo(
@@ -1073,8 +1019,7 @@ export default function ProductAllSection({
); );
const [mobileSendPopupOpen, setMobileSendPopupOpen] = useState(false); const [mobileSendPopupOpen, setMobileSendPopupOpen] = useState(false);
const [isShowUserReviewsFocused, setIsShowUserReviewsFocused] = const [isShowUserReviewsFocused, setIsShowUserReviewsFocused] = useState(false);
useState(false);
// 🆕 [251210] patnrId=21 SEE MORE PRODUCTS 버튼 표시 여부 // 🆕 [251210] patnrId=21 SEE MORE PRODUCTS 버튼 표시 여부
const [hasSeeMoreProducts, setHasSeeMoreProducts] = useState(false); const [hasSeeMoreProducts, setHasSeeMoreProducts] = useState(false);
const [seeMoreProductsData, setSeeMoreProductsData] = useState([]); const [seeMoreProductsData, setSeeMoreProductsData] = useState([]);
@@ -1103,8 +1048,8 @@ export default function ProductAllSection({
const handleUserReviewsClick = useCallback(() => { const handleUserReviewsClick = useCallback(() => {
scrollToSection('scroll-marker-user-reviews'); scrollToSection('scroll-marker-user-reviews');
setTimeout(()=>{ setTimeout(()=>{
Spotlight.focus('user-reviews-container'); Spotlight.focus("user-reviews-container");
}, 100); },100)
}, [scrollToSection]); }, [scrollToSection]);
// ProductVideo V1 전용 - MediaPanel minimize 포함 // ProductVideo V1 전용 - MediaPanel minimize 포함
@@ -1180,10 +1125,7 @@ export default function ProductAllSection({
document.addEventListener('detailpanel-scroll-reset', handleScrollReset); document.addEventListener('detailpanel-scroll-reset', handleScrollReset);
return () => { return () => {
document.removeEventListener( document.removeEventListener('detailpanel-scroll-reset', handleScrollReset);
'detailpanel-scroll-reset',
handleScrollReset
);
}; };
}, []); }, []);
const productDetailRef = useRef(null); //높이값 변경때문 const productDetailRef = useRef(null); //높이값 변경때문
@@ -1208,11 +1150,7 @@ export default function ProductAllSection({
} }
// 이미지들 추가 // 이미지들 추가
if ( if (productData && productData.imgUrls600 && productData.imgUrls600.length > 0) {
productData &&
productData.imgUrls600 &&
productData.imgUrls600.length > 0
) {
productData.imgUrls600.forEach((image, imgIndex) => { productData.imgUrls600.forEach((image, imgIndex) => {
items.push({ items.push({
type: 'image', type: 'image',
@@ -1232,9 +1170,7 @@ export default function ProductAllSection({
// renderItems에 Video가 존재하는지 확인하는 boolean 상태 // renderItems에 Video가 존재하는지 확인하는 boolean 상태
const hasVideo = useMemo(() => { const hasVideo = useMemo(() => {
return ( return (
renderItems && renderItems && renderItems.length > 0 && renderItems.some((item) => item.type === 'video')
renderItems.length > 0 &&
renderItems.some((item) => item.type === 'video')
); );
}, [renderItems]); }, [renderItems]);
@@ -1246,11 +1182,10 @@ export default function ProductAllSection({
const handleShopByMobileOpen = useCallback(() => { const handleShopByMobileOpen = useCallback(() => {
// sendLogShopByMobile - Source와 동일한 로깅 추가 // sendLogShopByMobile - Source와 동일한 로깅 추가
if (productData && Object.keys(productData).length > 0) { if (productData && Object.keys(productData).length > 0) {
const { priceInfo, patncNm, patnrId, prdtId, prdtNm, brndNm, catNm } = const { priceInfo, patncNm, patnrId, prdtId, prdtNm, brndNm, catNm } = productData;
productData; const regularPrice = priceInfo?.split("|")[0];
const regularPrice = priceInfo?.split('|')[0]; const discountPrice = priceInfo?.split("|")[1];
const discountPrice = priceInfo?.split('|')[1]; const discountRate = priceInfo?.split("|")[4];
const discountRate = priceInfo?.split('|')[4];
const logParams = { const logParams = {
prdtId, prdtId,
@@ -1298,8 +1233,7 @@ export default function ProductAllSection({
}, [promotions, isBillingProductVisible, shopByMobileId, stackOrder]); }, [promotions, isBillingProductVisible, shopByMobileId, stackOrder]);
const firstCouponId = useMemo( const firstCouponId = useMemo(
() => () => (promotions && promotions.length > 0 ? 'detail-coupon-button-0' : null),
promotions && promotions.length > 0 ? 'detail-coupon-button-0' : null,
[promotions] [promotions]
); );
@@ -1308,8 +1242,7 @@ export default function ProductAllSection({
[] []
); );
const handleThemeItemButtonClick = useCallback( const handleThemeItemButtonClick = useCallback((e) => {
(e) => {
e.stopPropagation(); e.stopPropagation();
dispatch( dispatch(
showToast({ showToast({
@@ -1336,29 +1269,24 @@ export default function ProductAllSection({
// }, // },
}) })
); );
}, }, [dispatch, themeProducts, setSelectedIndex, panelInfo]);
[dispatch, themeProducts, setSelectedIndex, panelInfo]
);
const handleProductDetailsClick = useCallback(() => { const handleProductDetailsClick = useCallback(() => {
// 제품 설명 데이터가 없을 때 클릭 방지
if (!hasProductDescription) return;
dispatch(minimizeModalMedia()); dispatch(minimizeModalMedia());
scrollToSection('scroll-marker-product-details'); scrollToSection('scroll-marker-product-details');
// Source의 handleIndicatorOptions와 동일한 로깅 기능 추가 // Source의 handleIndicatorOptions와 동일한 로깅 기능 추가
handleIndicatorOptions(); handleIndicatorOptions();
setTimeout(()=>{ setTimeout(()=>{
Spotlight.focus('product-description-content'); Spotlight.focus("product-description-content")
},100); },100);
}, [scrollToSection, dispatch, handleIndicatorOptions, hasProductDescription]); }, [scrollToSection, dispatch, handleIndicatorOptions]);
const handleYouMayAlsoLikeClick = useCallback(() => { const handleYouMayAlsoLikeClick = useCallback(() => {
dispatch(minimizeModalMedia()); dispatch(minimizeModalMedia());
scrollToSection('scroll-marker-you-may-also-like'); scrollToSection('scroll-marker-you-may-also-like');
setTimeout(()=>{ setTimeout(()=>{
Spotlight.focus('detail_youMayAlsoLike_area'); Spotlight.focus("detail_youMayAlsoLike_area")
},100); },100);
}, [scrollToSection, dispatch]); }, [scrollToSection, dispatch]);
@@ -1367,21 +1295,16 @@ export default function ProductAllSection({
// 버튼 클릭 시 스크롤만 처리 (데이터는 useEffect에서 처리) // 버튼 클릭 시 스크롤만 처리 (데이터는 useEffect에서 처리)
scrollToSection('scroll-marker-see-more-products'); scrollToSection('scroll-marker-see-more-products');
setTimeout(() => { setTimeout(() => {
Spotlight.focus('detail_seeMoreProducts_area'); Spotlight.focus("detail_seeMoreProducts_area");
}, 100); }, 100);
}, [scrollToSection]); }, [scrollToSection]);
// 🆕 [251210] patnrId=21인 경우 그룹 상품 데이터 미리 처리 // 🆕 [251210] patnrId=21인 경우 그룹 상품 데이터 미리 처리
useEffect(() => { useEffect(() => {
if (panelInfo?.patnrId === 21 || panelInfo?.patnrId === '21') { if (panelInfo?.patnrId === 21 || panelInfo?.patnrId === "21") {
console.log( console.log('[SEE MORE PRODUCTS] patnrId=21 detected - processing group data on panel mount');
'[SEE MORE PRODUCTS] patnrId=21 detected - processing group data on panel mount'
);
console.log('[SEE MORE PRODUCTS] panelInfo:', panelInfo); console.log('[SEE MORE PRODUCTS] panelInfo:', panelInfo);
console.log( console.log('[SEE MORE PRODUCTS] brandShopByShowCategoryGroups:', brandShopByShowCategoryGroups);
'[SEE MORE PRODUCTS] brandShopByShowCategoryGroups:',
brandShopByShowCategoryGroups
);
const patnrIdString = String(panelInfo.patnrId); const patnrIdString = String(panelInfo.patnrId);
const currentPrdtId = panelInfo.prdtId; const currentPrdtId = panelInfo.prdtId;
@@ -1400,25 +1323,16 @@ export default function ProductAllSection({
// 모든 contsId에서 현재 상품이 속한 그룹 찾기 // 모든 contsId에서 현재 상품이 속한 그룹 찾기
for (const contsId in categoryGroups) { for (const contsId in categoryGroups) {
const contsInfo = categoryGroups[contsId]; const contsInfo = categoryGroups[contsId];
console.log( console.log(`[SEE MORE PRODUCTS] Checking contsId: ${contsId}, contsNm: ${contsInfo.contsNm}`);
`[SEE MORE PRODUCTS] Checking contsId: ${contsId}, contsNm: ${contsInfo.contsNm}`
);
if (contsInfo.brandShopByShowClctInfos) { if (contsInfo.brandShopByShowClctInfos) {
for (const group of contsInfo.brandShopByShowClctInfos) { for (const group of contsInfo.brandShopByShowClctInfos) {
console.log( console.log(`[SEE MORE PRODUCTS] Checking group: ${group.clctNm} (${group.clctId})`);
`[SEE MORE PRODUCTS] Checking group: ${group.clctNm} (${group.clctId})`
);
if (group.brandProductInfos) { if (group.brandProductInfos) {
const foundProduct = group.brandProductInfos.find( const foundProduct = group.brandProductInfos.find(p => p.prdtId === currentPrdtId);
(p) => p.prdtId === currentPrdtId
);
if (foundProduct) { if (foundProduct) {
console.log( console.log('[SEE MORE PRODUCTS] 🎯 Found current product:', foundProduct);
'[SEE MORE PRODUCTS] 🎯 Found current product:',
foundProduct
);
foundGroup = group; foundGroup = group;
foundConts = contsInfo; foundConts = contsInfo;
break; break;
@@ -1437,28 +1351,22 @@ export default function ProductAllSection({
console.log(' - Group ID:', foundGroup.clctId); console.log(' - Group ID:', foundGroup.clctId);
// 현재 상품을 제외한 다른 상품들 확인 // 현재 상품을 제외한 다른 상품들 확인
const otherProducts = foundGroup.brandProductInfos.filter( const otherProducts = foundGroup.brandProductInfos.filter(p => p.prdtId !== currentPrdtId);
(p) => p.prdtId !== currentPrdtId
);
console.log(' - Other products count:', otherProducts.length); console.log(' - Other products count:', otherProducts.length);
if (otherProducts.length > 0) { if (otherProducts.length > 0) {
// 다른 상품이 있을 때만 버튼 표시 // 다른 상품이 있을 때만 버튼 표시
shouldShowButton = true; shouldShowButton = true;
console.log( console.log('[SEE MORE PRODUCTS] 📦 Showing button - group has other products:');
'[SEE MORE PRODUCTS] 📦 Showing button - group has other products:'
);
otherProducts.forEach((product, index) => { otherProducts.forEach((product, index) => {
console.log( console.log(` ${index + 1}. ${product.prdtNm} (${product.prdtId})`);
` ${index + 1}. ${product.prdtNm} (${product.prdtId})`
);
console.log(` - Price: ${product.priceInfo}`); console.log(` - Price: ${product.priceInfo}`);
console.log(` - Brand: ${product.brndNm || 'N/A'}`); console.log(` - Brand: ${product.brndNm || 'N/A'}`);
}); });
// 🆕 SeeMoreProducts 컴포넌트를 위한 데이터 변환 // 🆕 SeeMoreProducts 컴포넌트를 위한 데이터 변환
const formattedProducts = otherProducts.map((product) => ({ const formattedProducts = otherProducts.map(product => ({
prdtId: product.prdtId, prdtId: product.prdtId,
prdtNm: product.prdtNm, prdtNm: product.prdtNm,
priceInfo: product.priceInfo, priceInfo: product.priceInfo,
@@ -1474,20 +1382,14 @@ export default function ProductAllSection({
// YouMayAlsoLike 데이터 형식으로 맞추기 // YouMayAlsoLike 데이터 형식으로 맞추기
setSeeMoreProductsData(formattedProducts); setSeeMoreProductsData(formattedProducts);
} else { } else {
console.log( console.log('[SEE MORE PRODUCTS] ❌ No other products in group - hiding button');
'[SEE MORE PRODUCTS] ❌ No other products in group - hiding button'
);
setSeeMoreProductsData([]); setSeeMoreProductsData([]);
} }
} else { } else {
console.log( console.log('[SEE MORE PRODUCTS] ❌ No group found for current product - hiding button');
'[SEE MORE PRODUCTS] ❌ No group found for current product - hiding button'
);
} }
} else { } else {
console.log( console.log('[SEE MORE PRODUCTS] ❌ No category groups found for patnrId 21 - hiding button');
'[SEE MORE PRODUCTS] ❌ No category groups found for patnrId 21 - hiding button'
);
} }
// 버튼 표시 여부 상태 설정 // 버튼 표시 여부 상태 설정
@@ -1515,7 +1417,9 @@ export default function ProductAllSection({
const mediaMinimizedRef = useRef(false); const mediaMinimizedRef = useRef(false);
const getTotalContentHeight = useCallback(() => { const getTotalContentHeight = useCallback(() => {
const measuredHeight = const measuredHeight =
contentHeightRef.current || scrollContainerRef.current?.scrollHeight || 0; contentHeightRef.current ||
scrollContainerRef.current?.scrollHeight ||
0;
return measuredHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0); return measuredHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
}, []); }, []);
@@ -1548,8 +1452,7 @@ export default function ProductAllSection({
const prevScrollTop = prevScrollTopRef.current; const prevScrollTop = prevScrollTopRef.current;
scrollPositionRef.current = currentScrollTop; scrollPositionRef.current = currentScrollTop;
contentHeightRef.current = contentHeightRef.current = e?.scrollHeight || contentHeightRef.current || 0;
e?.scrollHeight || contentHeightRef.current || 0;
// 기존 bottom 체크 로직 유지 // 기존 bottom 체크 로직 유지
const totalHeight = getTotalContentHeight(); const totalHeight = getTotalContentHeight();
@@ -1607,13 +1510,7 @@ export default function ProductAllSection({
} }
// v2: onScrollStop에서 처리 (기존 로직 유지) // v2: onScrollStop에서 처리 (기존 로직 유지)
}, },
[ [isBottom, productVideoVersion, isVideoPlaying, dispatch, getTotalContentHeight]
isBottom,
productVideoVersion,
isVideoPlaying,
dispatch,
getTotalContentHeight,
]
); );
// 스크롤 멈추었을 때만 호출 (성능 최적화) // 스크롤 멈추었을 때만 호출 (성능 최적화)
@@ -1621,8 +1518,7 @@ export default function ProductAllSection({
(e) => { (e) => {
const currentScrollTop = e.scrollTop; const currentScrollTop = e.scrollTop;
scrollPositionRef.current = currentScrollTop; scrollPositionRef.current = currentScrollTop;
contentHeightRef.current = contentHeightRef.current = e?.scrollHeight || contentHeightRef.current || 0;
e?.scrollHeight || contentHeightRef.current || 0;
const totalHeight = getTotalContentHeight(); const totalHeight = getTotalContentHeight();
if (totalHeight) { if (totalHeight) {
const isAtBottom = currentScrollTop + 944 >= totalHeight; const isAtBottom = currentScrollTop + 944 >= totalHeight;
@@ -1687,9 +1583,7 @@ export default function ProductAllSection({
// 초기 로딩 후 Skeleton 숨기기 // 초기 로딩 후 Skeleton 숨기기
useEffect(() => { useEffect(() => {
const hasDataReady = const hasDataReady =
productType === 'theme' productType === 'theme' ? hasThemeContents && !!productData : !!productData;
? hasThemeContents && !!productData
: !!productData;
if (productType && hasDataReady && isInitialLoading) { if (productType && hasDataReady && isInitialLoading) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
@@ -1789,8 +1683,8 @@ export default function ProductAllSection({
}; };
}, []); }, []);
// 초기 로딩 중에는 Skeleton 표시 (비활성화됨) // 초기 로딩 중에는 Skeleton 표시
if (false && isInitialLoading) { if (isInitialLoading) {
return ( return (
<div className={css.detailArea}> <div className={css.detailArea}>
<DetailPanelSkeleton /> <DetailPanelSkeleton />
@@ -1799,11 +1693,7 @@ export default function ProductAllSection({
} }
return ( return (
<HorizontalContainer <HorizontalContainer className={css.detailArea} onClick={handleCloseToast} onFocus={handleFocus}>
className={css.detailArea}
onClick={handleCloseToast}
onFocus={handleFocus}
>
{/* Left Margin Section - 60px */} {/* Left Margin Section - 60px */}
<div className={css.leftMarginSection}></div> <div className={css.leftMarginSection}></div>
@@ -1837,10 +1727,7 @@ export default function ProductAllSection({
{isShowQRCode ? ( {isShowQRCode ? (
<> <>
{/* <QRCode productInfo={productData} productType={productType} kind={'detail'} /> */} {/* <QRCode productInfo={productData} productType={productType} kind={'detail'} /> */}
<QRCode <QRCode productInfo={productData} productType={productType} />
productInfo={productData}
productType={productType}
/>
</> </>
) : ( ) : (
<div className={css.qrRollingWrap}> <div className={css.qrRollingWrap}>
@@ -1876,9 +1763,7 @@ export default function ProductAllSection({
return ( return (
<div className={css.couponContainer} key={idx}> <div className={css.couponContainer} key={idx}>
<div className={css.couponTitleText}> <div className={css.couponTitleText}>
<div className={css.firstTitle}> <div className={css.firstTitle}>SPECIAL PROMOTION</div>
SPECIAL PROMOTION
</div>
<div className={css.secondTitle}> <div className={css.secondTitle}>
Coupon only applicable to this product! Coupon only applicable to this product!
</div> </div>
@@ -1930,9 +1815,7 @@ export default function ProductAllSection({
className={css.shopByMobileButton} className={css.shopByMobileButton}
onClick={handleShopByMobileOpen} onClick={handleShopByMobileOpen}
> >
<div className={css.shopByMobileText}> <div className={css.shopByMobileText}>{$L('SHOP BY MOBILE')}</div>
{$L('SHOP BY MOBILE')}
</div>
</TButton> </TButton>
{panelInfo && ( {panelInfo && (
<div className={css.favoriteBtnWrapper}> <div className={css.favoriteBtnWrapper}>
@@ -1952,9 +1835,7 @@ export default function ProductAllSection({
<div className={css.callToOrderSection}> <div className={css.callToOrderSection}>
{orderPhnNo && ( {orderPhnNo && (
<> <>
<div className={css.callToOrderText}> <div className={css.callToOrderText}>{$L('Call to Order')}</div>
{$L('Call to Order')}
</div>
<div className={css.phoneSection}> <div className={css.phoneSection}>
<div className={css.phoneIconContainer}> <div className={css.phoneIconContainer}>
<div className={css.phoneIcon} /> <div className={css.phoneIcon} />
@@ -1969,7 +1850,6 @@ export default function ProductAllSection({
className={css.actionButtonsWrapper} className={css.actionButtonsWrapper}
spotlightId="product-info-button-container" spotlightId="product-info-button-container"
> >
{hasProductDescription && (
<TButton <TButton
className={classNames( className={classNames(
css.productDetailsButton, css.productDetailsButton,
@@ -1980,7 +1860,6 @@ export default function ProductAllSection({
> >
{$L('PRODUCT DETAILS')} {$L('PRODUCT DETAILS')}
</TButton> </TButton>
)}
{isReviewDataComplete && ( {isReviewDataComplete && (
<> <>
{/* {/*
@@ -2002,8 +1881,7 @@ export default function ProductAllSection({
</TButton> </TButton>
</> </>
)} )}
{(panelInfo?.patnrId !== 21 || panelInfo?.patnrId !== '21') && {(panelInfo?.patnrId !== 21 || panelInfo?.patnrId !== "21") && hasYouMayAlsoLike && (
hasYouMayAlsoLike && (
<TButton <TButton
className={classNames( className={classNames(
css.youMayLikeButton, css.youMayLikeButton,
@@ -2015,8 +1893,7 @@ export default function ProductAllSection({
</TButton> </TButton>
)} )}
{/* 🆕 [251210] patnrId=21인 경우 SEE MORE PRODUCTS 버튼 */} {/* 🆕 [251210] patnrId=21인 경우 SEE MORE PRODUCTS 버튼 */}
{(panelInfo?.patnrId === 21 || panelInfo?.patnrId === '21') && {(panelInfo?.patnrId === 21 || panelInfo?.patnrId === "21") && hasSeeMoreProducts && (
hasSeeMoreProducts && (
<TButton <TButton
className={classNames( className={classNames(
css.seeMoreProductButton, css.seeMoreProductButton,
@@ -2058,10 +1935,7 @@ export default function ProductAllSection({
}} }}
> >
<div>{$L('THEME ITEM')}</div> <div>{$L('THEME ITEM')}</div>
<img <img src={arrowDownIcon} className={css.themeButtonIcon} />
src={arrowDownIcon}
className={css.themeButtonIcon}
/>
</TButton> </TButton>
</Container> </Container>
)} )}
@@ -2107,9 +1981,7 @@ export default function ProductAllSection({
onBlur={handleButtonBlur} onBlur={handleButtonBlur}
> >
{/* 비디오가 있으면 먼저 렌더링 (productVideoVersion이 3이 아닐 때만) */} {/* 비디오가 있으면 먼저 렌더링 (productVideoVersion이 3이 아닐 때만) */}
{hasVideo && {hasVideo && renderItems[0].type === 'video' && productVideoVersion !== 3 && (
renderItems[0].type === 'video' &&
productVideoVersion !== 3 && (
<> <>
{productVideoVersion === 1 ? ( {productVideoVersion === 1 ? (
<ProductVideo <ProductVideo
@@ -2135,10 +2007,7 @@ export default function ProductAllSection({
onScrollToImages={handleScrollToImagesV2} onScrollToImages={handleScrollToImagesV2}
/> />
)} )}
<div <div id="scroll-marker-after-video" className={css.scrollMarker}></div>
id="scroll-marker-after-video"
className={css.scrollMarker}
></div>
</> </>
)} )}
@@ -2159,10 +2028,7 @@ export default function ProductAllSection({
)) ))
: !hasVideo && <ProductDetail productInfo={productData} />} : !hasVideo && <ProductDetail productInfo={productData} />}
</div> </div>
<div <div id="scroll-marker-product-details" className={css.scrollMarker}></div>
id="scroll-marker-product-details"
className={css.scrollMarker}
></div>
<div <div
id="product-description-section" id="product-description-section"
ref={descriptionRef} ref={descriptionRef}
@@ -2172,10 +2038,7 @@ export default function ProductAllSection({
<ProductDescription productInfo={productData} /> <ProductDescription productInfo={productData} />
</div> </div>
{/* 리뷰 데이터가 완전할 때만 UserReviews 섹션 표시 */} {/* 리뷰 데이터가 완전할 때만 UserReviews 섹션 표시 */}
<div <div id="scroll-marker-user-reviews" className={css.scrollMarker}></div>
id="scroll-marker-user-reviews"
className={css.scrollMarker}
></div>
{isReviewDataComplete && ( {isReviewDataComplete && (
<div <div
id="user-reviews-section" id="user-reviews-section"
@@ -2203,11 +2066,8 @@ export default function ProductAllSection({
)} )}
</div> </div>
<div ref={youMayAlsoLikelRef}> <div ref={youMayAlsoLikelRef}>
<div <div id="scroll-marker-you-may-also-like" className={css.scrollMarker}></div>
id="scroll-marker-you-may-also-like" {(panelInfo?.patnrId !== 21 || panelInfo?.patnrId !== "21") && hasYouMayAlsoLike && (
className={css.scrollMarker}
></div>
{panelInfo?.patnrId !== '21' && hasYouMayAlsoLike && (
<div id="you-may-also-like-section"> <div id="you-may-also-like-section">
{/* {(() => { {/* {(() => {
console.log('[YouMayLike] YouMayAlsoLike 컴포넌트 렌더링:', { console.log('[YouMayLike] YouMayAlsoLike 컴포넌트 렌더링:', {
@@ -2230,16 +2090,10 @@ export default function ProductAllSection({
</div> </div>
{/* 🆕 [251210] patnrId=21인 경우 SEE MORE PRODUCTS 섹션 */} {/* 🆕 [251210] patnrId=21인 경우 SEE MORE PRODUCTS 섹션 */}
{panelInfo?.patnrId === '21' && hasSeeMoreProducts && ( {(panelInfo?.patnrId === 21 || panelInfo?.patnrId === "21") && hasSeeMoreProducts && (
<div ref={seeMoreProductsRef}> <div ref={seeMoreProductsRef}>
<div <div id="scroll-marker-see-more-products" className={css.scrollMarker}></div>
id="scroll-marker-see-more-products" <div id="see-more-products-section" data-spotlight-id="see-more-products-area">
className={css.scrollMarker}
></div>
<div
id="see-more-products-section"
data-spotlight-id="see-more-products-area"
>
<SeeMoreProducts <SeeMoreProducts
groupProducts={seeMoreProductsData} groupProducts={seeMoreProductsData}
sponserImage={sponserImage} sponserImage={sponserImage}
@@ -2302,9 +2156,7 @@ export default function ProductAllSection({
/> />
)} )}
</Container> </Container>
<div <div className={css.couponRemain}>{`1/${selectedCoupon?.length}`}</div>
className={css.couponRemain}
>{`1/${selectedCoupon?.length}`}</div>
</TPopUp> </TPopUp>
)} )}
</HorizontalContainer> </HorizontalContainer>

View File

@@ -1,32 +1,9 @@
import React, { import React, { useCallback } from "react";
useCallback, import css from "./ProductDescription.module.less";
useMemo, import { $L, removeSpecificTags } from "../../../../utils/helperMethods";
} from 'react'; import Spottable from "@enact/spotlight/Spottable";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import { import Spotlight from "@enact/spotlight";
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import {
setHidePopup,
setShowPopup,
} from '../../../../actions/commonActions';
import TButtonScroller
from '../../../../components/TButtonScroller/TButtonScroller';
import TNewPopUp from '../../../../components/TPopUp/TNewPopUp';
import * as Config from '../../../../utils/Config';
import {
$L,
removeSpecificTags,
} from '../../../../utils/helperMethods';
import css from './ProductDescription.module.less';
// TVerticalPagenator 제거됨 - TScrollerNew와 충돌 문제로 인해 // TVerticalPagenator 제거됨 - TScrollerNew와 충돌 문제로 인해
const SpottableComponent = Spottable("div"); const SpottableComponent = Spottable("div");
@@ -42,22 +19,11 @@ const Container = SpotlightContainerDecorator(
); );
export default function ProductDescription({ productInfo }) { export default function ProductDescription({ productInfo }) {
const { popupVisible, activePopup } = useSelector(
(state) => state.common.popup
);
const dispatch = useDispatch();
const productDescription = useCallback(() => { const productDescription = useCallback(() => {
const sanitizedString = removeSpecificTags(productInfo?.prdtDesc); const sanitizedString = removeSpecificTags(productInfo?.prdtDesc);
return { __html: sanitizedString }; return { __html: sanitizedString };
}, [productInfo?.prdtDesc]); }, [productInfo?.prdtDesc]);
const productDescriptionText = useMemo(() => {
return removeSpecificTags(productInfo?.prdtDesc);
}, [productInfo?.prdtDesc]);
// 왼쪽 화살표 키 이벤트 처리 // 왼쪽 화살표 키 이벤트 처리
const handleKeyDown = useCallback((ev) => { const handleKeyDown = useCallback((ev) => {
if (ev.keyCode === 37) { // 왼쪽 화살표 키 if (ev.keyCode === 37) { // 왼쪽 화살표 키
@@ -68,20 +34,6 @@ export default function ProductDescription({ productInfo }) {
} }
}, []); }, []);
const descriptionClick = useCallback(() => {
dispatch(setShowPopup(Config.ACTIVE_POPUP.scrollPopup));
},
[dispatch]
);
const _onClose = useCallback(()=>{
dispatch(setHidePopup());
// Restore focus to the description content after popup closes
setTimeout(() => {
Spotlight.focus('product-description-content');
}, 100);
},[dispatch])
// ProductDescription: Container 직접 사용 패턴 // ProductDescription: Container 직접 사용 패턴
// prdtDesc가 없으면 렌더링하지 않음 // prdtDesc가 없으면 렌더링하지 않음
if (!productInfo?.prdtDesc) { if (!productInfo?.prdtDesc) {
@@ -89,7 +41,6 @@ export default function ProductDescription({ productInfo }) {
} }
return ( return (
<>
<Container <Container
className={css.descriptionContainer} className={css.descriptionContainer}
spotlightId="product-description-container" spotlightId="product-description-container"
@@ -110,8 +61,7 @@ export default function ProductDescription({ productInfo }) {
<SpottableComponent <SpottableComponent
className={css.descriptionWrapper} className={css.descriptionWrapper}
spotlightId="product-description-content" spotlightId="product-description-content"
// onClick={() => console.log("[ProductDescription] Content clicked")} onClick={() => console.log("[ProductDescription] Content clicked")}
onClick={descriptionClick}
onFocus={() => console.log("[ProductDescription] Content focused")} onFocus={() => console.log("[ProductDescription] Content focused")}
onBlur={() => console.log("[ProductDescription] Content blurred")} onBlur={() => console.log("[ProductDescription] Content blurred")}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@@ -122,28 +72,5 @@ export default function ProductDescription({ productInfo }) {
/> />
</SpottableComponent> </SpottableComponent>
</Container> </Container>
{activePopup === Config.ACTIVE_POPUP.scrollPopup && (
<TNewPopUp
kind="scrollPopup"
open={popupVisible}
hasText
title={$L("DESCRIPTION")}
onClick={_onClose}
hasButton
button1Text={$L("OK")}
>
<TButtonScroller
boxHeight={460}
width={844}
kind={"figmaTermsPopup"}
>
<div
className={css.scrollContainer}
dangerouslySetInnerHTML={{ __html: productDescriptionText }}
/>
</TButtonScroller>
</TNewPopUp>
)}
</>
); );
} }

View File

@@ -51,8 +51,3 @@
} }
} }
.scrollContainer {
padding: 31px;
font-size: 26px;
line-height: 1.5;
}

View File

@@ -5,10 +5,11 @@
position: relative; position: relative;
width: 1114px; // ProductDetail과 동일한 고정 크기 width: 1114px; // ProductDetail과 동일한 고정 크기
max-width: 1114px; max-width: 1114px;
height: 632px !important; // ProductDetail과 동일한 고정 높이 height: 740px; // ProductDetail과 동일한 고정 높이
margin-bottom: 30px; // ProductDetail과 동일한 간격
cursor: pointer; cursor: pointer;
background-color: rgba(0, 0, 0, 1); background-color: rgba(0, 0, 0, 1);
border-radius: 12px 12px 0 0; border-radius: 12px;
box-sizing: border-box; box-sizing: border-box;
padding: 6px; // 포커스 테두리를 위한 공간 padding: 6px; // 포커스 테두리를 위한 공간
overflow: hidden; overflow: hidden;
@@ -79,7 +80,7 @@
z-index: 23; // MediaPanel(z-index: 22)보다 위에 표시되어야 비디오 재생 중에도 포커스 테두리가 보임 z-index: 23; // MediaPanel(z-index: 22)보다 위에 표시되어야 비디오 재생 중에도 포커스 테두리가 보임
border: 6px solid @PRIMARY_COLOR_RED; border: 6px solid @PRIMARY_COLOR_RED;
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5); box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
border-radius: 12px 12px 0px 0px; border-radius: 12px;
content: ""; content: "";
} }
@@ -216,13 +217,15 @@
} }
.notice { .notice {
width: calc(100% - 10px); width: 100%;
height: 54px; height: 54px;
background: #000000; background: #000000;
.flex(@justifyCenter:flex-start); .flex(@justifyCenter:flex-start);
padding: 6px 18px 18px 18px; padding: 6px 18px 18px 18px;
position: absolute;
bottom: 0;
border-radius: 0 0 12px 12px; border-radius: 0 0 12px 12px;
margin-bottom: 30px; // ProductDetail과 동일한 간격
.marquee { .marquee {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@@ -311,7 +311,6 @@ export default function ProductVideo({
if (!canPlayVideo) return null; if (!canPlayVideo) return null;
return ( return (
<>
<SpottableComponent <SpottableComponent
className={css.videoContainer} className={css.videoContainer}
onClick={handleVideoClick} onClick={handleVideoClick}
@@ -331,13 +330,12 @@ export default function ProductVideo({
<img src={playImg} alt="재생" /> <img src={playImg} alt="재생" />
</div> </div>
</div> </div>
</SpottableComponent>
<div className={css.notice}> <div className={css.notice}>
<Marquee className={css.marquee} marqueeOn="render"> <Marquee className={css.marquee} marqueeOn="render">
<img src={ic_warning} alt={disclaimer} /> <img src={ic_warning} alt={disclaimer} />
<span>{disclaimer}</span> <span>{disclaimer}</span>
</Marquee> </Marquee>
</div> </div>
</> </SpottableComponent>
); );
} }

View File

@@ -1,41 +1,21 @@
import React, { import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { import { useDispatch, useSelector } from 'react-redux';
useDispatch,
useSelector,
} from 'react-redux';
import { Job } from '@enact/core/util'; import { Job } from '@enact/core/util';
import SpotlightContainerDecorator import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable'; import Spottable from '@enact/spotlight/Spottable';
import { clearThemeDetail } from '../../../../actions/homeActions'; import { clearThemeDetail } from '../../../../actions/homeActions';
import { finishModalMediaForce } from '../../../../actions/mediaActions'; import { finishModalMediaForce } from '../../../../actions/mediaActions';
import { import { popPanel, pushPanel, updatePanel } from '../../../../actions/panelActions';
popPanel,
pushPanel,
updatePanel,
} from '../../../../actions/panelActions';
import { finishVideoPreview } from '../../../../actions/playActions'; import { finishVideoPreview } from '../../../../actions/playActions';
import THeader from '../../../../components/THeader/THeader'; import THeader from '../../../../components/THeader/THeader';
import TItemCardNew from '../../../../components/TItemCard/TItemCard.new'; import TItemCardNew from '../../../../components/TItemCard/TItemCard.new';
import TVerticalPagenator import TVerticalPagenator from '../../../../components/TVerticalPagenator/TVerticalPagenator';
from '../../../../components/TVerticalPagenator/TVerticalPagenator'; import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList';
import TVirtualGridList
from '../../../../components/TVirtualGridList/TVirtualGridList';
import useScrollTo from '../../../../hooks/useScrollTo'; import useScrollTo from '../../../../hooks/useScrollTo';
import { import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, panel_names } from '../../../../utils/Config';
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
panel_names,
} from '../../../../utils/Config';
import { $L } from '../../../../utils/helperMethods'; import { $L } from '../../../../utils/helperMethods';
import css from './YouMayAlsoLike.module.less'; import css from './YouMayAlsoLike.module.less';
@@ -178,7 +158,7 @@ export default function YouMayAlsoLike({
// DetailPanel을 언마운트하지 않고 상품 정보만 업데이트 // DetailPanel을 언마운트하지 않고 상품 정보만 업데이트
// 이렇게 하면 백그라운드 비디오 제어 상태가 유지됨 // 이렇게 하면 백그라운드 비디오 제어 상태가 유지됨
dispatch( dispatch(
pushPanel({ updatePanel({
name: panel_names.DETAIL_PANEL, name: panel_names.DETAIL_PANEL,
panelInfo: { panelInfo: {
showNm: panelInfo?.showNm, showNm: panelInfo?.showNm,

View File

@@ -78,35 +78,35 @@ export default function ThemeItemCard({
const { originalPrice, discountedPrice, discountRate } = const { originalPrice, discountedPrice, discountRate } =
usePriceInfo(priceInfo) || {}; usePriceInfo(priceInfo) || {};
// const mockEnergyLabel = [ const mockEnergyLabel = [
// { {
// "enrgLblExpsOrd": "0", "enrgLblExpsOrd": "0",
// "enrgLblTpCd": "EL_TYPE_05", "enrgLblTpCd": "EL_TYPE_05",
// "enrgLblCd": "MNLC", "enrgLblCd": "MNLC",
// "enrgClasCd": "A", "enrgClasCd": "A",
// "enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_a.png", "enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_a.png",
// "enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241113229264&ORIGINAL_NAME_b1_a1=27U511SA EU (E).pdf&FILE_NAME=27U511SA EU (E)[20241113011401634].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N", "enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241113229264&ORIGINAL_NAME_b1_a1=27U511SA EU (E).pdf&FILE_NAME=27U511SA EU (E)[20241113011401634].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N",
// "enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241125231113&ORIGINAL_NAME_b1_a1=27U511SA-W.pdf&FILE_NAME=27U511SA-W[20241125231113].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N" "enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241125231113&ORIGINAL_NAME_b1_a1=27U511SA-W.pdf&FILE_NAME=27U511SA-W[20241125231113].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N"
// }, },
// { {
// "enrgLblExpsOrd": "0", "enrgLblExpsOrd": "0",
// "enrgLblTpCd": "EL_TYPE_05", "enrgLblTpCd": "EL_TYPE_05",
// "enrgLblCd": "MNLC", "enrgLblCd": "MNLC",
// "enrgClasCd": "D", "enrgClasCd": "D",
// "enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_d.png", "enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_d.png",
// "enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241230236057&ORIGINAL_NAME_b1_a1=27U421A EU (E).pdf&FILE_NAME=27U421A EU (E)[20241230015816192].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N", "enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241230236057&ORIGINAL_NAME_b1_a1=27U421A EU (E).pdf&FILE_NAME=27U421A EU (E)[20241230015816192].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N",
// "enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241224235911&ORIGINAL_NAME_b1_a1=27U421A-B.pdf&FILE_NAME=27U421A-B[20241224235911].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N" "enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241224235911&ORIGINAL_NAME_b1_a1=27U421A-B.pdf&FILE_NAME=27U421A-B[20241224235911].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N"
// }, },
// { {
// "enrgLblExpsOrd": "0", "enrgLblExpsOrd": "0",
// "enrgLblTpCd": "EL_TYPE_05", "enrgLblTpCd": "EL_TYPE_05",
// "enrgLblCd": "MNLC", "enrgLblCd": "MNLC",
// "enrgClasCd": "D", "enrgClasCd": "D",
// "enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_d.png", "enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_d.png",
// "enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241230236057&ORIGINAL_NAME_b1_a1=27U421A EU (E).pdf&FILE_NAME=27U421A EU (E)[20241230015816192].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N", "enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241230236057&ORIGINAL_NAME_b1_a1=27U421A EU (E).pdf&FILE_NAME=27U421A EU (E)[20241230015816192].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N",
// "enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241224235911&ORIGINAL_NAME_b1_a1=27U421A-B.pdf&FILE_NAME=27U421A-B[20241224235911].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N" "enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241224235911&ORIGINAL_NAME_b1_a1=27U421A-B.pdf&FILE_NAME=27U421A-B[20241224235911].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N"
// } }
// ]; ];
const setEnactFitZIndex = (zIndexValue) => { const setEnactFitZIndex = (zIndexValue) => {
const target = document.getElementById("floatLayer"); const target = document.getElementById("floatLayer");
@@ -211,7 +211,7 @@ export default function ThemeItemCard({
))} ))}
</div> </div>
)} */} )} */}
{/* {mockEnergyLabel && mockEnergyLabel.length > 0 && ( {mockEnergyLabel && mockEnergyLabel.length > 0 && (
<div className={css.energyLabels}> <div className={css.energyLabels}>
{mockEnergyLabel.map((label, labelIndex) => ( {mockEnergyLabel.map((label, labelIndex) => (
<SpottableTemp <SpottableTemp
@@ -228,7 +228,7 @@ export default function ThemeItemCard({
</SpottableTemp> </SpottableTemp>
))} ))}
</div> </div>
)} */} )}
</div> </div>
</SpottableDiv> </SpottableDiv>
{(() => { {(() => {

View File

@@ -93,6 +93,9 @@ export default function THeaderCustom({
role="button" role="button"
/> />
)} )}
{type === "theme" && themeTitle && (
<span className={css.themeTitle} dangerouslySetInnerHTML={{ __html: themeTitle }} />
)}
{kind ? ( {kind ? (
"" ""
) : ( ) : (
@@ -104,9 +107,6 @@ export default function THeaderCustom({
}} }}
/> />
)} )}
{type === "theme" && themeTitle && (
<span className={css.themeTitle} dangerouslySetInnerHTML={{ __html: `[${themeTitle}]` }} />
)}
<Marquee <Marquee
marqueeOn="render" marqueeOn="render"
className={css.title} className={css.title}

View File

@@ -61,5 +61,4 @@
color: #eaeaea; color: #eaeaea;
width: max-content; width: max-content;
margin-right: 20px; margin-right: 20px;
margin-left: 10px;
} }

View File

@@ -1,26 +1,14 @@
import React, { import React, { useCallback, useEffect, useRef, useState, useMemo, forwardRef } from 'react';
forwardRef,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { import { off, on } from '@enact/core/dispatcher';
off,
on,
} from '@enact/core/dispatcher';
import { Job } from '@enact/core/util'; import { Job } from '@enact/core/util';
import Scroller from '@enact/sandstone/Scroller'; import Scroller from '@enact/sandstone/Scroller';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
import AutoScrollAreaDetail, { import AutoScrollAreaDetail, { POSITION } from '../AutoScrollAreaDetail/AutoScrollAreaDetail';
POSITION,
} from '../AutoScrollAreaDetail/AutoScrollAreaDetail';
import css from './TScrollerDetail.module.less'; import css from './TScrollerDetail.module.less';
/** /**
@@ -218,8 +206,7 @@ const TScrollerDetail = forwardRef(
onScrollStop={_onScrollStop} onScrollStop={_onScrollStop}
onScroll={_onScroll} onScroll={_onScroll}
scrollMode={scrollMode || 'translate'} scrollMode={scrollMode || 'translate'}
// focusableScrollbar={focusableScrollbar} focusableScrollbar={focusableScrollbar}
focusableScrollbar={false}
className={classNames(isMounted && css.tScroller, noScrollByWheel && css.preventScroll)} className={classNames(isMounted && css.tScroller, noScrollByWheel && css.preventScroll)}
direction={direction} direction={direction}
horizontalScrollbar={horizontalScrollbar} horizontalScrollbar={horizontalScrollbar}

View File

@@ -5,8 +5,8 @@ import { useDispatch, useSelector } from "react-redux";
import { Job } from "@enact/core/util"; import { Job } from "@enact/core/util";
import Spotlight from "@enact/spotlight"; import Spotlight from "@enact/spotlight";
import { pushPanel, updatePanel } from "../../../../../actions/panelActions"; import { updatePanel } from "../../../../../actions/panelActions";
// import { startVideoPlayer } from "../../../../../actions/playActions"; import { startVideoPlayer } from "../../../../../actions/playActions";
import TItemCard, { import TItemCard, {
removeDotAndColon, removeDotAndColon,
} from "../../../../../components/TItemCard/TItemCard"; } from "../../../../../components/TItemCard/TItemCard";
@@ -113,36 +113,27 @@ export default function RecommendedShowsProductList({
); );
} }
// 🆕 DetailPanel로 이동 (ShopByShow 방식) let y =
index < 2
? 0
: index === 2
? scaleH(208)
: scaleH(index * 248 - 248 - 40);
dispatch( dispatch(
pushPanel({ startVideoPlayer({
name: panel_names.DETAIL_PANEL, modal: false,
panelInfo: { patnrId, prdtId }, patnrId,
prdtId,
showId,
shptmBanrTpNm: "VOD",
thumbnail: videoThumbnail,
targetId: "spotlightId-" + prdtId,
y,
}) })
); );
// 🔴 기존 PlayerPanel 로직 (주석처리)
// let y =
// index < 2
// ? 0
// : index === 2
// ? scaleH(208)
// : scaleH(index * 248 - 248 - 40);
//
// dispatch(
// startVideoPlayer({
// modal: false,
// patnrId,
// prdtId,
// showId,
// shptmBanrTpNm: "VOD",
// thumbnail: videoThumbnail,
// targetId: "spotlightId-" + prdtId,
// y,
// })
// );
}, },
[catCd, dispatch, patnrId] [catCd, dispatch, patnrId, showId, videoThumbnail]
); );
const handleFocus = useCallback(() => { const handleFocus = useCallback(() => {

View File

@@ -6,11 +6,7 @@ import Spotlight from '@enact/spotlight';
import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator'; import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable'; import Spottable from '@enact/spotlight/Spottable';
import { import { pushPanel, updatePanel, navigateFromBestSeller } from '../../../actions/panelActions';
navigateFromBestSeller,
pushPanel,
updatePanel,
} from '../../../actions/panelActions';
import { navigateToDetailFromHome } from '../../../actions/panelNavigationActions'; import { navigateToDetailFromHome } from '../../../actions/panelNavigationActions';
import SectionTitle from '../../../components/SectionTitle/SectionTitle'; import SectionTitle from '../../../components/SectionTitle/SectionTitle';
import Tag from '../../../components/TItemCard/Tag'; import Tag from '../../../components/TItemCard/Tag';
@@ -19,20 +15,13 @@ import TItemCardNew from '../../../components/TItemCard/TItemCard.new';
import TScroller from '../../../components/TScroller/TScroller'; import TScroller from '../../../components/TScroller/TScroller';
import useScrollReset from '../../../hooks/useScrollReset'; import useScrollReset from '../../../hooks/useScrollReset';
import useScrollTo from '../../../hooks/useScrollTo'; import useScrollTo from '../../../hooks/useScrollTo';
import { import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, panel_names } from '../../../utils/Config';
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
panel_names,
} from '../../../utils/Config';
import { $L, scaleW } from '../../../utils/helperMethods'; import { $L, scaleW } from '../../../utils/helperMethods';
import { SpotlightIds } from '../../../utils/SpotlightIds'; import { SpotlightIds } from '../../../utils/SpotlightIds';
import css from './BestSeller.module.less'; import css from './BestSeller.module.less';
const SpottableComponent = Spottable('div'); const SpottableComponent = Spottable('div');
const Container = SpotlightContainerDecorator( const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
{ enterTo: 'last-focused' },
'div'
);
const BestSeller = ({ const BestSeller = ({
order, order,
@@ -44,26 +33,15 @@ const BestSeller = ({
shelfTitle, shelfTitle,
}) => { }) => {
const { getScrollTo, scrollLeft } = useScrollTo(); const { getScrollTo, scrollLeft } = useScrollTo();
const { handleScrollReset, handleStopScrolling } = useScrollReset( const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollLeft, true);
scrollLeft,
true
);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { cursorVisible } = useSelector((state) => state.common.appStatus); const { cursorVisible } = useSelector((state) => state.common.appStatus);
const bestSellerDatas = useSelector( const bestSellerDatas = useSelector((state) => state.product.bestSellerData?.bestSeller);
(state) => state.product.bestSellerData?.bestSeller
);
const bestSellerNewDatas = useSelector( const bestSellerNewDatas = useSelector((state) => state.foryou?.recommendInfo?.recommendProduct);
(state) => state.foryou?.recommendInfo?.recommendProduct
);
const { userNumber } = useSelector(
(state) => state.common.appStatus.loginUserData
);
const [drawChk, setDrawChk] = useState(false); const [drawChk, setDrawChk] = useState(false);
const [firstChk, setFirstChk] = useState(0); const [firstChk, setFirstChk] = useState(0);
@@ -73,14 +51,11 @@ const BestSeller = ({
useEffect(() => { useEffect(() => {
setBestInfos( setBestInfos(
bestSellerNewDatas?.filter( bestSellerNewDatas?.filter((item) => item.recommendTpCd === 'BESTSELLER') || [] // 기본값으로 빈 배열 설정
(item) => item.recommendTpCd === 'BESTSELLER'
) || [] // 기본값으로 빈 배열 설정
); );
}, [bestSellerNewDatas]); }, [bestSellerNewDatas]);
useEffect(() => { useEffect(() => {
if (userNumber) {
if (!bestInfos || bestInfos.length === 0) { if (!bestInfos || bestInfos.length === 0) {
const baseData = const baseData =
bestSellerDatas?.map((item) => ({ bestSellerDatas?.map((item) => ({
@@ -97,9 +72,7 @@ const BestSeller = ({
foryou: true, foryou: true,
})) || []; })) || [];
const recommendedPrdtIds = new Set( const recommendedPrdtIds = new Set(recommendedData.map((item) => item.prdtId));
recommendedData.map((item) => item.prdtId)
);
const baseData = const baseData =
bestSellerDatas?.map((item) => ({ bestSellerDatas?.map((item) => ({
@@ -108,10 +81,7 @@ const BestSeller = ({
})) || []; })) || [];
setBestItemNewData(baseData); setBestItemNewData(baseData);
} else { }, [bestSellerDatas, bestInfos]);
setBestItemNewData(bestSellerDatas);
}
}, [bestSellerDatas, bestInfos, userNumber]);
const orderStyle = useMemo(() => ({ order: order }), [order]); const orderStyle = useMemo(() => ({ order: order }), [order]);
@@ -174,10 +144,7 @@ const BestSeller = ({
if (c) { if (c) {
let cAriaLabel = c.getAttribute('aria-label'); let cAriaLabel = c.getAttribute('aria-label');
if (cAriaLabel) { if (cAriaLabel) {
const newcAriaLabel = cAriaLabel.replace( const newcAriaLabel = cAriaLabel.replace('Best Seller, Heading 1,', '');
'Best Seller, Heading 1,',
''
);
c.setAttribute('aria-label', newcAriaLabel); c.setAttribute('aria-label', newcAriaLabel);
} }
} }

View File

@@ -54,9 +54,6 @@ export default function HomeBanner({
const popupVisible = useSelector((state) => state.common.popup.popupVisible); const popupVisible = useSelector((state) => state.common.popup.popupVisible);
const panels = useSelector((state) => state.panels.panels); const panels = useSelector((state) => state.panels.panels);
const isDeepLinkEntry = useSelector(
(state) => state.home.homeInfo?.panelInfo?.isDeepLinkEntry
);
// 🔽 useFocusHistory - 경량화된 범용 포커스 히스토리 // 🔽 useFocusHistory - 경량화된 범용 포커스 히스토리
const focusHistory = useFocusHistory({ const focusHistory = useFocusHistory({
enableLogging: true, enableLogging: true,
@@ -167,11 +164,10 @@ export default function HomeBanner({
videoData = targetBannerData.bannerDetailInfos?.[0]; videoData = targetBannerData.bannerDetailInfos?.[0];
} }
// 🔽 [251221] DetailPanel이나 DeepLink PlayerPanel이 떠 있으면 배너 자동 재생 스킵 // DetailPanel이 떠 있는 동안에는 배너 자동 재생을 스킵 (PlayerPanel 모달 재설정 방지)
const hasDetailPanel = panels.some((p) => p.name === panel_names.DETAIL_PANEL); const hasDetailPanel = panels.some((p) => p.name === panel_names.DETAIL_PANEL);
const hasPlayerPanel = panels.some((p) => p.name === panel_names.PLAYER_PANEL);
if (!hasDetailPanel && !hasPlayerPanel && !isDeepLinkEntry && videoData && (videoData.shptmBanrTpNm === 'LIVE' || videoData.shptmBanrTpNm === 'VOD')) { if (!hasDetailPanel && videoData && (videoData.shptmBanrTpNm === 'LIVE' || videoData.shptmBanrTpNm === 'VOD')) {
console.log('[HomeBanner] 초기 비디오 자동 재생:', defaultFocus); console.log('[HomeBanner] 초기 비디오 자동 재생:', defaultFocus);
dispatch( dispatch(
@@ -189,7 +185,7 @@ export default function HomeBanner({
}) })
); );
} }
}, [bannerDataList, defaultFocus, dispatch, panels, isDeepLinkEntry]); }, [bannerDataList, defaultFocus, dispatch, panels]);
const renderItem = useCallback( const renderItem = useCallback(
(index, isHorizontal) => { (index, isHorizontal) => {

View File

@@ -617,15 +617,6 @@ export default function RandomUnit({
// 비디오 클릭 // 비디오 클릭
const videoClick = useCallback(() => { const videoClick = useCallback(() => {
// 🔽 비디오가 다른 배너에서 modal=true로 이미 재생 중이면 클릭 무시
if (playerPanelInfo?.modal === true && currentVideoBannerId && currentVideoBannerId !== spotlightId) {
console.log('[RandomUnit] videoClick 무시: 다른 배너에서 modal=true로 재생 중', {
currentVideoBannerId,
clickedBannerId: spotlightId,
});
return;
}
const lastFocusedTargetId = getContainerId(Spotlight.getCurrent()); const lastFocusedTargetId = getContainerId(Spotlight.getCurrent());
const currentSpot = Spotlight.getCurrent(); const currentSpot = Spotlight.getCurrent();
if (lastFocusedTargetId) { if (lastFocusedTargetId) {
@@ -683,7 +674,6 @@ export default function RandomUnit({
sendBannerLog, sendBannerLog,
onBlur, onBlur,
playerPanelInfo?.modal, playerPanelInfo?.modal,
currentVideoBannerId,
dispatch, dispatch,
handleStartVideo, handleStartVideo,
]); ]);

View File

@@ -7,7 +7,10 @@ import React, {
} from 'react'; } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux'; import {
useDispatch,
useSelector,
} from 'react-redux';
import { applyMiddleware } from 'redux'; import { applyMiddleware } from 'redux';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
@@ -20,11 +23,11 @@ import {
import hsn from '../../../assets/images/bg/hsn_new.png'; import hsn from '../../../assets/images/bg/hsn_new.png';
import koreaKiosk from '../../../assets/images/bg/koreaKiosk_new.png'; import koreaKiosk from '../../../assets/images/bg/koreaKiosk_new.png';
import lgelectronics from '../../../assets/images/bg/lgelectronics_new.png'; import lgelectronics from '../../../assets/images/bg/lgelectronics_new.png';
import nbcu from '../../../assets/images/bg/nbcu_new.png';
import ontv4u from '../../../assets/images/bg/ontv4u_new.png'; import ontv4u from '../../../assets/images/bg/ontv4u_new.png';
import Pinkfong from '../../../assets/images/bg/Pinkfong_new.png'; import Pinkfong from '../../../assets/images/bg/Pinkfong_new.png';
import qvc from '../../../assets/images/bg/qvc_new.png'; import qvc from '../../../assets/images/bg/qvc_new.png';
import shoplc from '../../../assets/images/bg/shoplc_new.png'; import shoplc from '../../../assets/images/bg/shoplc_new.png';
import nbcu from '../../../assets/images/bg/nbcu_new.png';
import { types } from '../../actions/actionTypes'; import { types } from '../../actions/actionTypes';
import { import {
changeAppStatus, changeAppStatus,
@@ -42,8 +45,14 @@ import {
getHomeMainContents, getHomeMainContents,
updateHomeInfo, updateHomeInfo,
} from '../../actions/homeActions'; } from '../../actions/homeActions';
import { sendLogGNB, sendLogTotalRecommend } from '../../actions/logActions'; import {
import { getSubCategory, getTop20Show } from '../../actions/mainActions'; sendLogGNB,
sendLogTotalRecommend,
} from '../../actions/logActions';
import {
getSubCategory,
getTop20Show,
} from '../../actions/mainActions';
import { setMyPageTermsAgree } from '../../actions/myPageActions'; import { setMyPageTermsAgree } from '../../actions/myPageActions';
import { getHomeOnSaleInfo } from '../../actions/onSaleActions'; import { getHomeOnSaleInfo } from '../../actions/onSaleActions';
import { updatePanel } from '../../actions/panelActions'; import { updatePanel } from '../../actions/panelActions';
@@ -60,7 +69,8 @@ import TButton, { TYPES } from '../../components/TButton/TButton';
import TPanel from '../../components/TPanel/TPanel'; import TPanel from '../../components/TPanel/TPanel';
import TNewPopUp from '../../components/TPopUp/TNewPopUp'; import TNewPopUp from '../../components/TPopUp/TNewPopUp';
import TPopUp from '../../components/TPopUp/TPopUp'; import TPopUp from '../../components/TPopUp/TPopUp';
import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator'; import TVerticalPagenator
from '../../components/TVerticalPagenator/TVerticalPagenator';
import useDebugKey from '../../hooks/useDebugKey'; import useDebugKey from '../../hooks/useDebugKey';
import { useFocusHistory } from '../../hooks/useFocusHistory/useFocusHistory'; import { useFocusHistory } from '../../hooks/useFocusHistory/useFocusHistory';
import usePrevious from '../../hooks/usePrevious'; import usePrevious from '../../hooks/usePrevious';
@@ -168,113 +178,68 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
// }); // });
const isGnbOpened = useSelector((state) => state.common.isGnbOpened); const isGnbOpened = useSelector((state) => state.common.isGnbOpened);
const homeLayoutInfo = useSelector((state) => state.home.layoutData); const homeLayoutInfo = useSelector((state) => state.home.layoutData);
const panelInfo = useSelector( const panelInfo = useSelector((state) => state.home.homeInfo?.panelInfo ?? {});
(state) => state.home.homeInfo?.panelInfo ?? {}
);
const panels = useSelector((state) => state.panels.panels); const panels = useSelector((state) => state.panels.panels);
const webOSVersion = useSelector( const webOSVersion = useSelector((state) => state.common.appStatus?.webOSVersion);
(state) => state.common.appStatus?.webOSVersion
);
const enterThroughGNB = useSelector((state) => state.home.enterThroughGNB); const enterThroughGNB = useSelector((state) => state.home.enterThroughGNB);
const defaultFocus = useSelector((state) => state.home.defaultFocus); const defaultFocus = useSelector((state) => state.home.defaultFocus);
const bannerDataList = useSelector( const bannerDataList = useSelector((state) => state.home.bannerData?.bannerInfos);
(state) => state.home.bannerData?.bannerInfos
);
// ✅ PlayerPanel의 shouldShrinkTo1px 상태 추적 // ✅ PlayerPanel의 shouldShrinkTo1px 상태 추적
const playerPanelShouldShrink = useSelector((state) => { const playerPanelShouldShrink = useSelector((state) => {
const playerPanel = state.panels.panels.find( const playerPanel = state.panels.panels.find((p) => p.name === panel_names.PLAYER_PANEL);
(p) => p.name === panel_names.PLAYER_PANEL
);
return playerPanel?.panelInfo?.shouldShrinkTo1px ?? false; return playerPanel?.panelInfo?.shouldShrinkTo1px ?? false;
}); });
// ✅ PlayerPanel의 modal 상태 추적 (false → true 감지용) // ✅ PlayerPanel의 modal 상태 추적 (false → true 감지용)
const playerModalState = useSelector((state) => { const playerModalState = useSelector((state) => {
const playerPanel = state.panels.panels.find( const playerPanel = state.panels.panels.find((p) => p.name === panel_names.PLAYER_PANEL);
(p) => p.name === panel_names.PLAYER_PANEL
);
return playerPanel?.panelInfo?.modal ?? false; return playerPanel?.panelInfo?.modal ?? false;
}); });
const prevPlayerModalStateRef = useRef(false); const prevPlayerModalStateRef = useRef(false);
const categoryInfos = useSelector( const categoryInfos = useSelector((state) => state.onSale.homeOnSaleData?.data?.categoryInfos);
(state) => state.onSale.homeOnSaleData?.data?.categoryInfos
);
const categoryItemInfos = useSelector( const categoryItemInfos = useSelector((state) => state.main.subCategoryData?.categoryItemInfos);
(state) => state.main.subCategoryData?.categoryItemInfos
);
const { popupVisible, activePopup } = useSelector( const { popupVisible, activePopup } = useSelector((state) => state.common.popup);
(state) => state.common.popup
);
const eventPopInfosData = useSelector( const eventPopInfosData = useSelector((state) => state.event.eventData.eventPopInfo);
(state) => state.event.eventData.eventPopInfo
);
const eventData = useSelector((state) => state.event.eventData); const eventData = useSelector((state) => state.event.eventData);
const eventClickSuccess = useSelector( const eventClickSuccess = useSelector((state) => state.event.eventClickSuccess);
(state) => state.event.eventClickSuccess const homeOnSaleInfos = useSelector((state) => state.onSale.homeOnSaleData?.data.homeOnSaleInfos);
); const bestSellerDatas = useSelector((state) => state.product.bestSellerData?.bestSeller);
const homeOnSaleInfos = useSelector(
(state) => state.onSale.homeOnSaleData?.data.homeOnSaleInfos
);
const bestSellerDatas = useSelector(
(state) => state.product.bestSellerData?.bestSeller
);
const topInfos = useSelector((state) => state.main.top20ShowData.topInfos); const topInfos = useSelector((state) => state.main.top20ShowData.topInfos);
const isDeepLink = useSelector( const isDeepLink = useSelector((state) => state.common.deepLinkInfo.isDeepLink);
(state) => state.common.deepLinkInfo.isDeepLink
);
// 선택약관 관련 Redux 상태 // 선택약관 관련 Redux 상태
const termsData = useSelector((state) => state.home.termsData); const termsData = useSelector((state) => state.home.termsData);
const termsIdMap = useSelector((state) => state.home.termsIdMap); const termsIdMap = useSelector((state) => state.home.termsIdMap);
const optionalTermsAvailable = useSelector( const optionalTermsAvailable = useSelector((state) => state.home.optionalTermsAvailable);
(state) => state.home.optionalTermsAvailable
);
const optionalTermsData = useSelector((state) => { const optionalTermsData = useSelector((state) => {
if ( if (state.home.termsData && state.home.termsData.data && state.home.termsData.data.terms) {
state.home.termsData && return state.home.termsData.data.terms.find((term) => term.trmsTpCd === 'MST00405');
state.home.termsData.data &&
state.home.termsData.data.terms
) {
return state.home.termsData.data.terms.find(
(term) => term.trmsTpCd === 'MST00405'
);
} }
return null; return null;
}); });
const termsLoading = useSelector((state) => state.common.termsLoading); const termsLoading = useSelector((state) => state.common.termsLoading);
const currentTermsFlag = useSelector((state) => state.common.termsFlag); const currentTermsFlag = useSelector((state) => state.common.termsFlag);
const optionalTermsPopupFlow = useSelector( const optionalTermsPopupFlow = useSelector((state) => state.common.optionalTermsPopupFlow);
(state) => state.common.optionalTermsPopupFlow
);
const { userNumber } = useSelector(
(state) => state.common.appStatus.loginUserData
);
const [btnDisabled, setBtnDisabled] = useState(true); const [btnDisabled, setBtnDisabled] = useState(true);
const [arrowBottom, setArrowBottom] = useState(true); const [arrowBottom, setArrowBottom] = useState(true);
const [firstSpot, setFirstSpot] = useState(false); const [firstSpot, setFirstSpot] = useState(false);
const [eventPopOpen, setEventPopOpen] = useState(false); const [eventPopOpen, setEventPopOpen] = useState(false);
const [nowShelf, setNowShelf] = useState(panelInfo.nowShelf); const [nowShelf, setNowShelf] = useState(panelInfo.nowShelf);
const [firstLgCatCd, setFirstLgCatCd] = useState( const [firstLgCatCd, setFirstLgCatCd] = useState(panelInfo.currentCatCd ?? null);
panelInfo.currentCatCd ?? null
);
const [cateCd, setCateCd] = useState(panelInfo.currentCatCd ?? null); const [cateCd, setCateCd] = useState(panelInfo.currentCatCd ?? null);
const [cateNm, setCateNm] = useState(panelInfo.currentCateName ?? null); const [cateNm, setCateNm] = useState(panelInfo.currentCateName ?? null);
// 선택약관 팝업 상태 // 선택약관 팝업 상태
const [isOptionalConfirmVisible, setIsOptionalConfirmVisible] = const [isOptionalConfirmVisible, setIsOptionalConfirmVisible] = useState(false);
useState(false);
const [isOptionalTermsVisible, setIsOptionalTermsVisible] = useState(false); const [isOptionalTermsVisible, setIsOptionalTermsVisible] = useState(false);
const [optionalTermsAgreed, setOptionalTermsAgreed] = useState(false); const [optionalTermsAgreed, setOptionalTermsAgreed] = useState(false);
const { entryMenu, nowMenu } = useSelector((state) => state.common.menu); const { entryMenu, nowMenu } = useSelector((state) => state.common.menu);
const [focusedContainerId, setFocusedContainerId] = useState( const [focusedContainerId, setFocusedContainerId] = useState(panelInfo.focusedContainerId);
panelInfo.focusedContainerId
);
// DetailPanel 진입 시 포커스 대상 저장 // DetailPanel 진입 시 포커스 대상 저장
const lastFocusedTargetRef = useRef(panelInfo.lastFocusedTargetId || null); const lastFocusedTargetRef = useRef(panelInfo.lastFocusedTargetId || null);
@@ -289,9 +254,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
useEffect(() => { useEffect(() => {
if (prevIsOnTopRef.current && !isOnTop) { if (prevIsOnTopRef.current && !isOnTop) {
const current = Spotlight.getCurrent(); const current = Spotlight.getCurrent();
const tBody = document.querySelector( const tBody = document.querySelector(`[data-spotlight-id="${SpotlightIds.HOME_TBODY}"]`);
`[data-spotlight-id="${SpotlightIds.HOME_TBODY}"]`
);
if (current && tBody && tBody.contains(current)) { if (current && tBody && tBody.contains(current)) {
const targetId = current.getAttribute('data-spotlight-id'); const targetId = current.getAttribute('data-spotlight-id');
@@ -323,9 +286,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
ImagePreloader.preloadAllImages(BACKGROUND_IMAGES) ImagePreloader.preloadAllImages(BACKGROUND_IMAGES)
.then((results) => { .then((results) => {
const successCount = results.filter((r) => r !== null).length; const successCount = results.filter((r) => r !== null).length;
dlog( dlog(`[HomePanel] Background images preloaded: ${successCount}/${results.length} images`);
`[HomePanel] Background images preloaded: ${successCount}/${results.length} images`
);
// 프리로딩 통계 정보 로깅 (디버깅용) // 프리로딩 통계 정보 로깅 (디버깅용)
const stats = ImagePreloader.getStats(); const stats = ImagePreloader.getStats();
@@ -351,9 +312,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
const sortedHomeLayoutInfo = useMemo(() => { const sortedHomeLayoutInfo = useMemo(() => {
if (homeLayoutInfo && homeLayoutInfo.homeLayoutInfo) { if (homeLayoutInfo && homeLayoutInfo.homeLayoutInfo) {
const sorted = [...homeLayoutInfo.homeLayoutInfo].sort( const sorted = [...homeLayoutInfo.homeLayoutInfo].sort((x, y) => x.expsOrd - y.expsOrd);
(x, y) => x.expsOrd - y.expsOrd
);
return sorted; return sorted;
} }
return []; return [];
@@ -378,9 +337,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
const expandAttemptRef = useRef(0); // 복구 시도 횟수 const expandAttemptRef = useRef(0); // 복구 시도 횟수
const loadingComplete = useSelector((state) => state.common?.loadingComplete); const loadingComplete = useSelector((state) => state.common?.loadingComplete);
const isVideoTransitionLocked = useSelector( const isVideoTransitionLocked = useSelector((state) => state.home.videoTransitionLocked);
(state) => state.home.videoTransitionLocked
);
// 선택약관 동의 핸들러 // 선택약관 동의 핸들러
const handleOptionalAgree = useCallback(() => { const handleOptionalAgree = useCallback(() => {
@@ -460,7 +417,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
}); });
setTimeout(()=>{ setTimeout(()=>{
Spotlight.focus('home_tbody'); Spotlight.focus('home_tbody');
}, 100); },100)
}, [handleOptionalAgree, dispatch, currentTermsFlag]); }, [handleOptionalAgree, dispatch, currentTermsFlag]);
const handleOptionalDeclineClick = useCallback(() => { const handleOptionalDeclineClick = useCallback(() => {
@@ -471,7 +428,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
setIsOptionalConfirmVisible(false); setIsOptionalConfirmVisible(false);
setTimeout(()=>{ setTimeout(()=>{
Spotlight.focus('home_tbody'); Spotlight.focus('home_tbody');
}, 100); },100)
}, [dispatch]); }, [dispatch]);
const handleTermsPopupClosed = useCallback(() => { const handleTermsPopupClosed = useCallback(() => {
@@ -542,12 +499,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [ }, [shouldShowOptionalTermsPopup, termsLoading, isOptionalConfirmVisible, dispatch]);
shouldShowOptionalTermsPopup,
termsLoading,
isOptionalConfirmVisible,
dispatch,
]);
const onCancel = useCallback(() => { const onCancel = useCallback(() => {
const currentSpot = Spotlight.getCurrent(); const currentSpot = Spotlight.getCurrent();
@@ -598,8 +550,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
const containerId = sortedHomeLayoutInfo[0].shptmApphmDspyOptCd; const containerId = sortedHomeLayoutInfo[0].shptmApphmDspyOptCd;
const navigableEls = getContainerNavigableElements(containerId); const navigableEls = getContainerNavigableElements(containerId);
const navigableIds = navigableEls.filter((el) => typeof el === 'string'); const navigableIds = navigableEls.filter((el) => typeof el === 'string');
const target = const target = containerId === TEMPLATE_CODE_CONF.TOP ? 'banner0' : containerId;
containerId === TEMPLATE_CODE_CONF.TOP ? 'banner0' : containerId;
if (navigableIds.length > 0) { if (navigableIds.length > 0) {
setContainerLastFocusedElement(null, navigableIds); setContainerLastFocusedElement(null, navigableIds);
@@ -675,9 +626,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
<HomeBanner <HomeBanner
key={el.shptmApphmDspyOptCd} key={el.shptmApphmDspyOptCd}
spotlightId={el.shptmApphmDspyOptCd} spotlightId={el.shptmApphmDspyOptCd}
firstSpot={ firstSpot={!panelInfo.focusedContainerId && !panelInfo.currentSpot}
!panelInfo.focusedContainerId && !panelInfo.currentSpot
}
className={css.homeBannerWrap} className={css.homeBannerWrap}
handleShelfFocus={handleItemFocus( handleShelfFocus={handleItemFocus(
el.shptmApphmDspyOptCd, el.shptmApphmDspyOptCd,
@@ -769,7 +718,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
} else break; } else break;
} }
case TEMPLATE_CODE_CONF.PICK_FOR_YOU: { case TEMPLATE_CODE_CONF.PICK_FOR_YOU: {
if (userNumber) { if (bestSellerDatas && bestSellerDatas.length > 0) {
return ( return (
<PickedForYou <PickedForYou
key={el.shptmApphmDspyOptCd} key={el.shptmApphmDspyOptCd}
@@ -788,9 +737,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
} }
} }
})} })}
{loadingComplete && {loadingComplete && sortedHomeLayoutInfo && sortedHomeLayoutInfo.length > 0 && (
sortedHomeLayoutInfo &&
sortedHomeLayoutInfo.length > 0 && (
<TButton <TButton
className={css.tButton} className={css.tButton}
onClick={handleTopButtonClick} onClick={handleTopButtonClick}
@@ -967,9 +914,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
// console.log('[HomePanel] playVideo 호출 완료'); // console.log('[HomePanel] playVideo 호출 완료');
if (isDeepLink || (!panels.length && !panelInfo.focusedContainerId)) { if (isDeepLink || (!panels.length && !panelInfo.focusedContainerId)) {
dispatch( dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } })
);
dispatch(getHomeMainContents()); dispatch(getHomeMainContents());
dispatch(getHomeLayout()); dispatch(getHomeLayout());
dispatch( dispatch(
@@ -1126,9 +1071,6 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
const detailPanelClosedTime = useSelector( const detailPanelClosedTime = useSelector(
(state) => state.home.homeInfo?.panelInfo?.detailPanelClosedAt (state) => state.home.homeInfo?.panelInfo?.detailPanelClosedAt
); );
const isDeepLinkEntry = useSelector(
(state) => state.home.homeInfo?.panelInfo?.isDeepLinkEntry
);
useEffect(() => { useEffect(() => {
if (detailPanelClosed && isOnTop) { if (detailPanelClosed && isOnTop) {
@@ -1140,18 +1082,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
// console.log('[HomePanel] *** videoPlayIntentRef.current:', videoPlayIntentRef.current); // console.log('[HomePanel] *** videoPlayIntentRef.current:', videoPlayIntentRef.current);
// console.log('[HomePanel] *** lastPlayedBannerIdRef.current:', lastPlayedBannerIdRef.current); // console.log('[HomePanel] *** lastPlayedBannerIdRef.current:', lastPlayedBannerIdRef.current);
// 🔽 [251221] DeepLink로 PlayerPanel이 진입한 경우 자동 재생 스킵
// (플래그 리셋은 PlayerPanel cleanup에서 처리하므로 여기서는 스킵만)
if (isDeepLinkEntry) {
dlog('[HomePanel] *** [DeepLink] isDeepLinkEntry=true 감지 - 자동 재생 스킵');
return;
}
// 🔽 videoPlayIntentRef가 null인 경우: 비디오 재생 가능한 첫 번째 배너 찾기 // 🔽 videoPlayIntentRef가 null인 경우: 비디오 재생 가능한 첫 번째 배너 찾기
if (!videoPlayIntentRef.current && bannerDataList) { if (!videoPlayIntentRef.current && bannerDataList) {
dlog( dlog('[HomePanel] *** videoPlayIntentRef가 null - 첫 번째 비디오 배너 검색');
'[HomePanel] *** videoPlayIntentRef가 null - 첫 번째 비디오 배너 검색'
);
// HomeBanner.jsx의 defaultFocus 계산 로직과 동일 // HomeBanner.jsx의 defaultFocus 계산 로직과 동일
let targetIndex = 0; let targetIndex = 0;
@@ -1205,10 +1138,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
}; };
lastPlayedBannerIdRef.current = bannerId; lastPlayedBannerIdRef.current = bannerId;
dlog( dlog('[HomePanel] *** videoPlayIntentRef 설정 완료:', videoPlayIntentRef.current);
'[HomePanel] *** videoPlayIntentRef 설정 완료:',
videoPlayIntentRef.current
);
} else { } else {
dlog('[HomePanel] *** ⚠️ 비디오 재생 가능한 배너를 찾지 못함'); dlog('[HomePanel] *** ⚠️ 비디오 재생 가능한 배너를 찾지 못함');
} }
@@ -1251,8 +1181,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
// 🔽 DetailPanel에서 돌아온 뒤 포커스를 마지막 포커스 대상에 복원 // 🔽 DetailPanel에서 돌아온 뒤 포커스를 마지막 포커스 대상에 복원
dlog('[HomePanel] *** 🎯 Focus 복원 준비'); dlog('[HomePanel] *** 🎯 Focus 복원 준비');
const targetFocusId = const targetFocusId = panelInfo.lastFocusedTargetId || lastFocusedTargetRef.current;
panelInfo.lastFocusedTargetId || lastFocusedTargetRef.current;
dlog( dlog(
'[HomePanel] *** 📍 targetFocusId:', '[HomePanel] *** 📍 targetFocusId:',
targetFocusId, targetFocusId,
@@ -1290,7 +1219,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
); );
} }
} }
}, [detailPanelClosed, isOnTop, bannerDataList, isDeepLinkEntry, dispatch]); }, [detailPanelClosed, isOnTop, bannerDataList, dispatch]);
// ======= // =======
// const justCameBack = !prevIsOnTopRef.current && isOnTop; // const justCameBack = !prevIsOnTopRef.current && isOnTop;
@@ -1321,9 +1250,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
targetSpotlightCateNm = c.getAttribute('data-catcd-nm'); targetSpotlightCateNm = c.getAttribute('data-catcd-nm');
} }
const tBody = document.querySelector( const tBody = document.querySelector(`[data-spotlight-id="${SpotlightIds.HOME_TBODY}"]`);
`[data-spotlight-id="${SpotlightIds.HOME_TBODY}"]`
);
const currentSpot = c && tBody.contains(c) ? targetSpotlightId : null; const currentSpot = c && tBody.contains(c) ? targetSpotlightId : null;
dispatch(checkEnterThroughGNB(false)); dispatch(checkEnterThroughGNB(false));
@@ -1336,8 +1263,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
currentCateName: targetSpotlightCateNm, currentCateName: targetSpotlightCateNm,
// <<<<<<< HEAD // <<<<<<< HEAD
focusedContainerId: focusedContainerIdRef.current, focusedContainerId: focusedContainerIdRef.current,
lastFocusedTargetId: lastFocusedTargetId: lastFocusedTargetRef.current || panelInfo.lastFocusedTargetId,
lastFocusedTargetRef.current || panelInfo.lastFocusedTargetId,
// ======= // =======
// focusedContainerId: focusedContainerId, // focusedContainerId: focusedContainerId,
// >>>>>>> gitlab/develop // >>>>>>> gitlab/develop
@@ -1413,9 +1339,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
<> <>
{/* HomePanel용 메모리 상주 그라데이션 배경 */} {/* HomePanel용 메모리 상주 그라데이션 배경 */}
<div <div
className={classNames(css.gradientBackground, { className={classNames(css.gradientBackground, { [css.visible]: showGradientBackground })}
[css.visible]: showGradientBackground,
})}
aria-hidden="true" aria-hidden="true"
/> />
@@ -1445,10 +1369,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
)} )}
{arrowBottom && ( {arrowBottom && (
<p <p className={classNames(css.arrow, css.arrowBottom)} onClick={handleArrowClick} />
className={classNames(css.arrow, css.arrowBottom)}
onClick={handleArrowClick}
/>
)} )}
{activePopup === ACTIVE_POPUP.exitPopup && ( {activePopup === ACTIVE_POPUP.exitPopup && (
@@ -1465,8 +1386,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
text={$L('Are you sure you want to exit Shop Time?')} text={$L('Are you sure you want to exit Shop Time?')}
/> />
)} )}
{(activePopup === ACTIVE_POPUP.eventPopup || {(activePopup === ACTIVE_POPUP.eventPopup || activePopup === ACTIVE_POPUP.smsPopup) && (
activePopup === ACTIVE_POPUP.smsPopup) && <EventPopUpBanner />} <EventPopUpBanner />
)}
{/* 선택약관 동의 팝업 */} {/* 선택약관 동의 팝업 */}
<OptionalConfirm <OptionalConfirm
open={isOptionalConfirmVisible} open={isOptionalConfirmVisible}

View File

@@ -1,14 +1,24 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, {
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux'; import {
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator'; import {
SpotlightContainerDecorator,
} from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable'; import Spottable from '@enact/spotlight/Spottable';
import { getContainerId } from '@enact/spotlight/src/container'; import { getContainerId } from '@enact/spotlight/src/container';
import { updateHomeInfo } from '../../../actions/homeActions'; import { updateHomeInfo } from '../../../actions/homeActions';
import { popPanel, pushPanel } from '../../../actions/panelActions'; import { pushPanel, popPanel } from '../../../actions/panelActions';
import { startVideoPlayer } from '../../../actions/playActions'; import { startVideoPlayer } from '../../../actions/playActions';
import SectionTitle from '../../../components/SectionTitle/SectionTitle'; import SectionTitle from '../../../components/SectionTitle/SectionTitle';
import Tag from '../../../components/TItemCard/Tag'; import Tag from '../../../components/TItemCard/Tag';
@@ -25,15 +35,18 @@ import {
LOG_MESSAGE_ID, LOG_MESSAGE_ID,
panel_names, panel_names,
} from '../../../utils/Config'; } from '../../../utils/Config';
import { $L, scaleW } from '../../../utils/helperMethods'; import {
$L,
scaleW,
} from '../../../utils/helperMethods';
import { SpotlightIds } from '../../../utils/SpotlightIds'; import { SpotlightIds } from '../../../utils/SpotlightIds';
import { TEMPLATE_CODE_CONF } from '../HomePanel'; import { TEMPLATE_CODE_CONF } from '../HomePanel';
import css from '../PopularShow/PopularShow.module.less'; import css from '../PopularShow/PopularShow.module.less';
const SpottableComponent = Spottable('div'); const SpottableComponent = Spottable("div");
const Container = SpotlightContainerDecorator( const Container = SpotlightContainerDecorator(
{ enterTo: 'last-focused' }, { enterTo: "last-focused" },
'div' "div"
); );
const PopularShow = ({ const PopularShow = ({
@@ -59,13 +72,9 @@ const PopularShow = ({
const panels = useSelector((state) => state.panels.panels); const panels = useSelector((state) => state.panels.panels);
const topInfos = useSelector((state) => state.main.top20ShowData.topInfos); const topInfos = useSelector((state) => state.main.top20ShowData.topInfos);
const recommendInfo = useSelector( const recommendInfo = useSelector((state) => state.foryou?.recommendInfo?.recommendShow);
(state) => state.foryou?.recommendInfo?.recommendShow
);
const { userNumber } = useSelector(
(state) => state.common.appStatus.loginUserData
);
const orderStyle = useMemo(() => ({ order: order }), [order]); const orderStyle = useMemo(() => ({ order: order }), [order]);
const [drawChk, setDrawChk] = useState(false); const [drawChk, setDrawChk] = useState(false);
@@ -80,16 +89,15 @@ const PopularShow = ({
useEffect(()=>{ useEffect(()=>{
setShowInfos( setShowInfos(
recommendInfo?.filter((item) => item.recommendTpCd === 'POPULARSHOW') || recommendInfo?.filter(
[] (item) => item.recommendTpCd === "POPULARSHOW"
); ) || []
}, [recommendInfo]); )
},[recommendInfo])
useEffect(() => { useEffect(() => {
if (userNumber) {
if (!showInfos || showInfos.length === 0) { if (!showInfos || showInfos.length === 0) {
const baseData = const baseData = topInfos?.map((item) => ({
topInfos?.map((item) => ({
...item, ...item,
foryou: false, foryou: false,
})) || []; })) || [];
@@ -97,27 +105,23 @@ const PopularShow = ({
return; return;
} }
const recommendedData = const recommendedData = showInfos[0].showInfos?.map((item) => ({
showInfos[0].showInfos?.map((item) => ({
...item, ...item,
foryou: true, foryou: true,
})) || []; })) || [];
const recommendedPrdtIds = new Set( const recommendedPrdtIds = new Set(recommendedData?.map(item => item.showId));
recommendedData?.map((item) => item.showId)
);
const baseData = const baseData = topInfos?.map((item) => ({
topInfos?.map((item) => ({
...item, ...item,
foryou: recommendedPrdtIds.has(item.showId), foryou: recommendedPrdtIds.has(item.showId),
})) || []; })) || [];
setShowNewInfos([ ...baseData]); setShowNewInfos([ ...baseData]);
} else {
setShowNewInfos(topInfos); }, [topInfos, showInfos]);
}
}, [topInfos, showInfos, userNumber]);
const handleCardClick = useCallback( const handleCardClick = useCallback(
(patnrId, showId, catCd, showUrl) => () => { (patnrId, showId, catCd, showUrl) => () => {
@@ -131,7 +135,7 @@ const PopularShow = ({
startVideoPlayer({ startVideoPlayer({
showId, showId,
patnrId, patnrId,
shptmBanrTpNm: 'VOD', shptmBanrTpNm: "VOD",
lgCatCd: catCd, lgCatCd: catCd,
modal: false, modal: false,
showUrl: showUrl, showUrl: showUrl,
@@ -147,7 +151,7 @@ const PopularShow = ({
{ {
name: panel_names.TRENDING_NOW_PANEL, name: panel_names.TRENDING_NOW_PANEL,
panelInfo: { panelInfo: {
pageName: 'PS', pageName: "PS",
focusedContainerId: SpotlightIds.TRENDING_NOW_POPULAR_SHOW, focusedContainerId: SpotlightIds.TRENDING_NOW_POPULAR_SHOW,
}, },
}, },
@@ -175,23 +179,23 @@ const PopularShow = ({
if (firstChk === 0 && itemIndex === 0) { if (firstChk === 0 && itemIndex === 0) {
const c = Spotlight.getCurrent(); const c = Spotlight.getCurrent();
const getAriaLabel = c.getAttribute('aria-label'); const getAriaLabel = c.getAttribute("aria-label");
if (c) { if (c) {
let cAriaLabel = c.getAttribute('aria-label'); let cAriaLabel = c.getAttribute("aria-label");
cAriaLabel = 'POPULAR SHOW, Heading 1,' + cAriaLabel; cAriaLabel = "POPULAR SHOW, Heading 1," + cAriaLabel;
c.setAttribute('aria-label', cAriaLabel); c.setAttribute("aria-label", cAriaLabel);
} }
setFirstChk(1); setFirstChk(1);
} else if (firstChk === 1 && itemIndex === 0) { } else if (firstChk === 1 && itemIndex === 0) {
const c = Spotlight.getCurrent(); const c = Spotlight.getCurrent();
if (c) { if (c) {
let cAriaLabel = c.getAttribute('aria-label'); let cAriaLabel = c.getAttribute("aria-label");
if (cAriaLabel) { if (cAriaLabel) {
const newcAriaLabel = cAriaLabel.replace( const newcAriaLabel = cAriaLabel.replace(
'POPULAR SHOW, Heading 1,', "POPULAR SHOW, Heading 1,",
'' ""
); );
c.setAttribute('aria-label', newcAriaLabel); c.setAttribute("aria-label", newcAriaLabel);
} }
} }
} else { } else {
@@ -242,7 +246,7 @@ const PopularShow = ({
> >
<SectionTitle <SectionTitle
className={css.subTitle} className={css.subTitle}
title={$L('POPULAR SHOW')} title={$L("POPULAR SHOW")}
data-title-index="homePopularShow" data-title-index="homePopularShow"
label="POPULAR SHOW" label="POPULAR SHOW"
/> />
@@ -289,8 +293,8 @@ const PopularShow = ({
showId={showId} showId={showId}
showTitle={showNm} showTitle={showNm}
imageSource={ imageSource={
thumbnailUrl && thumbnailUrl960 (thumbnailUrl && thumbnailUrl960) ?
? thumbnailUrl !== thumbnailUrl960 thumbnailUrl !== thumbnailUrl960
? thumbnailUrl960 ? thumbnailUrl960
: thumbnailUrl : thumbnailUrl
: thumbnailUrl : thumbnailUrl
@@ -300,7 +304,7 @@ const PopularShow = ({
nonPosition={true} nonPosition={true}
type={TYPES.videoShow} type={TYPES.videoShow}
imgType={ imgType={
vtctpYn !== 'Y' vtctpYn !== "Y"
? IMAGETYPES.imgHorizontal ? IMAGETYPES.imgHorizontal
: IMAGETYPES.imgVertical : IMAGETYPES.imgVertical
} }
@@ -309,11 +313,11 @@ const PopularShow = ({
onFocus={handleFocus(itemIndex)} onFocus={handleFocus(itemIndex)}
onBlur={handleBlur(itemIndex)} onBlur={handleBlur(itemIndex)}
onClick={handleCardClick(patnrId, showId, catCd, showUrl)} onClick={handleCardClick(patnrId, showId, catCd, showUrl)}
firstLabel={patncNm + ' '} firstLabel={patncNm + " "}
label={itemIndex * 1 + 1 + ' of ' + showNewInfos.length} label={itemIndex * 1 + 1 + " of " + showNewInfos.length}
lastLabel=" go to detail, button" lastLabel=" go to detail, button"
> >
{foryou === true && <Tag text={'For You'} />} {foryou === true && <Tag text={"For You"} />}
</TItemCardNew> </TItemCardNew>
); );
} }
@@ -325,7 +329,7 @@ const PopularShow = ({
className={css.displayBox} className={css.displayBox}
onClick={handleMoreCardClick} onClick={handleMoreCardClick}
onFocus={_handleItemFocus} onFocus={_handleItemFocus}
spotlightId={'home-popularshow-more-btn'} spotlightId={"home-popularshow-more-btn"}
></SpottableComponent> ></SpottableComponent>
</div> </div>
)} )}

View File

@@ -8,32 +8,21 @@ import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
import { sendLogCuration } from '../../../actions/logActions'; import { sendLogCuration } from '../../../actions/logActions';
import { getSubCategory } from '../../../actions/mainActions'; import { getSubCategory } from '../../../actions/mainActions';
import { import { pushPanel, navigateFromSubCategory } from '../../../actions/panelActions';
navigateFromSubCategory,
pushPanel,
} from '../../../actions/panelActions';
import Tag from '../../../components/TItemCard/Tag'; import Tag from '../../../components/TItemCard/Tag';
import TItemCardNew from '../../../components/TItemCard/TItemCard.new'; import TItemCardNew from '../../../components/TItemCard/TItemCard.new';
import TScroller from '../../../components/TScroller/TScroller'; import TScroller from '../../../components/TScroller/TScroller';
import usePrevious from '../../../hooks/usePrevious'; import usePrevious from '../../../hooks/usePrevious';
import useScrollReset from '../../../hooks/useScrollReset'; import useScrollReset from '../../../hooks/useScrollReset';
import useScrollTo from '../../../hooks/useScrollTo'; import useScrollTo from '../../../hooks/useScrollTo';
import { import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, LOG_TP_NO, panel_names } from '../../../utils/Config';
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
LOG_TP_NO,
panel_names,
} from '../../../utils/Config';
import { SpotlightIds } from '../../../utils/SpotlightIds'; import { SpotlightIds } from '../../../utils/SpotlightIds';
import CategoryNav from '../../HomePanel/SubCategory/CategoryNav/CategoryNav'; import CategoryNav from '../../HomePanel/SubCategory/CategoryNav/CategoryNav';
import css from '../../HomePanel/SubCategory/SubCategory.module.less'; import css from '../../HomePanel/SubCategory/SubCategory.module.less';
const SpottableComponent = Spottable('div'); const SpottableComponent = Spottable('div');
const Container = SpotlightContainerDecorator({ enterTo: null }, 'div'); const Container = SpotlightContainerDecorator({ enterTo: null }, 'div');
const ContainerBasic = SpotlightContainerDecorator( const ContainerBasic = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
{ enterTo: 'last-focused' },
'div'
);
const getExpsOrdByLgCatCd = (array, value) => { const getExpsOrdByLgCatCd = (array, value) => {
const expsOrd = array.findIndex(({ lgCatCd }) => value === lgCatCd) + 1; const expsOrd = array.findIndex(({ lgCatCd }) => value === lgCatCd) + 1;
@@ -51,24 +40,12 @@ export default memo(function SubCategory({
}) { }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { getScrollTo, scrollLeft } = useScrollTo(); const { getScrollTo, scrollLeft } = useScrollTo();
const { handleScrollReset, handleStopScrolling } = useScrollReset( const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollLeft, false);
scrollLeft,
false
);
const categoryInfos = useSelector( const categoryInfos = useSelector((state) => state.home.menuData?.data?.homeCategory);
(state) => state.home.menuData?.data?.homeCategory const categoryItemInfos = useSelector((state) => state.main.subCategoryData?.categoryItemInfos);
);
const categoryItemInfos = useSelector(
(state) => state.main.subCategoryData?.categoryItemInfos
);
const foruItemInfos = useSelector( const foruItemInfos = useSelector((state) => state.main.recommendProduct[0]?.productInfos);
(state) => state.main.recommendProduct[0]?.productInfos
);
const { userNumber } = useSelector(
(state) => state.common.appStatus.loginUserData
);
const nowMenu = useSelector((state) => state.common.menu.nowMenu); const nowMenu = useSelector((state) => state.common.menu.nowMenu);
@@ -230,7 +207,6 @@ export default memo(function SubCategory({
}, [handleShelfFocus]); }, [handleShelfFocus]);
useEffect(() => { useEffect(() => {
if (userNumber) {
if (!foruItemInfos || foruItemInfos.length === 0) { if (!foruItemInfos || foruItemInfos.length === 0) {
const baseData = const baseData =
categoryItemInfos?.subCatItemList?.map((item) => ({ categoryItemInfos?.subCatItemList?.map((item) => ({
@@ -247,9 +223,7 @@ export default memo(function SubCategory({
foryou: true, foryou: true,
})) || []; })) || [];
const recommendedPrdtIds = new Set( const recommendedPrdtIds = new Set(recommendedData.map((item) => item.prdtId));
recommendedData.map((item) => item.prdtId)
);
const baseData = const baseData =
categoryItemInfos?.subCatItemList?.map((item) => ({ categoryItemInfos?.subCatItemList?.map((item) => ({
@@ -258,17 +232,10 @@ export default memo(function SubCategory({
})) || []; })) || [];
setCategoryItemNewData([...baseData]); setCategoryItemNewData([...baseData]);
} else { }, [categoryItemInfos?.subCatItemList, foruItemInfos]);
setCategoryItemNewData(categoryItemInfos?.subCatItemList);
}
}, [categoryItemInfos?.subCatItemList, foruItemInfos, userNumber]);
return ( return (
<Container <Container spotlightId={spotlightId} data-wheel-point onFocus={_handleShelfFocus}>
spotlightId={spotlightId}
data-wheel-point
onFocus={_handleShelfFocus}
>
<CategoryNav <CategoryNav
categoryInfos={categoryInfos} categoryInfos={categoryInfos}
currentCategoryCode={currentLgCatCd} currentCategoryCode={currentLgCatCd}
@@ -326,12 +293,7 @@ export default memo(function SubCategory({
offerInfo={offerInfo} offerInfo={offerInfo}
data-catcd-num={currentLgCatCd} data-catcd-num={currentLgCatCd}
data-catcd-nm={currentLgCatNm} data-catcd-nm={currentLgCatNm}
label={ label={itemIndex * 1 + 1 + ' of ' + (categoryItemNewData?.length || 0)}
itemIndex * 1 +
1 +
' of ' +
(categoryItemNewData?.length || 0)
}
lastLabel=" go to detail, button" lastLabel=" go to detail, button"
euEnrgLblInfos={euEnrgLblInfos} euEnrgLblInfos={euEnrgLblInfos}
> >

View File

@@ -209,9 +209,6 @@ export default function MainView({ className, initService }) {
const hasFeaturedBrandsPanel = panels.some( const hasFeaturedBrandsPanel = panels.some(
(panel) => panel?.name === Config.panel_names.FEATURED_BRANDS_PANEL (panel) => panel?.name === Config.panel_names.FEATURED_BRANDS_PANEL
); );
const hasTrendingNowPanel = panels.some(
(panel) => panel?.name === Config.panel_names.TRENDING_NOW_PANEL
);
// 단독 패널 체크 - CheckOutPanel, CartPanel 등 단독으로 렌더링되어야 하는 패널들 // 단독 패널 체크 - CheckOutPanel, CartPanel 등 단독으로 렌더링되어야 하는 패널들
if (DEBUG_MODE) { if (DEBUG_MODE) {
console.log(`[PANEL_MainView] 🔍 Top panel name: ${topPanel?.name}`); console.log(`[PANEL_MainView] 🔍 Top panel name: ${topPanel?.name}`);
@@ -253,11 +250,7 @@ export default function MainView({ className, initService }) {
'[MainView] Rendering 3-layer structure: PlayerPanel + DetailPanel + MediaPanel' '[MainView] Rendering 3-layer structure: PlayerPanel + DetailPanel + MediaPanel'
); );
} }
if (hasFeaturedBrandsPanel || hasTrendingNowPanel) { renderingPanels = hasFeaturedBrandsPanel ? panels.slice(-4) : panels.slice(-3);
renderingPanels = panels.slice(-4);
} else {
renderingPanels = panels.slice(-3);
}
} else if ( } else if (
panels[panels.length - 1]?.name === Config.panel_names.PLAYER_PANEL || panels[panels.length - 1]?.name === Config.panel_names.PLAYER_PANEL ||
panels[panels.length - 1]?.name === Config.panel_names.PLAYER_PANEL_NEW || panels[panels.length - 1]?.name === Config.panel_names.PLAYER_PANEL_NEW ||
@@ -265,11 +258,7 @@ export default function MainView({ className, initService }) {
panels[panels.length - 2]?.name === Config.panel_names.PLAYER_PANEL || panels[panels.length - 2]?.name === Config.panel_names.PLAYER_PANEL ||
panels[panels.length - 2]?.name === Config.panel_names.MEDIA_PANEL panels[panels.length - 2]?.name === Config.panel_names.MEDIA_PANEL
) { ) {
if (hasFeaturedBrandsPanel || hasTrendingNowPanel) { renderingPanels = hasFeaturedBrandsPanel ? panels.slice(-3) : panels.slice(-2);
renderingPanels = panels.slice(-3);
} else {
renderingPanels = panels.slice(-2);
}
} else { } else {
renderingPanels = panels.slice(-1); renderingPanels = panels.slice(-1);
} }

View File

@@ -391,7 +391,6 @@ const MediaPanel = React.forwardRef(
const onEnded = useCallback( const onEnded = useCallback(
(e) => { (e) => {
debugLog('[MediaPanel] Video ended'); debugLog('[MediaPanel] Video ended');
// console.log('[🔥UNIQUE_MEDIA_ENDED🔥] MediaPanel onEnded triggered - will pop after 1500ms');
// continuousPlay는 MediaPlayer(VideoPlayer) 컴포넌트 내부에서 loop 속성으로 처리 // continuousPlay는 MediaPlayer(VideoPlayer) 컴포넌트 내부에서 loop 속성으로 처리
// onEnded가 호출되면 loop=false 인 경우이므로 패널을 닫음 // onEnded가 호출되면 loop=false 인 경우이므로 패널을 닫음
Spotlight.pause(); Spotlight.pause();
@@ -403,7 +402,6 @@ const MediaPanel = React.forwardRef(
// ✅ 새로운 타이머 저장 (cleanup 시 정리용) // ✅ 새로운 타이머 저장 (cleanup 시 정리용)
onEndedTimerRef.current = setTimeout(() => { onEndedTimerRef.current = setTimeout(() => {
// console.log('[🔥UNIQUE_MEDIA_ENDED🔥] Executing popPanel(MEDIA_PANEL) after 1500ms');
Spotlight.resume(); Spotlight.resume();
dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL)); dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL));
onEndedTimerRef.current = null; onEndedTimerRef.current = null;

View File

@@ -26,7 +26,7 @@
video { video {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
object-fit: contain; /* 높이 기준으로 맞추고 좌우는 잘림 */ object-fit: contain; /* 비율 유지하면서 컨테이너 안에 맞춤 */
} }
} }

View File

@@ -912,7 +912,7 @@ const MediaPanel = React.forwardRef(
} }
if (!panelInfo.modal) { if (!panelInfo.modal) {
dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL)); dispatch(PanelActions.popPanel());
dispatch(changeAppStatus({ cursorVisible: false })); dispatch(changeAppStatus({ cursorVisible: false }));
document?.dispatchEvent?.(new CustomEvent('detailpanel-scroll-reset')); document?.dispatchEvent?.(new CustomEvent('detailpanel-scroll-reset'));
@@ -937,7 +937,7 @@ const MediaPanel = React.forwardRef(
// 패널이 2개 존재할때만 popPanel 진행 // 패널이 2개 존재할때만 popPanel 진행
// fullscreen 전환 중이면 popPanel하지 않음 // fullscreen 전환 중이면 popPanel하지 않음
if (panelInfo.modal && !isOnTop && !isTransitioningToFullscreen.current) { if (panelInfo.modal && !isOnTop && !isTransitioningToFullscreen.current) {
dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL)); dispatch(PanelActions.popPanel());
} else { } else {
Spotlight.focus('tbody'); Spotlight.focus('tbody');
} }
@@ -1576,7 +1576,7 @@ const MediaPanel = React.forwardRef(
) { ) {
// case: Featured Brands // case: Featured Brands
if (panelInfo?.sourcePanel === panel_names.FEATURED_BRANDS_PANEL) { if (panelInfo?.sourcePanel === panel_names.FEATURED_BRANDS_PANEL) {
dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL)); dispatch(PanelActions.popPanel());
} }
} }
}, [ }, [
@@ -2043,25 +2043,19 @@ const MediaPanel = React.forwardRef(
(e) => { (e) => {
const currentInfo = panelInfoRef.current; const currentInfo = panelInfoRef.current;
// console.log('[🔥UNIQUE_MEDIA_ENDED🔥] onEnded triggered - shptmBanrTpNm:', currentInfo?.shptmBanrTpNm);
// MEDIA: 기존 동작 유지 (배경 복원 없이 즉시 pop) // MEDIA: 기존 동작 유지 (배경 복원 없이 즉시 pop)
if (currentInfo.shptmBanrTpNm === 'MEDIA') { if (currentInfo.shptmBanrTpNm === 'MEDIA') {
// console.log('[🔥UNIQUE_MEDIA_ENDED🔥] MEDIA type - popPanel will be called'); console.log('[MediaPanel] 🚫 Skipping background restoration for ended media');
Spotlight.pause(); Spotlight.pause();
setTimeout(() => { setTimeout(() => {
// console.log('[🔥UNIQUE_MEDIA_ENDED🔥] setTimeout fired - dispatching popPanel(MEDIA_PANEL)');
Spotlight.resume(); Spotlight.resume();
dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL)); dispatch(PanelActions.popPanel());
}, VIDEO_END_ACTION_DELAY); }, VIDEO_END_ACTION_DELAY);
e?.stopPropagation();
e?.preventDefault();
return; return;
} }
// VOD: modal 여부에 따라 동작 분리 // VOD: modal 여부에 따라 동작 분리
if (currentInfo.shptmBanrTpNm === 'VOD') { if (currentInfo.shptmBanrTpNm === 'VOD') {
// console.log('[🔥UNIQUE_MEDIA_ENDED🔥] VOD type - popPanel will be called');
Spotlight.pause(); Spotlight.pause();
setTimeout(() => { setTimeout(() => {
stopExternalPlayer(); stopExternalPlayer();
@@ -2080,8 +2074,6 @@ const MediaPanel = React.forwardRef(
e?.preventDefault(); e?.preventDefault();
return; return;
} }
// console.log('[🔥UNIQUE_MEDIA_ENDED🔥] Unknown shptmBanrTpNm - no action taken');
}, },
[dispatch, focusBackButtonOrFallback, stopExternalPlayer] [dispatch, focusBackButtonOrFallback, stopExternalPlayer]
); );

View File

@@ -390,7 +390,7 @@ function PlayerOverlayContents({
e.preventDefault(); e.preventDefault();
// tabIndexV2가 2일 때만 LiveChannelNext로 포커스 // tabIndexV2가 2일 때만 LiveChannelNext로 포커스
if (tabContainerVersion === 2 && tabIndexV2 === 2) { if (tabContainerVersion === 2 && tabIndexV2 === 2) {
Spotlight.focus('below-tab-shop-now-button'); Spotlight.focus('live-channel-next-button');
} }
}} }}
onSpotlightDown={(e) => { onSpotlightDown={(e) => {

View File

@@ -13,8 +13,8 @@
background-image: url("../../../../assets/images/btn/btn-video-cc-nor@3x.png"); background-image: url("../../../../assets/images/btn/btn-video-cc-nor@3x.png");
background-size: cover; background-size: cover;
position: absolute; position: absolute;
right: 300px; right: 60px;
top: 680px; top: 800px;
z-index: 10; z-index: 10;
&.videoVericalSubtitleButton { &.videoVericalSubtitleButton {

View File

@@ -30,7 +30,6 @@ import {
getMainLiveShow, getMainLiveShow,
getMainLiveShowNowProduct, getMainLiveShowNowProduct,
} from '../../actions/mainActions'; } from '../../actions/mainActions';
import { updateHomeInfo } from '../../actions/homeActions';
import * as PanelActions from '../../actions/panelActions'; import * as PanelActions from '../../actions/panelActions';
import { updatePanel } from '../../actions/panelActions'; import { updatePanel } from '../../actions/panelActions';
import { import {
@@ -91,51 +90,6 @@ const findSelector = (selector, maxAttempts = 5, currentAttempts = 0) => {
} }
}; };
// 배너 위치 수집 함수 (top, left만 저장)
const collectBannerPositions = () => {
const positions = [];
// banner0, banner1 등의 배너 위치 수집
for (let i = 0; i < 10; i++) {
const bannerId = `banner${i}`;
const node = document.querySelector(`[data-spotlight-id="${bannerId}"]`);
if (node) {
const { top, left } = node.getBoundingClientRect();
positions.push({
bannerId,
position: { top: Math.round(top), left: Math.round(left) }
});
dlog(`[PlayerPanel] 배너 위치 수집: ${bannerId}`, { top: Math.round(top), left: Math.round(left) });
}
}
return positions;
};
// 위치 검증 함수 (오차 범위: 1px)
const isPositionMatching = (bannerPositions, bannerId, currentPosition) => {
const validPosition = bannerPositions.find(p => p.bannerId === bannerId);
if (!validPosition) {
dlog(`[PlayerPanel] 배너 위치 검증 실패: ${bannerId} 배너를 찾을 수 없음`);
return false;
}
const tolerance = 1; // 1px 오차 범위
const isMatching =
Math.abs(currentPosition.top - validPosition.position.top) <= tolerance &&
Math.abs(currentPosition.left - validPosition.position.left) <= tolerance;
dlog(`[PlayerPanel] 배너 위치 검증: ${bannerId}`, {
expected: validPosition.position,
current: currentPosition,
matching: isMatching
});
return isMatching;
};
const getLogTpNo = (type, nowMenu) => { const getLogTpNo = (type, nowMenu) => {
if (type === 'LIVE') { if (type === 'LIVE') {
switch (nowMenu) { switch (nowMenu) {
@@ -264,7 +218,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
const [tabIndexV2, setTabIndexV2] = USE_STATE('tabIndexV2', 1); // 0: ShopNow, 1: LiveChannel, 2: ShopNowButton 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 [tabContainerVersion, setTabContainerVersion] = USE_STATE('tabContainerVersion', 2); // 1: TabContainer (우측), 2: TabContainerV2 (하단)
const [isModalClosed, setIsModalClosed] = USE_STATE('isModalClosed', true); // 모달이 false 상태인지 나타내는 플래그 const [isModalClosed, setIsModalClosed] = USE_STATE('isModalClosed', true); // 모달이 false 상태인지 나타내는 플래그
const [validBannerPositions, setValidBannerPositions] = USE_STATE('validBannerPositions', []); // 유효한 배너 위치 (top, left)
const panels = USE_SELECTOR('panels', (state) => state.panels.panels); const panels = USE_SELECTOR('panels', (state) => state.panels.panels);
const chatData = USE_SELECTOR('chatData', (state) => state.play.chatData); const chatData = USE_SELECTOR('chatData', (state) => state.play.chatData);
@@ -454,10 +407,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
lastFocusedTargetId: panelInfo.lastFocusedTargetId, lastFocusedTargetId: panelInfo.lastFocusedTargetId,
}); });
// TabContainerV2의 tabIndex를 ShopNowContents(0)로 리셋
dlog('[PlayerPanel] 📑 TabContainerV2 tabIndex를 ShopNowContents(0)로 리셋');
setTabIndexV2(0);
// 포커스 복원 로직 추가 (1000ms 지연) // 포커스 복원 로직 추가 (1000ms 지연)
if (panelInfo.lastFocusedTargetId) { if (panelInfo.lastFocusedTargetId) {
dlog( dlog(
@@ -482,7 +431,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
}; };
} }
console.log('[PANEL] PlayerPanel updatePanel - detailPanelClosed reset');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, name: panel_names.PLAYER_PANEL,
@@ -555,10 +503,10 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
// PanelInfo 상태 변화 모니터링 useEffect (isPaused가 실제로 변경될 때만) // PanelInfo 상태 변화 모니터링 useEffect (isPaused가 실제로 변경될 때만)
useEffect(() => { useEffect(() => {
const isOnTop = panel_names.HOME_PANEL === topPanel?.name;
const isPausedChanged = previousPanelInfo.current?.isPaused !== panelInfo?.isPaused; const isPausedChanged = previousPanelInfo.current?.isPaused !== panelInfo?.isPaused;
// isOnTop 여부와 관계없이 isPaused 변경을 감지하여 비디오 제어 if (isOnTop && panelInfo?.isPaused !== undefined && isPausedChanged) {
if (panelInfo?.isPaused !== undefined && isPausedChanged) {
// 상태 변경 시에만 디버깅 로그 출력 // 상태 변경 시에만 디버깅 로그 출력
dlog('🔍 [PlayerPanel] PanelInfo isPaused changed', { dlog('🔍 [PlayerPanel] PanelInfo isPaused changed', {
previousIsPaused: previousPanelInfo.current?.isPaused, previousIsPaused: previousPanelInfo.current?.isPaused,
@@ -591,7 +539,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
} }
previousPanelInfo.current = panelInfo; previousPanelInfo.current = panelInfo;
}, [panelInfo?.isPaused, currentPlayingUrl]); }, [panelInfo?.isPaused, topPanel?.name, currentPlayingUrl]);
// VideoPlayer 인스턴스 및 소스 변경 모니터링 (중요 변화만) // VideoPlayer 인스턴스 및 소스 변경 모니터링 (중요 변화만)
useEffect(() => { useEffect(() => {
@@ -699,7 +647,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
useEffect(() => { useEffect(() => {
if (currentLiveShowInfo && Object.keys(currentLiveShowInfo).length > 0) { if (currentLiveShowInfo && Object.keys(currentLiveShowInfo).length > 0) {
if (currentLiveShowInfo.showId !== panelInfo?.showId) { if (currentLiveShowInfo.showId !== panelInfo?.showId) {
console.log('[PANEL] PlayerPanel updatePanel - LIVE showId update');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, name: panel_names.PLAYER_PANEL,
@@ -1199,7 +1146,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
videoPlayer.current?.hideControls(); videoPlayer.current?.hideControls();
setSelectedIndex(backupInitialIndex); setSelectedIndex(backupInitialIndex);
if (panelInfo.shptmBanrTpNm === 'MEDIA') { if (panelInfo.shptmBanrTpNm === 'MEDIA') {
console.log('[PANEL] PlayerPanel updatePanel - DETAIL_PANEL launchedFromPlayer false');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.DETAIL_PANEL, name: panel_names.DETAIL_PANEL,
@@ -1217,7 +1163,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
if (!panelInfo.modal) { if (!panelInfo.modal) {
console.log('[PlayerPanel] popPanel - closeButtonHandler'); console.log('[PlayerPanel] popPanel - closeButtonHandler');
dispatch(PanelActions.popPanel()); dispatch(PanelActions.popPanel());
// 🔽 [251221] cleanup useEffect에서 isDeepLinkEntry 리셋 처리
dispatch(changeAppStatus({ cursorVisible: false })); dispatch(changeAppStatus({ cursorVisible: false }));
//딮링크로 플레이어 진입 후 이전버튼 클릭시 //딮링크로 플레이어 진입 후 이전버튼 클릭시
@@ -1248,23 +1193,12 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
return () => { return () => {
// 패널이 2개 존재할때만 popPanel 진행 // 패널이 2개 존재할때만 popPanel 진행
// 현재 스택의 top이 PlayerPanel일 때만 pop 수행 (다른 패널이 올라온 상태에서 오작동 방지) // 현재 스택의 top이 PlayerPanel일 때만 pop 수행 (다른 패널이 올라온 상태에서 오작동 방지)
// console.log('[PP-TRACE] cleanup start', { console.log('[PP-TRACE] cleanup start', {
// modal: panelInfo.modal, modal: panelInfo.modal,
// isOnTop, isOnTop,
// topPanel: panels[panels.length - 1]?.name, topPanel: panels[panels.length - 1]?.name,
// stack: panels.map((p) => p.name), stack: panels.map((p) => p.name),
// }); });
// 🔽 [251221] PlayerPanel unmount 시 DeepLink 플래그 리셋
dispatch(
updateHomeInfo({
name: panel_names.HOME_PANEL,
panelInfo: {
isDeepLinkEntry: false,
},
})
);
const topPanelName = panels[panels.length - 1]?.name; const topPanelName = panels[panels.length - 1]?.name;
if ( if (
panelInfo.modal && panelInfo.modal &&
@@ -1272,13 +1206,13 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
topPanelName === panel_names.PLAYER_PANEL && topPanelName === panel_names.PLAYER_PANEL &&
panels.length === 1 // 다른 패널 존재 시 pop 금지 (DetailPanel 제거 방지) panels.length === 1 // 다른 패널 존재 시 pop 금지 (DetailPanel 제거 방지)
) { ) {
// console.log('[PP-TRACE] popPanel - useEffect cleanup (top is PlayerPanel)'); console.log('[PP-TRACE] popPanel - useEffect cleanup (top is PlayerPanel)');
dispatch(PanelActions.popPanel()); dispatch(PanelActions.popPanel());
} else { } else {
Spotlight.focus('tbody'); Spotlight.focus('tbody');
} }
}; };
}, [panelInfo?.modal, isOnTop, panels, dispatch]); }, [panelInfo?.modal, isOnTop, panels]);
useEffect(() => { useEffect(() => {
if (showNowInfos && panelInfo.shptmBanrTpNm === 'LIVE') { if (showNowInfos && panelInfo.shptmBanrTpNm === 'LIVE') {
@@ -1375,21 +1309,10 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
// 최상단 패널이 DetailPanel이고 PlayerPanel에서 진입했는지 확인 // 최상단 패널이 DetailPanel이고 PlayerPanel에서 진입했는지 확인
const isTopPanelDetailFromPlayer = useMemo(() => { const isTopPanelDetailFromPlayer = useMemo(() => {
let result = const result =
topPanel?.name === panel_names.DETAIL_PANEL && topPanel?.name === panel_names.DETAIL_PANEL &&
topPanel?.panelInfo?.launchedFromPlayer === true; topPanel?.panelInfo?.launchedFromPlayer === true;
// MediaPanel이 최상단에 있고 그 아래가 DetailPanel인 경우도 체크
if (!result && topPanel?.name === panel_names.MEDIA_PANEL) {
const prevPanel = panels[panels.length - 2];
if (
prevPanel?.name === panel_names.DETAIL_PANEL &&
prevPanel?.panelInfo?.launchedFromPlayer === true
) {
result = true;
}
}
// 🔍 DetailPanel 상태 변화 로깅 // 🔍 DetailPanel 상태 변화 로깅
if (result) { if (result) {
dlog('🎬 [PlayerPanel] DetailPanel is now on top (from Player)', { dlog('🎬 [PlayerPanel] DetailPanel is now on top (from Player)', {
@@ -1845,7 +1768,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
if (watchIntervalLive.current) clearInterval(watchIntervalLive.current); if (watchIntervalLive.current) clearInterval(watchIntervalLive.current);
if (watchIntervalVod.current) clearInterval(watchIntervalVod.current); if (watchIntervalVod.current) clearInterval(watchIntervalVod.current);
if (watchIntervalMedia.current) clearInterval(watchIntervalMedia.current); if (watchIntervalMedia.current) clearInterval(watchIntervalMedia.current);
if (activityCheckIntervalRef.current) clearInterval(activityCheckIntervalRef.current);
}; };
}, []); }, []);
@@ -1936,7 +1858,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
dlog('[PlayerPanel] Condition 2.5: Removing skipFlag after DOM render'); dlog('[PlayerPanel] Condition 2.5: Removing skipFlag after DOM render');
console.log('[PANEL] PlayerPanel updatePanel - skipModalStyleRecalculation remove');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, name: panel_names.PLAYER_PANEL,
@@ -1977,37 +1898,12 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
scale = width / window.innerWidth; scale = width / window.innerWidth;
setModalScale(scale); setModalScale(scale);
} }
console.log('[PANEL] PlayerPanel updatePanel - modalStyle and scale update');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, name: panel_names.PLAYER_PANEL,
panelInfo: { modalStyle: modalStyle, modalScale: scale }, panelInfo: { modalStyle: modalStyle, modalScale: scale },
}) })
); );
// 🔽 배너 위치 수집 (초기 로드 시에만 실행)
if (validBannerPositions.length === 0) {
const positions = collectBannerPositions();
if (positions.length > 0) {
setValidBannerPositions(positions);
dlog('[PlayerPanel] ✅ 배너 위치 초기 수집 완료:', positions);
}
}
// 🔽 배너 위치 검증 (위치가 맞지 않으면 비디오 재생 중단)
if (validBannerPositions.length > 0) {
const currentPosition = { top: Math.round(top), left: Math.round(left) };
const isValidPosition = isPositionMatching(validBannerPositions, panelInfo.modalContainerId, currentPosition);
if (!isValidPosition) {
dlog('[PlayerPanel] ⚠️ 배너 위치 검증 실패 - 비디오 재생 중단', {
bannerId: panelInfo.modalContainerId,
currentPosition,
validBannerPositions
});
return; // 비디오 재생 중단
}
}
} else { } else {
dlog('[PlayerPanel] Condition 1: Node not found, using saved modalStyle'); dlog('[PlayerPanel] Condition 1: Node not found, using saved modalStyle');
setModalStyle(panelInfo.modalStyle); setModalStyle(panelInfo.modalStyle);
@@ -2327,7 +2223,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
); );
Spotlight.focus('playVideoShopNowBox'); Spotlight.focus('playVideoShopNowBox');
} else { } else {
console.log('[PANEL] PlayerPanel updatePanel - handleIndicatorDownClick');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, name: panel_names.PLAYER_PANEL,
@@ -2346,7 +2241,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
setPrevChannelIndex(selectedIndex); setPrevChannelIndex(selectedIndex);
} }
setSideContentsVisible(true); setSideContentsVisible(true);
}, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter, tabContainerVersion, tabIndexV2]); }, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter]);
const handleIndicatorUpClick = useCallback(() => { const handleIndicatorUpClick = useCallback(() => {
if (!initialEnter) { if (!initialEnter) {
@@ -2375,7 +2270,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
); );
Spotlight.focus('playVideoShopNowBox'); Spotlight.focus('playVideoShopNowBox');
} else { } else {
console.log('[PANEL] PlayerPanel updatePanel - handleIndicatorUpClick');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, name: panel_names.PLAYER_PANEL,
@@ -2394,7 +2288,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
setPrevChannelIndex(selectedIndex); setPrevChannelIndex(selectedIndex);
} }
setSideContentsVisible(true); setSideContentsVisible(true);
}, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter, tabContainerVersion, tabIndexV2]); }, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter]);
useEffect(() => { useEffect(() => {
if (panelInfo.shptmBanrTpNm === 'VOD' && panelInfo.patnrId && panelInfo.showId) { if (panelInfo.shptmBanrTpNm === 'VOD' && panelInfo.patnrId && panelInfo.showId) {
@@ -2495,7 +2389,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
const onEnded = useCallback( const onEnded = useCallback(
(e) => { (e) => {
if (panelInfoRef.current.shptmBanrTpNm === 'MEDIA') { if (panelInfoRef.current.shptmBanrTpNm === 'MEDIA') {
console.log('[PANEL] PlayerPanel updatePanel - DETAIL_PANEL video ended');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.DETAIL_PANEL, name: panel_names.DETAIL_PANEL,
@@ -2536,105 +2429,14 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
); );
const onKeyDown = (ev) => { const onKeyDown = (ev) => {
// tabIndex === 1 (LiveChannelContents 표시)이고 비디오 배너에 포커스가 있는 경우 if (ev.keyCode === 34) {
const currentFocused = Spotlight.getCurrent();
const spotlightId = currentFocused?.getAttribute('data-spotlight-id');
const isVideoItemFocused = spotlightId?.startsWith('tabChannel-video');
// LiveChannelContents의 비디오 배너에 포커스가 있는 경우: PageUp/PageDown을 좌우 이동으로 변환
if (tabIndexV2 === 1 && isVideoItemFocused) {
// DOM에서 실제로 렌더링된 모든 비디오 배너 찾기 (가상화 대응)
const allVideoBanners = Array.from(
document.querySelectorAll('[data-spotlight-id^="tabChannel-video-"]')
);
if (allVideoBanners.length > 0) {
// 현재 포커스된 배너의 인덱스 찾기
const currentBannerIndex = allVideoBanners.findIndex(
(el) => el === currentFocused
);
if (currentBannerIndex !== -1) {
if (ev.keyCode === 34) { // PageDown -> 오른쪽 배너로 포커스 이동
ev.stopPropagation();
ev.preventDefault();
// DOM에 렌더링된 다음 배너로 이동 (마지막이면 무시 또는 첫 번째로)
if (currentBannerIndex < allVideoBanners.length - 1) {
// 다음 배너가 DOM에 있으면 이동
const nextBanner = allVideoBanners[currentBannerIndex + 1];
const nextSpotlightId = nextBanner.getAttribute('data-spotlight-id');
dlog('[PlayerPanel] 🎯 PageDown (비디오 배너) -> 오른쪽으로 이동', {
current: spotlightId,
next: nextSpotlightId,
currentBannerIndex,
totalVisibleBanners: allVideoBanners.length,
});
Spotlight.focus(nextSpotlightId);
} else {
// 마지막 배너면 첫 번째로 이동 시도 (DOM에 있으면)
const firstBanner = allVideoBanners[0];
const firstSpotlightId = firstBanner.getAttribute('data-spotlight-id');
dlog('[PlayerPanel] 🎯 PageDown (마지막 배너) -> 첫 번째 배너로 이동 시도', {
current: spotlightId,
next: firstSpotlightId,
isWrapAround: true,
});
Spotlight.focus(firstSpotlightId);
}
return;
} else if (ev.keyCode === 33) { // PageUp -> 왼쪽 배너로 포커스 이동
ev.stopPropagation();
ev.preventDefault();
// DOM에 렌더링된 이전 배너로 이동 (첫 번째면 무시 또는 마지막으로)
if (currentBannerIndex > 0) {
// 이전 배너가 DOM에 있으면 이동
const prevBanner = allVideoBanners[currentBannerIndex - 1];
const prevSpotlightId = prevBanner.getAttribute('data-spotlight-id');
dlog('[PlayerPanel] 🎯 PageUp (비디오 배너) -> 왼쪽으로 이동', {
current: spotlightId,
prev: prevSpotlightId,
currentBannerIndex,
totalVisibleBanners: allVideoBanners.length,
});
Spotlight.focus(prevSpotlightId);
} else {
// 첫 번째 배너면 마지막으로 이동 시도 (DOM에 있으면)
const lastBanner = allVideoBanners[allVideoBanners.length - 1];
const lastSpotlightId = lastBanner.getAttribute('data-spotlight-id');
dlog('[PlayerPanel] 🎯 PageUp (첫 번째 배너) -> 마지막 배너로 이동 시도', {
current: spotlightId,
prev: lastSpotlightId,
isWrapAround: true,
});
Spotlight.focus(lastSpotlightId);
}
return;
}
}
}
}
// 기존 로직: LiveChannelButton 또는 다른 경우에는 상/하 이동
if (ev.keyCode === 34) { // PageDown
handleIndicatorDownClick(); handleIndicatorDownClick();
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
dlog('[PlayerPanel] 📺 PageDown (버튼 또는 다른 경우) -> 다음 비디오'); } else if (ev.keyCode === 33) {
} else if (ev.keyCode === 33) { // PageUp
handleIndicatorUpClick(); handleIndicatorUpClick();
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
dlog('[PlayerPanel] 📺 PageUp (버튼 또는 다른 경우) -> 이전 비디오');
} }
}; };
@@ -2645,11 +2447,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
const timerIdTabAutoAdvance = useRef(null); const timerIdTabAutoAdvance = useRef(null);
const prevTabIndexV2 = useRef(null); const prevTabIndexV2 = useRef(null);
// Activity Check for tabIndex auto-advance
const lastActivityTimeRef = useRef(Date.now());
const activityCheckIntervalRef = useRef(null);
const ACTIVITY_TIMEOUT = 1000; // 1초 동안 활동이 없으면 타이머 진행
const showSideContents = useMemo(() => { const showSideContents = useMemo(() => {
return ( return (
sideContentsVisible && sideContentsVisible &&
@@ -2761,62 +2558,17 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
timerIdTabAutoAdvance.current = null; timerIdTabAutoAdvance.current = null;
}, []); }, []);
// Activity 감지 함수
const onActivityDetected = useCallback(() => {
lastActivityTimeRef.current = Date.now();
dlog('[PlayerPanel] 🎯 Activity detected - timer will be delayed', {
timestamp: new Date().toISOString(),
});
}, []);
// Activity 여부를 확인하는 함수 (1초 타임아웃 체크)
const isInactive = useCallback(() => {
const now = Date.now();
const timeSinceLastActivity = now - lastActivityTimeRef.current;
return timeSinceLastActivity > ACTIVITY_TIMEOUT;
}, []);
const resetTimerTabAutoAdvance = useCallback( const resetTimerTabAutoAdvance = useCallback(
(timeout) => { (timeout) => {
if (timerIdTabAutoAdvance.current) { if (timerIdTabAutoAdvance.current) {
clearTimerTabAutoAdvance(); clearTimerTabAutoAdvance();
} }
// Activity check interval 설정 (매 100ms마다 체크) timerIdTabAutoAdvance.current = setTimeout(() => {
if (activityCheckIntervalRef.current) {
clearInterval(activityCheckIntervalRef.current);
}
let elapsedTime = 0;
activityCheckIntervalRef.current = setInterval(() => {
// 활동이 없을 때만 경과 시간 증가
if (isInactive()) {
elapsedTime += 100;
dlog('[PlayerPanel] ⏱️ TabIndex auto-advance: inactive', {
elapsedTime,
requiredTime: timeout,
});
// 필요한 시간만큼 경과했으면 타이머 실행
if (elapsedTime >= timeout) {
dlog('[PlayerPanel] ✅ TabIndex auto-advance executing - setTabIndexV2(2)', {
totalElapsed: elapsedTime,
timeout,
});
clearInterval(activityCheckIntervalRef.current);
setTabIndexV2(2); setTabIndexV2(2);
} }, timeout);
} else {
// 활동이 감지되면 경과 시간 리셋
dlog('[PlayerPanel] 🔄 Activity detected - resetting elapsed time', {
previousElapsed: elapsedTime,
});
elapsedTime = 0;
}
}, 100);
}, },
[clearTimerTabAutoAdvance, isInactive] [clearTimerTabAutoAdvance]
); );
// Redux로 오버레이 숨김 // Redux로 오버레이 숨김
@@ -2841,10 +2593,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
if (timerIdTabAutoAdvance.current) { if (timerIdTabAutoAdvance.current) {
clearTimerTabAutoAdvance(); clearTimerTabAutoAdvance();
} }
if (activityCheckIntervalRef.current) {
clearInterval(activityCheckIntervalRef.current);
activityCheckIntervalRef.current = null;
}
dispatch(resetPlayerOverlays()); dispatch(resetPlayerOverlays());
} }
@@ -3059,53 +2807,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
panelInfo?.modal, panelInfo?.modal,
]); ]);
// PageUp/PageDown으로 비디오 변경 시 현재 재생 배너로 포커스 이동
useEffect(() => {
if (tabContainerVersion === 2 &&
tabIndexV2 === 1 &&
panelInfo?.isIndicatorByClick &&
selectedIndex !== null &&
selectedIndex >= 0) {
dlog('[PlayerPanel] 🎯 PageUp/PageDown 후 포커스 이동 준비', {
selectedIndex,
tabContainerVersion,
tabIndexV2,
isIndicatorByClick: panelInfo.isIndicatorByClick
});
const bannerSpotlightId = `banner${selectedIndex}`;
setTimeout(() => {
dlog('[PlayerPanel] 🔍 포커스 이동 시도:', bannerSpotlightId);
const bannerElement = document.querySelector(`[data-spotlight-id="${bannerSpotlightId}"]`);
if (bannerElement) {
dlog('[PlayerPanel] ✅ 배너 요소 찾음, 포커스 이동 실행');
Spotlight.focus(bannerElement);
} else {
dlog('[PlayerPanel] ⚠️ 배너 요소 찾지 못함:', bannerSpotlightId);
// 모든 배너 요소 목록 출력
const allBanners = document.querySelectorAll('[data-spotlight-id^="banner"]');
dlog('[PlayerPanel] 🔍 사용 가능한 배너 목록:',
Array.from(allBanners).map(el => el.getAttribute('data-spotlight-id'))
);
}
// 플래그 리셋
dispatch(
updatePanel({
name: panel_names.PLAYER_PANEL,
panelInfo: {
isIndicatorByClick: false
},
})
);
}, 200); // DOM 업데이트 대기
}
}, [selectedIndex, tabContainerVersion, tabIndexV2, panelInfo?.isIndicatorByClick, dispatch]);
// TabIndex 1 자동 다음 단계로 이동 // TabIndex 1 자동 다음 단계로 이동
useEffect(() => { useEffect(() => {
// tabIndex === 1일 때만 실행 // tabIndex === 1일 때만 실행
@@ -3132,31 +2833,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
clearTimerTabAutoAdvance, clearTimerTabAutoAdvance,
]); ]);
// Activity detection for tabIndex auto-advance (mousemove, keydown, click)
useEffect(() => {
// tabIndex === 1일 때만 Activity 감지 활성화
if (tabIndexV2 !== 1 || !belowContentsVisible) {
return;
}
dlog('[PlayerPanel] 🎙️ Activity listener registered for tabIndex=1');
const handleMouseMove = onActivityDetected;
const handleKeyDown = onActivityDetected;
const handleClick = onActivityDetected;
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('click', handleClick);
return () => {
dlog('[PlayerPanel] 🎙️ Activity listener unregistered');
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('click', handleClick);
};
}, [tabIndexV2, belowContentsVisible, onActivityDetected]);
useLayoutEffect(() => { useLayoutEffect(() => {
const videoContainer = document.querySelector(`.${css.videoContainer}`); const videoContainer = document.querySelector(`.${css.videoContainer}`);

View File

@@ -5,8 +5,9 @@ import { useDispatch } from 'react-redux';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
import { sendLogTotalRecommend } from '../../../../actions/logActions'; import { sendLogTotalRecommend } from '../../../../actions/logActions';
// <<<<<<< HEAD
import { updatePanel } from '../../../../actions/panelActions'; import { updatePanel } from '../../../../actions/panelActions';
import TScrollerLiveChannel from './TScrollerLiveChannel'; import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList';
import { import {
LOG_CONTEXT_NAME, LOG_CONTEXT_NAME,
LOG_MENU, LOG_MENU,
@@ -19,11 +20,26 @@ import ListEmptyContents from '../TabContents/ListEmptyContents/ListEmptyContent
import css from './LiveChannelContents.module.less'; import css from './LiveChannelContents.module.less';
import cssV2 from './LiveChannelContents.v2.module.less'; import cssV2 from './LiveChannelContents.v2.module.less';
// =======
// import { updatePanel } from "../../../../actions/panelActions";
// import TVirtualGridList from "../../../../components/TVirtualGridList/TVirtualGridList";
// import {
// LOG_CONTEXT_NAME,
// LOG_MENU,
// LOG_MESSAGE_ID,
// panel_names,
// } from "../../../../utils/Config";
// import { $L } from "../../../../utils/helperMethods";
// import PlayerItemCard, { TYPES } from "../../PlayerItemCard/PlayerItemCard";
// import ListEmptyContents from "../TabContents/ListEmptyContents/ListEmptyContents";
// import css from "./LiveChannelContents.module.less";
// import { sendLogTotalRecommend } from "../../../../actions/logActions";
// >>>>>>> gitlab/develop
export default function LiveChannelContents({ export default function LiveChannelContents({
liveInfos, liveInfos,
currentTime, currentTime,
setSelectedIndex, setSelectedIndex,
selectedIndex,
videoVerticalVisible, videoVerticalVisible,
currentVideoShowId, currentVideoShowId,
tabIndex, tabIndex,
@@ -67,19 +83,6 @@ export default function LiveChannelContents({
} }
}, [isFilteredByPatnr19]); }, [isFilteredByPatnr19]);
// currentVideoShowId 변경 시 해당 배너가 보이도록 스크롤
// (LiveChannelButton에서 PageUp/PageDown으로 동영상 변경 시)
// currentVideoShowId 기반으로 스크롤하면 포커스 이동 없이 배너만 화면에 보임
useEffect(() => {
if (currentVideoShowId && liveInfos && liveInfos.length > 0 && scrollToRef.current) {
// currentVideoShowId와 일치하는 배너의 인덱스 찾기
const index = liveInfos.findIndex((item) => item.showId === currentVideoShowId);
if (index !== -1) {
scrollToRef.current({ index, animate: true, focus: false });
}
}
}, [currentVideoShowId, liveInfos]);
const renderItem = useCallback( const renderItem = useCallback(
({ index, ...rest }) => { ({ index, ...rest }) => {
const { const {
@@ -219,7 +222,7 @@ export default function LiveChannelContents({
<> <>
<div className={containerClass}> <div className={containerClass}>
{liveInfos && liveInfos.length > 0 ? ( {liveInfos && liveInfos.length > 0 ? (
<TScrollerLiveChannel <TVirtualGridList
cbScrollTo={handleScrollTo} cbScrollTo={handleScrollTo}
dataSize={liveInfos.length} dataSize={liveInfos.length}
direction={direction} direction={direction}
@@ -227,6 +230,7 @@ export default function LiveChannelContents({
itemWidth={version === 2 ? 470 : videoVerticalVisible ? 540 : 600} itemWidth={version === 2 ? 470 : videoVerticalVisible ? 540 : 600}
itemHeight={version === 2 ? 155 : 236} itemHeight={version === 2 ? 155 : 236}
spacing={version === 2 ? 30 : 12} spacing={version === 2 ? 30 : 12}
noScrollByWheel={false}
/> />
) : ( ) : (
<ListEmptyContents tabIndex={tabIndex} /> <ListEmptyContents tabIndex={tabIndex} />

View File

@@ -1,250 +0,0 @@
import React, { useCallback, useEffect, useRef, useMemo } from 'react';
import classNames from 'classnames';
import { scaleH, scaleW } from '../../../../utils/helperMethods';
import css from './TScrollerLiveChannel.module.less';
/**
* TScrollerLiveChannel - Live Channel용 간단한 스크롤 컴포넌트
*
* TVirtualGridList의 가상화 대신 모든 아이템을 DOM에 렌더링
* 20개 미만의 아이템에 최적화되어 있음
*
* @param {number} dataSize - 아이템 개수
* @param {string} direction - 'horizontal' 또는 'vertical'
* @param {function} renderItem - 아이템 렌더링 함수 ({ index })
* @param {number} itemWidth - 아이템 너비
* @param {number} itemHeight - 아이템 높이
* @param {number} spacing - 아이템 간 간격
* @param {function} cbScrollTo - 스크롤 함수를 받을 콜백
* @param {string} className - 추가 CSS 클래스
* @param {string} spotlightId - Spotlight 포커스 ID prefix
*/
export default function TScrollerLiveChannel({
dataSize,
direction = 'horizontal',
renderItem,
itemWidth,
itemHeight,
spacing,
cbScrollTo,
className,
spotlightId,
}) {
const scrollContainerRef = useRef(null);
const itemsRef = useRef([]);
// 스크롤 컨테이너 크기 계산
const containerStyle = useMemo(() => {
if (direction === 'horizontal') {
return {
display: 'flex',
overflowX: 'auto',
overflowY: 'hidden',
width: '100%',
height: scaleH(itemHeight),
alignItems: 'center',
};
} else {
return {
display: 'flex',
flexDirection: 'column',
overflowY: 'auto',
overflowX: 'hidden',
width: '100%',
};
}
}, [direction, itemHeight]);
// 아이템 래퍼 스타일 계산
const itemsWrapperStyle = useMemo(() => {
if (direction === 'horizontal') {
return {
display: 'flex',
flexDirection: 'row',
gap: scaleW(spacing),
padding: `0 ${scaleW(spacing)}px`,
alignItems: 'center',
};
} else {
return {
display: 'flex',
flexDirection: 'column',
gap: scaleH(spacing),
padding: `${scaleH(spacing)}px 0`,
};
}
}, [direction, spacing]);
// 스크롤 함수 생성
const scrollToIndex = useCallback(
(index, options = {}) => {
const container = scrollContainerRef.current;
if (!container || !itemsRef.current[index]) return;
const item = itemsRef.current[index];
const { animate = true } = options;
if (direction === 'horizontal') {
// 수평 스크롤: 현재 아이템 + 다음 아이템까지 보이도록
const itemLeft = item.offsetLeft;
const itemWidth = item.offsetWidth;
const containerWidth = container.clientWidth;
// 다음 아이템도 일부 보일 수 있도록 스크롤
// 현재 아이템 + 다음 아이템의 일부가 보이는 위치로 스크롤
const nextItem = itemsRef.current[index + 1];
let scrollLeft = itemLeft - scaleW(spacing);
if (nextItem) {
// 다음 아이템의 왼쪽 끝이 컨테이너의 오른쪽 끝과 같은 위치가 되도록
const nextItemLeft = nextItem.offsetLeft;
const nextItemWidth = nextItem.offsetWidth;
const targetScrollLeft = nextItemLeft + nextItemWidth - containerWidth + scaleW(spacing);
// 두 가지 중에서 현재 아이템이 더 잘 보이는 쪽 선택
scrollLeft = Math.min(scrollLeft, targetScrollLeft);
}
// 음수 스크롤 방지
scrollLeft = Math.max(0, scrollLeft);
if (animate) {
container.scrollTo({
left: scrollLeft,
behavior: 'smooth',
});
} else {
container.scrollLeft = scrollLeft;
}
} else {
// 수직 스크롤: 현재 아이템 + 다음 아이템까지 보이도록
const itemTop = item.offsetTop;
const itemHeight = item.offsetHeight;
const containerHeight = container.clientHeight;
// 다음 아이템도 일부 보일 수 있도록 스크롤
const nextItem = itemsRef.current[index + 1];
let scrollTop = itemTop - scaleH(spacing);
if (nextItem) {
// 다음 아이템의 위쪽 끝이 컨테이너의 아래쪽 끝과 같은 위치가 되도록
const nextItemTop = nextItem.offsetTop;
const nextItemHeight = nextItem.offsetHeight;
const targetScrollTop = nextItemTop + nextItemHeight - containerHeight + scaleH(spacing);
// 두 가지 중에서 현재 아이템이 더 잘 보이는 쪽 선택
scrollTop = Math.min(scrollTop, targetScrollTop);
}
// 음수 스크롤 방지
scrollTop = Math.max(0, scrollTop);
if (animate) {
container.scrollTo({
top: scrollTop,
behavior: 'smooth',
});
} else {
container.scrollTop = scrollTop;
}
}
},
[direction, spacing]
);
// TVirtualGridList와 호환되는 콜백 인터페이스 제공
useEffect(() => {
if (cbScrollTo) {
cbScrollTo((options) => {
const { index, animate = true, focus = true } = options;
if (typeof index === 'number' && index >= 0 && index < dataSize) {
scrollToIndex(index, { animate });
}
});
}
}, [cbScrollTo, scrollToIndex, dataSize]);
// 아이템 ref 할당 함수
const setItemRef = useCallback((el, index) => {
if (el) {
itemsRef.current[index] = el;
} else {
delete itemsRef.current[index];
}
}, []);
// 포커스된 아이템을 화면에 완전히 보이도록 스크롤
const handleItemFocus = useCallback(
(index) => {
const container = scrollContainerRef.current;
const item = itemsRef.current[index];
if (!container || !item) return;
if (direction === 'horizontal') {
const itemLeft = item.offsetLeft;
const itemWidth = item.offsetWidth;
const containerWidth = container.clientWidth;
const containerScrollLeft = container.scrollLeft;
// 아이템이 완전히 보이는지 확인
const itemRight = itemLeft + itemWidth;
const containerRight = containerScrollLeft + containerWidth;
// 아이템이 왼쪽으로 밖에 나가 있으면 왼쪽 끝에 맞춤
if (itemLeft < containerScrollLeft) {
container.scrollLeft = itemLeft - scaleW(spacing);
}
// 아이템이 오른쪽으로 밖에 나가 있으면 오른쪽 끝에 맞춤
else if (itemRight > containerRight) {
container.scrollLeft = itemRight - containerWidth + scaleW(spacing);
}
} else {
const itemTop = item.offsetTop;
const itemHeight = item.offsetHeight;
const containerHeight = container.clientHeight;
const containerScrollTop = container.scrollTop;
// 아이템이 완전히 보이는지 확인
const itemBottom = itemTop + itemHeight;
const containerBottom = containerScrollTop + containerHeight;
// 아이템이 위로 밖에 나가 있으면 위쪽 끝에 맞춤
if (itemTop < containerScrollTop) {
container.scrollTop = itemTop - scaleH(spacing);
}
// 아이템이 아래로 밖에 나가 있으면 아래쪽 끝에 맞춤
else if (itemBottom > containerBottom) {
container.scrollTop = itemBottom - containerHeight + scaleH(spacing);
}
}
},
[direction, spacing]
);
return (
<div
className={classNames(css.tScrollerLiveChannelContainer, className)}
style={containerStyle}
ref={scrollContainerRef}
>
<div className={css.itemsWrapper} style={itemsWrapperStyle}>
{Array.from({ length: dataSize }).map((_, index) => (
<div
key={`item-${index}`}
ref={(el) => setItemRef(el, index)}
className={css.item}
style={{
width: direction === 'horizontal' ? scaleW(itemWidth) : 'auto',
height: direction === 'horizontal' ? 'auto' : scaleH(itemHeight),
flexShrink: 0,
}}
onFocus={() => handleItemFocus(index)}
>
{renderItem({ index })}
</div>
))}
</div>
</div>
);
}

View File

@@ -1,41 +0,0 @@
.tScrollerLiveChannelContainer {
position: relative;
width: 100%;
height: 100%;
// 스크롤바 스타일
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
&:hover {
background: rgba(255, 255, 255, 0.5);
}
}
}
.itemsWrapper {
width: 100%;
height: 100%;
}
.item {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
// 포커스 상태 처리
&:focus-within {
outline: none;
}
}

View File

@@ -5,13 +5,9 @@ import { compose } from 'ramda/src/compose';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
import Spottable from '@enact/spotlight/Spottable'; import Spottable from '@enact/spotlight/Spottable';
import { import { Marquee, MarqueeController } from '@enact/ui/Marquee';
Marquee,
MarqueeController,
} from '@enact/ui/Marquee';
import icon_arrow_dwon import icon_arrow_dwon from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png';
from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png';
import CustomImage from '../../../../components/CustomImage/CustomImage'; import CustomImage from '../../../../components/CustomImage/CustomImage';
import { SpotlightIds } from '../../../../utils/SpotlightIds'; import { SpotlightIds } from '../../../../utils/SpotlightIds';
import css from './LiveChannelNext.module.less'; import css from './LiveChannelNext.module.less';
@@ -22,7 +18,7 @@ export default function LiveChannelNext({
channelLogo, channelLogo,
channelName = 'ShopLC', channelName = 'ShopLC',
programName = 'Sandal Black...', programName = 'Sandal Black...',
backgroundColor = 'transparent', backgroundColor = 'linear-gradient(180deg, #284998 0%, #06B0EE 100%)',
onClick, onClick,
onFocus, onFocus,
spotlightId = 'live-channel-next-button', spotlightId = 'live-channel-next-button',
@@ -58,6 +54,7 @@ export default function LiveChannelNext({
<div className={css.logoWrapper}> <div className={css.logoWrapper}>
<div <div
className={css.logoBackground} className={css.logoBackground}
style={{ background: backgroundColor }}
> >
{channelLogo ? ( {channelLogo ? (
<CustomImage <CustomImage

View File

@@ -41,7 +41,6 @@
height: 72px; height: 72px;
position: relative; position: relative;
flex-shrink: 0; flex-shrink: 0;
background-color: transparent;
} }
.logoBackground { .logoBackground {
@@ -52,7 +51,6 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: transparent;
} }
.logoImage { .logoImage {
@@ -62,7 +60,6 @@
&.qvcLogoImg { &.qvcLogoImg {
width: 70%; width: 70%;
height: 70%; height: 70%;
background-color: transparent;
} }
} }

View File

@@ -18,19 +18,11 @@ export default function ShopNowButton({ onClick }) {
}; };
const handleSpotlightDown = (e) => { const handleSpotlightDown = (e) => {
e.stopPropagation();
e.preventDefault();
// tabIndexV2가 2일 때만 CC 버튼으로 내려가기
Spotlight.focus('live-channel-next-button');
};
const handleSpotlightLeft = (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
// tabIndexV2가 2일 때만 CC 버튼으로 내려가기 // tabIndexV2가 2일 때만 CC 버튼으로 내려가기
Spotlight.focus('player-subtitlebutton'); Spotlight.focus('player-subtitlebutton');
}; };
return ( return (
<div className={css.container}> <div className={css.container}>
<SpottableDiv <SpottableDiv
@@ -39,7 +31,6 @@ export default function ShopNowButton({ onClick }) {
spotlightId="below-tab-shop-now-button" spotlightId="below-tab-shop-now-button"
onSpotlightUp={handleSpotlightUp} onSpotlightUp={handleSpotlightUp}
onSpotlightDown={handleSpotlightDown} onSpotlightDown={handleSpotlightDown}
onSpotlightLeft={handleSpotlightLeft}
> >
<span className={css.buttonText}>SHOP NOW</span> <span className={css.buttonText}>SHOP NOW</span>
</SpottableDiv> </SpottableDiv>

View File

@@ -196,7 +196,7 @@ export default function TabContainerV2({
// tabIndex = 2 (ShopNowButton) // tabIndex = 2 (ShopNowButton)
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
Spotlight.focus('below-tab-shop-now-button'); Spotlight.focus('below-tab-shop-now-button');
}, 10); }, 100);
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
} }
}, []); }, []);

View File

@@ -1,11 +1,5 @@
// src/views/SearchPanel/SearchPanel.new.jsx // src/views/SearchPanel/SearchPanel.new.jsx
import React, { import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
@@ -39,9 +33,7 @@ import {
// showWarningToast, // showWarningToast,
// } from '../../actions/toastActions'; // } from '../../actions/toastActions';
import TBody from '../../components/TBody/TBody'; import TBody from '../../components/TBody/TBody';
import TItemCardNew, { import TItemCardNew, { removeDotAndColon } from '../../components/TItemCard/TItemCard.new';
removeDotAndColon,
} from '../../components/TItemCard/TItemCard.new';
import TPanel from '../../components/TPanel/TPanel'; import TPanel from '../../components/TPanel/TPanel';
import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator'; import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator';
import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList'; import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
@@ -49,22 +41,15 @@ import usePanelHistory from '../../hooks/usePanelHistory/usePanelHistory';
// import VirtualKeyboardContainer from "../../components/TToast/VirtualKeyboardContainer"; // import VirtualKeyboardContainer from "../../components/TToast/VirtualKeyboardContainer";
import usePrevious from '../../hooks/usePrevious'; import usePrevious from '../../hooks/usePrevious';
import { useSearchHistory } from '../../hooks/useSearchHistory'; import { useSearchHistory } from '../../hooks/useSearchHistory';
import { import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../utils/Config';
LOG_CONTEXT_NAME,
LOG_MENU,
LOG_MESSAGE_ID,
panel_names,
} from '../../utils/Config';
import { createDebugHelpers } from '../../utils/debug';
import NoSearchResults from './NoSearchResults/NoSearchResults'; import NoSearchResults from './NoSearchResults/NoSearchResults';
// import NoSearchResults from './NoSearchResults/NoSearchResults'; // import NoSearchResults from './NoSearchResults/NoSearchResults';
import SearchInputOverlay from './SearchInputOverlay'; import SearchInputOverlay from './SearchInputOverlay';
import css from './SearchPanel.new.module.less'; import css from './SearchPanel.new.module.less';
import SearchResultsNew from './SearchResults.new.v2'; import SearchResultsNew from './SearchResults.new.v2';
import TInputSimple, { ICONS, KINDS } from './TInput/TInputSimple'; import TInputSimple, { ICONS, KINDS } from './TInput/TInputSimple';
import VoiceInputOverlay, { import VoiceInputOverlay, { VOICE_MODES } from './VoiceInputOverlay/VoiceInputOverlay';
VOICE_MODES, import { createDebugHelpers } from '../../utils/debug';
} from './VoiceInputOverlay/VoiceInputOverlay';
// 디버그 헬퍼 설정 // 디버그 헬퍼 설정
const DEBUG_MODE = false; const DEBUG_MODE = false;
@@ -90,22 +75,13 @@ export const SEARCH_PANEL_MODES = {
VOICE_RESULT: 'voice_result', // 음성 검색 결과 표시 VOICE_RESULT: 'voice_result', // 음성 검색 결과 표시
}; };
const ContainerBasic = SpotlightContainerDecorator( const ContainerBasic = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
{ enterTo: 'last-focused' },
'div'
);
// 검색 입력 영역 컨테이너 // 검색 입력 영역 컨테이너
const InputContainer = SpotlightContainerDecorator( const InputContainer = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
{ enterTo: 'last-focused' },
'div'
);
// 콘텐츠 섹션 컨테이너 // 콘텐츠 섹션 컨테이너
const SectionContainer = SpotlightContainerDecorator( const SectionContainer = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
{ enterTo: 'last-focused' },
'div'
);
// 메모리 누수 방지를 위한 안전한 이미지 컴포넌트 (컴포넌트 외부로 이동) // 메모리 누수 방지를 위한 안전한 이미지 컴포넌트 (컴포넌트 외부로 이동)
const SafeImageComponent = ({ src, alt, className, ...props }) => { const SafeImageComponent = ({ src, alt, className, ...props }) => {
@@ -142,9 +118,7 @@ const SafeImageComponent = ({ src, alt, className, ...props }) => {
}; };
}, []); }, []);
return ( return <img ref={imgRef} src={src} alt={alt} className={className} {...props} />;
<img ref={imgRef} src={src} alt={alt} className={className} {...props} />
);
}; };
const ITEMS_PER_PAGE = 9; const ITEMS_PER_PAGE = 9;
@@ -178,36 +152,22 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 0hun: 패널 전역 상태 // 0hun: 패널 전역 상태
const panels = useSelector((state) => state.panels.panels); const panels = useSelector((state) => state.panels.panels);
// 0hun: 음성 검색 결과에 대한 전역 상태 // 0hun: 음성 검색 결과에 대한 전역 상태
const shopperHouseData = useSelector( const shopperHouseData = useSelector((state) => state.search.shopperHouseData);
(state) => state.search.shopperHouseData const shopperHouseError = useSelector((state) => state.search.shopperHouseError);
);
const shopperHouseError = useSelector(
(state) => state.search.shopperHouseError
);
// 0hun: 음성 검색 searchId (Redux에서 별도 관리) // 0hun: 음성 검색 searchId (Redux에서 별도 관리)
const shopperHouseSearchId = useSelector( const shopperHouseSearchId = useSelector((state) => state.search.shopperHouseSearchId);
(state) => state.search.shopperHouseSearchId
);
// 0hun: 음성 검색 relativeQueries (Redux에서 별도 관리) // 0hun: 음성 검색 relativeQueries (Redux에서 별도 관리)
const shopperHouseRelativeQueries = useSelector( const shopperHouseRelativeQueries = useSelector(
(state) => state.search.shopperHouseRelativeQueries (state) => state.search.shopperHouseRelativeQueries
); );
// 🔄 이전 shopperHouseData (sortingType 변경 시 사용) // 🔄 이전 shopperHouseData (sortingType 변경 시 사용)
const preShopperHouseData = useSelector( const preShopperHouseData = useSelector((state) => state.search.preShopperHouseData);
(state) => state.search.preShopperHouseData
);
// 0hun: 검색 메인, Hot Picks for you 영역에 대한 전역 상태 값 // 0hun: 검색 메인, Hot Picks for you 영역에 대한 전역 상태 값
const hotPicksForYou = useSelector( const hotPicksForYou = useSelector((state) => state.search.searchMainData.hotPicksForYou);
(state) => state.search.searchMainData.hotPicksForYou
);
// 0hun: 검색 메인, Popular Brands 영역에 대한 전역 상태 값 // 0hun: 검색 메인, Popular Brands 영역에 대한 전역 상태 값
const popularBrands = useSelector( const popularBrands = useSelector((state) => state.search.searchMainData.popularBrands);
(state) => state.search.searchMainData.popularBrands
);
// 0hun: 검색 메인, Top Searchs 영역에 대한 전역 상태 값 // 0hun: 검색 메인, Top Searchs 영역에 대한 전역 상태 값
const topSearchs = useSelector( const topSearchs = useSelector((state) => state.search.searchMainData.topSearchs);
(state) => state.search.searchMainData.topSearchs
);
// jhun: 검색 메인, Today Deals 영역에 대한 전역 상태 값 // jhun: 검색 메인, Today Deals 영역에 대한 전역 상태 값
const tsvInfo = useSelector((state) => state.search.searchMainData.tsvInfo); const tsvInfo = useSelector((state) => state.search.searchMainData.tsvInfo);
@@ -217,9 +177,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 0hun: 초기 포커스 유무를 나타내는 Boolean 상태 // 0hun: 초기 포커스 유무를 나타내는 Boolean 상태
const [firstSpot, setFirstSpot] = useState(false); const [firstSpot, setFirstSpot] = useState(false);
// 0hun: 검색어 상태 // 0hun: 검색어 상태
const [searchQuery, setSearchQuery] = useState( const [searchQuery, setSearchQuery] = useState(panelInfo.searchVal ? panelInfo.searchVal : null);
panelInfo.searchVal ? panelInfo.searchVal : null
);
// 0hun: 검색 컨테이너 포커스 position 상태 값 // 0hun: 검색 컨테이너 포커스 position 상태 값
const [position, setPosition] = useState(null); const [position, setPosition] = useState(null);
// 0hun: 가상 키보드 Display 유무 Boolean 값 (주석: 현재 VirtualKeyboardContainer가 비활성화됨) // 0hun: 가상 키보드 Display 유무 Boolean 값 (주석: 현재 VirtualKeyboardContainer가 비활성화됨)
@@ -233,17 +191,14 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// ✨ [Phase 3] TInput의 입력 모드 상태 제거 (더 이상 InputField가 없으므로 불필요) // ✨ [Phase 3] TInput의 입력 모드 상태 제거 (더 이상 InputField가 없으므로 불필요)
// const [isInputModeActive, setIsInputModeActive] = useState(false); // const [isInputModeActive, setIsInputModeActive] = useState(false);
// 0hun: 현재 포커스된 container의 spotlightId를 관리하는 상태 값 // 0hun: 현재 포커스된 container의 spotlightId를 관리하는 상태 값
const [focusedContainerId, setFocusedContainerId] = useState( const [focusedContainerId, setFocusedContainerId] = useState(panelInfo?.focusedContainerId);
panelInfo?.focusedContainerId
);
// ✨ [Phase 1] SearchPanel의 현재 모드 상태 (VoiceInputOverlay의 VOICE_MODES와 동일한 개념) // ✨ [Phase 1] SearchPanel의 현재 모드 상태 (VoiceInputOverlay의 VOICE_MODES와 동일한 개념)
const [currentMode, setCurrentMode] = useState(SEARCH_PANEL_MODES.INITIAL); const [currentMode, setCurrentMode] = useState(SEARCH_PANEL_MODES.INITIAL);
const [isShopperHousePending, setIsShopperHousePending] = useState(false); const [isShopperHousePending, setIsShopperHousePending] = useState(false);
const [voiceOverlayMode, setVoiceOverlayMode] = useState(VOICE_MODES.PROMPT); const [voiceOverlayMode, setVoiceOverlayMode] = useState(VOICE_MODES.PROMPT);
const [voiceOverlayResponseText, setVoiceOverlayResponseText] = useState(''); const [voiceOverlayResponseText, setVoiceOverlayResponseText] = useState('');
const [isVoiceOverlayBubbleSearch, setIsVoiceOverlayBubbleSearch] = const [isVoiceOverlayBubbleSearch, setIsVoiceOverlayBubbleSearch] = useState(false);
useState(false);
const [shouldFocusVoiceResult, setShouldFocusVoiceResult] = useState(false); const [shouldFocusVoiceResult, setShouldFocusVoiceResult] = useState(false);
// 🎯 HowAboutThese 포커스 관리 - 검색 입력 영역 포커스 감지용 상태 // 🎯 HowAboutThese 포커스 관리 - 검색 입력 영역 포커스 감지용 상태
@@ -330,16 +285,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
const unifiedFocusTimerRef = useRef(null); const unifiedFocusTimerRef = useRef(null);
// ShopperHouse 에러 팝업 상태 가져오기 // ShopperHouse 에러 팝업 상태 가져오기
const shopperHouseErrorPopup = useSelector( const shopperHouseErrorPopup = useSelector((state) => state.search.shopperHouseErrorPopup);
(state) => state.search.shopperHouseErrorPopup
);
// API 실패 시 fallback reference 초기화 // API 실패 시 fallback reference 초기화
useEffect(() => { useEffect(() => {
if ( if (shopperHouseErrorPopup?.visible && shopperHouseErrorPopup?.type === 'API_FAILURE') {
shopperHouseErrorPopup?.visible &&
shopperHouseErrorPopup?.type === 'API_FAILURE'
) {
dlog('[SearchPanel] 🧹 API 실패 감지 - fallbackShopperHouseData 초기화'); dlog('[SearchPanel] 🧹 API 실패 감지 - fallbackShopperHouseData 초기화');
shopperHouseDataRef.current = null; shopperHouseDataRef.current = null;
} }
@@ -357,12 +307,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
/** /**
* useSearchHistory Hook 적용 * useSearchHistory Hook 적용
*/ */
const { const { normalSearches, addNormalSearch, refreshHistory, executeSearchFromHistory } =
normalSearches, useSearchHistory();
addNormalSearch,
refreshHistory,
executeSearchFromHistory,
} = useSearchHistory();
/** /**
* 🎯 [DetailPanel 복귀 감지] usePanelHistory Hook 적용 * 🎯 [DetailPanel 복귀 감지] usePanelHistory Hook 적용
@@ -476,9 +422,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// ✨ [Phase 4] Enter/OK 키 처리 - SearchInputOverlay 표시 // ✨ [Phase 4] Enter/OK 키 처리 - SearchInputOverlay 표시
if (e.key === 'Enter' || e.keyCode === 13) { if (e.key === 'Enter' || e.keyCode === 13) {
dlog( dlog('[DEBUG] [SearchPanel] TInputSimple에서 Enter/OK 키 감지 → SearchInputOverlay 오픈');
'[DEBUG] [SearchPanel] TInputSimple에서 Enter/OK 키 감지 → SearchInputOverlay 오픈'
);
e.preventDefault(); e.preventDefault();
// ✨ [Phase 6] SearchInputOverlay 오픈 후 자동으로 입력 준비 완료 // ✨ [Phase 6] SearchInputOverlay 오픈 후 자동으로 입력 준비 완료
@@ -506,11 +450,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
]; ];
if (arrowKeys.includes(e.key)) { if (arrowKeys.includes(e.key)) {
// 입력 필드가 비어있고 왼쪽 화살표인 경우에만 방지 // 입력 필드가 비어있고 왼쪽 화살표인 경우에만 방지
if ( if (position === 0 && (e.key === 'Left' || e.key === 'ArrowLeft') && !searchQuery) {
position === 0 &&
(e.key === 'Left' || e.key === 'ArrowLeft') &&
!searchQuery
) {
e.preventDefault(); e.preventDefault();
return; return;
} }
@@ -521,9 +461,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// DOM 쿼리 최적화: 캐싱된 input element 사용 // DOM 쿼리 최적화: 캐싱된 input element 사용
const input = const input =
inputElementRef.current || inputElementRef.current ||
document.querySelector( document.querySelector(`[data-spotlight-id="input-field-box"] > input`);
`[data-spotlight-id="input-field-box"] > input`
);
if (input) { if (input) {
inputElementRef.current = input; // 캐싱 inputElementRef.current = input; // 캐싱
if (position === input.value.length) { if (position === input.value.length) {
@@ -720,9 +658,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
'[DEBUG]-VOICE_RESULT: Clearing ShopperHouse data (searchId will be preserved for 2nd search)' '[DEBUG]-VOICE_RESULT: Clearing ShopperHouse data (searchId will be preserved for 2nd search)'
); );
dlog('[VoiceInput]-SearchPanel-onCancel-VOICE_RESULT'); dlog('[VoiceInput]-SearchPanel-onCancel-VOICE_RESULT');
dlog( dlog('[VoiceInput] 🧹 VOICE_RESULT 모드에서 ESC 누름 - clearShopperHouseData 호출');
'[VoiceInput] 🧹 VOICE_RESULT 모드에서 ESC 누름 - clearShopperHouseData 호출'
);
} }
// 🎯 [포커스 로직 통합] 포커스는 상태 변경에 의해 자동으로 처리됨 // 🎯 [포커스 로직 통합] 포커스는 상태 변경에 의해 자동으로 처리됨
setIsShopperHousePending(false); setIsShopperHousePending(false);
@@ -880,9 +816,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
if (isReturningFromDetailPanel) { if (isReturningFromDetailPanel) {
const currentSpot = currentPanel?.panelInfo?.currentSpot; const currentSpot = currentPanel?.panelInfo?.currentSpot;
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog( dlog('[Focus] usePanelHistory로 DetailPanel 복귀 감지 - 이전 상품으로 포커스 이동');
'[Focus] usePanelHistory로 DetailPanel 복귀 감지 - 이전 상품으로 포커스 이동'
);
dlog('[FOCUS] 🎯 Scenario: DETAIL_PANEL_RETURN (usePanelHistory)', { dlog('[FOCUS] 🎯 Scenario: DETAIL_PANEL_RETURN (usePanelHistory)', {
currentSpot, currentSpot,
mode: currentMode, mode: currentMode,
@@ -926,9 +860,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
) { ) {
const usedHistoryOnTop = currentIsOnTop && isOnTopChange?.becameOnTop; const usedHistoryOnTop = currentIsOnTop && isOnTopChange?.becameOnTop;
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog( dlog('[Focus] 개선된 방식으로 DetailPanel 복귀 감지 - 이전 상품으로 포커스 이동');
'[Focus] 개선된 방식으로 DetailPanel 복귀 감지 - 이전 상품으로 포커스 이동'
);
dlog('[FOCUS] 🎯 Scenario: DETAIL_PANEL_RETURN (improved fallback)', { dlog('[FOCUS] 🎯 Scenario: DETAIL_PANEL_RETURN (improved fallback)', {
currentSpot: panelInfo.currentSpot, currentSpot: panelInfo.currentSpot,
mode: currentMode, mode: currentMode,
@@ -996,8 +928,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
prevMode: currentModeRef.current, prevMode: currentModeRef.current,
nextMode: currentMode, nextMode: currentMode,
isOnTopChanged: isOnTop !== isOnTopRef.current, isOnTopChanged: isOnTop !== isOnTopRef.current,
modeChanged: modeChanged: currentModeRef.current !== SEARCH_PANEL_MODES.VOICE_RESULT,
currentModeRef.current !== SEARCH_PANEL_MODES.VOICE_RESULT,
dataChanged: shopperHouseDataRef.current !== shopperHouseData, dataChanged: shopperHouseDataRef.current !== shopperHouseData,
}); });
} }
@@ -1015,9 +946,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 이렇게 하면 VOICE_OVERLAY_CLOSED 시나리오에서 TInput으로 가는 것을 방지 // 이렇게 하면 VOICE_OVERLAY_CLOSED 시나리오에서 TInput으로 가는 것을 방지
if (shopperHouseData && currentMode === SEARCH_PANEL_MODES.VOICE_RESULT) { if (shopperHouseData && currentMode === SEARCH_PANEL_MODES.VOICE_RESULT) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog( dlog('[FOCUS] 🔄 VOICE_OVERLAY_CLOSED + new data → NEW_SEARCH_LOADED 우선 처리');
'[FOCUS] 🔄 VOICE_OVERLAY_CLOSED + new data → NEW_SEARCH_LOADED 우선 처리'
);
} }
return 'NEW_SEARCH_LOADED'; return 'NEW_SEARCH_LOADED';
} }
@@ -1080,10 +1009,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
let currentSpot = null; let currentSpot = null;
// 1. usePanelHistory의 currentSpot 우선 사용 // 1. usePanelHistory의 currentSpot 우선 사용
if ( if (isReturningFromDetailPanel && currentPanel?.panelInfo?.currentSpot) {
isReturningFromDetailPanel &&
currentPanel?.panelInfo?.currentSpot
) {
currentSpot = currentPanel.panelInfo.currentSpot; currentSpot = currentPanel.panelInfo.currentSpot;
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog('[FOCUS] 🎯 usePanelHistory currentSpot 사용:', currentSpot); dlog('[FOCUS] 🎯 usePanelHistory currentSpot 사용:', currentSpot);
@@ -1093,19 +1019,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
else if (panelInfo?.currentSpot) { else if (panelInfo?.currentSpot) {
currentSpot = panelInfo.currentSpot; currentSpot = panelInfo.currentSpot;
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog( dlog('[FOCUS] 🔄 fallback으로 panelInfo.currentSpot 사용:', currentSpot);
'[FOCUS] 🔄 fallback으로 panelInfo.currentSpot 사용:',
currentSpot
);
} }
} }
if (currentSpot && currentSpot.startsWith('searchItemContents')) { if (currentSpot && currentSpot.startsWith('searchItemContents')) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog( dlog('[FOCUS] 🎯 DETAIL_PANEL_RETURN: 이전 상품으로 포커스 복원:', currentSpot);
'[FOCUS] 🎯 DETAIL_PANEL_RETURN: 이전 상품으로 포커스 복원:',
currentSpot
);
} }
return currentSpot; return currentSpot;
} else { } else {
@@ -1146,9 +1066,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// SearchInputOverlay에서 검색을 실행하면 isSearchOverlayVisible이 false로 설정되고 // SearchInputOverlay에서 검색을 실행하면 isSearchOverlayVisible이 false로 설정되고
// 동시에 검색 결과에 따라 모드가 변경되므로, 이 케이스는 검색어 선택 후 닫을 때만 발생 // 동시에 검색 결과에 따라 모드가 변경되므로, 이 케이스는 검색어 선택 후 닫을 때만 발생
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog( dlog('[FOCUS] 🎯 Scenario: SEARCH_OVERLAY_CLOSED - TInputSimple으로 포커스');
'[FOCUS] 🎯 Scenario: SEARCH_OVERLAY_CLOSED - TInputSimple으로 포커스'
);
} }
return SPOTLIGHT_IDS.SEARCH_INPUT_BOX; return SPOTLIGHT_IDS.SEARCH_INPUT_BOX;
@@ -1372,27 +1290,17 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
{...rest} {...rest}
> >
<div className={css.productImageWrapper}> <div className={css.productImageWrapper}>
<SafeImage <SafeImage src={bgImgPath} alt={curationNm} className={css.productImage} />
src={bgImgPath}
alt={curationNm}
className={css.productImage}
/>
</div> </div>
<div className={css.productInfo}> <div className={css.productInfo}>
{showBrandLogo && ( {showBrandLogo && (
<div className={css.productBrandWrapper}> <div className={css.productBrandWrapper}>
<SafeImage <SafeImage src={patncLogoPath} alt={patncNm} className={css.brandLogo} />
src={patncLogoPath}
alt={patncNm}
className={css.brandLogo}
/>
</div> </div>
)} )}
<div className={css.productDetails}> <div className={css.productDetails}>
{showBrandName && <div className={css.brandName}>{patncNm}</div>} {showBrandName && <div className={css.brandName}>{patncNm}</div>}
{showProductTitle && ( {showProductTitle && <div className={css.productTitle}>{curationNm}</div>}
<div className={css.productTitle}>{curationNm}</div>
)}
</div> </div>
</div> </div>
</SpottableProduct> </SpottableProduct>
@@ -1415,8 +1323,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
const renderTsvItem = useCallback( const renderTsvItem = useCallback(
({ index, ...rest }) => { ({ index, ...rest }) => {
const { offerInfo, prdtId, imgUrl, patnrId, prdtNm, priceInfo } = const { offerInfo, prdtId, imgUrl, patnrId, prdtNm, priceInfo } = tsvInfo[index];
tsvInfo[index];
return ( return (
<TItemCardNew <TItemCardNew
@@ -1429,9 +1336,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
priceInfo={priceInfo} priceInfo={priceInfo}
productId={prdtId} productId={prdtId}
productName={prdtNm} productName={prdtNm}
spotlightId={ spotlightId={'searchMain-tsvInfo-spotlightId-' + removeDotAndColon(prdtId)}
'searchMain-tsvInfo-spotlightId-' + removeDotAndColon(prdtId)
}
{...rest} {...rest}
/> />
); );
@@ -1698,13 +1603,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 우선순위 2: 음성 검색 결과가 있으면 VOICE_RESULT 모드 // 우선순위 2: 음성 검색 결과가 있으면 VOICE_RESULT 모드
else if (shopperHouseData || isShopperHousePending) { else if (shopperHouseData || isShopperHousePending) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog( dlog('[DEBUG]-MODE: shopperHouseData EXISTS or pending VOICE_RESULT', {
'[DEBUG]-MODE: shopperHouseData EXISTS or pending VOICE_RESULT',
{
hasData: !!shopperHouseData, hasData: !!shopperHouseData,
isPending: isShopperHousePending, isPending: isShopperHousePending,
} });
);
} }
nextMode = SEARCH_PANEL_MODES.VOICE_RESULT; nextMode = SEARCH_PANEL_MODES.VOICE_RESULT;
} }
@@ -1738,9 +1640,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 모드가 변경되었을 때만 업데이트 // 모드가 변경되었을 때만 업데이트
if (nextMode !== currentMode) { if (nextMode !== currentMode) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog( dlog(`[DEBUG]-VOICE_RESULT 🔀 Mode changed: ${currentMode} → ${nextMode}`, {
`[DEBUG]-VOICE_RESULT 🔀 Mode changed: ${currentMode} → ${nextMode}`,
{
isVoiceOverlayVisible, isVoiceOverlayVisible,
shopperHouseData: !!shopperHouseData, shopperHouseData: !!shopperHouseData,
isShopperHousePending, isShopperHousePending,
@@ -1753,8 +1653,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
), ),
isSearchOverlayVisible, isSearchOverlayVisible,
inputFocus, inputFocus,
} });
);
} }
setCurrentMode(nextMode); setCurrentMode(nextMode);
} else { } else {
@@ -1880,14 +1779,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// NEW_SEARCH_LOADED: 음성 검색 결과 로드 시 VoiceInputOverlay와 충돌 방지 // NEW_SEARCH_LOADED: 음성 검색 결과 로드 시 VoiceInputOverlay와 충돌 방지
// 다른 시나리오에서는 기존과 같은 지연 시간 (100ms) // 다른 시나리오에서는 기존과 같은 지연 시간 (100ms)
const focusDelay = const focusDelay =
scenario === 'DETAIL_PANEL_RETURN' || scenario === 'NEW_SEARCH_LOADED' scenario === 'DETAIL_PANEL_RETURN' || scenario === 'NEW_SEARCH_LOADED' ? 50 : 100;
? 50
: 100;
unifiedFocusTimerRef.current = setTimeout(() => { unifiedFocusTimerRef.current = setTimeout(() => {
const targetElement = document.querySelector( const targetElement = document.querySelector(`[data-spotlight-id="${targetId}"]`);
`[data-spotlight-id="${targetId}"]`
);
if (targetElement || targetId === SPOTLIGHT_IDS.SEARCH_INPUT_BOX) { if (targetElement || targetId === SPOTLIGHT_IDS.SEARCH_INPUT_BOX) {
Spotlight.focus(targetId); Spotlight.focus(targetId);
@@ -1910,14 +1805,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
} }
// 🎯 DETAIL_PANEL_RETURN에서 요소를 찾지 못하면 fallback으로 첫 번째 상품 시도 // 🎯 DETAIL_PANEL_RETURN에서 요소를 찾지 못하면 fallback으로 첫 번째 상품 시도
if ( if (scenario === 'DETAIL_PANEL_RETURN' && targetId.startsWith('searchItemContents')) {
scenario === 'DETAIL_PANEL_RETURN' &&
targetId.startsWith('searchItemContents')
) {
const fallbackTarget = 'searchItemContents0'; const fallbackTarget = 'searchItemContents0';
const fallbackElement = document.querySelector( const fallbackElement = document.querySelector(`[data-spotlight-id="${fallbackTarget}"]`);
`[data-spotlight-id="${fallbackTarget}"]`
);
if (fallbackElement) { if (fallbackElement) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog( dlog(
@@ -1935,15 +1825,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 🎯 [NEW_SEARCH_LOADED] 1초 후 다시 첫 번째 아이템으로 포커스 이동 // 🎯 [NEW_SEARCH_LOADED] 1초 후 다시 첫 번째 아이템으로 포커스 이동
// TInputSimple과 Mic Icon의 포커스 충돌 해결을 위해 // TInputSimple과 Mic Icon의 포커스 충돌 해결을 위해
if ( if (scenario === 'NEW_SEARCH_LOADED' && targetId === 'searchItemContents0') {
scenario === 'NEW_SEARCH_LOADED' &&
targetId === 'searchItemContents0'
) {
setTimeout(() => { setTimeout(() => {
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog( dlog('[FOCUS] 🔄 NEW_SEARCH_LOADED: 1 번째 상품으로 다시 포커스 이동');
'[FOCUS] 🔄 NEW_SEARCH_LOADED: 1 번째 상품으로 다시 포커스 이동'
);
} }
Spotlight.focus('searchItemContents0'); Spotlight.focus('searchItemContents0');
}, 500); // 0.5초 후 }, 500); // 0.5초 후
@@ -1981,13 +1866,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
isVoiceOverlayVisibleRef.current = isVoiceOverlayVisible; isVoiceOverlayVisibleRef.current = isVoiceOverlayVisible;
isSearchOverlayVisibleRef.current = isSearchOverlayVisible; isSearchOverlayVisibleRef.current = isSearchOverlayVisible;
currentModeRef.current = currentMode; currentModeRef.current = currentMode;
}, [ }, [shopperHouseData, searchDatas, isVoiceOverlayVisible, isSearchOverlayVisible, currentMode]);
shopperHouseData,
searchDatas,
isVoiceOverlayVisible,
isSearchOverlayVisible,
currentMode,
]);
/** /**
* 🎯 SearchInputOverlay 닫힘 후 포커스 관리 * 🎯 SearchInputOverlay 닫힘 후 포커스 관리
@@ -2010,13 +1889,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}); });
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog( dlog('[FOCUS] 🎯 SearchInputOverlay 닫힘 포커스 관리 useEffect 실행', {
'[FOCUS] 🎯 SearchInputOverlay 닫힘 포커스 관리 useEffect 실행',
{
shouldFocusSearchInput, shouldFocusSearchInput,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
} });
);
} }
// 500ms 후 TInputSimple에 포커스 이동 // 500ms 후 TInputSimple에 포커스 이동
@@ -2027,13 +1903,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}); });
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog( dlog('[FOCUS] 500ms 타이머 콜백 실행 - TInputSimple으로 포커스 이동', {
'[FOCUS] 500ms 타이머 콜백 실행 - TInputSimple으로 포커스 이동',
{
targetId: SPOTLIGHT_IDS.SEARCH_INPUT_BOX, targetId: SPOTLIGHT_IDS.SEARCH_INPUT_BOX,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
} });
);
} }
dlog('[DEBUG] Spotlight.focus() 호출 직전', { dlog('[DEBUG] Spotlight.focus() 호출 직전', {
@@ -2077,12 +1950,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}); });
if (focusTimer) { if (focusTimer) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog( dlog('[FOCUS] 🧹 SearchInputOverlay 포커스 관리 useEffect cleanup - 타이머 정리', {
'[FOCUS] 🧹 SearchInputOverlay 포커스 관리 useEffect cleanup - 타이머 정리',
{
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
} });
);
} }
clearTimeout(focusTimer); clearTimeout(focusTimer);
} }
@@ -2183,18 +2053,12 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}, [currentMode, isOnTop, refreshHistory]); }, [currentMode, isOnTop, refreshHistory]);
return ( return (
<TPanel <TPanel className={css.container} handleCancel={onCancel} spotlightId={spotlightId}>
className={css.container}
handleCancel={onCancel}
spotlightId={spotlightId}
>
{/* ✨ [Phase 2] spotlightDisabled를 currentMode로 제어 */} {/* ✨ [Phase 2] spotlightDisabled를 currentMode로 제어 */}
<TBody <TBody
className={css.tBody} className={css.tBody}
scrollable scrollable
spotlightDisabled={ spotlightDisabled={!isOnTop || currentMode === SEARCH_PANEL_MODES.SEARCH_INPUT}
!isOnTop || currentMode === SEARCH_PANEL_MODES.SEARCH_INPUT
}
> >
<ContainerBasic> <ContainerBasic>
{isOnTop && ( {isOnTop && (
@@ -2214,8 +2078,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
className={classNames( className={classNames(
css.inputContainer, css.inputContainer,
inputFocus === true && css.inputFocus, inputFocus === true && css.inputFocus,
searchDatas && searchDatas && css.searchValue /* 이건 결과값 있을때만. 조건 추가필요 */,
css.searchValue /* 이건 결과값 있을때만. 조건 추가필요 */,
(currentMode === SEARCH_PANEL_MODES.VOICE_INPUT || (currentMode === SEARCH_PANEL_MODES.VOICE_INPUT ||
currentMode === SEARCH_PANEL_MODES.INPUT_FOCUSED) && currentMode === SEARCH_PANEL_MODES.INPUT_FOCUSED) &&
css.hidden css.hidden
@@ -2266,7 +2129,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
placeholder="Search products or brands" placeholder="Search products or brands"
/> />
</div> </div>
{/* <SpottableMicButton <SpottableMicButton
className={css.microphoneButton} className={css.microphoneButton}
onClick={onClickMic} onClick={onClickMic}
onFocus={onFocusMic} onFocus={onFocusMic}
@@ -2285,7 +2148,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
<div className={css.microphoneCircle}> <div className={css.microphoneCircle}>
<SafeImage src={micIcon} alt="Microphone" className={css.microphoneIcon} /> <SafeImage src={micIcon} alt="Microphone" className={css.microphoneIcon} />
</div> </div>
</SpottableMicButton> */} </SpottableMicButton>
</div> </div>
</InputContainer> </InputContainer>

View File

@@ -291,7 +291,7 @@ const TrendingNowPanel = ({ panelInfo, spotlightId, isOnTop, ...rest }) => {
return ( return (
<div className={css.trendingNowWrap}> <div className={css.trendingNowWrap}>
{isOnTop && selectedIndex >= 1 && showButton && ( {selectedIndex >= 1 && showButton && (
<TButton <TButton
className={classNames(css.button, css.prevBtn)} className={classNames(css.button, css.prevBtn)}
onClick={handleIndicatorClick("prev")} onClick={handleIndicatorClick("prev")}
@@ -411,7 +411,7 @@ const TrendingNowPanel = ({ panelInfo, spotlightId, isOnTop, ...rest }) => {
</TVerticalPagenator> </TVerticalPagenator>
</TBody> </TBody>
</TPanel> </TPanel>
{isOnTop && topInfos && {topInfos &&
topInfos?.length > 0 && topInfos?.length > 0 &&
selectedIndex !== topInfos?.length - 1 && selectedIndex !== topInfos?.length - 1 &&
showButton && ( showButton && (