[251115] fix: MediaPanel.v3.jsx 비디오재생-1
🕐 커밋 시간: 2025. 11. 15. 12:22:59 📊 변경 통계: • 총 파일: 12개 • 추가: +246줄 • 삭제: -27줄 📁 추가된 파일: + com.twin.app.shoptime/src/utils/focusPanelGuide.js 📝 수정된 파일: ~ com.twin.app.shoptime/src/actions/actionTypes.js ~ com.twin.app.shoptime/src/actions/mediaActions.js ~ com.twin.app.shoptime/src/actions/panelActions.js ~ com.twin.app.shoptime/src/reducers/panelReducer.js ~ com.twin.app.shoptime/src/utils/SpotlightIds.js ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v3.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/YouMayAlsoLike.jsx ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/actions/panelActions.js (javascript): ✅ Added: resetPanels() 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx (javascript): 🔄 Modified: extractProductMeta() 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v3.jsx (javascript): 🔄 Modified: Spottable() 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/YouMayAlsoLike.jsx (javascript): 🔄 Modified: SpotlightContainerDecorator() 📄 com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx (javascript): 🔄 Modified: normalizeModalStyle() 📄 com.twin.app.shoptime/src/utils/focusPanelGuide.js (javascript): ✅ Added: DetailPanel(), handleProductSelect() 🔧 주요 변경 내용: • 타입 시스템 안정성 강화 • 핵심 비즈니스 로직 개선 • 공통 유틸리티 함수 최적화
This commit is contained in:
@@ -6,6 +6,7 @@ export const types = {
|
||||
POP_PANEL: 'POP_PANEL',
|
||||
UPDATE_PANEL: 'UPDATE_PANEL',
|
||||
RESET_PANELS: 'RESET_PANELS',
|
||||
FOCUS_PANEL: 'FOCUS_PANEL', // 🔽 [251114] 명시적 포커스 이동
|
||||
|
||||
// 🔽 [신규] panel history actions
|
||||
ENQUEUE_PANEL_HISTORY: 'ENQUEUE_PANEL_HISTORY',
|
||||
@@ -82,12 +83,12 @@ export const types = {
|
||||
CLEAR_CART: 'CLEAR_CART',
|
||||
//cart api action
|
||||
GET_MY_INFO_CART_SEARCH: 'GET_MY_INFO_CART_SEARCH',
|
||||
INSERT_MY_INFO_CART : "INSERT_MY_INFO_CART",
|
||||
DELETE_MY_INFO_CART : "DELETE_MY_INFO_CART",
|
||||
DELETE_ALL_MY_INFO_CART : "DELETE_ALL_MY_INFO_CART",
|
||||
UPDATE_MY_INFO_CART : "UPDATE_MY_INFO_CART",
|
||||
INSERT_MY_INFO_CART: 'INSERT_MY_INFO_CART',
|
||||
DELETE_MY_INFO_CART: 'DELETE_MY_INFO_CART',
|
||||
DELETE_ALL_MY_INFO_CART: 'DELETE_ALL_MY_INFO_CART',
|
||||
UPDATE_MY_INFO_CART: 'UPDATE_MY_INFO_CART',
|
||||
//cart checkbox toggle action
|
||||
TOGGLE_CHECK_CART : "TOGGLE_CHECK_CART",
|
||||
TOGGLE_CHECK_CART: 'TOGGLE_CHECK_CART',
|
||||
|
||||
// appData actions
|
||||
ADD_MAIN_INDEX: 'ADD_MAIN_INDEX',
|
||||
|
||||
@@ -23,15 +23,16 @@ export const startMediaPlayer =
|
||||
const topPanel = panels[panels.length - 1];
|
||||
let panelWorkingAction = pushPanel;
|
||||
|
||||
console.log('[startMediaPlayer] ========== Called ==========');
|
||||
console.log('[startMediaPlayer] Current panels:', JSON.stringify(panels, null, 2));
|
||||
console.log('[startMediaPlayer] topPanel:', JSON.stringify(topPanel, null, 2));
|
||||
console.log('[startMediaPlayer]-LoadingVideo 🚀 시작:', {
|
||||
showUrl: rest?.showUrl?.substring(0, 50),
|
||||
showNm: rest?.showNm,
|
||||
prdtId: rest?.prdtId,
|
||||
modal,
|
||||
modalContainerId,
|
||||
});
|
||||
|
||||
if (topPanel && topPanel.name === panel_names.MEDIA_PANEL) {
|
||||
panelWorkingAction = updatePanel;
|
||||
console.log('[startMediaPlayer] Using updatePanel (existing MediaPanel)');
|
||||
} else {
|
||||
console.log('[startMediaPanel] Using pushPanel (new MediaPanel)');
|
||||
}
|
||||
|
||||
const allParams = {
|
||||
@@ -42,8 +43,6 @@ export const startMediaPlayer =
|
||||
...rest,
|
||||
};
|
||||
|
||||
console.log('[startMediaPlayer] All parameters:', JSON.stringify(allParams, null, 2));
|
||||
|
||||
dispatch(
|
||||
panelWorkingAction(
|
||||
{
|
||||
@@ -54,7 +53,7 @@ export const startMediaPlayer =
|
||||
)
|
||||
);
|
||||
|
||||
console.log('[startMediaPlayer] Panel action dispatched');
|
||||
console.log('[startMediaPlayer]-LoadingVideo ✅ MediaPanel dispatch 완료');
|
||||
|
||||
if (modal && modalContainerId && !spotlightDisable) {
|
||||
Spotlight.setPointerMode(false);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { types } from "./actionTypes";
|
||||
import { types } from './actionTypes';
|
||||
import Spotlight from '@enact/spotlight';
|
||||
|
||||
/*
|
||||
name: panel_names.PLAYER_PANEL,
|
||||
@@ -27,3 +28,85 @@ export const resetPanels = (panels) => ({
|
||||
type: types.RESET_PANELS,
|
||||
payload: panels,
|
||||
});
|
||||
|
||||
/**
|
||||
* [251114] 명시적 포커스 이동
|
||||
* Panel의 비동기 작업(useEffect, 타이머 등)이 포커스를 탈취하는 것을 방지
|
||||
* @param {string} panelName - 대상 Panel 이름
|
||||
* @param {string} focusTarget - 포커스할 요소 ID
|
||||
* @returns {Function} Redux thunk
|
||||
*/
|
||||
export const focusPanel = (panelName, focusTarget) => {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const panels = state.panels.panels;
|
||||
|
||||
console.log('[focusPanel] 포커스 이동 시도', {
|
||||
panelName,
|
||||
focusTarget,
|
||||
currentPanels: panels.map((p) => p.name),
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// 안전성 체크 1: Panel이 존재하고 최상단 또는 그 아래에 있는가?
|
||||
const targetPanelIndex = panels.findIndex((p) => p.name === panelName);
|
||||
const targetPanel = panels[targetPanelIndex];
|
||||
const topPanel = panels[panels.length - 1];
|
||||
|
||||
if (!targetPanel) {
|
||||
console.warn(`[focusPanel] ❌ Panel을 찾을 수 없음: ${panelName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Panel이 최상단 또는 그 아래 레이어에 있는지 확인
|
||||
// MediaPanel(최상단) 위에 다른 Modal이 있는 경우는 허용하지 않음
|
||||
const panelsAboveTarget = panels.slice(targetPanelIndex + 1);
|
||||
const hasBlockingModalAbove = panelsAboveTarget.some(
|
||||
(panel) => panel?.panelInfo?.modal === true && panel.name !== panelName
|
||||
);
|
||||
|
||||
if (hasBlockingModalAbove) {
|
||||
const blockingModal = panelsAboveTarget.find((panel) => panel?.panelInfo?.modal === true);
|
||||
console.warn(
|
||||
`[focusPanel] ⚠️ 상위에 Modal이 있음. ` +
|
||||
`${panelName}(${targetPanelIndex}층)에 포커스할 수 없음. ` +
|
||||
`상단 Modal: ${blockingModal?.name}(${panelsAboveTarget.indexOf(blockingModal) + targetPanelIndex + 1}층)`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[focusPanel] ✅ Panel 위치 확인: ${panelName}(${targetPanelIndex}층), ` +
|
||||
`전체 Panel: ${panels.length}층`
|
||||
);
|
||||
|
||||
// 포커스 이동
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(focusTarget);
|
||||
|
||||
if (!element) {
|
||||
console.warn(`[focusPanel] ❌ 요소를 찾을 수 없음: ${focusTarget}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.offsetParent === null) {
|
||||
console.warn(`[focusPanel] ⚠️ 요소가 숨겨져있음: ${focusTarget}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ 포커스 이동
|
||||
Spotlight.focus(focusTarget);
|
||||
console.log(`[focusPanel] ✅ 포커스 이동 성공: ${panelName} → ${focusTarget}`);
|
||||
|
||||
// Reducer에 반영
|
||||
dispatch({
|
||||
type: types.FOCUS_PANEL,
|
||||
payload: {
|
||||
panelName,
|
||||
focusTarget,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ const initialState = {
|
||||
// 기존 상태 - 완전히 호환됨
|
||||
panels: [],
|
||||
lastPanelAction: '', //"", "push", "pop", "update", "reset", "previewPush", "previewPop", "previewUpdate"
|
||||
lastFocusTarget: null, // [251114] 마지막 포커스 대상 (panelName + elementId)
|
||||
|
||||
// [251106] 패널 액션 큐 관련 상태 - 기존 기능에 전혀 영향 없음
|
||||
panelActionQueue: [], // 처리 대기 중인 패널 액션 큐
|
||||
@@ -13,7 +14,7 @@ const initialState = {
|
||||
queueStats: {
|
||||
totalProcessed: 0, // 총 처리된 액션 수
|
||||
failedCount: 0, // 실패한 액션 수
|
||||
averageProcessingTime: 0 // 평균 처리 시간
|
||||
averageProcessingTime: 0, // 평균 처리 시간
|
||||
},
|
||||
|
||||
// [251106] 비동기 액션 관련 상태
|
||||
@@ -30,7 +31,7 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
case types.PUSH_PANEL: {
|
||||
console.log('[panelReducer] 🔵 PUSH_PANEL START', {
|
||||
newPanelName: action.payload.name,
|
||||
currentPanels: state.panels.map(p => p.name),
|
||||
currentPanels: state.panels.map((p) => p.name),
|
||||
duplicatable: action.duplicatable,
|
||||
});
|
||||
|
||||
@@ -76,7 +77,7 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
}
|
||||
|
||||
console.log('[panelReducer] 🔵 PUSH_PANEL END', {
|
||||
resultPanels: newState.map(p => p.name),
|
||||
resultPanels: newState.map((p) => p.name),
|
||||
lastAction,
|
||||
});
|
||||
|
||||
@@ -90,7 +91,7 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
case types.POP_PANEL: {
|
||||
console.log('[panelReducer] 🔴 POP_PANEL START', {
|
||||
targetPanel: action.payload || 'last_panel',
|
||||
currentPanels: state.panels.map(p => p.name),
|
||||
currentPanels: state.panels.map((p) => p.name),
|
||||
});
|
||||
|
||||
let lastAction = 'pop';
|
||||
@@ -113,7 +114,7 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
}
|
||||
|
||||
console.log('[panelReducer] 🔴 POP_PANEL END', {
|
||||
resultPanels: resultPanels.map(p => p.name),
|
||||
resultPanels: resultPanels.map((p) => p.name),
|
||||
lastAction,
|
||||
});
|
||||
|
||||
@@ -159,7 +160,7 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
}
|
||||
case types.RESET_PANELS: {
|
||||
console.log('[panelReducer] 🟢 RESET_PANELS START', {
|
||||
currentPanels: state.panels.map(p => p.name),
|
||||
currentPanels: state.panels.map((p) => p.name),
|
||||
payloadProvided: !!action.payload,
|
||||
});
|
||||
|
||||
@@ -171,7 +172,7 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
: [];
|
||||
|
||||
console.log('[panelReducer] 🟢 RESET_PANELS END', {
|
||||
resultPanels: updatedPanels.map(p => p.name),
|
||||
resultPanels: updatedPanels.map((p) => p.name),
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -267,7 +268,8 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
const processingTime = Date.now() - startTime;
|
||||
const newTotalProcessed = state.queueStats.totalProcessed + 1;
|
||||
const newAverageTime =
|
||||
(state.queueStats.averageProcessingTime * state.queueStats.totalProcessed + processingTime) /
|
||||
(state.queueStats.averageProcessingTime * state.queueStats.totalProcessed +
|
||||
processingTime) /
|
||||
newTotalProcessed;
|
||||
|
||||
console.log('[panelReducer] ✅ QUEUE_ITEM_PROCESSED', {
|
||||
@@ -352,7 +354,7 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
...action.payload,
|
||||
status: 'running',
|
||||
startTime: Date.now(),
|
||||
}
|
||||
},
|
||||
},
|
||||
queueError: null, // 에러 초기화
|
||||
};
|
||||
@@ -381,16 +383,19 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
result: action.payload.result,
|
||||
executionTime,
|
||||
completedAt: action.payload.timestamp,
|
||||
}
|
||||
},
|
||||
].slice(-100), // 최근 100개만 유지
|
||||
queueError: null,
|
||||
queueStats: {
|
||||
...state.queueStats,
|
||||
totalProcessed: state.queueStats.totalProcessed + 1,
|
||||
averageProcessingTime: Math.round(
|
||||
((state.queueStats.averageProcessingTime * state.queueStats.totalProcessed) + executionTime) /
|
||||
(state.queueStats.totalProcessed + 1) * 100
|
||||
) / 100,
|
||||
averageProcessingTime:
|
||||
Math.round(
|
||||
((state.queueStats.averageProcessingTime * state.queueStats.totalProcessed +
|
||||
executionTime) /
|
||||
(state.queueStats.totalProcessed + 1)) *
|
||||
100
|
||||
) / 100,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -419,7 +424,7 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
error: action.payload.error,
|
||||
executionTime,
|
||||
failedAt: action.payload.timestamp,
|
||||
}
|
||||
},
|
||||
].slice(-100), // 최근 100개만 유지
|
||||
queueError: {
|
||||
actionId: action.payload.actionId,
|
||||
@@ -433,6 +438,24 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
};
|
||||
}
|
||||
|
||||
// [251114] 명시적 포커스 이동
|
||||
case types.FOCUS_PANEL: {
|
||||
console.log('[panelReducer] 🎯 FOCUS_PANEL', {
|
||||
panelName: action.payload.panelName,
|
||||
focusTarget: action.payload.focusTarget,
|
||||
timestamp: action.payload.timestamp,
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
lastFocusTarget: {
|
||||
panelName: action.payload.panelName,
|
||||
focusTarget: action.payload.focusTarget,
|
||||
timestamp: action.payload.timestamp,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,59 +1,60 @@
|
||||
export const SpotlightIds = {
|
||||
TPANEL: "tpanel",
|
||||
TBODY: "tbody",
|
||||
TPOPUP: "tpopup",
|
||||
HOME_TBODY: "home_tbody",
|
||||
TITEM_CARD: "tItemCard",
|
||||
TAB_LAYOUT: "tablayout",
|
||||
TPANEL: 'tpanel',
|
||||
TBODY: 'tbody',
|
||||
TPOPUP: 'tpopup',
|
||||
HOME_TBODY: 'home_tbody',
|
||||
TITEM_CARD: 'tItemCard',
|
||||
TAB_LAYOUT: 'tablayout',
|
||||
// homePanel
|
||||
HOME_CATEGORY_NAV: "homeCategoryNav",
|
||||
HOME_CATEGORY_NAV: 'homeCategoryNav',
|
||||
|
||||
// FeaturedBrandsPanel
|
||||
BRAND_VERTICAL_PAGENATOR: "brandVerticalPagenator",
|
||||
BRAND_QUICK_MENU: "brandQuickMenu",
|
||||
BRAND_TOP_BUTTON: "brandTopButton",
|
||||
BRAND_VERTICAL_PAGENATOR: 'brandVerticalPagenator',
|
||||
BRAND_QUICK_MENU: 'brandQuickMenu',
|
||||
BRAND_TOP_BUTTON: 'brandTopButton',
|
||||
|
||||
// TrendingNowPanel
|
||||
TRENDING_NOW_VERTICAL_PAGINATOR: "trendingNowVerticalPaginator",
|
||||
TRENDING_NOW_POPULAR_SHOW: "trendingNowPopularShow",
|
||||
TRENDING_NOW_POPULAR_VIDEO: "trendingNowPopularVideo",
|
||||
TRENDING_NOW_POPULAR_GRID_LIST: "trendingNowVirtualGridList",
|
||||
TRENDING_NOW_PREV_INDICATOR: "trendingNowPrevIndicator",
|
||||
TRENDING_NOW_NEXT_INDICATOR: "trendingNowNextIndicator",
|
||||
TRENDING_NOW_BEST_SELLER: "trendingNowBestSeller",
|
||||
TRENDING_NOW_TOP_BUTTON: "trendingNowTopButton",
|
||||
TRENDING_NOW_VERTICAL_PAGINATOR: 'trendingNowVerticalPaginator',
|
||||
TRENDING_NOW_POPULAR_SHOW: 'trendingNowPopularShow',
|
||||
TRENDING_NOW_POPULAR_VIDEO: 'trendingNowPopularVideo',
|
||||
TRENDING_NOW_POPULAR_GRID_LIST: 'trendingNowVirtualGridList',
|
||||
TRENDING_NOW_PREV_INDICATOR: 'trendingNowPrevIndicator',
|
||||
TRENDING_NOW_NEXT_INDICATOR: 'trendingNowNextIndicator',
|
||||
TRENDING_NOW_BEST_SELLER: 'trendingNowBestSeller',
|
||||
TRENDING_NOW_TOP_BUTTON: 'trendingNowTopButton',
|
||||
|
||||
// myPagePanel
|
||||
MY_PAGE_FAVORITES_BOX: "myPageFavoritesBox",
|
||||
MY_PAGE_REMINDRES_BOX: "myPageRemindresBox",
|
||||
MY_PAGE_MY_ORDER_BOX: "myPageMyOrderBox",
|
||||
MY_PAGE_MY_ORDER_TAB_CONTAINER: "myPageMyOrderTabContainer",
|
||||
MY_PAGE_FAVORITES_BOX: 'myPageFavoritesBox',
|
||||
MY_PAGE_REMINDRES_BOX: 'myPageRemindresBox',
|
||||
MY_PAGE_MY_ORDER_BOX: 'myPageMyOrderBox',
|
||||
MY_PAGE_MY_ORDER_TAB_CONTAINER: 'myPageMyOrderTabContainer',
|
||||
|
||||
// categoryPanel
|
||||
CATEGORY_CONTENTS_BOX: "categoryContentsBox",
|
||||
CATEGORY_TAB_CONTAINER: "categorytabContainer",
|
||||
SHOW_PRODUCTS_BOX: "showProductsBox",
|
||||
SHOW_CONTENTS_BOX: "showContentsBox",
|
||||
CATEGORY_CONTENTS_BOX: 'categoryContentsBox',
|
||||
CATEGORY_TAB_CONTAINER: 'categorytabContainer',
|
||||
SHOW_PRODUCTS_BOX: 'showProductsBox',
|
||||
SHOW_CONTENTS_BOX: 'showContentsBox',
|
||||
|
||||
// video player
|
||||
PLAYER_SKIPINTRO: "skipintro",
|
||||
PLAYER_TITLE_LAYER: "playerTitleLayer",
|
||||
PLAYER_SLIDER: "playerslider",
|
||||
PLAYER_TAB_BUTTON: "playerTabArrow",
|
||||
PLAYER_BACK_BUTTON: "player-back-button",
|
||||
PLAYER_SUBTITLE_BUTTON: "player-subtitlebutton",
|
||||
PLAYER_SKIPINTRO: 'skipintro',
|
||||
PLAYER_TITLE_LAYER: 'playerTitleLayer',
|
||||
PLAYER_SLIDER: 'playerslider',
|
||||
PLAYER_TAB_BUTTON: 'playerTabArrow',
|
||||
PLAYER_BACK_BUTTON: 'player-back-button',
|
||||
PLAYER_SUBTITLE_BUTTON: 'player-subtitlebutton',
|
||||
|
||||
// searchPanel
|
||||
SEARCH_THEME: "search_theme",
|
||||
SEARCH_SHOW: "search_show",
|
||||
SEARCH_ITEM: "search_item",
|
||||
SEARCH_BESTSELLER: "search_bestseller",
|
||||
SEARCH_TAB_CONTAINER: "searchtabContainer",
|
||||
SEARCH_THEME: 'search_theme',
|
||||
SEARCH_SHOW: 'search_show',
|
||||
SEARCH_ITEM: 'search_item',
|
||||
SEARCH_BESTSELLER: 'search_bestseller',
|
||||
SEARCH_TAB_CONTAINER: 'searchtabContainer',
|
||||
|
||||
// pin Code Popup
|
||||
PINCODE_CONTAINER: "pincodeContainer",
|
||||
PINCODE_CONTAINER: 'pincodeContainer',
|
||||
|
||||
// detailPanel
|
||||
DETAIL_BUYNOW: "detail_buynow",
|
||||
DETAIL_SHOPBYMOBILE: "detail_shop_by_mobile",
|
||||
DETAIL_BUYNOW: 'detail_buynow',
|
||||
DETAIL_SHOPBYMOBILE: 'detail_shop_by_mobile',
|
||||
DETAIL_PRODUCTVIDEO: 'product-video-player',
|
||||
};
|
||||
|
||||
286
com.twin.app.shoptime/src/utils/focusPanelGuide.js
Normal file
286
com.twin.app.shoptime/src/utils/focusPanelGuide.js
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* [251114] focusPanel 액션 사용 가이드
|
||||
*
|
||||
* Panel의 비동기 작업(useEffect, 타이머 등)이 포커스를 탈취하는 문제를 해결하기 위한
|
||||
* 명시적 포커스 제어 시스템입니다.
|
||||
*
|
||||
* 문제 상황:
|
||||
* - updatePanel 호출 → Panel의 useEffect 시작
|
||||
* - pushPanel 호출 → 새 Panel에 포커스 설정
|
||||
* - Panel의 useEffect 완료 → 기존 Panel이 포커스 탈취 (원하지 않는 동작)
|
||||
*
|
||||
* 해결책:
|
||||
* - updatePanel, pushPanel 등으로 Panel을 제어할 때
|
||||
* - focusPanel()로 명시적으로 포커스 대상을 지정
|
||||
* - Panel의 useEffect는 더 이상 자동 포커스 이동 안 함
|
||||
*/
|
||||
|
||||
import { focusPanel } from '../actions/panelActions';
|
||||
import { panel_names } from './Config';
|
||||
|
||||
// ============================================================================
|
||||
// 예시 1: 단일 Panel 포커스 제어 (가장 간단)
|
||||
// ============================================================================
|
||||
|
||||
export const example1_singlePanelFocus = (dispatch) => {
|
||||
console.log('✅ 예시 1: 단일 Panel 포커스 제어');
|
||||
|
||||
// DETAIL_PANEL 표시 + 포커스 설정
|
||||
dispatch(pushPanel({ name: panel_names.DETAIL_PANEL }));
|
||||
dispatch(focusPanel(panel_names.DETAIL_PANEL, 'detail-buy-button'));
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 예시 2: 2중 구조 (PLAYER + DETAIL)에서 포커스 제어
|
||||
// ============================================================================
|
||||
|
||||
export const example2_twoLayerFocus = (dispatch) => {
|
||||
console.log('✅ 예시 2: 2중 구조 포커스 제어');
|
||||
|
||||
// 상황: [PLAYER_PANEL]이 이미 있는 상태
|
||||
|
||||
// 1단계: DETAIL_PANEL 추가
|
||||
dispatch(pushPanel({ name: panel_names.DETAIL_PANEL }));
|
||||
|
||||
// 2단계: DETAIL_PANEL의 특정 요소에 포커스
|
||||
dispatch(focusPanel(panel_names.DETAIL_PANEL, 'detail-buy-button'));
|
||||
|
||||
// 또는 PLAYER_PANEL의 플레이버튼에 포커스
|
||||
// dispatch(focusPanel(panel_names.PLAYER_PANEL, 'player-play-button'));
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 예시 3: 3중 구조 (PLAYER + DETAIL + MEDIA)에서 포커스 제어
|
||||
// ============================================================================
|
||||
|
||||
export const example3_threeLayerFocus = (dispatch) => {
|
||||
console.log('✅ 예시 3: 3중 구조 포커스 제어');
|
||||
|
||||
// 상황: [PLAYER_PANEL, DETAIL_PANEL]이 이미 있는 상태
|
||||
|
||||
// 1단계: MEDIA_PANEL 추가 (modal=true)
|
||||
dispatch(
|
||||
pushPanel({
|
||||
name: panel_names.MEDIA_PANEL,
|
||||
panelInfo: { modal: true },
|
||||
})
|
||||
);
|
||||
|
||||
// 2단계: MEDIA_PANEL의 닫기 버튼에 포커스
|
||||
dispatch(focusPanel(panel_names.MEDIA_PANEL, 'media-close-button'));
|
||||
|
||||
// 결과:
|
||||
// - PLAYER_PANEL (백그라운드)
|
||||
// - DETAIL_PANEL (중간, 보임)
|
||||
// - MEDIA_PANEL (맨 위, 모달, 포커스 받음)
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 예시 4: updatePanel과 함께 사용
|
||||
// ============================================================================
|
||||
|
||||
import { updatePanel } from '../actions/panelActions';
|
||||
|
||||
export const example4_updateWithFocus = (dispatch) => {
|
||||
console.log('✅ 예시 4: updatePanel과 focusPanel 함께 사용');
|
||||
|
||||
// ❌ 문제가 될 수 있는 방식:
|
||||
// dispatch(updatePanel({
|
||||
// name: 'DETAIL_PANEL',
|
||||
// panelInfo: { productId: '123', isLoading: false }
|
||||
// }));
|
||||
// DETAIL_PANEL의 useEffect가 나중에 포커스를 탈취할 수 있음
|
||||
|
||||
// ✅ 올바른 방식:
|
||||
dispatch(
|
||||
updatePanel({
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: { productId: '123', isLoading: false },
|
||||
})
|
||||
);
|
||||
|
||||
// 명시적으로 포커스 지정
|
||||
dispatch(focusPanel(panel_names.DETAIL_PANEL, 'detail-product-title'));
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 예시 5: API 호출 후 Panel 제어 및 포커스
|
||||
// ============================================================================
|
||||
|
||||
export const example5_apiAndFocus = (dispatch, getState) => {
|
||||
console.log('✅ 예시 5: API 호출 후 Panel 제어 및 포커스');
|
||||
|
||||
// API 호출 시뮬레이션
|
||||
fetch('/api/products/123')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
// API 성공 → Panel 업데이트
|
||||
dispatch(
|
||||
updatePanel({
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: {
|
||||
product: data,
|
||||
isLoading: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// 명시적으로 포커스 지정 (DETAIL_PANEL의 useEffect보다 먼저 실행되지만
|
||||
// focusPanel은 비동기로 처리되므로 먼저 끝나더라도 안전)
|
||||
dispatch(focusPanel(panel_names.DETAIL_PANEL, 'detail-add-to-cart-button'));
|
||||
})
|
||||
.catch((error) => {
|
||||
// API 실패 → 에러 Panel 표시
|
||||
dispatch(
|
||||
pushPanel({
|
||||
name: panel_names.ERROR_PANEL,
|
||||
panelInfo: { error: error.message },
|
||||
})
|
||||
);
|
||||
|
||||
// 에러 Panel의 확인 버튼에 포커스
|
||||
dispatch(focusPanel(panel_names.ERROR_PANEL, 'error-ok-button'));
|
||||
});
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 예시 6: Queue와 함께 사용 (순서 보장)
|
||||
// ============================================================================
|
||||
|
||||
import { pushPanelQueued, updatePanelQueued } from '../actions/queuedPanelActions';
|
||||
|
||||
export const example6_queueWithFocus = (dispatch) => {
|
||||
console.log('✅ 예시 6: Panel Queue와 focusPanel 함께 사용');
|
||||
|
||||
// Panel 제어는 Queue로 순서 보장
|
||||
dispatch(
|
||||
updatePanelQueued({
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: { productId: '456', isLoading: false },
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(
|
||||
pushPanelQueued({
|
||||
name: panel_names.MEDIA_PANEL,
|
||||
panelInfo: { modal: true },
|
||||
})
|
||||
);
|
||||
|
||||
// 포커스는 명시적으로 지정
|
||||
dispatch(focusPanel(panel_names.MEDIA_PANEL, 'media-close-button'));
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 예시 7: Panel에서 focusPanel 호출
|
||||
// ============================================================================
|
||||
|
||||
/*
|
||||
// DetailPanel.jsx에서:
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { focusPanel } from '../actions/panelActions';
|
||||
import { panel_names } from '../utils/Config';
|
||||
|
||||
function DetailPanel({ panelInfo }) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleProductSelect = (productId) => {
|
||||
// 선택한 상품으로 Panel 업데이트
|
||||
dispatch(updatePanel({
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: { productId }
|
||||
}));
|
||||
|
||||
// 특정 요소에 포커스 (Panel의 useEffect가 포커스를 탈취하지 않음)
|
||||
dispatch(focusPanel(
|
||||
panel_names.DETAIL_PANEL,
|
||||
'detail-product-description'
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => handleProductSelect('123')}>
|
||||
Product 123
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// 예시 8: 에러 처리
|
||||
// ============================================================================
|
||||
|
||||
export const example8_errorHandling = (dispatch) => {
|
||||
console.log('✅ 예시 8: 에러 처리');
|
||||
|
||||
// focusPanel은 안전성 체크를 수행합니다:
|
||||
|
||||
// 1. Panel이 존재하지 않으면:
|
||||
// [focusPanel] ❌ Panel을 찾을 수 없음: INVALID_PANEL
|
||||
|
||||
// 2. 상위에 Modal이 있으면 (새로운 로직):
|
||||
// [focusPanel] ⚠️ 상위에 Modal이 있음. DETAIL_PANEL(1층)에 포커스할 수 없음.
|
||||
// 상단 Modal: ANOTHER_MODAL_PANEL(3층)
|
||||
|
||||
// 3. 요소를 찾을 수 없으면:
|
||||
// [focusPanel] ❌ 요소를 찾을 수 없음: invalid-id
|
||||
|
||||
// 4. 요소가 숨겨져있으면:
|
||||
// [focusPanel] ⚠️ 요소가 숨겨져있음: hidden-element
|
||||
|
||||
// ✅ 성공 로그 (새로 추가):
|
||||
// [focusPanel] ✅ Panel 위치 확인: DETAIL_PANEL(1층), 전체 Panel: 3층
|
||||
|
||||
// 모든 경우에 console에 상세한 로그가 출력되므로 디버깅이 쉬움
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 핵심 원칙
|
||||
// ============================================================================
|
||||
|
||||
/*
|
||||
1. updatePanel/pushPanel 호출 후 focusPanel 호출
|
||||
dispatch(updatePanel(...));
|
||||
dispatch(focusPanel(panelName, elementId));
|
||||
|
||||
2. Panel의 useEffect에서는 focusPanel을 호출하지 말 것
|
||||
- Panel의 로직은 updatePanel의 panelInfo 변경만 감지
|
||||
- 포커스 제어는 액션 호출처에서 명시적으로 지정
|
||||
|
||||
3. focusPanel은 매번 안전성을 체크함
|
||||
- Panel 존재 여부
|
||||
- Panel 레이어 위치 (최상단 또는 그 아래 패널만 허용)
|
||||
- 상위 Modal 존재 여부 (상위에 Modal이 있으면 포커스 차단)
|
||||
- 요소 존재 여부
|
||||
- 요소 가시성
|
||||
|
||||
4. 2중/3중 구조에서도 안전함
|
||||
- 최상단 또는 그 아래 레이어 패널에만 포커스 허용
|
||||
- 상위에 Modal이 있는 경우에만 포커스 차단
|
||||
- [PLAYER_PANEL, DETAIL_PANEL, MEDIA_PANEL] 구조에서:
|
||||
• MEDIA_PANEL에 포커스: ✅ 가능 (최상단)
|
||||
• DETAIL_PANEL에 포커스: ✅ 가능 (MEDIA_PANEL이 Modal이어도 하위 패널이므로 가능)
|
||||
• PLAYER_PANEL에 포커스: ✅ 가능 (최하위 패널)
|
||||
- 렌더링되지 않은 패널의 요소에 접근 불가
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Redux DevTools에서 확인
|
||||
// ============================================================================
|
||||
|
||||
/*
|
||||
Redux DevTools를 열면:
|
||||
|
||||
FOCUS_PANEL 액션이 나타나고, payload에:
|
||||
{
|
||||
panelName: 'DETAIL_PANEL',
|
||||
focusTarget: 'detail-buy-button',
|
||||
timestamp: 1731014400000
|
||||
}
|
||||
|
||||
state.panels.lastFocusTarget에도 같은 정보가 저장되어
|
||||
마지막 포커스 상태를 추적할 수 있습니다.
|
||||
*/
|
||||
@@ -662,8 +662,10 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
);
|
||||
|
||||
const handleProductAllSectionReady = useCallback(() => {
|
||||
console.log('############## ShopByMobile focus');
|
||||
const spotTime = setTimeout(() => {
|
||||
Spotlight.focus(SpotlightIds.DETAIL_SHOPBYMOBILE);
|
||||
// Spotlight.focus(SpotlightIds.DETAIL_SHOPBYMOBILE);
|
||||
Spotlight.focus(SpotlightIds.DETAIL_PRODUCTVIDEO);
|
||||
}, 100);
|
||||
return () => {
|
||||
clearTimeout(spotTime);
|
||||
@@ -726,7 +728,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
console.log('[DetailPanel] MediaPanel modal=true detected - focusing ProductVideo');
|
||||
const focusTimer = setTimeout(() => {
|
||||
Spotlight.focus('product-video-player');
|
||||
}, 500);
|
||||
}, 2500);
|
||||
return () => clearTimeout(focusTimer);
|
||||
}
|
||||
}, [panels]);
|
||||
|
||||
@@ -341,7 +341,57 @@ export default function ProductAllSection({
|
||||
dispatch(resetShowAllReviews());
|
||||
}, []); // 빈 dependency array = 마운트 시에만 실행
|
||||
|
||||
// ⚠️ REMOVED: 상품 변경 시 필터 초기화 로직 제거
|
||||
// 임시: 무조건 1.5초 후에 product-video-player에 포커스
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
'[ProductAllSection] 포커스 시도 전 - hasVideo:',
|
||||
hasVideo,
|
||||
'productVideoVersion:',
|
||||
productVideoVersion
|
||||
);
|
||||
const timer = setTimeout(() => {
|
||||
console.log('[ProductAllSection] 포커스 호출 시도: product-video-player');
|
||||
|
||||
// DOM에 요소가 존재하는지 확인
|
||||
const element =
|
||||
document.querySelector('[data-spotlight-id="product-video-player"]') ||
|
||||
document.getElementById('product-video-player') ||
|
||||
document.querySelector('[spotlight-id="product-video-player"]');
|
||||
|
||||
console.log('[ProductAllSection] DOM 요소 확인:', {
|
||||
element: element,
|
||||
elementExists: !!element,
|
||||
elementTag: element?.tagName,
|
||||
elementId: element?.id,
|
||||
elementSpotlightId:
|
||||
element?.getAttribute('data-spotlight-id') || element?.getAttribute('spotlight-id'),
|
||||
});
|
||||
|
||||
try {
|
||||
Spotlight.focus('product-video-player');
|
||||
console.log('[ProductAllSection] 포커스 호출 성공');
|
||||
|
||||
// 포커스 후 현재 포커스된 요소 확인
|
||||
setTimeout(() => {
|
||||
const activeElement = document.activeElement;
|
||||
console.log('[ProductAllSection] 포커스 후 activeElement:', {
|
||||
activeElement: activeElement,
|
||||
activeElementTag: activeElement?.tagName,
|
||||
activeElementId: activeElement?.id,
|
||||
activeElementSpotlightId:
|
||||
activeElement?.getAttribute('data-spotlight-id') ||
|
||||
activeElement?.getAttribute('spotlight-id'),
|
||||
});
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('[ProductAllSection] 포커스 호출 실패:', error);
|
||||
}
|
||||
}, 1500); // 1.5초 = 1500ms
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, []);
|
||||
// 이미 useReviews 훅에서 동일한 기능을 수행하고 있음
|
||||
// ProductAllSection에서 중복으로 호출하면 UserReviewPanel 진입 시
|
||||
// reviewListData가 반복적으로 초기화되어 Chrome에서 진입 불가 발생
|
||||
@@ -859,10 +909,16 @@ export default function ProductAllSection({
|
||||
prevMediaPanelModalStateRef.current === false
|
||||
) {
|
||||
console.log(
|
||||
'[ProductAllSection] 🔄 MediaPanel이 전체화면에서 모달로 복귀 - ProductVideo로 포커스 복구'
|
||||
'[ProductAllSection] 🔄 MediaPanel이 전체화면에서 모달로 복귀 - ProductVideo로 포커스 복구 시도'
|
||||
);
|
||||
const focusTimer = setTimeout(() => {
|
||||
Spotlight.focus('product-video-player');
|
||||
console.log('[ProductAllSection] MediaPanel 복귀 후 포커스 호출: product-video-player');
|
||||
try {
|
||||
Spotlight.focus('product-video-player');
|
||||
console.log('[ProductAllSection] MediaPanel 복귀 후 포커스 호출 성공');
|
||||
} catch (error) {
|
||||
console.error('[ProductAllSection] MediaPanel 복귀 후 포커스 호출 실패:', error);
|
||||
}
|
||||
}, 100);
|
||||
return () => clearTimeout(focusTimer);
|
||||
}
|
||||
@@ -1148,6 +1204,7 @@ export default function ProductAllSection({
|
||||
onVideoPlaying={() => setIsVideoPlaying(true)}
|
||||
onScrollToImages={handleScrollToImagesV1}
|
||||
onFocus={() => console.log('[ProductVideo V1] Focused')}
|
||||
data-spotlight-id="product-video-player-container"
|
||||
/>
|
||||
) : (
|
||||
<ProductVideoV2
|
||||
|
||||
@@ -488,6 +488,7 @@
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
cursor: default !important;
|
||||
&.shopByMobileOne {
|
||||
margin: 0 6px 0 0;
|
||||
}
|
||||
|
||||
@@ -75,9 +75,18 @@ export default function ProductVideo({
|
||||
}, [topPanel]);
|
||||
|
||||
// autoPlay 기능: 컴포넌트 마운트 시 자동으로 비디오 재생
|
||||
// 단, 이미 MediaPanel이 modal로 재생 중이면 autoPlay 하지 않음
|
||||
useEffect(() => {
|
||||
if (autoPlay && canPlayVideo && !hasAutoPlayed && productInfo) {
|
||||
console.log('[ProductVideo] Auto-playing video');
|
||||
// MediaPanel이 이미 modal로 재생 중인지 확인
|
||||
const isMediaPanelAlreadyPlaying =
|
||||
topPanel?.name === panel_names.MEDIA_PANEL && topPanel?.panelInfo?.modal === true;
|
||||
|
||||
if (autoPlay && canPlayVideo && !hasAutoPlayed && productInfo && !isMediaPanelAlreadyPlaying) {
|
||||
console.log('[ProductVideo]-LoadingVideo 🎯 AutoPlay 시작:', {
|
||||
prdtId: productInfo?.prdtId,
|
||||
prdtNm: productInfo?.prdtNm,
|
||||
prdtMediaUrl: productInfo?.prdtMediaUrl?.substring(0, 50),
|
||||
});
|
||||
setHasAutoPlayed(true);
|
||||
|
||||
// 짧은 딜레이 후 재생 시작 (컴포넌트 마운트 완료 후)
|
||||
@@ -121,6 +130,8 @@ export default function ProductVideo({
|
||||
dispatch,
|
||||
modalClassNameChange,
|
||||
continuousPlay,
|
||||
topPanel,
|
||||
panels,
|
||||
]);
|
||||
|
||||
// 비디오 재생 가능 여부 체크
|
||||
|
||||
@@ -1,40 +1,20 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
} from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { Job } from '@enact/core/util';
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
|
||||
import { clearThemeDetail } from '../../../../actions/homeActions';
|
||||
import {
|
||||
popPanel,
|
||||
pushPanel,
|
||||
updatePanel,
|
||||
} from '../../../../actions/panelActions';
|
||||
import { popPanel, pushPanel, updatePanel } from '../../../../actions/panelActions';
|
||||
import { finishVideoPreview } from '../../../../actions/playActions';
|
||||
import THeader from '../../../../components/THeader/THeader';
|
||||
import TItemCardNew from '../../../../components/TItemCard/TItemCard.new';
|
||||
import TVerticalPagenator
|
||||
from '../../../../components/TVerticalPagenator/TVerticalPagenator';
|
||||
import TVirtualGridList
|
||||
from '../../../../components/TVirtualGridList/TVirtualGridList';
|
||||
import TVerticalPagenator from '../../../../components/TVerticalPagenator/TVerticalPagenator';
|
||||
import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList';
|
||||
import useScrollTo from '../../../../hooks/useScrollTo';
|
||||
import {
|
||||
LOG_CONTEXT_NAME,
|
||||
LOG_MESSAGE_ID,
|
||||
panel_names,
|
||||
} from '../../../../utils/Config';
|
||||
import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, panel_names } from '../../../../utils/Config';
|
||||
import { $L } from '../../../../utils/helperMethods';
|
||||
import css from './YouMayAlsoLike.module.less';
|
||||
|
||||
@@ -186,9 +166,11 @@ export default function YouMayAlsoLike({
|
||||
);
|
||||
cursorOpen.current.stop();
|
||||
};
|
||||
// prdtId가 없는 경우를 대비한 안정적인 key 생성
|
||||
const itemKey = prdtId ? `${patnrId}-${prdtId}` : `product-${index}`;
|
||||
return (
|
||||
<TItemCardNew
|
||||
key={prdtId}
|
||||
key={itemKey}
|
||||
className={css.itemCardNew}
|
||||
contextName={LOG_CONTEXT_NAME.YOUMAYLIKE}
|
||||
messageId={LOG_MESSAGE_ID.CONTENTCLICK}
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
getMainLiveShowNowProduct,
|
||||
} from '../../actions/mainActions';
|
||||
import * as PanelActions from '../../actions/panelActions';
|
||||
import { updatePanel } from '../../actions/panelActions';
|
||||
import { updatePanel, focusPanel } from '../../actions/panelActions';
|
||||
import {
|
||||
CLEAR_PLAYER_INFO,
|
||||
getChatLog,
|
||||
@@ -385,13 +385,14 @@ const MediaPanel = React.forwardRef(
|
||||
// },[isOnTop, panelInfo])
|
||||
|
||||
// MediaPanel.jsx의 라인 313-327 useEffect 수정
|
||||
// MEDIA 타입은 isOnTop 체크 하지 않음 (패널 구조 복잡도 때문에)
|
||||
useEffect(() => {
|
||||
console.log('[MediaPanel] isOnTop:', {
|
||||
isOnTop,
|
||||
panelInfo,
|
||||
});
|
||||
|
||||
if (panelInfo && panelInfo.modal) {
|
||||
if (panelInfo && panelInfo.modal && panelInfo?.shptmBanrTpNm !== 'MEDIA') {
|
||||
if (!isOnTop) {
|
||||
console.log('[MediaPanel] Not on top - pausing video');
|
||||
dispatch(pauseModalMedia());
|
||||
@@ -875,7 +876,7 @@ const MediaPanel = React.forwardRef(
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onClickBack = useCallback(
|
||||
const handleClickBack = useCallback(
|
||||
(ev, isEnd) => {
|
||||
//modal로부터 Full 전환된 경우 다시 preview 모드로 돌아감.
|
||||
if (panelInfo.modalContainerId && !panelInfo.modal) {
|
||||
@@ -904,6 +905,11 @@ const MediaPanel = React.forwardRef(
|
||||
'[MediaPanel] Back button pressed - returning to modal, focus will be handled by ProductVideo'
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('[MediaPanel] focusPanel ');
|
||||
dispatch(focusPanel('DETAIL_PANEL', 'product-video-player'));
|
||||
}, 100);
|
||||
|
||||
ev?.stopPropagation();
|
||||
// ev?.preventDefault();
|
||||
return;
|
||||
@@ -921,6 +927,7 @@ const MediaPanel = React.forwardRef(
|
||||
}
|
||||
ev?.stopPropagation();
|
||||
// ev?.preventDefault();
|
||||
|
||||
return;
|
||||
}
|
||||
},
|
||||
@@ -1036,8 +1043,9 @@ const MediaPanel = React.forwardRef(
|
||||
}, [topPanel]);
|
||||
|
||||
const cannotPlay = useMemo(() => {
|
||||
return !isOnTop && topPanel?.name === panel_names.MEDIA_PANEL;
|
||||
}, [topPanel, isOnTop]);
|
||||
// URL이 있으면 항상 재생 가능 (isOnTop 조건 제거)
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
const getPlayer = useCallback((ref) => {
|
||||
videoPlayer.current = ref;
|
||||
@@ -1678,8 +1686,24 @@ const MediaPanel = React.forwardRef(
|
||||
return null;
|
||||
}
|
||||
|
||||
return playListInfo && playListInfo[selectedIndex]?.showUrl;
|
||||
}, [playListInfo, selectedIndex, broadcast]);
|
||||
// MEDIA 타입일 때: panelInfo.showUrl 사용
|
||||
if (panelInfo?.shptmBanrTpNm === 'MEDIA') {
|
||||
console.log('[MediaPanel]-LoadingVideo 📺 MEDIA URL:', {
|
||||
showUrl: panelInfo?.showUrl?.substring(0, 50),
|
||||
prdtId: panelInfo?.prdtId,
|
||||
});
|
||||
return panelInfo?.showUrl;
|
||||
}
|
||||
|
||||
// 기타 타입: playListInfo 사용
|
||||
const url = playListInfo && playListInfo[selectedIndex]?.showUrl;
|
||||
if (url) {
|
||||
console.log('[MediaPanel]-LoadingVideo 🎬 PlayList URL:', {
|
||||
url: url.substring(0, 50),
|
||||
});
|
||||
}
|
||||
return url;
|
||||
}, [playListInfo, selectedIndex, broadcast, panelInfo?.shptmBanrTpNm, panelInfo?.showUrl]);
|
||||
|
||||
const isYoutube = useMemo(() => {
|
||||
if (currentPlayingUrl && currentPlayingUrl.includes('youtu')) {
|
||||
@@ -1698,11 +1722,17 @@ const MediaPanel = React.forwardRef(
|
||||
|
||||
const isReadyToPlay = useMemo(() => {
|
||||
if (!currentPlayingUrl) {
|
||||
console.log('[MediaPanel]-LoadingVideo ❌ isReadyToPlay = false (no URL)');
|
||||
return false;
|
||||
}
|
||||
if (!Config.DEBUG_VIDEO_SUBTITLE_TEST && currentSubtitleUrl && !currentSubtitleBlob) {
|
||||
console.log('[MediaPanel]-LoadingVideo ❌ isReadyToPlay = false (subtitle not loaded):', {
|
||||
currentSubtitleUrl,
|
||||
currentSubtitleBlob: !!currentSubtitleBlob,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
console.log('[MediaPanel]-LoadingVideo ✅ isReadyToPlay = true');
|
||||
return true;
|
||||
}, [currentPlayingUrl, currentSubtitleUrl, currentSubtitleBlob, broadcast]);
|
||||
|
||||
@@ -2168,7 +2198,7 @@ const MediaPanel = React.forwardRef(
|
||||
isTabActivated={false}
|
||||
{...props}
|
||||
className={containerClassName}
|
||||
handleCancel={onClickBack}
|
||||
handleCancel={handleClickBack}
|
||||
spotlightId={spotlightId}
|
||||
>
|
||||
<Container
|
||||
@@ -2181,15 +2211,32 @@ const MediaPanel = React.forwardRef(
|
||||
// }
|
||||
// }}
|
||||
>
|
||||
{(() => {
|
||||
if (isReadyToPlay) {
|
||||
console.log('[MediaPanel]-LoadingVideo 🎬 VideoPlayer 렌더링:', {
|
||||
src: currentPlayingUrl?.substring(0, 50),
|
||||
disabled: panelInfo.modal,
|
||||
cannotPlay,
|
||||
isYoutube,
|
||||
videoComponent:
|
||||
(typeof window === 'object' && !window.PalmSystem) || isYoutube
|
||||
? 'TReactPlayer'
|
||||
: 'Media',
|
||||
});
|
||||
} else {
|
||||
console.log('[MediaPanel]-LoadingVideo 🚫 VideoPlayer 렌더링 스킵됨');
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
{isReadyToPlay && (
|
||||
<div className={css.videoFrame}>
|
||||
<VideoPlayer
|
||||
setApiProvider={getPlayer}
|
||||
disabled={panelInfo.modal}
|
||||
onEnded={onEnded}
|
||||
noAutoPlay={cannotPlay}
|
||||
noAutoPlay={false}
|
||||
autoCloseTimeout={3000}
|
||||
onBackButton={onClickBack}
|
||||
onBackButton={handleClickBack}
|
||||
spotlightDisabled={false}
|
||||
isYoutube={isYoutube}
|
||||
src={currentPlayingUrl}
|
||||
|
||||
Reference in New Issue
Block a user