[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 { getHomeMenu, getHomeTerms } from '../actions/homeActions';
|
||||||
import { getMyRecommandedKeyword, getMyUpcomingAlertShow } from '../actions/myPageActions';
|
import { getMyRecommandedKeyword, getMyUpcomingAlertShow } from '../actions/myPageActions';
|
||||||
import { pushPanel } from '../actions/panelActions';
|
import { pushPanel } from '../actions/panelActions';
|
||||||
|
import { enqueuePanelHistory } from '../actions/panelHistoryActions';
|
||||||
import NotSupportedVersion from '../components/NotSupportedVersion/NotSupportedVersion';
|
import NotSupportedVersion from '../components/NotSupportedVersion/NotSupportedVersion';
|
||||||
import ToastContainer from '../components/TToast/ToastContainer';
|
import ToastContainer from '../components/TToast/ToastContainer';
|
||||||
import usePrevious from '../hooks/usePrevious';
|
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] initService,httpHeaderRef.current', httpHeaderRef.current);
|
||||||
console.log('[App.js] haveyInit', haveyInit);
|
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 (httpHeaderRef.current) {
|
||||||
if (haveyInit) {
|
if (haveyInit) {
|
||||||
dispatch(changeAppStatus({ connectionFailed: false }));
|
dispatch(changeAppStatus({ connectionFailed: false }));
|
||||||
|
|||||||
@@ -9,16 +9,21 @@ import { types } from './actionTypes';
|
|||||||
* Panel을 히스토리에 추가
|
* Panel을 히스토리에 추가
|
||||||
* @param {string} panelName - Panel 이름
|
* @param {string} panelName - Panel 이름
|
||||||
* @param {Object} panelInfo - 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
|
* @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,
|
type: types.ENQUEUE_PANEL_HISTORY,
|
||||||
payload: {
|
payload: {
|
||||||
panelName,
|
panelName,
|
||||||
panelInfo,
|
panelInfo,
|
||||||
action,
|
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, ...]
|
* [current, previous, older, oldest, ...]
|
||||||
*/
|
*/
|
||||||
const getHistory = useCallback(() => {
|
const getHistory = useCallback(() => {
|
||||||
const { history, head, size } = panelHistory;
|
const { history } = panelHistory;
|
||||||
if (size === 0) return [];
|
if (history.length === 0) return [];
|
||||||
|
|
||||||
const sorted = [];
|
// history는 오래된 순서이므로, 최신 순으로 뒤집기
|
||||||
for (let i = 0; i < size; i++) {
|
return [...history].reverse();
|
||||||
const index = (head - i + 10) % 10;
|
|
||||||
if (history[index]) {
|
|
||||||
sorted.push(history[index]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sorted;
|
|
||||||
}, [panelHistory]);
|
}, [panelHistory]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,10 +49,11 @@ export const usePanelHistory = () => {
|
|||||||
*/
|
*/
|
||||||
const getHistoryAt = useCallback(
|
const getHistoryAt = useCallback(
|
||||||
(distance) => {
|
(distance) => {
|
||||||
const { history, head, size } = panelHistory;
|
const { history } = panelHistory;
|
||||||
if (distance >= size || distance < 0) return null;
|
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;
|
return history[index] || null;
|
||||||
},
|
},
|
||||||
[panelHistory]
|
[panelHistory]
|
||||||
@@ -76,7 +71,7 @@ export const usePanelHistory = () => {
|
|||||||
return { pattern: 'no-panel', confidence: 0 };
|
return { pattern: 'no-panel', confidence: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔽 Back navigation: A → B → A
|
// Back navigation: A → B → A
|
||||||
if (
|
if (
|
||||||
current &&
|
current &&
|
||||||
older &&
|
older &&
|
||||||
@@ -92,7 +87,7 @@ export const usePanelHistory = () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔽 Detail flow: Search/Home → Detail
|
// Detail flow: Search/Home → Detail
|
||||||
if (current.panelName === 'detailpanel' && previous) {
|
if (current.panelName === 'detailpanel' && previous) {
|
||||||
if (previous.panelName === 'searchpanel') {
|
if (previous.panelName === 'searchpanel') {
|
||||||
return {
|
return {
|
||||||
@@ -108,7 +103,7 @@ export const usePanelHistory = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔽 Modal overlay: base → modal
|
// Modal overlay: base → modal
|
||||||
const modalPanels = ['imagepanel', 'mediapanel', 'playerpanel'];
|
const modalPanels = ['imagepanel', 'mediapanel', 'playerpanel'];
|
||||||
if (modalPanels.includes(current.panelName) && previous) {
|
if (modalPanels.includes(current.panelName) && previous) {
|
||||||
return {
|
return {
|
||||||
@@ -119,7 +114,7 @@ export const usePanelHistory = () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔽 Search flow: Home → Search
|
// Search flow: Home → Search
|
||||||
if (current.panelName === 'searchpanel' && previous?.panelName === 'homepanel') {
|
if (current.panelName === 'searchpanel' && previous?.panelName === 'homepanel') {
|
||||||
return {
|
return {
|
||||||
pattern: 'home-to-search',
|
pattern: 'home-to-search',
|
||||||
@@ -127,7 +122,7 @@ export const usePanelHistory = () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔽 Cart flow: Product → Cart
|
// Cart flow: Product → Cart
|
||||||
if (current.panelName === 'cartpanel' && previous) {
|
if (current.panelName === 'cartpanel' && previous) {
|
||||||
return {
|
return {
|
||||||
pattern: 'product-to-cart',
|
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') {
|
if (current.panelName === 'checkoutpanel' && previous?.panelName === 'cartpanel') {
|
||||||
return {
|
return {
|
||||||
pattern: 'cart-to-checkout',
|
pattern: 'cart-to-checkout',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Panel history 자동 추적 middleware
|
* Panel history 자동 추적 middleware
|
||||||
*
|
*
|
||||||
* Panel action (PUSH, POP, UPDATE, RESET)을 감지하고
|
* Panel action (PUSH, POP, UPDATE, RESET)을 감지하고
|
||||||
* 자동으로 panel history에 기록합니다.
|
* 자동으로 panel history에 기록
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { types } from '../actions/actionTypes';
|
import { types } from '../actions/actionTypes';
|
||||||
@@ -17,6 +17,37 @@ import { enqueuePanelHistory, clearPanelHistory } from '../actions/panelHistoryA
|
|||||||
* @returns {Function} middleware function
|
* @returns {Function} middleware function
|
||||||
*/
|
*/
|
||||||
export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
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);
|
const result = next(action);
|
||||||
|
|
||||||
@@ -32,7 +63,19 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
|||||||
const { name: panelName, panelInfo = {} } = action.payload;
|
const { name: panelName, panelInfo = {} } = action.payload;
|
||||||
|
|
||||||
if (panelName) {
|
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;
|
break;
|
||||||
@@ -44,7 +87,19 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
|||||||
if (panels.length > 0) {
|
if (panels.length > 0) {
|
||||||
const topPanel = panels[panels.length - 1];
|
const topPanel = panels[panels.length - 1];
|
||||||
if (topPanel && topPanel.name) {
|
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;
|
break;
|
||||||
@@ -56,15 +111,76 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
|||||||
const { name: panelName, panelInfo = {} } = action.payload;
|
const { name: panelName, panelInfo = {} } = action.payload;
|
||||||
|
|
||||||
if (panelName) {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RESET_PANELS: 히스토리 초기화
|
// RESET_PANELS: GNB 네비게이션 또는 완전 초기화
|
||||||
case types.RESET_PANELS: {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,51 +10,49 @@ import { types } from '../actions/actionTypes';
|
|||||||
|
|
||||||
// 초기 상태
|
// 초기 상태
|
||||||
const initialState = {
|
const initialState = {
|
||||||
history: new Array(10).fill(null), // 10개 ring buffer
|
history: [], // 최대 10개의 히스토리 배열 (오래된 순서)
|
||||||
head: -1, // 현재 위치 (-1 ~ 9)
|
|
||||||
size: 0, // 현재 저장된 개수 (0 ~ 10)
|
|
||||||
current: null, // 현재 panel entry
|
current: null, // 현재 panel entry
|
||||||
previous: null, // 이전 panel entry
|
previous: null, // 이전 panel entry
|
||||||
hasTransition: false, // panel 전환 여부
|
hasTransition: false, // panel 전환 여부
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Panel을 Ring buffer에 추가
|
* Panel을 히스토리 배열에 추가
|
||||||
* @param {Object} state - 현재 상태
|
* @param {Object} state - 현재 상태
|
||||||
* @param {string} panelName - 추가할 panel 이름
|
* @param {string} panelName - 추가할 panel 이름
|
||||||
* @param {Object} panelInfo - panel 정보
|
* @param {Object} panelInfo - panel 정보
|
||||||
* @param {string} action - Redux 액션 타입 ('PUSH' | 'POP' | 'UPDATE')
|
* @param {string} action - Redux 액션 타입 ('PUSH' | 'POP' | 'UPDATE')
|
||||||
* @param {string} timestamp - ISO 8601 timestamp
|
* @param {string} timestamp - ISO 8601 timestamp
|
||||||
|
* @param {boolean} fromGNB - GNB를 통해 이동했는지 여부
|
||||||
|
* @param {boolean} fromResetPanel - RESET_PANELS를 통해 이동했는지 여부
|
||||||
* @returns {Object} 새로운 상태
|
* @returns {Object} 새로운 상태
|
||||||
*/
|
*/
|
||||||
const enqueuePanel = (state, panelName, panelInfo, action, timestamp) => {
|
const enqueuePanel = (state, panelName, panelInfo, action, timestamp, fromGNB = false, fromResetPanel = false) => {
|
||||||
// State 불변성 유지
|
|
||||||
const newHistory = state.history.map((item) => item);
|
|
||||||
|
|
||||||
// 다음 위치 계산 (ring buffer 순환)
|
|
||||||
const newHead = (state.head + 1) % 10;
|
|
||||||
|
|
||||||
// 새 entry 생성
|
// 새 entry 생성
|
||||||
const newEntry = {
|
const newEntry = {
|
||||||
panelName,
|
panelName,
|
||||||
panelInfo: { ...panelInfo }, // Deep copy
|
panelInfo: { ...panelInfo }, // Deep copy
|
||||||
timestamp,
|
timestamp,
|
||||||
action,
|
action,
|
||||||
|
fromGNB, // GNB 이동 여부
|
||||||
|
fromResetPanel, // RESET_PANELS 이동 여부
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ring buffer에 저장
|
// 최대 10개 유지하며 새로운 히스토리 생성
|
||||||
newHistory[newHead] = newEntry;
|
let newHistory;
|
||||||
|
if (state.history.length >= 10) {
|
||||||
// Size 업데이트 (최대 10)
|
// 10개가 차면 첫 번째 요소 제거하고 추가
|
||||||
const newSize = Math.min(state.size + 1, 10);
|
newHistory = [...state.history.slice(1), newEntry];
|
||||||
|
} else {
|
||||||
|
// 10개 미만이면 그냥 추가
|
||||||
|
newHistory = [...state.history, newEntry];
|
||||||
|
}
|
||||||
|
|
||||||
// 이전 panel 추출
|
// 이전 panel 추출
|
||||||
const previousEntry = state.current;
|
const previousEntry = state.current;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
history: newHistory,
|
history: newHistory,
|
||||||
head: newHead,
|
|
||||||
size: newSize,
|
|
||||||
current: newEntry,
|
current: newEntry,
|
||||||
previous: previousEntry,
|
previous: previousEntry,
|
||||||
hasTransition: previousEntry !== null && previousEntry.panelName !== newEntry.panelName,
|
hasTransition: previousEntry !== null && previousEntry.panelName !== newEntry.panelName,
|
||||||
@@ -68,7 +66,7 @@ export const panelHistoryReducer = (state = initialState, action) => {
|
|||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
// 🔽 Panel history에 새 entry 추가
|
// 🔽 Panel history에 새 entry 추가
|
||||||
case types.ENQUEUE_PANEL_HISTORY: {
|
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') {
|
if (!panelName || typeof panelName !== 'string') {
|
||||||
@@ -85,12 +83,14 @@ export const panelHistoryReducer = (state = initialState, action) => {
|
|||||||
panelInfo: { ...panelInfo }, // panelInfo만 업데이트
|
panelInfo: { ...panelInfo }, // panelInfo만 업데이트
|
||||||
timestamp,
|
timestamp,
|
||||||
action: actionType, // 최신 action 기록
|
action: actionType, // 최신 action 기록
|
||||||
|
fromGNB: fromGNB || state.current.fromGNB, // GNB 플래그 유지 또는 업데이트
|
||||||
|
fromResetPanel: fromResetPanel || state.current.fromResetPanel, // ResetPanel 플래그 유지 또는 업데이트
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔽 다른 panelName이면 새로운 버퍼 항목 추가
|
// 🔽 다른 panelName이면 새로운 버퍼 항목 추가
|
||||||
return enqueuePanel(state, panelName, panelInfo, actionType, timestamp);
|
return enqueuePanel(state, panelName, panelInfo, actionType, timestamp, fromGNB, fromResetPanel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔽 History 초기화
|
// 🔽 History 초기화
|
||||||
@@ -98,10 +98,8 @@ export const panelHistoryReducer = (state = initialState, action) => {
|
|||||||
return { ...initialState };
|
return { ...initialState };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔽 Panel reset 시 history도 리셋
|
// 🔽 RESET_PANELS는 middleware에서 직접 처리하므로 여기서는 초기화하지 않음
|
||||||
case types.RESET_PANELS: {
|
// GNB 네비게이션을 위해 히스토리를 유지해야 함
|
||||||
return { ...initialState };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔽 Panel history 명시적 리셋
|
// 🔽 Panel history 명시적 리셋
|
||||||
case types.RESET_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 selectCurrentPanel = (state) => state.panelHistory.current;
|
||||||
export const selectPreviousPanel = (state) => state.panelHistory.previous;
|
export const selectPreviousPanel = (state) => state.panelHistory.previous;
|
||||||
export const selectPanelHistoryList = (state) => {
|
export const selectPanelHistoryList = (state) => {
|
||||||
const { history, head, size } = state.panelHistory;
|
const { history } = state.panelHistory;
|
||||||
if (size === 0) return [];
|
if (history.length === 0) return [];
|
||||||
|
|
||||||
// 최신순으로 정렬된 히스토리 반환
|
// 최신순으로 정렬된 히스토리 반환 (오래된 순서를 뒤집기)
|
||||||
const sorted = [];
|
return [...history].reverse();
|
||||||
for (let i = 0; i < size; i++) {
|
|
||||||
const index = (head - i + 10) % 10;
|
|
||||||
if (history[index]) {
|
|
||||||
sorted.push(history[index]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sorted;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default panelHistoryReducer;
|
export default panelHistoryReducer;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
pushPanel,
|
pushPanel,
|
||||||
updatePanel,
|
updatePanel,
|
||||||
} from '../../../actions/panelActions';
|
} from '../../../actions/panelActions';
|
||||||
|
import { navigateToDetailFromHome } from '../../../actions/panelNavigationActions';
|
||||||
import SectionTitle from '../../../components/SectionTitle/SectionTitle';
|
import SectionTitle from '../../../components/SectionTitle/SectionTitle';
|
||||||
import Tag from '../../../components/TItemCard/Tag';
|
import Tag from '../../../components/TItemCard/Tag';
|
||||||
import TItemCard from '../../../components/TItemCard/TItemCard';
|
import TItemCard from '../../../components/TItemCard/TItemCard';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useDispatch } from 'react-redux';
|
|||||||
import Spotlight from '@enact/spotlight';
|
import Spotlight from '@enact/spotlight';
|
||||||
|
|
||||||
import { pushPanel, updatePanel } from '../../../actions/panelActions';
|
import { pushPanel, updatePanel } from '../../../actions/panelActions';
|
||||||
|
import { navigateToDetailPanel } from '../../../actions/panelNavigationActions';
|
||||||
import TItemCardNew from '../../../components/TItemCard/TItemCard.new';
|
import TItemCardNew from '../../../components/TItemCard/TItemCard.new';
|
||||||
import TScroller from '../../../components/TScroller/TScroller';
|
import TScroller from '../../../components/TScroller/TScroller';
|
||||||
import { panel_names } from '../../../utils/Config';
|
import { panel_names } from '../../../utils/Config';
|
||||||
@@ -19,25 +20,15 @@ const ItemCard = ({ onClick, itemInfo, searchQuery }) => {
|
|||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick(ev);
|
onClick(ev);
|
||||||
}
|
}
|
||||||
dispatch(
|
|
||||||
updatePanel({
|
// 순차 네비게이션 사용 (Chrome 68 호환)
|
||||||
name: panel_names.SEARCH_PANEL,
|
dispatch(navigateToDetailPanel(
|
||||||
panelInfo: {
|
patnrId,
|
||||||
searchVal: searchQuery,
|
prdtId,
|
||||||
currentSpot,
|
searchQuery,
|
||||||
tab: 0,
|
currentSpot,
|
||||||
},
|
{ tab: 0 }
|
||||||
})
|
));
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
pushPanel({
|
|
||||||
name: panel_names.DETAIL_PANEL,
|
|
||||||
panelInfo: {
|
|
||||||
patnrId: patnrId,
|
|
||||||
prdtId: prdtId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[onClick, dispatch, searchQuery]
|
[onClick, dispatch, searchQuery]
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user