[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',
|
POP_PANEL: 'POP_PANEL',
|
||||||
UPDATE_PANEL: 'UPDATE_PANEL',
|
UPDATE_PANEL: 'UPDATE_PANEL',
|
||||||
RESET_PANELS: 'RESET_PANELS',
|
RESET_PANELS: 'RESET_PANELS',
|
||||||
|
FOCUS_PANEL: 'FOCUS_PANEL', // 🔽 [251114] 명시적 포커스 이동
|
||||||
|
|
||||||
// 🔽 [신규] panel history actions
|
// 🔽 [신규] panel history actions
|
||||||
ENQUEUE_PANEL_HISTORY: 'ENQUEUE_PANEL_HISTORY',
|
ENQUEUE_PANEL_HISTORY: 'ENQUEUE_PANEL_HISTORY',
|
||||||
@@ -82,12 +83,12 @@ export const types = {
|
|||||||
CLEAR_CART: 'CLEAR_CART',
|
CLEAR_CART: 'CLEAR_CART',
|
||||||
//cart api action
|
//cart api action
|
||||||
GET_MY_INFO_CART_SEARCH: 'GET_MY_INFO_CART_SEARCH',
|
GET_MY_INFO_CART_SEARCH: 'GET_MY_INFO_CART_SEARCH',
|
||||||
INSERT_MY_INFO_CART : "INSERT_MY_INFO_CART",
|
INSERT_MY_INFO_CART: 'INSERT_MY_INFO_CART',
|
||||||
DELETE_MY_INFO_CART : "DELETE_MY_INFO_CART",
|
DELETE_MY_INFO_CART: 'DELETE_MY_INFO_CART',
|
||||||
DELETE_ALL_MY_INFO_CART : "DELETE_ALL_MY_INFO_CART",
|
DELETE_ALL_MY_INFO_CART: 'DELETE_ALL_MY_INFO_CART',
|
||||||
UPDATE_MY_INFO_CART : "UPDATE_MY_INFO_CART",
|
UPDATE_MY_INFO_CART: 'UPDATE_MY_INFO_CART',
|
||||||
//cart checkbox toggle action
|
//cart checkbox toggle action
|
||||||
TOGGLE_CHECK_CART : "TOGGLE_CHECK_CART",
|
TOGGLE_CHECK_CART: 'TOGGLE_CHECK_CART',
|
||||||
|
|
||||||
// appData actions
|
// appData actions
|
||||||
ADD_MAIN_INDEX: 'ADD_MAIN_INDEX',
|
ADD_MAIN_INDEX: 'ADD_MAIN_INDEX',
|
||||||
@@ -315,7 +316,7 @@ export const types = {
|
|||||||
SET_MODAL_BORDER: 'SET_MODAL_BORDER',
|
SET_MODAL_BORDER: 'SET_MODAL_BORDER',
|
||||||
SET_BANNER_VISIBILITY: 'SET_BANNER_VISIBILITY',
|
SET_BANNER_VISIBILITY: 'SET_BANNER_VISIBILITY',
|
||||||
|
|
||||||
// 🔽 [추가] JustForYou 상품 관리 부분
|
// 🔽 [추가] JustForYou 상품 관리 부분
|
||||||
JUSTFORYOU: 'JUSTFORYOU',
|
JUSTFORYOU: 'JUSTFORYOU',
|
||||||
|
|
||||||
// 🔽 Voice Conductor 관련 액션 타입
|
// 🔽 Voice Conductor 관련 액션 타입
|
||||||
|
|||||||
@@ -23,15 +23,16 @@ export const startMediaPlayer =
|
|||||||
const topPanel = panels[panels.length - 1];
|
const topPanel = panels[panels.length - 1];
|
||||||
let panelWorkingAction = pushPanel;
|
let panelWorkingAction = pushPanel;
|
||||||
|
|
||||||
console.log('[startMediaPlayer] ========== Called ==========');
|
console.log('[startMediaPlayer]-LoadingVideo 🚀 시작:', {
|
||||||
console.log('[startMediaPlayer] Current panels:', JSON.stringify(panels, null, 2));
|
showUrl: rest?.showUrl?.substring(0, 50),
|
||||||
console.log('[startMediaPlayer] topPanel:', JSON.stringify(topPanel, null, 2));
|
showNm: rest?.showNm,
|
||||||
|
prdtId: rest?.prdtId,
|
||||||
|
modal,
|
||||||
|
modalContainerId,
|
||||||
|
});
|
||||||
|
|
||||||
if (topPanel && topPanel.name === panel_names.MEDIA_PANEL) {
|
if (topPanel && topPanel.name === panel_names.MEDIA_PANEL) {
|
||||||
panelWorkingAction = updatePanel;
|
panelWorkingAction = updatePanel;
|
||||||
console.log('[startMediaPlayer] Using updatePanel (existing MediaPanel)');
|
|
||||||
} else {
|
|
||||||
console.log('[startMediaPanel] Using pushPanel (new MediaPanel)');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const allParams = {
|
const allParams = {
|
||||||
@@ -42,8 +43,6 @@ export const startMediaPlayer =
|
|||||||
...rest,
|
...rest,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[startMediaPlayer] All parameters:', JSON.stringify(allParams, null, 2));
|
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
panelWorkingAction(
|
panelWorkingAction(
|
||||||
{
|
{
|
||||||
@@ -54,7 +53,7 @@ export const startMediaPlayer =
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('[startMediaPlayer] Panel action dispatched');
|
console.log('[startMediaPlayer]-LoadingVideo ✅ MediaPanel dispatch 완료');
|
||||||
|
|
||||||
if (modal && modalContainerId && !spotlightDisable) {
|
if (modal && modalContainerId && !spotlightDisable) {
|
||||||
Spotlight.setPointerMode(false);
|
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,
|
name: panel_names.PLAYER_PANEL,
|
||||||
@@ -27,3 +28,85 @@ export const resetPanels = (panels) => ({
|
|||||||
type: types.RESET_PANELS,
|
type: types.RESET_PANELS,
|
||||||
payload: 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: [],
|
panels: [],
|
||||||
lastPanelAction: '', //"", "push", "pop", "update", "reset", "previewPush", "previewPop", "previewUpdate"
|
lastPanelAction: '', //"", "push", "pop", "update", "reset", "previewPush", "previewPop", "previewUpdate"
|
||||||
|
lastFocusTarget: null, // [251114] 마지막 포커스 대상 (panelName + elementId)
|
||||||
|
|
||||||
// [251106] 패널 액션 큐 관련 상태 - 기존 기능에 전혀 영향 없음
|
// [251106] 패널 액션 큐 관련 상태 - 기존 기능에 전혀 영향 없음
|
||||||
panelActionQueue: [], // 처리 대기 중인 패널 액션 큐
|
panelActionQueue: [], // 처리 대기 중인 패널 액션 큐
|
||||||
@@ -13,7 +14,7 @@ const initialState = {
|
|||||||
queueStats: {
|
queueStats: {
|
||||||
totalProcessed: 0, // 총 처리된 액션 수
|
totalProcessed: 0, // 총 처리된 액션 수
|
||||||
failedCount: 0, // 실패한 액션 수
|
failedCount: 0, // 실패한 액션 수
|
||||||
averageProcessingTime: 0 // 평균 처리 시간
|
averageProcessingTime: 0, // 평균 처리 시간
|
||||||
},
|
},
|
||||||
|
|
||||||
// [251106] 비동기 액션 관련 상태
|
// [251106] 비동기 액션 관련 상태
|
||||||
@@ -30,7 +31,7 @@ export const panelsReducer = (state = initialState, action) => {
|
|||||||
case types.PUSH_PANEL: {
|
case types.PUSH_PANEL: {
|
||||||
console.log('[panelReducer] 🔵 PUSH_PANEL START', {
|
console.log('[panelReducer] 🔵 PUSH_PANEL START', {
|
||||||
newPanelName: action.payload.name,
|
newPanelName: action.payload.name,
|
||||||
currentPanels: state.panels.map(p => p.name),
|
currentPanels: state.panels.map((p) => p.name),
|
||||||
duplicatable: action.duplicatable,
|
duplicatable: action.duplicatable,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ export const panelsReducer = (state = initialState, action) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('[panelReducer] 🔵 PUSH_PANEL END', {
|
console.log('[panelReducer] 🔵 PUSH_PANEL END', {
|
||||||
resultPanels: newState.map(p => p.name),
|
resultPanels: newState.map((p) => p.name),
|
||||||
lastAction,
|
lastAction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ export const panelsReducer = (state = initialState, action) => {
|
|||||||
case types.POP_PANEL: {
|
case types.POP_PANEL: {
|
||||||
console.log('[panelReducer] 🔴 POP_PANEL START', {
|
console.log('[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),
|
||||||
});
|
});
|
||||||
|
|
||||||
let lastAction = 'pop';
|
let lastAction = 'pop';
|
||||||
@@ -113,7 +114,7 @@ export const panelsReducer = (state = initialState, action) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('[panelReducer] 🔴 POP_PANEL END', {
|
console.log('[panelReducer] 🔴 POP_PANEL END', {
|
||||||
resultPanels: resultPanels.map(p => p.name),
|
resultPanels: resultPanels.map((p) => p.name),
|
||||||
lastAction,
|
lastAction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -159,7 +160,7 @@ export const panelsReducer = (state = initialState, action) => {
|
|||||||
}
|
}
|
||||||
case types.RESET_PANELS: {
|
case types.RESET_PANELS: {
|
||||||
console.log('[panelReducer] 🟢 RESET_PANELS START', {
|
console.log('[panelReducer] 🟢 RESET_PANELS START', {
|
||||||
currentPanels: state.panels.map(p => p.name),
|
currentPanels: state.panels.map((p) => p.name),
|
||||||
payloadProvided: !!action.payload,
|
payloadProvided: !!action.payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -171,7 +172,7 @@ export const panelsReducer = (state = initialState, action) => {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
console.log('[panelReducer] 🟢 RESET_PANELS END', {
|
console.log('[panelReducer] 🟢 RESET_PANELS END', {
|
||||||
resultPanels: updatedPanels.map(p => p.name),
|
resultPanels: updatedPanels.map((p) => p.name),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -267,7 +268,8 @@ export const panelsReducer = (state = initialState, action) => {
|
|||||||
const processingTime = Date.now() - startTime;
|
const processingTime = Date.now() - startTime;
|
||||||
const newTotalProcessed = state.queueStats.totalProcessed + 1;
|
const newTotalProcessed = state.queueStats.totalProcessed + 1;
|
||||||
const newAverageTime =
|
const newAverageTime =
|
||||||
(state.queueStats.averageProcessingTime * state.queueStats.totalProcessed + processingTime) /
|
(state.queueStats.averageProcessingTime * state.queueStats.totalProcessed +
|
||||||
|
processingTime) /
|
||||||
newTotalProcessed;
|
newTotalProcessed;
|
||||||
|
|
||||||
console.log('[panelReducer] ✅ QUEUE_ITEM_PROCESSED', {
|
console.log('[panelReducer] ✅ QUEUE_ITEM_PROCESSED', {
|
||||||
@@ -352,7 +354,7 @@ export const panelsReducer = (state = initialState, action) => {
|
|||||||
...action.payload,
|
...action.payload,
|
||||||
status: 'running',
|
status: 'running',
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
queueError: null, // 에러 초기화
|
queueError: null, // 에러 초기화
|
||||||
};
|
};
|
||||||
@@ -381,16 +383,19 @@ export const panelsReducer = (state = initialState, action) => {
|
|||||||
result: action.payload.result,
|
result: action.payload.result,
|
||||||
executionTime,
|
executionTime,
|
||||||
completedAt: action.payload.timestamp,
|
completedAt: action.payload.timestamp,
|
||||||
}
|
},
|
||||||
].slice(-100), // 최근 100개만 유지
|
].slice(-100), // 최근 100개만 유지
|
||||||
queueError: null,
|
queueError: null,
|
||||||
queueStats: {
|
queueStats: {
|
||||||
...state.queueStats,
|
...state.queueStats,
|
||||||
totalProcessed: state.queueStats.totalProcessed + 1,
|
totalProcessed: state.queueStats.totalProcessed + 1,
|
||||||
averageProcessingTime: Math.round(
|
averageProcessingTime:
|
||||||
((state.queueStats.averageProcessingTime * state.queueStats.totalProcessed) + executionTime) /
|
Math.round(
|
||||||
(state.queueStats.totalProcessed + 1) * 100
|
((state.queueStats.averageProcessingTime * state.queueStats.totalProcessed +
|
||||||
) / 100,
|
executionTime) /
|
||||||
|
(state.queueStats.totalProcessed + 1)) *
|
||||||
|
100
|
||||||
|
) / 100,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -419,7 +424,7 @@ export const panelsReducer = (state = initialState, action) => {
|
|||||||
error: action.payload.error,
|
error: action.payload.error,
|
||||||
executionTime,
|
executionTime,
|
||||||
failedAt: action.payload.timestamp,
|
failedAt: action.payload.timestamp,
|
||||||
}
|
},
|
||||||
].slice(-100), // 최근 100개만 유지
|
].slice(-100), // 최근 100개만 유지
|
||||||
queueError: {
|
queueError: {
|
||||||
actionId: action.payload.actionId,
|
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:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,60 @@
|
|||||||
export const SpotlightIds = {
|
export const SpotlightIds = {
|
||||||
TPANEL: "tpanel",
|
TPANEL: 'tpanel',
|
||||||
TBODY: "tbody",
|
TBODY: 'tbody',
|
||||||
TPOPUP: "tpopup",
|
TPOPUP: 'tpopup',
|
||||||
HOME_TBODY: "home_tbody",
|
HOME_TBODY: 'home_tbody',
|
||||||
TITEM_CARD: "tItemCard",
|
TITEM_CARD: 'tItemCard',
|
||||||
TAB_LAYOUT: "tablayout",
|
TAB_LAYOUT: 'tablayout',
|
||||||
// homePanel
|
// homePanel
|
||||||
HOME_CATEGORY_NAV: "homeCategoryNav",
|
HOME_CATEGORY_NAV: 'homeCategoryNav',
|
||||||
|
|
||||||
// FeaturedBrandsPanel
|
// FeaturedBrandsPanel
|
||||||
BRAND_VERTICAL_PAGENATOR: "brandVerticalPagenator",
|
BRAND_VERTICAL_PAGENATOR: 'brandVerticalPagenator',
|
||||||
BRAND_QUICK_MENU: "brandQuickMenu",
|
BRAND_QUICK_MENU: 'brandQuickMenu',
|
||||||
BRAND_TOP_BUTTON: "brandTopButton",
|
BRAND_TOP_BUTTON: 'brandTopButton',
|
||||||
|
|
||||||
// TrendingNowPanel
|
// TrendingNowPanel
|
||||||
TRENDING_NOW_VERTICAL_PAGINATOR: "trendingNowVerticalPaginator",
|
TRENDING_NOW_VERTICAL_PAGINATOR: 'trendingNowVerticalPaginator',
|
||||||
TRENDING_NOW_POPULAR_SHOW: "trendingNowPopularShow",
|
TRENDING_NOW_POPULAR_SHOW: 'trendingNowPopularShow',
|
||||||
TRENDING_NOW_POPULAR_VIDEO: "trendingNowPopularVideo",
|
TRENDING_NOW_POPULAR_VIDEO: 'trendingNowPopularVideo',
|
||||||
TRENDING_NOW_POPULAR_GRID_LIST: "trendingNowVirtualGridList",
|
TRENDING_NOW_POPULAR_GRID_LIST: 'trendingNowVirtualGridList',
|
||||||
TRENDING_NOW_PREV_INDICATOR: "trendingNowPrevIndicator",
|
TRENDING_NOW_PREV_INDICATOR: 'trendingNowPrevIndicator',
|
||||||
TRENDING_NOW_NEXT_INDICATOR: "trendingNowNextIndicator",
|
TRENDING_NOW_NEXT_INDICATOR: 'trendingNowNextIndicator',
|
||||||
TRENDING_NOW_BEST_SELLER: "trendingNowBestSeller",
|
TRENDING_NOW_BEST_SELLER: 'trendingNowBestSeller',
|
||||||
TRENDING_NOW_TOP_BUTTON: "trendingNowTopButton",
|
TRENDING_NOW_TOP_BUTTON: 'trendingNowTopButton',
|
||||||
|
|
||||||
// myPagePanel
|
// myPagePanel
|
||||||
MY_PAGE_FAVORITES_BOX: "myPageFavoritesBox",
|
MY_PAGE_FAVORITES_BOX: 'myPageFavoritesBox',
|
||||||
MY_PAGE_REMINDRES_BOX: "myPageRemindresBox",
|
MY_PAGE_REMINDRES_BOX: 'myPageRemindresBox',
|
||||||
MY_PAGE_MY_ORDER_BOX: "myPageMyOrderBox",
|
MY_PAGE_MY_ORDER_BOX: 'myPageMyOrderBox',
|
||||||
MY_PAGE_MY_ORDER_TAB_CONTAINER: "myPageMyOrderTabContainer",
|
MY_PAGE_MY_ORDER_TAB_CONTAINER: 'myPageMyOrderTabContainer',
|
||||||
|
|
||||||
// categoryPanel
|
// categoryPanel
|
||||||
CATEGORY_CONTENTS_BOX: "categoryContentsBox",
|
CATEGORY_CONTENTS_BOX: 'categoryContentsBox',
|
||||||
CATEGORY_TAB_CONTAINER: "categorytabContainer",
|
CATEGORY_TAB_CONTAINER: 'categorytabContainer',
|
||||||
SHOW_PRODUCTS_BOX: "showProductsBox",
|
SHOW_PRODUCTS_BOX: 'showProductsBox',
|
||||||
SHOW_CONTENTS_BOX: "showContentsBox",
|
SHOW_CONTENTS_BOX: 'showContentsBox',
|
||||||
|
|
||||||
// video player
|
// video player
|
||||||
PLAYER_SKIPINTRO: "skipintro",
|
PLAYER_SKIPINTRO: 'skipintro',
|
||||||
PLAYER_TITLE_LAYER: "playerTitleLayer",
|
PLAYER_TITLE_LAYER: 'playerTitleLayer',
|
||||||
PLAYER_SLIDER: "playerslider",
|
PLAYER_SLIDER: 'playerslider',
|
||||||
PLAYER_TAB_BUTTON: "playerTabArrow",
|
PLAYER_TAB_BUTTON: 'playerTabArrow',
|
||||||
PLAYER_BACK_BUTTON: "player-back-button",
|
PLAYER_BACK_BUTTON: 'player-back-button',
|
||||||
PLAYER_SUBTITLE_BUTTON: "player-subtitlebutton",
|
PLAYER_SUBTITLE_BUTTON: 'player-subtitlebutton',
|
||||||
|
|
||||||
// searchPanel
|
// searchPanel
|
||||||
SEARCH_THEME: "search_theme",
|
SEARCH_THEME: 'search_theme',
|
||||||
SEARCH_SHOW: "search_show",
|
SEARCH_SHOW: 'search_show',
|
||||||
SEARCH_ITEM: "search_item",
|
SEARCH_ITEM: 'search_item',
|
||||||
SEARCH_BESTSELLER: "search_bestseller",
|
SEARCH_BESTSELLER: 'search_bestseller',
|
||||||
SEARCH_TAB_CONTAINER: "searchtabContainer",
|
SEARCH_TAB_CONTAINER: 'searchtabContainer',
|
||||||
|
|
||||||
// pin Code Popup
|
// pin Code Popup
|
||||||
PINCODE_CONTAINER: "pincodeContainer",
|
PINCODE_CONTAINER: 'pincodeContainer',
|
||||||
|
|
||||||
// detailPanel
|
// detailPanel
|
||||||
DETAIL_BUYNOW: "detail_buynow",
|
DETAIL_BUYNOW: 'detail_buynow',
|
||||||
DETAIL_SHOPBYMOBILE: "detail_shop_by_mobile",
|
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(() => {
|
const handleProductAllSectionReady = useCallback(() => {
|
||||||
|
console.log('############## ShopByMobile focus');
|
||||||
const spotTime = setTimeout(() => {
|
const spotTime = setTimeout(() => {
|
||||||
Spotlight.focus(SpotlightIds.DETAIL_SHOPBYMOBILE);
|
// Spotlight.focus(SpotlightIds.DETAIL_SHOPBYMOBILE);
|
||||||
|
Spotlight.focus(SpotlightIds.DETAIL_PRODUCTVIDEO);
|
||||||
}, 100);
|
}, 100);
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(spotTime);
|
clearTimeout(spotTime);
|
||||||
@@ -726,7 +728,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
console.log('[DetailPanel] MediaPanel modal=true detected - focusing ProductVideo');
|
console.log('[DetailPanel] MediaPanel modal=true detected - focusing ProductVideo');
|
||||||
const focusTimer = setTimeout(() => {
|
const focusTimer = setTimeout(() => {
|
||||||
Spotlight.focus('product-video-player');
|
Spotlight.focus('product-video-player');
|
||||||
}, 500);
|
}, 2500);
|
||||||
return () => clearTimeout(focusTimer);
|
return () => clearTimeout(focusTimer);
|
||||||
}
|
}
|
||||||
}, [panels]);
|
}, [panels]);
|
||||||
|
|||||||
@@ -341,7 +341,57 @@ export default function ProductAllSection({
|
|||||||
dispatch(resetShowAllReviews());
|
dispatch(resetShowAllReviews());
|
||||||
}, []); // 빈 dependency array = 마운트 시에만 실행
|
}, []); // 빈 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 훅에서 동일한 기능을 수행하고 있음
|
// 이미 useReviews 훅에서 동일한 기능을 수행하고 있음
|
||||||
// ProductAllSection에서 중복으로 호출하면 UserReviewPanel 진입 시
|
// ProductAllSection에서 중복으로 호출하면 UserReviewPanel 진입 시
|
||||||
// reviewListData가 반복적으로 초기화되어 Chrome에서 진입 불가 발생
|
// reviewListData가 반복적으로 초기화되어 Chrome에서 진입 불가 발생
|
||||||
@@ -859,10 +909,16 @@ export default function ProductAllSection({
|
|||||||
prevMediaPanelModalStateRef.current === false
|
prevMediaPanelModalStateRef.current === false
|
||||||
) {
|
) {
|
||||||
console.log(
|
console.log(
|
||||||
'[ProductAllSection] 🔄 MediaPanel이 전체화면에서 모달로 복귀 - ProductVideo로 포커스 복구'
|
'[ProductAllSection] 🔄 MediaPanel이 전체화면에서 모달로 복귀 - ProductVideo로 포커스 복구 시도'
|
||||||
);
|
);
|
||||||
const focusTimer = setTimeout(() => {
|
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);
|
}, 100);
|
||||||
return () => clearTimeout(focusTimer);
|
return () => clearTimeout(focusTimer);
|
||||||
}
|
}
|
||||||
@@ -1148,6 +1204,7 @@ export default function ProductAllSection({
|
|||||||
onVideoPlaying={() => setIsVideoPlaying(true)}
|
onVideoPlaying={() => setIsVideoPlaying(true)}
|
||||||
onScrollToImages={handleScrollToImagesV1}
|
onScrollToImages={handleScrollToImagesV1}
|
||||||
onFocus={() => console.log('[ProductVideo V1] Focused')}
|
onFocus={() => console.log('[ProductVideo V1] Focused')}
|
||||||
|
data-spotlight-id="product-video-player-container"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ProductVideoV2
|
<ProductVideoV2
|
||||||
|
|||||||
@@ -488,6 +488,7 @@
|
|||||||
display: flex !important;
|
display: flex !important;
|
||||||
align-items: center !important;
|
align-items: center !important;
|
||||||
justify-content: center !important;
|
justify-content: center !important;
|
||||||
|
cursor: default !important;
|
||||||
&.shopByMobileOne {
|
&.shopByMobileOne {
|
||||||
margin: 0 6px 0 0;
|
margin: 0 6px 0 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,9 +75,18 @@ export default function ProductVideo({
|
|||||||
}, [topPanel]);
|
}, [topPanel]);
|
||||||
|
|
||||||
// autoPlay 기능: 컴포넌트 마운트 시 자동으로 비디오 재생
|
// autoPlay 기능: 컴포넌트 마운트 시 자동으로 비디오 재생
|
||||||
|
// 단, 이미 MediaPanel이 modal로 재생 중이면 autoPlay 하지 않음
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoPlay && canPlayVideo && !hasAutoPlayed && productInfo) {
|
// MediaPanel이 이미 modal로 재생 중인지 확인
|
||||||
console.log('[ProductVideo] Auto-playing video');
|
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);
|
setHasAutoPlayed(true);
|
||||||
|
|
||||||
// 짧은 딜레이 후 재생 시작 (컴포넌트 마운트 완료 후)
|
// 짧은 딜레이 후 재생 시작 (컴포넌트 마운트 완료 후)
|
||||||
@@ -121,6 +130,8 @@ export default function ProductVideo({
|
|||||||
dispatch,
|
dispatch,
|
||||||
modalClassNameChange,
|
modalClassNameChange,
|
||||||
continuousPlay,
|
continuousPlay,
|
||||||
|
topPanel,
|
||||||
|
panels,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 비디오 재생 가능 여부 체크
|
// 비디오 재생 가능 여부 체크
|
||||||
|
|||||||
@@ -1,40 +1,20 @@
|
|||||||
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 {
|
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';
|
||||||
|
|
||||||
@@ -186,9 +166,11 @@ export default function YouMayAlsoLike({
|
|||||||
);
|
);
|
||||||
cursorOpen.current.stop();
|
cursorOpen.current.stop();
|
||||||
};
|
};
|
||||||
|
// prdtId가 없는 경우를 대비한 안정적인 key 생성
|
||||||
|
const itemKey = prdtId ? `${patnrId}-${prdtId}` : `product-${index}`;
|
||||||
return (
|
return (
|
||||||
<TItemCardNew
|
<TItemCardNew
|
||||||
key={prdtId}
|
key={itemKey}
|
||||||
className={css.itemCardNew}
|
className={css.itemCardNew}
|
||||||
contextName={LOG_CONTEXT_NAME.YOUMAYLIKE}
|
contextName={LOG_CONTEXT_NAME.YOUMAYLIKE}
|
||||||
messageId={LOG_MESSAGE_ID.CONTENTCLICK}
|
messageId={LOG_MESSAGE_ID.CONTENTCLICK}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
getMainLiveShowNowProduct,
|
getMainLiveShowNowProduct,
|
||||||
} from '../../actions/mainActions';
|
} from '../../actions/mainActions';
|
||||||
import * as PanelActions from '../../actions/panelActions';
|
import * as PanelActions from '../../actions/panelActions';
|
||||||
import { updatePanel } from '../../actions/panelActions';
|
import { updatePanel, focusPanel } from '../../actions/panelActions';
|
||||||
import {
|
import {
|
||||||
CLEAR_PLAYER_INFO,
|
CLEAR_PLAYER_INFO,
|
||||||
getChatLog,
|
getChatLog,
|
||||||
@@ -385,13 +385,14 @@ const MediaPanel = React.forwardRef(
|
|||||||
// },[isOnTop, panelInfo])
|
// },[isOnTop, panelInfo])
|
||||||
|
|
||||||
// MediaPanel.jsx의 라인 313-327 useEffect 수정
|
// MediaPanel.jsx의 라인 313-327 useEffect 수정
|
||||||
|
// MEDIA 타입은 isOnTop 체크 하지 않음 (패널 구조 복잡도 때문에)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('[MediaPanel] isOnTop:', {
|
console.log('[MediaPanel] isOnTop:', {
|
||||||
isOnTop,
|
isOnTop,
|
||||||
panelInfo,
|
panelInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (panelInfo && panelInfo.modal) {
|
if (panelInfo && panelInfo.modal && panelInfo?.shptmBanrTpNm !== 'MEDIA') {
|
||||||
if (!isOnTop) {
|
if (!isOnTop) {
|
||||||
console.log('[MediaPanel] Not on top - pausing video');
|
console.log('[MediaPanel] Not on top - pausing video');
|
||||||
dispatch(pauseModalMedia());
|
dispatch(pauseModalMedia());
|
||||||
@@ -875,7 +876,7 @@ const MediaPanel = React.forwardRef(
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onClickBack = useCallback(
|
const handleClickBack = useCallback(
|
||||||
(ev, isEnd) => {
|
(ev, isEnd) => {
|
||||||
//modal로부터 Full 전환된 경우 다시 preview 모드로 돌아감.
|
//modal로부터 Full 전환된 경우 다시 preview 모드로 돌아감.
|
||||||
if (panelInfo.modalContainerId && !panelInfo.modal) {
|
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'
|
'[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?.stopPropagation();
|
||||||
// ev?.preventDefault();
|
// ev?.preventDefault();
|
||||||
return;
|
return;
|
||||||
@@ -921,6 +927,7 @@ const MediaPanel = React.forwardRef(
|
|||||||
}
|
}
|
||||||
ev?.stopPropagation();
|
ev?.stopPropagation();
|
||||||
// ev?.preventDefault();
|
// ev?.preventDefault();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1036,8 +1043,9 @@ const MediaPanel = React.forwardRef(
|
|||||||
}, [topPanel]);
|
}, [topPanel]);
|
||||||
|
|
||||||
const cannotPlay = useMemo(() => {
|
const cannotPlay = useMemo(() => {
|
||||||
return !isOnTop && topPanel?.name === panel_names.MEDIA_PANEL;
|
// URL이 있으면 항상 재생 가능 (isOnTop 조건 제거)
|
||||||
}, [topPanel, isOnTop]);
|
return false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const getPlayer = useCallback((ref) => {
|
const getPlayer = useCallback((ref) => {
|
||||||
videoPlayer.current = ref;
|
videoPlayer.current = ref;
|
||||||
@@ -1678,8 +1686,24 @@ const MediaPanel = React.forwardRef(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return playListInfo && playListInfo[selectedIndex]?.showUrl;
|
// MEDIA 타입일 때: panelInfo.showUrl 사용
|
||||||
}, [playListInfo, selectedIndex, broadcast]);
|
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(() => {
|
const isYoutube = useMemo(() => {
|
||||||
if (currentPlayingUrl && currentPlayingUrl.includes('youtu')) {
|
if (currentPlayingUrl && currentPlayingUrl.includes('youtu')) {
|
||||||
@@ -1698,11 +1722,17 @@ const MediaPanel = React.forwardRef(
|
|||||||
|
|
||||||
const isReadyToPlay = useMemo(() => {
|
const isReadyToPlay = useMemo(() => {
|
||||||
if (!currentPlayingUrl) {
|
if (!currentPlayingUrl) {
|
||||||
|
console.log('[MediaPanel]-LoadingVideo ❌ isReadyToPlay = false (no URL)');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!Config.DEBUG_VIDEO_SUBTITLE_TEST && currentSubtitleUrl && !currentSubtitleBlob) {
|
if (!Config.DEBUG_VIDEO_SUBTITLE_TEST && currentSubtitleUrl && !currentSubtitleBlob) {
|
||||||
|
console.log('[MediaPanel]-LoadingVideo ❌ isReadyToPlay = false (subtitle not loaded):', {
|
||||||
|
currentSubtitleUrl,
|
||||||
|
currentSubtitleBlob: !!currentSubtitleBlob,
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
console.log('[MediaPanel]-LoadingVideo ✅ isReadyToPlay = true');
|
||||||
return true;
|
return true;
|
||||||
}, [currentPlayingUrl, currentSubtitleUrl, currentSubtitleBlob, broadcast]);
|
}, [currentPlayingUrl, currentSubtitleUrl, currentSubtitleBlob, broadcast]);
|
||||||
|
|
||||||
@@ -2168,7 +2198,7 @@ const MediaPanel = React.forwardRef(
|
|||||||
isTabActivated={false}
|
isTabActivated={false}
|
||||||
{...props}
|
{...props}
|
||||||
className={containerClassName}
|
className={containerClassName}
|
||||||
handleCancel={onClickBack}
|
handleCancel={handleClickBack}
|
||||||
spotlightId={spotlightId}
|
spotlightId={spotlightId}
|
||||||
>
|
>
|
||||||
<Container
|
<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 && (
|
{isReadyToPlay && (
|
||||||
<div className={css.videoFrame}>
|
<div className={css.videoFrame}>
|
||||||
<VideoPlayer
|
<VideoPlayer
|
||||||
setApiProvider={getPlayer}
|
setApiProvider={getPlayer}
|
||||||
disabled={panelInfo.modal}
|
disabled={panelInfo.modal}
|
||||||
onEnded={onEnded}
|
onEnded={onEnded}
|
||||||
noAutoPlay={cannotPlay}
|
noAutoPlay={false}
|
||||||
autoCloseTimeout={3000}
|
autoCloseTimeout={3000}
|
||||||
onBackButton={onClickBack}
|
onBackButton={handleClickBack}
|
||||||
spotlightDisabled={false}
|
spotlightDisabled={false}
|
||||||
isYoutube={isYoutube}
|
isYoutube={isYoutube}
|
||||||
src={currentPlayingUrl}
|
src={currentPlayingUrl}
|
||||||
|
|||||||
Reference in New Issue
Block a user