Files
shoptime/com.twin.app.shoptime/src/actions/panelActions.js
optrader 251e1ee3d4 [251122] fix: Comment정리-1
🕐 커밋 시간: 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: 코드 최적화로 성능 개선 기대
2025-11-22 18:19:45 +09:00

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);
};
};