[251025] feat: usePanelHistory
🕐 커밋 시간: 2025. 10. 25. 05:33:22 📊 변경 통계: • 총 파일: 8개 • 추가: +83줄 • 삭제: -217줄 📁 추가된 파일: + com.twin.app.shoptime/src/actions/panelHistoryActions.js + com.twin.app.shoptime/src/hooks/usePanelHistory/index.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/package-lock.json ~ com.twin.app.shoptime/src/actions/actionTypes.js ~ com.twin.app.shoptime/src/store/store.js 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/reducers/panelHistoryReducer.js (javascript): ✅ Added: enqueuePanel() 🔧 주요 변경 내용: • 타입 시스템 안정성 강화 • 핵심 비즈니스 로직 개선 Performance: 코드 최적화로 성능 개선 기대
This commit is contained in:
290
com.twin.app.shoptime/package-lock.json
generated
290
com.twin.app.shoptime/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,11 @@ export const types = {
|
||||
UPDATE_PANEL: 'UPDATE_PANEL',
|
||||
RESET_PANELS: 'RESET_PANELS',
|
||||
|
||||
// 🔽 [신규] panel history actions
|
||||
ENQUEUE_PANEL_HISTORY: 'ENQUEUE_PANEL_HISTORY',
|
||||
CLEAR_PANEL_HISTORY: 'CLEAR_PANEL_HISTORY',
|
||||
RESET_PANEL_HISTORY: 'RESET_PANEL_HISTORY',
|
||||
|
||||
// device actions
|
||||
GET_AUTHENTICATION_CODE: 'GET_AUTHENTICATION_CODE',
|
||||
REGISTER_DEVICE: 'REGISTER_DEVICE',
|
||||
|
||||
39
com.twin.app.shoptime/src/actions/panelHistoryActions.js
Normal file
39
com.twin.app.shoptime/src/actions/panelHistoryActions.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* src/actions/panelHistoryActions.js
|
||||
* Panel history 액션 크리에이터
|
||||
*/
|
||||
|
||||
import { types } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Panel을 히스토리에 추가
|
||||
* @param {string} panelName - Panel 이름
|
||||
* @param {Object} panelInfo - Panel 상태 정보
|
||||
* @param {string} action - 발생한 Redux 액션 ('PUSH' | 'POP' | 'UPDATE' | 'RESET')
|
||||
* @returns {Object} Redux action
|
||||
*/
|
||||
export const enqueuePanelHistory = (panelName, panelInfo = {}, action = 'PUSH') => ({
|
||||
type: types.ENQUEUE_PANEL_HISTORY,
|
||||
payload: {
|
||||
panelName,
|
||||
panelInfo,
|
||||
action,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel history 초기화
|
||||
* @returns {Object} Redux action
|
||||
*/
|
||||
export const clearPanelHistory = () => ({
|
||||
type: types.CLEAR_PANEL_HISTORY,
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel history 리셋 (로그아웃 등)
|
||||
* @returns {Object} Redux action
|
||||
*/
|
||||
export const resetPanelHistory = () => ({
|
||||
type: types.RESET_PANEL_HISTORY,
|
||||
});
|
||||
1
com.twin.app.shoptime/src/hooks/usePanelHistory/index.js
Normal file
1
com.twin.app.shoptime/src/hooks/usePanelHistory/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { usePanelHistory, default } from './usePanelHistory';
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* src/hooks/usePanelHistory/usePanelHistory.js
|
||||
* Panel history를 조회하는 hook
|
||||
*
|
||||
* Redux의 panelHistory state를 useSelector로 조회하고
|
||||
* 편리한 API로 변환하여 제공합니다.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
/**
|
||||
* Panel history를 조회하는 hook
|
||||
*
|
||||
* @example
|
||||
* const panelHistory = usePanelHistory();
|
||||
* console.log(panelHistory.currentPanel); // 현재 panel
|
||||
* console.log(panelHistory.previousPanel); // 이전 panel
|
||||
* console.log(panelHistory.history); // 전체 히스토리
|
||||
* console.log(panelHistory.detectPattern()); // 네비게이션 패턴
|
||||
*
|
||||
* @returns {Object} Panel history API
|
||||
*/
|
||||
export const usePanelHistory = () => {
|
||||
// Redux state에서 panelHistory 조회
|
||||
const panelHistory = useSelector((state) => state.panelHistory);
|
||||
|
||||
// 편의 선택자
|
||||
const currentPanel = useSelector((state) => state.panelHistory.current);
|
||||
const previousPanel = useSelector((state) => state.panelHistory.previous);
|
||||
const hasTransition = useSelector((state) => state.panelHistory.hasTransition);
|
||||
|
||||
/**
|
||||
* 히스토리를 최신순 배열로 반환
|
||||
* [current, previous, older, oldest, ...]
|
||||
*/
|
||||
const getHistory = useCallback(() => {
|
||||
const { history, head, size } = panelHistory;
|
||||
if (size === 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;
|
||||
}, [panelHistory]);
|
||||
|
||||
/**
|
||||
* 특정 거리의 panel 조회
|
||||
* @param {number} distance - 거리 (0=current, 1=previous, 2=older, ...)
|
||||
* @returns {Object|null} Panel entry 또는 null
|
||||
*/
|
||||
const getHistoryAt = useCallback(
|
||||
(distance) => {
|
||||
const { history, head, size } = panelHistory;
|
||||
if (distance >= size || distance < 0) return null;
|
||||
|
||||
const index = (head - distance + 10) % 10;
|
||||
return history[index] || null;
|
||||
},
|
||||
[panelHistory]
|
||||
);
|
||||
|
||||
/**
|
||||
* 네비게이션 패턴 감지
|
||||
* 사용자의 네비게이션 행동을 분석하여 패턴을 반환
|
||||
*/
|
||||
const detectPattern = useCallback(() => {
|
||||
const history = getHistory();
|
||||
const [current, previous, older] = history;
|
||||
|
||||
if (!current) {
|
||||
return { pattern: 'no-panel', confidence: 0 };
|
||||
}
|
||||
|
||||
// 🔽 Back navigation: A → B → A
|
||||
if (
|
||||
current &&
|
||||
older &&
|
||||
current.panelName === older.panelName &&
|
||||
previous &&
|
||||
previous.panelName !== current.panelName
|
||||
) {
|
||||
return {
|
||||
pattern: 'back-navigation',
|
||||
from: previous.panelName,
|
||||
to: current.panelName,
|
||||
confidence: 0.95,
|
||||
};
|
||||
}
|
||||
|
||||
// 🔽 Detail flow: Search/Home → Detail
|
||||
if (current.panelName === 'detailpanel' && previous) {
|
||||
if (previous.panelName === 'searchpanel') {
|
||||
return {
|
||||
pattern: 'search-to-detail',
|
||||
confidence: 1.0,
|
||||
};
|
||||
}
|
||||
if (previous.panelName === 'homepanel') {
|
||||
return {
|
||||
pattern: 'home-to-detail',
|
||||
confidence: 1.0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 🔽 Modal overlay: base → modal
|
||||
const modalPanels = ['imagepanel', 'mediapanel', 'playerpanel'];
|
||||
if (modalPanels.includes(current.panelName) && previous) {
|
||||
return {
|
||||
pattern: 'modal-overlay',
|
||||
basePanel: previous.panelName,
|
||||
modalPanel: current.panelName,
|
||||
confidence: 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
// 🔽 Search flow: Home → Search
|
||||
if (current.panelName === 'searchpanel' && previous?.panelName === 'homepanel') {
|
||||
return {
|
||||
pattern: 'home-to-search',
|
||||
confidence: 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
// 🔽 Cart flow: Product → Cart
|
||||
if (current.panelName === 'cartpanel' && previous) {
|
||||
return {
|
||||
pattern: 'product-to-cart',
|
||||
confidence: 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
// 🔽 Checkout flow: Cart → Checkout
|
||||
if (current.panelName === 'checkoutpanel' && previous?.panelName === 'cartpanel') {
|
||||
return {
|
||||
pattern: 'cart-to-checkout',
|
||||
confidence: 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
// Default: 순차 네비게이션
|
||||
return {
|
||||
pattern: 'sequential-navigation',
|
||||
from: previous?.panelName,
|
||||
to: current.panelName,
|
||||
confidence: 0.5,
|
||||
};
|
||||
}, [getHistory]);
|
||||
|
||||
/**
|
||||
* 디버그 정보
|
||||
*/
|
||||
const getDebugInfo = useCallback(() => {
|
||||
const history = getHistory();
|
||||
const pattern = detectPattern();
|
||||
|
||||
return {
|
||||
state: panelHistory,
|
||||
history: history,
|
||||
historyLabeled: {
|
||||
current: history[0] || null,
|
||||
previous: history[1] || null,
|
||||
older: history[2] || null,
|
||||
oldest: history[3] || null,
|
||||
},
|
||||
pattern,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}, [panelHistory, getHistory, detectPattern]);
|
||||
|
||||
/**
|
||||
* 메모이제이션된 히스토리 배열
|
||||
* useCallback의 의존성 변경 시에만 재계산
|
||||
*/
|
||||
const memoizedHistory = useMemo(() => getHistory(), [getHistory]);
|
||||
|
||||
/**
|
||||
* API 반환
|
||||
*/
|
||||
return {
|
||||
// 현재 상태
|
||||
currentPanel,
|
||||
previousPanel,
|
||||
hasTransition,
|
||||
|
||||
// 배열 형태
|
||||
history: memoizedHistory,
|
||||
panelTransition:
|
||||
previousPanel && currentPanel
|
||||
? [previousPanel.panelName, currentPanel.panelName]
|
||||
: currentPanel
|
||||
? [null, currentPanel.panelName]
|
||||
: [null, null],
|
||||
|
||||
// 조회 메서드
|
||||
getHistory,
|
||||
getHistoryAt,
|
||||
detectPattern,
|
||||
getDebugInfo,
|
||||
|
||||
// 전체 상태 (필요시)
|
||||
panelHistory,
|
||||
};
|
||||
};
|
||||
|
||||
export default usePanelHistory;
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* src/middleware/panelHistoryMiddleware.js
|
||||
* Panel history 자동 추적 middleware
|
||||
*
|
||||
* Panel action (PUSH, POP, UPDATE, RESET)을 감지하고
|
||||
* 자동으로 panel history에 기록합니다.
|
||||
*/
|
||||
|
||||
import { types } from '../actions/actionTypes';
|
||||
import { enqueuePanelHistory, clearPanelHistory } from '../actions/panelHistoryActions';
|
||||
|
||||
/**
|
||||
* Panel history middleware
|
||||
* 모든 panel action을 감지하여 panelHistory reducer로 자동 기록
|
||||
*
|
||||
* @param {Object} store - Redux store
|
||||
* @returns {Function} middleware function
|
||||
*/
|
||||
export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
||||
// 원래 액션 실행
|
||||
const result = next(action);
|
||||
|
||||
// Panel action 후 history 업데이트
|
||||
const state = store.getState();
|
||||
const panels = state.panels?.panels || [];
|
||||
|
||||
try {
|
||||
switch (action.type) {
|
||||
// PUSH_PANEL: 새 panel 추가
|
||||
case types.PUSH_PANEL: {
|
||||
if (action.payload) {
|
||||
const { name: panelName, panelInfo = {} } = action.payload;
|
||||
|
||||
if (panelName) {
|
||||
store.dispatch(enqueuePanelHistory(panelName, panelInfo, 'PUSH'));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// POP_PANEL: panel 제거 후 이전 panel 기록
|
||||
case types.POP_PANEL: {
|
||||
// POP 후 top panel을 기록 (이전 패널로 돌아감)
|
||||
if (panels.length > 0) {
|
||||
const topPanel = panels[panels.length - 1];
|
||||
if (topPanel && topPanel.name) {
|
||||
store.dispatch(enqueuePanelHistory(topPanel.name, topPanel.panelInfo || {}, 'POP'));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// UPDATE_PANEL: panel 정보 업데이트 기록
|
||||
case types.UPDATE_PANEL: {
|
||||
if (action.payload) {
|
||||
const { name: panelName, panelInfo = {} } = action.payload;
|
||||
|
||||
if (panelName) {
|
||||
store.dispatch(enqueuePanelHistory(panelName, panelInfo, 'UPDATE'));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// RESET_PANELS: 히스토리 초기화
|
||||
case types.RESET_PANELS: {
|
||||
store.dispatch(clearPanelHistory());
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// Other actions are ignored
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[panelHistoryMiddleware] 오류:', error);
|
||||
// Middleware 오류가 앱 전체에 영향 주지 않도록 처리
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default panelHistoryMiddleware;
|
||||
137
com.twin.app.shoptime/src/reducers/panelHistoryReducer.js
Normal file
137
com.twin.app.shoptime/src/reducers/panelHistoryReducer.js
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* src/reducers/panelHistoryReducer.js
|
||||
* Panel history Ring Buffer reducer
|
||||
*
|
||||
* 10개의 panel entry를 저장하는 ring buffer 구현
|
||||
* 각 entry는 { panelName, panelInfo, timestamp, action } 객체
|
||||
*/
|
||||
|
||||
import { types } from '../actions/actionTypes';
|
||||
|
||||
// 초기 상태
|
||||
const initialState = {
|
||||
history: new Array(10).fill(null), // 10개 ring buffer
|
||||
head: -1, // 현재 위치 (-1 ~ 9)
|
||||
size: 0, // 현재 저장된 개수 (0 ~ 10)
|
||||
current: null, // 현재 panel entry
|
||||
previous: null, // 이전 panel entry
|
||||
hasTransition: false, // panel 전환 여부
|
||||
};
|
||||
|
||||
/**
|
||||
* Panel을 Ring buffer에 추가
|
||||
* @param {Object} state - 현재 상태
|
||||
* @param {string} panelName - 추가할 panel 이름
|
||||
* @param {Object} panelInfo - panel 정보
|
||||
* @param {string} action - Redux 액션 타입 ('PUSH' | 'POP' | 'UPDATE')
|
||||
* @param {string} timestamp - ISO 8601 timestamp
|
||||
* @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;
|
||||
|
||||
// 새 entry 생성
|
||||
const newEntry = {
|
||||
panelName,
|
||||
panelInfo: { ...panelInfo }, // Deep copy
|
||||
timestamp,
|
||||
action,
|
||||
};
|
||||
|
||||
// Ring buffer에 저장
|
||||
newHistory[newHead] = newEntry;
|
||||
|
||||
// Size 업데이트 (최대 10)
|
||||
const newSize = Math.min(state.size + 1, 10);
|
||||
|
||||
// 이전 panel 추출
|
||||
const previousEntry = state.current;
|
||||
|
||||
return {
|
||||
history: newHistory,
|
||||
head: newHead,
|
||||
size: newSize,
|
||||
current: newEntry,
|
||||
previous: previousEntry,
|
||||
hasTransition: previousEntry !== null && previousEntry.panelName !== newEntry.panelName,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer - Panel history 상태 관리
|
||||
*/
|
||||
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;
|
||||
|
||||
// 입력값 검증
|
||||
if (!panelName || typeof panelName !== 'string') {
|
||||
return state;
|
||||
}
|
||||
|
||||
// 🔽 [중요] 동일한 panelName이 들어오면 panelInfo만 업데이트
|
||||
// PUSH/POP/UPDATE 모두 같은 방식으로 처리
|
||||
if (state.current && state.current.panelName === panelName) {
|
||||
return {
|
||||
...state,
|
||||
current: {
|
||||
...state.current,
|
||||
panelInfo: { ...panelInfo }, // panelInfo만 업데이트
|
||||
timestamp,
|
||||
action: actionType, // 최신 action 기록
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 🔽 다른 panelName이면 새로운 버퍼 항목 추가
|
||||
return enqueuePanel(state, panelName, panelInfo, actionType, timestamp);
|
||||
}
|
||||
|
||||
// 🔽 History 초기화
|
||||
case types.CLEAR_PANEL_HISTORY: {
|
||||
return { ...initialState };
|
||||
}
|
||||
|
||||
// 🔽 Panel reset 시 history도 리셋
|
||||
case types.RESET_PANELS: {
|
||||
return { ...initialState };
|
||||
}
|
||||
|
||||
// 🔽 Panel history 명시적 리셋
|
||||
case types.RESET_PANEL_HISTORY: {
|
||||
return { ...initialState };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Selector 헬퍼 함수들
|
||||
*/
|
||||
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 sorted = [];
|
||||
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;
|
||||
@@ -1,11 +1,8 @@
|
||||
import {
|
||||
applyMiddleware,
|
||||
combineReducers,
|
||||
createStore,
|
||||
} from 'redux';
|
||||
import { applyMiddleware, combineReducers, createStore } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { autoCloseMiddleware } from '../middleware/autoCloseMiddleware';
|
||||
import { panelHistoryMiddleware } from '../middleware/panelHistoryMiddleware';
|
||||
import { appDataReducer } from '../reducers/appDataReducer';
|
||||
import { billingReducer } from '../reducers/billingReducer';
|
||||
import { brandReducer } from '../reducers/brandReducer';
|
||||
@@ -29,6 +26,7 @@ import { myPageReducer } from '../reducers/myPageReducer';
|
||||
import { onSaleReducer } from '../reducers/onSaleReducer';
|
||||
import { orderReducer } from '../reducers/orderReducer';
|
||||
import { panelsReducer } from '../reducers/panelReducer';
|
||||
import { panelHistoryReducer } from '../reducers/panelHistoryReducer';
|
||||
import { pinCodeReducer } from '../reducers/pinCodeReducer';
|
||||
import { playReducer } from '../reducers/playReducer';
|
||||
import { productReducer } from '../reducers/productReducer';
|
||||
@@ -41,6 +39,7 @@ import { voiceReducer } from '../reducers/voiceReducer';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
panels: panelsReducer,
|
||||
panelHistory: panelHistoryReducer,
|
||||
device: deviceReducer,
|
||||
common: commonReducer,
|
||||
localSettings: localSettingsReducer,
|
||||
@@ -74,5 +73,5 @@ const rootReducer = combineReducers({
|
||||
|
||||
export const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(thunk, autoCloseMiddleware)
|
||||
applyMiddleware(thunk, panelHistoryMiddleware, autoCloseMiddleware)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user