[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:
2025-10-25 05:33:22 +00:00
parent f03e78932c
commit d09a59a86e
8 changed files with 555 additions and 222 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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',

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

View File

@@ -0,0 +1 @@
export { usePanelHistory, default } from './usePanelHistory';

View File

@@ -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;

View File

@@ -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;

View 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;

View File

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