🕐 커밋 시간: 2025. 11. 22. 18:19:45 📊 변경 통계: • 총 파일: 10개 • 추가: +150줄 • 삭제: -152줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/App/App.js ~ com.twin.app.shoptime/src/actions/panelActions.js ~ com.twin.app.shoptime/src/middleware/panelHistoryMiddleware.js ~ com.twin.app.shoptime/src/utils/ImagePreloader.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/components/DetailPanelBackground/DetailPanelBackground.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.v2.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.jsx ~ com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/App/App.js (javascript): ❌ Deleted: resolveSpotlightIdFromEvent() 📄 com.twin.app.shoptime/src/actions/panelActions.js (javascript): 🔄 Modified: resetPanels() 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx (javascript): 🔄 Modified: extractProductMeta() 📄 com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.v2.jsx (javascript): ❌ Deleted: logDetailPanelInit(), logImageLoaded(), logImageError() 📄 com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.jsx (javascript): 🔄 Modified: Spottable() 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • 공통 유틸리티 함수 최적화 • UI 컴포넌트 아키텍처 개선 Performance: 코드 최적화로 성능 개선 기대
614 lines
18 KiB
JavaScript
614 lines
18 KiB
JavaScript
import { types } from './actionTypes';
|
|
import Spotlight from '@enact/spotlight';
|
|
import { getContainerId } from '@enact/spotlight/src/container';
|
|
import { panel_names } from '../utils/Config';
|
|
import { updateHomeInfo } from './homeActions';
|
|
|
|
// 시작 메뉴 추적을 위한 상수
|
|
export const SOURCE_MENUS = {
|
|
HOME_BEST_SELLER: 'home_best_seller',
|
|
HOME_PICKED_FOR_YOU: 'home_picked_for_you',
|
|
HOME_SUB_CATEGORY: 'home_sub_category',
|
|
HOME_RANDOM_UNIT: 'home_random_unit',
|
|
HOME_ROLLING_UNIT: 'home_rolling_unit',
|
|
HOME_EVENT_POPUP: 'home_event_popup',
|
|
HOME_TODAYS_DEAL: 'home_todays_deal',
|
|
SEARCH_RESULT: 'search_result',
|
|
HOME_GENERAL: 'home_general',
|
|
THEMED_PRODUCT: 'themed_product',
|
|
GENERAL_PRODUCT: 'general_product',
|
|
PLAYER_SHOP_NOW: 'player_shop_now', // PlayerPanel의 ShopNow에서 진입
|
|
PLAYER_MEDIA: 'player_media', // PlayerPanel의 Media에서 진입
|
|
};
|
|
|
|
/*
|
|
name: panel_names.PLAYER_PANEL,
|
|
panelInfo: {
|
|
modal: true //only for video player
|
|
etc...
|
|
},
|
|
*/
|
|
export const pushPanel = (panel, duplicatable = false) => ({
|
|
type: types.PUSH_PANEL,
|
|
payload: panel,
|
|
duplicatable: duplicatable,
|
|
});
|
|
|
|
export const popPanel = (panelName) => ({
|
|
type: types.POP_PANEL,
|
|
payload: panelName,
|
|
});
|
|
|
|
export const updatePanel = (panelInfo) => ({
|
|
type: types.UPDATE_PANEL,
|
|
payload: panelInfo,
|
|
});
|
|
|
|
export const resetPanels = (panels) => ({
|
|
type: types.RESET_PANELS,
|
|
payload: panels,
|
|
});
|
|
|
|
/**
|
|
* DetailPanel로 이동하는 공통 액션 함수
|
|
* @param {Object} params - 이동 파라미터
|
|
* @param {string} params.patnrId - 파트너 ID
|
|
* @param {string} params.prdtId - 상품 ID
|
|
* @param {string} [params.curationId] - 큐레이션 ID (테마 상품인 경우)
|
|
* @param {string} [params.nowShelf] - 현재 셸프 ID
|
|
* @param {string} [params.type] - 상품 타입 ('theme' 등)
|
|
* @param {string} [params.sourceMenu] - 시작 메뉴 (SOURCE_MENUS 상수 사용)
|
|
* @param {Object} [params.additionalInfo] - 추가 정보
|
|
* @returns {Function} Redux thunk 함수
|
|
*/
|
|
export const navigateToDetail = ({
|
|
patnrId,
|
|
prdtId,
|
|
curationId,
|
|
nowShelf,
|
|
type,
|
|
sourceMenu,
|
|
additionalInfo = {},
|
|
}) => {
|
|
return (dispatch, getState) => {
|
|
// 🔽 현재 포커스 정보 저장 (HomePanel 복귀 시 포커스 복원용)
|
|
const currentSpotNode = Spotlight.getCurrent();
|
|
const currentSpotId = currentSpotNode?.getAttribute('data-spotlight-id');
|
|
const currentContainerId = currentSpotNode ? getContainerId(currentSpotNode) : null;
|
|
const focusSnapshot = currentSpotId
|
|
? {
|
|
lastFocusedTargetId: currentContainerId || currentSpotId,
|
|
currentSpot: currentSpotId,
|
|
}
|
|
: {};
|
|
|
|
const panelInfo = {
|
|
patnrId,
|
|
prdtId,
|
|
...additionalInfo,
|
|
};
|
|
|
|
// 선택적 파라미터들 추가
|
|
if (curationId) panelInfo.curationId = curationId;
|
|
if (nowShelf) panelInfo.nowShelf = nowShelf;
|
|
if (type) panelInfo.type = type;
|
|
if (sourceMenu) panelInfo.sourceMenu = sourceMenu;
|
|
|
|
// 로깅
|
|
console.log(`[navigateToDetail] ${sourceMenu || 'unknown'} → DetailPanel`, {
|
|
patnrId,
|
|
prdtId,
|
|
curationId,
|
|
nowShelf,
|
|
type,
|
|
sourceMenu,
|
|
timestamp: Date.now(),
|
|
});
|
|
|
|
// ✅ 그라데이션 배경은 HomePanel 내부 switch 문에서 처리
|
|
|
|
// sourceMenu에 따른 사전 처리
|
|
switch (sourceMenu) {
|
|
case SOURCE_MENUS.HOME_BEST_SELLER:
|
|
case SOURCE_MENUS.HOME_PICKED_FOR_YOU:
|
|
case SOURCE_MENUS.HOME_SUB_CATEGORY:
|
|
case SOURCE_MENUS.HOME_EVENT_POPUP:
|
|
case SOURCE_MENUS.HOME_TODAYS_DEAL:
|
|
case SOURCE_MENUS.HOME_RANDOM_UNIT:
|
|
case SOURCE_MENUS.HOME_ROLLING_UNIT:
|
|
case SOURCE_MENUS.HOME_GENERAL: {
|
|
// ✅ 그라데이션 배경 표시 - HomePanel→DetailPanel 전환 시 (PlayerPanel 출신 제외)
|
|
|
|
if (!panelInfo.launchedFromPlayer) {
|
|
dispatch(
|
|
updateHomeInfo({
|
|
name: panel_names.HOME_PANEL,
|
|
panelInfo: {
|
|
showGradientBackground: true,
|
|
},
|
|
})
|
|
);
|
|
// console.log('[TRACE-GRADIENT] 🟢 navigateToDetail set showGradientBackground: true - source:', sourceMenu);
|
|
} else {
|
|
console.log(
|
|
'[TRACE-GRADIENT] 🔵 navigateToDetail skipped gradient - launchedFromPlayer: true'
|
|
);
|
|
}
|
|
|
|
// HomePanel Redux 상태에 포커스 스냅샷 저장 (Detail→Home 복귀 시 사용)
|
|
if (Object.keys(focusSnapshot).length > 0) {
|
|
dispatch(
|
|
updateHomeInfo({
|
|
name: panel_names.HOME_PANEL,
|
|
panelInfo: {
|
|
...focusSnapshot,
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
// 🔽 모든 HomePanel에서 DetailPanel로 이동 시 HomeBanner modal 비디오 정지
|
|
const state = getState();
|
|
const playerPanelInfo = state.panels.panels.find(
|
|
(p) => p.name === panel_names.PLAYER_PANEL
|
|
);
|
|
|
|
// playerPanel이 없는 경우 비디오 정지 로직 건너뛰기
|
|
if (!playerPanelInfo) {
|
|
// 비디오가 없어도 HomePanel 상태 저장
|
|
dispatch(
|
|
updatePanel({
|
|
name: panel_names.HOME_PANEL,
|
|
panelInfo: {
|
|
lastSelectedProduct: { patnrId, prdtId },
|
|
lastActionSource: sourceMenu,
|
|
...focusSnapshot,
|
|
...additionalInfo,
|
|
},
|
|
})
|
|
);
|
|
panelInfo.fromHome = true;
|
|
break;
|
|
}
|
|
|
|
const isCurrentBannerVideoPlaying = playerPanelInfo.panelInfo?.modal !== false;
|
|
|
|
// HomeBanner의 modal=true 비디오가 재생 중이면 정지
|
|
if (isCurrentBannerVideoPlaying) {
|
|
// 🔽 비디오 상태 저장 후 정지
|
|
const { finishVideoPreview } = require('./playActions');
|
|
|
|
// 비디오 복원을 위한 상태 저장
|
|
const videoStateToRestore = {
|
|
...playerPanelInfo.panelInfo,
|
|
wasPlaying: true,
|
|
restoreOnBack: true,
|
|
sourceMenu,
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
// HomePanel에 비디오 복원 상태 저장
|
|
dispatch(
|
|
updatePanel({
|
|
name: panel_names.HOME_PANEL,
|
|
panelInfo: {
|
|
videoStateToRestore,
|
|
lastSelectedProduct: { patnrId, prdtId },
|
|
lastActionSource: sourceMenu,
|
|
...focusSnapshot,
|
|
...additionalInfo,
|
|
},
|
|
})
|
|
);
|
|
|
|
// 비디오 상태 저장 후 정지 (로그는 개발 시 필요 시 주석 해제)
|
|
|
|
dispatch(finishVideoPreview());
|
|
} else {
|
|
// 비디오가 재생 중이 아니어도 HomePanel 상태 저장
|
|
dispatch(
|
|
updatePanel({
|
|
name: panel_names.HOME_PANEL,
|
|
panelInfo: {
|
|
lastSelectedProduct: { patnrId, prdtId },
|
|
lastActionSource: sourceMenu,
|
|
...focusSnapshot,
|
|
...additionalInfo,
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
// HomePanel 내부 컴포넌트들: 기본 HomePanel 상태 저장
|
|
dispatch(
|
|
updatePanel({
|
|
name: panel_names.HOME_PANEL,
|
|
panelInfo: {
|
|
lastSelectedProduct: { patnrId, prdtId },
|
|
lastActionSource: sourceMenu,
|
|
...focusSnapshot,
|
|
...additionalInfo,
|
|
},
|
|
})
|
|
);
|
|
panelInfo.sourcePanel = panel_names.HOME_PANEL; // ✅ source panel 정보
|
|
panelInfo.fromHome = true;
|
|
break;
|
|
}
|
|
|
|
case SOURCE_MENUS.SEARCH_RESULT:
|
|
// Search: 현재 패널 상태 저장 (updatePanel)
|
|
if (additionalInfo.searchVal && additionalInfo.currentSpot) {
|
|
dispatch(
|
|
updatePanel({
|
|
name: panel_names.SEARCH_PANEL,
|
|
panelInfo: {
|
|
searchVal: additionalInfo.searchVal,
|
|
currentSpot: additionalInfo.currentSpot,
|
|
tab: additionalInfo.tab || 0,
|
|
},
|
|
})
|
|
);
|
|
}
|
|
panelInfo.sourcePanel = panel_names.SEARCH_PANEL; // ✅ source panel 정보
|
|
panelInfo.fromSearch = true;
|
|
panelInfo.searchQuery = additionalInfo.searchVal;
|
|
break;
|
|
|
|
case SOURCE_MENUS.THEMED_PRODUCT:
|
|
// 테마 상품: 별도 처리 필요할 경우
|
|
panelInfo.sourcePanel = panel_names.HOME_PANEL; // ✅ source panel 정보 (HOME으로 간주)
|
|
break;
|
|
|
|
case SOURCE_MENUS.PLAYER_SHOP_NOW:
|
|
case SOURCE_MENUS.PLAYER_MEDIA: {
|
|
// PlayerPanel에서 온 경우
|
|
const { hidePlayerOverlays } = require('./videoPlayActions');
|
|
|
|
// DetailPanel push 전에 VideoPlayer 오버레이 숨김
|
|
dispatch(hidePlayerOverlays());
|
|
|
|
// 현재 포커스된 요소 저장
|
|
if (Object.keys(focusSnapshot).length > 0) {
|
|
panelInfo.lastFocusedTargetId = focusSnapshot.lastFocusedTargetId;
|
|
}
|
|
|
|
// PlayerPanel 정보 보존 (복귀 시 필요)
|
|
panelInfo.sourcePanel = panel_names.PLAYER_PANEL; // ✅ source panel 정보
|
|
panelInfo.fromPlayer = true;
|
|
break;
|
|
}
|
|
|
|
case SOURCE_MENUS.GENERAL_PRODUCT:
|
|
default:
|
|
// 일반 상품: 기본 처리
|
|
break;
|
|
}
|
|
|
|
// DetailPanel push
|
|
dispatch(
|
|
pushPanel({
|
|
name: panel_names.DETAIL_PANEL,
|
|
panelInfo,
|
|
})
|
|
);
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 테마 상품을 위한 DetailPanel 이동 헬퍼 함수
|
|
* @param {Object} params - 이동 파라미터
|
|
* @returns {Function} Redux thunk
|
|
*/
|
|
export const navigateToThemeDetail = ({
|
|
patnrId,
|
|
prdtId,
|
|
curationId,
|
|
sourceMenu = SOURCE_MENUS.THEMED_PRODUCT,
|
|
...additionalInfo
|
|
}) => {
|
|
return navigateToDetail({
|
|
patnrId,
|
|
prdtId,
|
|
curationId,
|
|
type: 'theme',
|
|
sourceMenu,
|
|
...additionalInfo,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 홈패널 BestSeller에서 DetailPanel로 이동
|
|
* @param {Object} params - 상품 정보
|
|
* @returns {Function} Redux thunk
|
|
*/
|
|
export const navigateFromBestSeller = ({ patnrId, prdtId, spotlightId }) => {
|
|
return navigateToDetail({
|
|
patnrId,
|
|
prdtId,
|
|
nowShelf: spotlightId,
|
|
sourceMenu: SOURCE_MENUS.HOME_BEST_SELLER,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 홈패널 PickedForYou에서 DetailPanel로 이동
|
|
* @param {Object} params - 상품 정보
|
|
* @returns {Function} Redux thunk
|
|
*/
|
|
export const navigateFromPickedForYou = ({ patnrId, prdtId, spotlightId }) => {
|
|
return navigateToDetail({
|
|
patnrId,
|
|
prdtId,
|
|
nowShelf: spotlightId,
|
|
sourceMenu: SOURCE_MENUS.HOME_PICKED_FOR_YOU,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 홈패널 SubCategory에서 DetailPanel로 이동
|
|
* @param {Object} params - 상품 정보
|
|
* @returns {Function} Redux thunk
|
|
*/
|
|
export const navigateFromSubCategory = ({ patnrId, prdtId, spotlightId }) => {
|
|
return navigateToDetail({
|
|
patnrId,
|
|
prdtId,
|
|
nowShelf: spotlightId,
|
|
sourceMenu: SOURCE_MENUS.HOME_SUB_CATEGORY,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 홈패널 RandomUnit 배너에서 DetailPanel로 이동
|
|
* @param {Object} params - 상품 정보
|
|
* @returns {Function} Redux thunk
|
|
*/
|
|
export const navigateFromRandomUnit = ({ patnrId, prdtId, curationId, type = 'product' }) => {
|
|
return navigateToDetail({
|
|
patnrId,
|
|
prdtId,
|
|
curationId,
|
|
type: type === 'theme' ? 'theme' : undefined,
|
|
sourceMenu: SOURCE_MENUS.HOME_RANDOM_UNIT,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 홈패널 RollingUnit 배너에서 DetailPanel로 이동
|
|
* @param {Object} params - 상품 정보
|
|
* @returns {Function} Redux thunk
|
|
*/
|
|
export const navigateFromRollingUnit = ({ patnrId, prdtId, curationId, additionalInfo = {} }) => {
|
|
return navigateToDetail({
|
|
patnrId,
|
|
prdtId,
|
|
curationId,
|
|
sourceMenu: SOURCE_MENUS.HOME_ROLLING_UNIT,
|
|
...additionalInfo,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 홈패널 EventPopUpBanner에서 DetailPanel로 이동
|
|
* @param {Object} params - 상품 정보
|
|
* @returns {Function} Redux thunk
|
|
*/
|
|
export const navigateFromEventPopup = ({ patnrId, prdtId }) => {
|
|
return navigateToDetail({
|
|
patnrId,
|
|
prdtId,
|
|
sourceMenu: SOURCE_MENUS.HOME_EVENT_POPUP,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* SearchPanel에서 DetailPanel로 이동
|
|
* @param {Object} params - 검색 및 상품 정보
|
|
* @returns {Function} Redux thunk
|
|
*/
|
|
export const navigateFromSearch = ({
|
|
patnrId,
|
|
prdtId,
|
|
searchQuery,
|
|
currentSpot,
|
|
additionalInfo = {},
|
|
}) => {
|
|
return navigateToDetail({
|
|
patnrId,
|
|
prdtId,
|
|
sourceMenu: SOURCE_MENUS.SEARCH_RESULT,
|
|
additionalInfo: {
|
|
searchVal: searchQuery,
|
|
currentSpot,
|
|
tab: 0,
|
|
...additionalInfo,
|
|
},
|
|
});
|
|
};
|
|
|
|
/**
|
|
* HomePanel 일반 클릭에서 DetailPanel로 이동
|
|
* @param {Object} params - 상품 정보
|
|
* @returns {Function} Redux thunk
|
|
*/
|
|
export const navigateFromHomeGeneral = ({ patnrId, prdtId, additionalInfo = {} }) => {
|
|
return navigateToDetail({
|
|
patnrId,
|
|
prdtId,
|
|
sourceMenu: SOURCE_MENUS.HOME_GENERAL,
|
|
additionalInfo,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* DetailPanel에서 돌아올 때 비디오 복원 함수
|
|
* HomePanel에 저장된 비디오 상태를 확인하고 복원
|
|
* @returns {Function} Redux thunk
|
|
*/
|
|
export const restoreVideoOnBack = () => {
|
|
return (dispatch, getState) => {
|
|
const state = getState();
|
|
const panels = state.panels.panels;
|
|
|
|
// HomePanel 찾기
|
|
const homePanel = panels.find((p) => p.name === panel_names.HOME_PANEL);
|
|
const videoStateToRestore = homePanel?.panelInfo?.videoStateToRestore;
|
|
|
|
if (!videoStateToRestore || !videoStateToRestore.restoreOnBack) {
|
|
return;
|
|
}
|
|
|
|
// 비디오 복원 시작 (로그는 개발 시 필요 시 주석 해제)
|
|
|
|
// 비디오 상태 복원
|
|
const { startVideoPlayerNew } = require('./playActions');
|
|
|
|
// 복원할 비디오 정보 추출
|
|
const restoreInfo = {
|
|
bannerId: videoStateToRestore.bannerId || videoStateToRestore.playerState?.currentBannerId,
|
|
patnrId: videoStateToRestore.patnrId,
|
|
showId: videoStateToRestore.showId,
|
|
showUrl: videoStateToRestore.showUrl,
|
|
shptmBanrTpNm: videoStateToRestore.shptmBanrTpNm,
|
|
lgCatCd: videoStateToRestore.lgCatCd,
|
|
modal: true, // HomeBanner는 항상 modal
|
|
modalContainerId: videoStateToRestore.modalContainerId,
|
|
modalClassName: videoStateToRestore.modalClassName,
|
|
chanId: videoStateToRestore.chanId,
|
|
};
|
|
|
|
// 비디오 재생 시작
|
|
dispatch(
|
|
startVideoPlayerNew({
|
|
...restoreInfo,
|
|
spotlightDisable: false,
|
|
})
|
|
);
|
|
|
|
// 복원 상태 정리
|
|
dispatch(
|
|
updatePanel({
|
|
name: panel_names.HOME_PANEL,
|
|
panelInfo: {
|
|
...homePanel.panelInfo,
|
|
videoStateToRestore: {
|
|
...videoStateToRestore,
|
|
restoreOnBack: false, // 복원 완료 후 플래그 초기화
|
|
},
|
|
},
|
|
})
|
|
);
|
|
};
|
|
};
|
|
|
|
/**
|
|
* DetailPanel 닫기 시 비디오 복원 확인 함수
|
|
* DetailPanel 패널이 제거될 때 자동으로 비디오 복원 시도
|
|
* @returns {Function} Redux thunk
|
|
*/
|
|
export const handleDetailPanelCloseWithVideoRestore = () => {
|
|
return (dispatch, getState) => {
|
|
const state = getState();
|
|
const panels = state.panels.panels;
|
|
|
|
// 현재 최상단 패널이 DetailPanel인지 확인
|
|
const topPanel = panels[panels.length - 1];
|
|
|
|
if (topPanel?.name === panel_names.DETAIL_PANEL) {
|
|
// 기존 DetailPanel 닫기 로직 수행
|
|
dispatch({
|
|
type: 'POP_PANEL_WITH_VIDEO_RESTORE',
|
|
payload: panel_names.DETAIL_PANEL,
|
|
});
|
|
|
|
// 비디오 복원 시도 (약간의 지연 후)
|
|
setTimeout(() => {
|
|
dispatch(restoreVideoOnBack());
|
|
}, 100);
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* [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);
|
|
};
|
|
};
|