[251025] feat: usePanelHistory && panelHistoryMiddleware
🕐 커밋 시간: 2025. 10. 25. 09:46:43 📊 변경 통계: • 총 파일: 8개 • 추가: +195줄 • 삭제: -84줄 📁 추가된 파일: + com.twin.app.shoptime/src/actions/panelNavigationActions.js 📝 수정된 파일: ~ com.twin.app.shoptime/src/App/App.js ~ com.twin.app.shoptime/src/actions/panelHistoryActions.js ~ com.twin.app.shoptime/src/hooks/usePanelHistory/usePanelHistory.js ~ com.twin.app.shoptime/src/middleware/panelHistoryMiddleware.js ~ com.twin.app.shoptime/src/reducers/panelHistoryReducer.js ~ com.twin.app.shoptime/src/views/HomePanel/BestSeller/BestSeller.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/SearchResultsNew/ItemCard.jsx 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • 중간 규모 기능 개선 • 모듈 구조 개선
This commit is contained in:
@@ -42,6 +42,7 @@ import { getShoptimeTerms } from '../actions/empActions';
|
||||
import { getHomeMenu, getHomeTerms } from '../actions/homeActions';
|
||||
import { getMyRecommandedKeyword, getMyUpcomingAlertShow } from '../actions/myPageActions';
|
||||
import { pushPanel } from '../actions/panelActions';
|
||||
import { enqueuePanelHistory } from '../actions/panelHistoryActions';
|
||||
import NotSupportedVersion from '../components/NotSupportedVersion/NotSupportedVersion';
|
||||
import ToastContainer from '../components/TToast/ToastContainer';
|
||||
import usePrevious from '../hooks/usePrevious';
|
||||
@@ -387,6 +388,17 @@ function AppBase(props) {
|
||||
console.log('[App.js] initService,httpHeaderRef.current', httpHeaderRef.current);
|
||||
console.log('[App.js] haveyInit', haveyInit);
|
||||
|
||||
// 앱 초기화 시 HomePanel 자동 기록
|
||||
console.log('[App.js] Recording initial HomePanel on app start');
|
||||
dispatch(enqueuePanelHistory(
|
||||
'homepanel',
|
||||
{},
|
||||
'APP_INITIALIZE',
|
||||
new Date().toISOString(),
|
||||
false, // fromGNB: false (앱 초기화)
|
||||
false // fromResetPanel: false (앱 초기화)
|
||||
));
|
||||
|
||||
if (httpHeaderRef.current) {
|
||||
if (haveyInit) {
|
||||
dispatch(changeAppStatus({ connectionFailed: false }));
|
||||
|
||||
@@ -9,16 +9,21 @@ import { types } from './actionTypes';
|
||||
* Panel을 히스토리에 추가
|
||||
* @param {string} panelName - Panel 이름
|
||||
* @param {Object} panelInfo - Panel 상태 정보
|
||||
* @param {string} action - 발생한 Redux 액션 ('PUSH' | 'POP' | 'UPDATE' | 'RESET')
|
||||
* @param {string} action - 발생한 Redux 액션 ('PUSH' | 'POP' | 'UPDATE' | 'RESET' | 'APP_INITIALIZE')
|
||||
* @param {string} timestamp - 타임스탬프 (선택사항)
|
||||
* @param {boolean} fromGNB - GNB를 통해 이동했는지 여부
|
||||
* @param {boolean} fromResetPanel - RESET_PANELS를 통해 이동했는지 여부
|
||||
* @returns {Object} Redux action
|
||||
*/
|
||||
export const enqueuePanelHistory = (panelName, panelInfo = {}, action = 'PUSH') => ({
|
||||
export const enqueuePanelHistory = (panelName, panelInfo = {}, action = 'PUSH', timestamp, fromGNB = false, fromResetPanel = false) => ({
|
||||
type: types.ENQUEUE_PANEL_HISTORY,
|
||||
payload: {
|
||||
panelName,
|
||||
panelInfo,
|
||||
action,
|
||||
timestamp: new Date().toISOString(),
|
||||
timestamp: timestamp || new Date().toISOString(),
|
||||
fromGNB,
|
||||
fromResetPanel,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
185
com.twin.app.shoptime/src/actions/panelNavigationActions.js
Normal file
185
com.twin.app.shoptime/src/actions/panelNavigationActions.js
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* src/actions/panelNavigationActions.js
|
||||
* Panel navigation 순차 처리를 위한 액션 크리에이터
|
||||
*
|
||||
* Chrome 68 호환성을 위한 callback-free 순차 네비게이션
|
||||
*/
|
||||
|
||||
import { pushPanel, updatePanel } from './panelActions';
|
||||
import { panel_names } from '../utils/Config';
|
||||
|
||||
/**
|
||||
* 상품 클릭 시 순차 네비게이션 (Search → Detail)
|
||||
* @param {string} patnrId - 파트너 ID
|
||||
* @param {string} prdtId - 상품 ID
|
||||
* @param {string} searchQuery - 검색어
|
||||
* @param {string} currentSpot - 현재 spotlight ID
|
||||
* @param {Object} additionalInfo - 추가 패널 정보
|
||||
* @returns {Function} Redux thunk function
|
||||
*/
|
||||
export const navigateToDetailPanel = (
|
||||
patnrId,
|
||||
prdtId,
|
||||
searchQuery,
|
||||
currentSpot,
|
||||
additionalInfo = {}
|
||||
) => (dispatch, getState) => {
|
||||
// 현재 상태에서 lastPanelAction 카운트 저장
|
||||
const currentActionCount = getState().panels.lastPanelAction || 0;
|
||||
|
||||
console.log('[PanelNavigation] Starting navigation to detail:', {
|
||||
patnrId,
|
||||
prdtId,
|
||||
searchQuery,
|
||||
currentSpot,
|
||||
currentActionCount
|
||||
});
|
||||
|
||||
// 1. 먼저 현재 패널(예: SearchPanel) 업데이트
|
||||
dispatch(updatePanel({
|
||||
name: panel_names.SEARCH_PANEL,
|
||||
panelInfo: {
|
||||
searchVal: searchQuery,
|
||||
currentSpot,
|
||||
tab: 0,
|
||||
...additionalInfo
|
||||
}
|
||||
}));
|
||||
|
||||
// 2. Redux store 구독하여 상태 변화 감지
|
||||
// 직접 store 접근 대신 타이머 기반 방식 사용 (Chrome 68 호환)
|
||||
const storeUnsubscribe = (() => {
|
||||
let isUnsubscribed = false;
|
||||
|
||||
const checkStateChange = () => {
|
||||
if (isUnsubscribed) return;
|
||||
|
||||
const newState = getState();
|
||||
const newActionCount = newState.panels.lastPanelAction || 0;
|
||||
|
||||
// updatePanel이 완료되면 (action count가 변경되면)
|
||||
if (newActionCount !== currentActionCount) {
|
||||
console.log('[PanelNavigation] UpdatePanel completed, pushing DetailPanel');
|
||||
|
||||
// 구독 해제
|
||||
isUnsubscribed = true;
|
||||
|
||||
// 3. DetailPanel push
|
||||
dispatch(pushPanel({
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: {
|
||||
patnrId,
|
||||
prdtId,
|
||||
fromSearch: true,
|
||||
searchQuery,
|
||||
...additionalInfo
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// 즉시 한번 확인하고, 그 후 주기적으로 확인
|
||||
setTimeout(checkStateChange, 0);
|
||||
const intervalId = setInterval(checkStateChange, 16); // 60fps
|
||||
|
||||
return () => {
|
||||
isUnsubscribed = true;
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
})();
|
||||
|
||||
// 타임아웃 방어 (최대 1초 대기)
|
||||
setTimeout(() => {
|
||||
storeUnsubscribe();
|
||||
console.log('[PanelNavigation] Timeout fallback, pushing DetailPanel');
|
||||
|
||||
dispatch(pushPanel({
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: {
|
||||
patnrId,
|
||||
prdtId,
|
||||
fromSearch: true,
|
||||
searchQuery,
|
||||
...additionalInfo
|
||||
}
|
||||
}));
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
* HomePanel에서 상품 클릭 시 순차 네비게이션
|
||||
* @param {string} patnrId - 파트너 ID
|
||||
* @param {string} prdtId - 상품 ID
|
||||
* @param {Object} additionalInfo - 추가 패널 정보
|
||||
* @returns {Function} Redux thunk function
|
||||
*/
|
||||
export const navigateToDetailFromHome = (
|
||||
patnrId,
|
||||
prdtId,
|
||||
additionalInfo = {}
|
||||
) => (dispatch, getState) => {
|
||||
const currentActionCount = getState().panels.lastPanelAction || 0;
|
||||
|
||||
console.log('[PanelNavigation] Starting navigation from home:', {
|
||||
patnrId,
|
||||
prdtId,
|
||||
currentActionCount
|
||||
});
|
||||
|
||||
dispatch(updatePanel({
|
||||
name: panel_names.HOME_PANEL,
|
||||
panelInfo: {
|
||||
lastSelectedProduct: { patnrId, prdtId },
|
||||
...additionalInfo
|
||||
}
|
||||
}));
|
||||
|
||||
const storeUnsubscribe = (() => {
|
||||
let isUnsubscribed = false;
|
||||
|
||||
const checkStateChange = () => {
|
||||
if (isUnsubscribed) return;
|
||||
|
||||
const newState = getState();
|
||||
const newActionCount = newState.panels.lastPanelAction || 0;
|
||||
|
||||
if (newActionCount !== currentActionCount) {
|
||||
console.log('[PanelNavigation] HomePanel update completed, pushing DetailPanel');
|
||||
isUnsubscribed = true;
|
||||
|
||||
dispatch(pushPanel({
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: {
|
||||
patnrId,
|
||||
prdtId,
|
||||
fromHome: true,
|
||||
...additionalInfo
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(checkStateChange, 0);
|
||||
const intervalId = setInterval(checkStateChange, 16);
|
||||
|
||||
return () => {
|
||||
isUnsubscribed = true;
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
})();
|
||||
|
||||
setTimeout(() => {
|
||||
storeUnsubscribe();
|
||||
console.log('[PanelNavigation] Timeout fallback from home');
|
||||
|
||||
dispatch(pushPanel({
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: {
|
||||
patnrId,
|
||||
prdtId,
|
||||
fromHome: true,
|
||||
...additionalInfo
|
||||
}
|
||||
}));
|
||||
}, 1000);
|
||||
};
|
||||
@@ -35,17 +35,11 @@ export const usePanelHistory = () => {
|
||||
* [current, previous, older, oldest, ...]
|
||||
*/
|
||||
const getHistory = useCallback(() => {
|
||||
const { history, head, size } = panelHistory;
|
||||
if (size === 0) return [];
|
||||
const { history } = panelHistory;
|
||||
if (history.length === 0) return [];
|
||||
|
||||
const sorted = [];
|
||||
for (let i = 0; i < size; i++) {
|
||||
const index = (head - i + 10) % 10;
|
||||
if (history[index]) {
|
||||
sorted.push(history[index]);
|
||||
}
|
||||
}
|
||||
return sorted;
|
||||
// history는 오래된 순서이므로, 최신 순으로 뒤집기
|
||||
return [...history].reverse();
|
||||
}, [panelHistory]);
|
||||
|
||||
/**
|
||||
@@ -55,10 +49,11 @@ export const usePanelHistory = () => {
|
||||
*/
|
||||
const getHistoryAt = useCallback(
|
||||
(distance) => {
|
||||
const { history, head, size } = panelHistory;
|
||||
if (distance >= size || distance < 0) return null;
|
||||
const { history } = panelHistory;
|
||||
if (distance >= history.length || distance < 0) return null;
|
||||
|
||||
const index = (head - distance + 10) % 10;
|
||||
// history는 오래된 순서, distance는 최신부터 계산
|
||||
const index = history.length - 1 - distance;
|
||||
return history[index] || null;
|
||||
},
|
||||
[panelHistory]
|
||||
@@ -76,7 +71,7 @@ export const usePanelHistory = () => {
|
||||
return { pattern: 'no-panel', confidence: 0 };
|
||||
}
|
||||
|
||||
// 🔽 Back navigation: A → B → A
|
||||
// Back navigation: A → B → A
|
||||
if (
|
||||
current &&
|
||||
older &&
|
||||
@@ -92,7 +87,7 @@ export const usePanelHistory = () => {
|
||||
};
|
||||
}
|
||||
|
||||
// 🔽 Detail flow: Search/Home → Detail
|
||||
// Detail flow: Search/Home → Detail
|
||||
if (current.panelName === 'detailpanel' && previous) {
|
||||
if (previous.panelName === 'searchpanel') {
|
||||
return {
|
||||
@@ -108,7 +103,7 @@ export const usePanelHistory = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 🔽 Modal overlay: base → modal
|
||||
// Modal overlay: base → modal
|
||||
const modalPanels = ['imagepanel', 'mediapanel', 'playerpanel'];
|
||||
if (modalPanels.includes(current.panelName) && previous) {
|
||||
return {
|
||||
@@ -119,7 +114,7 @@ export const usePanelHistory = () => {
|
||||
};
|
||||
}
|
||||
|
||||
// 🔽 Search flow: Home → Search
|
||||
// Search flow: Home → Search
|
||||
if (current.panelName === 'searchpanel' && previous?.panelName === 'homepanel') {
|
||||
return {
|
||||
pattern: 'home-to-search',
|
||||
@@ -127,7 +122,7 @@ export const usePanelHistory = () => {
|
||||
};
|
||||
}
|
||||
|
||||
// 🔽 Cart flow: Product → Cart
|
||||
// Cart flow: Product → Cart
|
||||
if (current.panelName === 'cartpanel' && previous) {
|
||||
return {
|
||||
pattern: 'product-to-cart',
|
||||
@@ -135,7 +130,7 @@ export const usePanelHistory = () => {
|
||||
};
|
||||
}
|
||||
|
||||
// 🔽 Checkout flow: Cart → Checkout
|
||||
// Checkout flow: Cart → Checkout
|
||||
if (current.panelName === 'checkoutpanel' && previous?.panelName === 'cartpanel') {
|
||||
return {
|
||||
pattern: 'cart-to-checkout',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Panel history 자동 추적 middleware
|
||||
*
|
||||
* Panel action (PUSH, POP, UPDATE, RESET)을 감지하고
|
||||
* 자동으로 panel history에 기록합니다.
|
||||
* 자동으로 panel history에 기록
|
||||
*/
|
||||
|
||||
import { types } from '../actions/actionTypes';
|
||||
@@ -17,6 +17,37 @@ import { enqueuePanelHistory, clearPanelHistory } from '../actions/panelHistoryA
|
||||
* @returns {Function} middleware function
|
||||
*/
|
||||
export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
||||
// 모든 PANEL 관련 액션 로깅
|
||||
if (action.type && (
|
||||
action.type.includes('PANEL') ||
|
||||
action.type === 'CLEAR_PANEL_HISTORY'
|
||||
)) {
|
||||
const caller = new Error().stack.split('\n')[1]?.trim();
|
||||
console.log(`[PANEL DEBUG] ${action.type} from: ${caller}`);
|
||||
console.log(' Payload:', action.payload);
|
||||
}
|
||||
|
||||
// GNB 호출 식별을 위한 helper 함수
|
||||
const isGNBCall = () => {
|
||||
const stack = new Error().stack;
|
||||
if (!stack) return false;
|
||||
|
||||
const stackLines = stack.split('\n');
|
||||
for (const line of stackLines) {
|
||||
if (line.includes('TabLayout.jsx') ||
|
||||
line.includes('TIconButton.jsx')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 임시: CLEAR_PANEL_HISTORY 무력화
|
||||
if (action.type === 'CLEAR_PANEL_HISTORY') {
|
||||
console.log('[PANEL DEBUG] CLEAR_PANEL_HISTORY BLOCKED!');
|
||||
return action; // 액션을 전달하지 않음
|
||||
}
|
||||
|
||||
// 원래 액션 실행
|
||||
const result = next(action);
|
||||
|
||||
@@ -32,7 +63,19 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
||||
const { name: panelName, panelInfo = {} } = action.payload;
|
||||
|
||||
if (panelName) {
|
||||
store.dispatch(enqueuePanelHistory(panelName, panelInfo, 'PUSH'));
|
||||
const isGNB = isGNBCall();
|
||||
console.log('[PANEL] PUSH_PANEL:', { panelName, panelInfo, isGNB, timestamp: new Date().toISOString() });
|
||||
store.dispatch(enqueuePanelHistory(panelName, panelInfo, 'PUSH', new Date().toISOString(), isGNB, false));
|
||||
|
||||
// PanelHistory 상태 로그 (state 업데이트 후)
|
||||
const logPanelHistoryAfter = () => {
|
||||
const stateAfter = store.getState();
|
||||
const panelHistoryAfter = stateAfter.panelHistory;
|
||||
console.log('[PANEL_HISTORY] After PUSH_PANEL:', panelHistoryAfter);
|
||||
};
|
||||
|
||||
// state 업데이트가 완료된 후 로그
|
||||
Promise.resolve().then(logPanelHistoryAfter);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -44,7 +87,19 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
||||
if (panels.length > 0) {
|
||||
const topPanel = panels[panels.length - 1];
|
||||
if (topPanel && topPanel.name) {
|
||||
store.dispatch(enqueuePanelHistory(topPanel.name, topPanel.panelInfo || {}, 'POP'));
|
||||
const isGNB = isGNBCall();
|
||||
console.log('[PANEL] POP_PANEL:', { panelName: topPanel.name, panelInfo: topPanel.panelInfo || {}, isGNB, timestamp: new Date().toISOString() });
|
||||
store.dispatch(enqueuePanelHistory(topPanel.name, topPanel.panelInfo || {}, 'POP', new Date().toISOString(), isGNB, false));
|
||||
|
||||
// PanelHistory 상태 로그 (state 업데이트 후)
|
||||
const logPanelHistoryAfter = () => {
|
||||
const stateAfter = store.getState();
|
||||
const panelHistoryAfter = stateAfter.panelHistory;
|
||||
console.log('[PANEL_HISTORY] After POP_PANEL:', panelHistoryAfter);
|
||||
};
|
||||
|
||||
// state 업데이트가 완료된 후 로그
|
||||
Promise.resolve().then(logPanelHistoryAfter);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -56,15 +111,76 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
||||
const { name: panelName, panelInfo = {} } = action.payload;
|
||||
|
||||
if (panelName) {
|
||||
store.dispatch(enqueuePanelHistory(panelName, panelInfo, 'UPDATE'));
|
||||
const isGNB = isGNBCall();
|
||||
console.log('[PANEL] UPDATE_PANEL:', { panelName, panelInfo, isGNB, timestamp: new Date().toISOString() });
|
||||
store.dispatch(enqueuePanelHistory(panelName, panelInfo, 'UPDATE', new Date().toISOString(), isGNB, false));
|
||||
|
||||
// PanelHistory 상태 로그 (state 업데이트 후)
|
||||
const logPanelHistoryAfter = () => {
|
||||
const stateAfter = store.getState();
|
||||
const panelHistoryAfter = stateAfter.panelHistory;
|
||||
console.log('[PANEL_HISTORY] After UPDATE_PANEL:', panelHistoryAfter);
|
||||
};
|
||||
|
||||
// state 업데이트가 완료된 후 로그
|
||||
Promise.resolve().then(logPanelHistoryAfter);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// RESET_PANELS: 히스토리 초기화
|
||||
// RESET_PANELS: GNB 네비게이션 또는 완전 초기화
|
||||
case types.RESET_PANELS: {
|
||||
store.dispatch(clearPanelHistory());
|
||||
console.log('[PANEL] RESET_PANELS:', {
|
||||
payload: action.payload,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
console.log('[PANEL_HISTORY] Before RESET_PANELS:', store.getState().panelHistory);
|
||||
|
||||
// 모든 RESET_PANELS를 기록
|
||||
const isGNB = isGNBCall();
|
||||
|
||||
if (action.payload && action.payload.length > 0) {
|
||||
// payload가 있는 경우 (GNB 탭, 패널 내 네비게이션 등)
|
||||
const firstPanel = action.payload[0];
|
||||
if (firstPanel && firstPanel.name) {
|
||||
console.log('[PANEL_DEBUG] RESET_PANELS to:', firstPanel.name, { isGNB, fromResetPanel: true });
|
||||
// RESET 이동을 히스토리에 기록
|
||||
store.dispatch(enqueuePanelHistory(
|
||||
firstPanel.name,
|
||||
firstPanel.panelInfo || {},
|
||||
'RESET',
|
||||
new Date().toISOString(),
|
||||
isGNB, // TabLayout/TIconButton이면 true
|
||||
true // fromResetPanel: 항상 true
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// 완전 초기화 (payload가 없는 경우 - HomePanel, 앱 초기화 등)
|
||||
console.log('[PANEL_DEBUG] Complete panel history reset (payload empty)', { isGNB, fromResetPanel: true });
|
||||
store.dispatch(clearPanelHistory());
|
||||
|
||||
// HomePanel 초기화 기록 (앱 시작 시)
|
||||
console.log('[PANEL_DEBUG] Recording initial HomePanel');
|
||||
store.dispatch(enqueuePanelHistory(
|
||||
'homepanel',
|
||||
{},
|
||||
'APP_INITIALIZE',
|
||||
new Date().toISOString(),
|
||||
isGNB, // TIconButton Home 버튼이면 true
|
||||
true // fromResetPanel: true
|
||||
));
|
||||
}
|
||||
|
||||
// PanelHistory 상태 로그 (초기화 후)
|
||||
const logPanelHistoryAfter = () => {
|
||||
const stateAfter = store.getState();
|
||||
const panelHistoryAfter = stateAfter.panelHistory;
|
||||
console.log('[PANEL_HISTORY] After RESET_PANELS:', panelHistoryAfter);
|
||||
};
|
||||
|
||||
// state 업데이트가 완료된 후 로그
|
||||
Promise.resolve().then(logPanelHistoryAfter);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,51 +10,49 @@ import { types } from '../actions/actionTypes';
|
||||
|
||||
// 초기 상태
|
||||
const initialState = {
|
||||
history: new Array(10).fill(null), // 10개 ring buffer
|
||||
head: -1, // 현재 위치 (-1 ~ 9)
|
||||
size: 0, // 현재 저장된 개수 (0 ~ 10)
|
||||
history: [], // 최대 10개의 히스토리 배열 (오래된 순서)
|
||||
current: null, // 현재 panel entry
|
||||
previous: null, // 이전 panel entry
|
||||
hasTransition: false, // panel 전환 여부
|
||||
};
|
||||
|
||||
/**
|
||||
* Panel을 Ring buffer에 추가
|
||||
* Panel을 히스토리 배열에 추가
|
||||
* @param {Object} state - 현재 상태
|
||||
* @param {string} panelName - 추가할 panel 이름
|
||||
* @param {Object} panelInfo - panel 정보
|
||||
* @param {string} action - Redux 액션 타입 ('PUSH' | 'POP' | 'UPDATE')
|
||||
* @param {string} timestamp - ISO 8601 timestamp
|
||||
* @param {boolean} fromGNB - GNB를 통해 이동했는지 여부
|
||||
* @param {boolean} fromResetPanel - RESET_PANELS를 통해 이동했는지 여부
|
||||
* @returns {Object} 새로운 상태
|
||||
*/
|
||||
const enqueuePanel = (state, panelName, panelInfo, action, timestamp) => {
|
||||
// State 불변성 유지
|
||||
const newHistory = state.history.map((item) => item);
|
||||
|
||||
// 다음 위치 계산 (ring buffer 순환)
|
||||
const newHead = (state.head + 1) % 10;
|
||||
|
||||
const enqueuePanel = (state, panelName, panelInfo, action, timestamp, fromGNB = false, fromResetPanel = false) => {
|
||||
// 새 entry 생성
|
||||
const newEntry = {
|
||||
panelName,
|
||||
panelInfo: { ...panelInfo }, // Deep copy
|
||||
timestamp,
|
||||
action,
|
||||
fromGNB, // GNB 이동 여부
|
||||
fromResetPanel, // RESET_PANELS 이동 여부
|
||||
};
|
||||
|
||||
// Ring buffer에 저장
|
||||
newHistory[newHead] = newEntry;
|
||||
|
||||
// Size 업데이트 (최대 10)
|
||||
const newSize = Math.min(state.size + 1, 10);
|
||||
// 최대 10개 유지하며 새로운 히스토리 생성
|
||||
let newHistory;
|
||||
if (state.history.length >= 10) {
|
||||
// 10개가 차면 첫 번째 요소 제거하고 추가
|
||||
newHistory = [...state.history.slice(1), newEntry];
|
||||
} else {
|
||||
// 10개 미만이면 그냥 추가
|
||||
newHistory = [...state.history, newEntry];
|
||||
}
|
||||
|
||||
// 이전 panel 추출
|
||||
const previousEntry = state.current;
|
||||
|
||||
return {
|
||||
history: newHistory,
|
||||
head: newHead,
|
||||
size: newSize,
|
||||
current: newEntry,
|
||||
previous: previousEntry,
|
||||
hasTransition: previousEntry !== null && previousEntry.panelName !== newEntry.panelName,
|
||||
@@ -68,7 +66,7 @@ export const panelHistoryReducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
// 🔽 Panel history에 새 entry 추가
|
||||
case types.ENQUEUE_PANEL_HISTORY: {
|
||||
const { panelName, panelInfo = {}, action: actionType, timestamp } = action.payload;
|
||||
const { panelName, panelInfo = {}, action: actionType, timestamp, fromGNB = false, fromResetPanel = false } = action.payload;
|
||||
|
||||
// 입력값 검증
|
||||
if (!panelName || typeof panelName !== 'string') {
|
||||
@@ -85,12 +83,14 @@ export const panelHistoryReducer = (state = initialState, action) => {
|
||||
panelInfo: { ...panelInfo }, // panelInfo만 업데이트
|
||||
timestamp,
|
||||
action: actionType, // 최신 action 기록
|
||||
fromGNB: fromGNB || state.current.fromGNB, // GNB 플래그 유지 또는 업데이트
|
||||
fromResetPanel: fromResetPanel || state.current.fromResetPanel, // ResetPanel 플래그 유지 또는 업데이트
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 🔽 다른 panelName이면 새로운 버퍼 항목 추가
|
||||
return enqueuePanel(state, panelName, panelInfo, actionType, timestamp);
|
||||
return enqueuePanel(state, panelName, panelInfo, actionType, timestamp, fromGNB, fromResetPanel);
|
||||
}
|
||||
|
||||
// 🔽 History 초기화
|
||||
@@ -98,10 +98,8 @@ export const panelHistoryReducer = (state = initialState, action) => {
|
||||
return { ...initialState };
|
||||
}
|
||||
|
||||
// 🔽 Panel reset 시 history도 리셋
|
||||
case types.RESET_PANELS: {
|
||||
return { ...initialState };
|
||||
}
|
||||
// 🔽 RESET_PANELS는 middleware에서 직접 처리하므로 여기서는 초기화하지 않음
|
||||
// GNB 네비게이션을 위해 히스토리를 유지해야 함
|
||||
|
||||
// 🔽 Panel history 명시적 리셋
|
||||
case types.RESET_PANEL_HISTORY: {
|
||||
@@ -120,18 +118,11 @@ export const selectPanelHistory = (state) => state.panelHistory;
|
||||
export const selectCurrentPanel = (state) => state.panelHistory.current;
|
||||
export const selectPreviousPanel = (state) => state.panelHistory.previous;
|
||||
export const selectPanelHistoryList = (state) => {
|
||||
const { history, head, size } = state.panelHistory;
|
||||
if (size === 0) return [];
|
||||
const { history } = state.panelHistory;
|
||||
if (history.length === 0) return [];
|
||||
|
||||
// 최신순으로 정렬된 히스토리 반환
|
||||
const sorted = [];
|
||||
for (let i = 0; i < size; i++) {
|
||||
const index = (head - i + 10) % 10;
|
||||
if (history[index]) {
|
||||
sorted.push(history[index]);
|
||||
}
|
||||
}
|
||||
return sorted;
|
||||
// 최신순으로 정렬된 히스토리 반환 (오래된 순서를 뒤집기)
|
||||
return [...history].reverse();
|
||||
};
|
||||
|
||||
export default panelHistoryReducer;
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
pushPanel,
|
||||
updatePanel,
|
||||
} from '../../../actions/panelActions';
|
||||
import { navigateToDetailFromHome } from '../../../actions/panelNavigationActions';
|
||||
import SectionTitle from '../../../components/SectionTitle/SectionTitle';
|
||||
import Tag from '../../../components/TItemCard/Tag';
|
||||
import TItemCard from '../../../components/TItemCard/TItemCard';
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useDispatch } from 'react-redux';
|
||||
import Spotlight from '@enact/spotlight';
|
||||
|
||||
import { pushPanel, updatePanel } from '../../../actions/panelActions';
|
||||
import { navigateToDetailPanel } from '../../../actions/panelNavigationActions';
|
||||
import TItemCardNew from '../../../components/TItemCard/TItemCard.new';
|
||||
import TScroller from '../../../components/TScroller/TScroller';
|
||||
import { panel_names } from '../../../utils/Config';
|
||||
@@ -19,25 +20,15 @@ const ItemCard = ({ onClick, itemInfo, searchQuery }) => {
|
||||
if (onClick) {
|
||||
onClick(ev);
|
||||
}
|
||||
dispatch(
|
||||
updatePanel({
|
||||
name: panel_names.SEARCH_PANEL,
|
||||
panelInfo: {
|
||||
searchVal: searchQuery,
|
||||
currentSpot,
|
||||
tab: 0,
|
||||
},
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
pushPanel({
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: {
|
||||
patnrId: patnrId,
|
||||
prdtId: prdtId,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// 순차 네비게이션 사용 (Chrome 68 호환)
|
||||
dispatch(navigateToDetailPanel(
|
||||
patnrId,
|
||||
prdtId,
|
||||
searchQuery,
|
||||
currentSpot,
|
||||
{ tab: 0 }
|
||||
));
|
||||
},
|
||||
[onClick, dispatch, searchQuery]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user