Files
shoptime/com.twin.app.shoptime/src/reducers/panelReducer.js
optrader f9290a12ba [251106] fix: Dispatch Queue implementation
🕐 커밋 시간: 2025. 11. 06. 20:46:35

📊 변경 통계:
  • 총 파일: 7개
  • 추가: +398줄
  • 삭제: -1줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/utils/advancedAsyncPanelExamples.js
  + com.twin.app.shoptime/src/utils/asyncActionUtils.js
  + com.twin.app.shoptime/src/utils/asyncPanelQueueExamples.js
  + com.twin.app.shoptime/src/utils/compatibleAsyncPanelExamples.js

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/actionTypes.js
  ~ com.twin.app.shoptime/src/actions/queuedPanelActions.js
  ~ com.twin.app.shoptime/src/reducers/panelReducer.js

🔧 주요 변경 내용:
  • 타입 시스템 안정성 강화
  • 핵심 비즈니스 로직 개선
  • 공통 유틸리티 함수 최적화
  • 대규모 기능 개발
  • 모듈 구조 개선
2025-11-06 20:46:36 +09:00

440 lines
14 KiB
JavaScript

import { types } from '../actions/actionTypes';
import { panel_names } from '../utils/Config';
const initialState = {
// 기존 상태 - 완전히 호환됨
panels: [],
lastPanelAction: '', //"", "push", "pop", "update", "reset", "previewPush", "previewPop", "previewUpdate"
// [251106] 패널 액션 큐 관련 상태 - 기존 기능에 전혀 영향 없음
panelActionQueue: [], // 처리 대기 중인 패널 액션 큐
isProcessingQueue: false, // 현재 큐 처리 중인지 여부
queueError: null, // 큐 처리 중 발생한 에러
queueStats: {
totalProcessed: 0, // 총 처리된 액션 수
failedCount: 0, // 실패한 액션 수
averageProcessingTime: 0 // 평균 처리 시간
},
// [251106] 비동기 액션 관련 상태
asyncActions: {}, // 실행 중인 비동기 액션들 { actionId: { ... } }
completedAsyncActions: [], // 완료된 비동기 액션 ID들
failedAsyncActions: [], // 실패한 비동기 액션 ID들
};
// last one will be on top
const forceTopPanels = [panel_names.ERROR_PANEL, panel_names.INTRO_PANEL, panel_names.DEBUG_PANEL];
export const panelsReducer = (state = initialState, action) => {
switch (action.type) {
case types.PUSH_PANEL: {
console.log('[panelReducer] 🔵 PUSH_PANEL START', {
newPanelName: action.payload.name,
currentPanels: state.panels.map(p => p.name),
duplicatable: action.duplicatable,
});
const panelInfo = action.payload.panelInfo || {};
const forceTopPanelsInfo = [];
const newState = [];
state.panels.forEach((panel) => {
const forceTopIndex = forceTopPanels.indexOf(panel.name);
if (forceTopIndex >= 0) {
forceTopPanelsInfo[forceTopIndex] = panel;
} else if (panel.name !== action.payload.name || action.duplicatable) {
newState.push(panel);
}
});
const newPanelForceTopIndex = forceTopPanels.indexOf(action.payload.name);
if (newPanelForceTopIndex >= 0) {
forceTopPanelsInfo[newPanelForceTopIndex] = {
...action.payload,
panelInfo,
};
} else {
newState.push({ ...action.payload, panelInfo });
}
forceTopPanels.forEach((_, index) => {
if (forceTopPanelsInfo[index]) {
newState.push(forceTopPanelsInfo[index]);
}
});
let lastAction = 'push';
if (
action.payload.name === panel_names.PLAYER_PANEL ||
action.payload.name === panel_names.MEDIA_PANEL
) {
if (action.payload.panelInfo.modal) {
lastAction = 'previewPush';
}
}
console.log('[panelReducer] 🔵 PUSH_PANEL END', {
resultPanels: newState.map(p => p.name),
lastAction,
});
return {
...state,
panels: newState,
lastPanelAction: lastAction,
};
}
case types.POP_PANEL: {
console.log('[panelReducer] 🔴 POP_PANEL START', {
targetPanel: action.payload || 'last_panel',
currentPanels: state.panels.map(p => p.name),
});
let lastAction = 'pop';
let resultPanels;
if (action.payload) {
if (
state.lastPanelAction.indexOf('preview') === 0 &&
(action.payload === panel_names.PLAYER_PANEL ||
action.payload === panel_names.MEDIA_PANEL)
) {
lastAction = 'previewPop';
}
resultPanels = state.panels.filter((panel) => panel.name !== action.payload);
} else {
if (state.lastPanelAction.indexOf('preview') === 0) {
lastAction = 'previewPop';
}
resultPanels = state.panels.slice(0, state.panels.length - 1);
}
console.log('[panelReducer] 🔴 POP_PANEL END', {
resultPanels: resultPanels.map(p => p.name),
lastAction,
});
return {
...state,
panels: resultPanels,
lastPanelAction: lastAction,
};
}
case types.UPDATE_PANEL: {
let lastIndex = -1;
let lastAction = 'update';
// 배열의 끝에서부터 시작하여 조건에 맞는 마지막 인덱스 찾기
for (let i = state.panels.length - 1; i >= 0; i--) {
if (state.panels[i].name === action.payload.name) {
lastIndex = i;
break; // 조건에 맞는 첫 번째 요소를 찾으면 루프 종료
}
}
const newPanels = state.panels.map((panel, index) =>
index === lastIndex
? {
...panel,
panelInfo: { ...panel.panelInfo, ...action.payload.panelInfo },
}
: panel
);
if (newPanels.length > 0) {
const lastPanel = newPanels[newPanels.length - 1];
if (
(lastPanel.name === panel_names.PLAYER_PANEL ||
lastPanel.name === panel_names.MEDIA_PANEL) &&
lastPanel.panelInfo.modal
) {
lastAction = 'previewUpdate';
}
}
return {
...state,
panels: newPanels,
lastPanelAction: lastAction,
};
}
case types.RESET_PANELS: {
console.log('[panelReducer] 🟢 RESET_PANELS START', {
currentPanels: state.panels.map(p => p.name),
payloadProvided: !!action.payload,
});
const updatedPanels = action.payload
? action.payload.map((panel) => ({
...panel,
panelInfo: panel.panelInfo || {},
}))
: [];
console.log('[panelReducer] 🟢 RESET_PANELS END', {
resultPanels: updatedPanels.map(p => p.name),
});
return {
...state,
panels: updatedPanels,
lastPanelAction: 'reset',
};
}
// [251106] 패널 액션 큐 관련 reducer 케이스들
case types.ENQUEUE_PANEL_ACTION: {
console.log('[panelReducer] 🟠 ENQUEUE_PANEL_ACTION', {
action: action.payload.action,
queueId: action.payload.id,
currentQueueLength: state.panelActionQueue.length,
});
const newQueueItem = action.payload;
const updatedQueue = [...state.panelActionQueue, newQueueItem];
return {
...state,
panelActionQueue: updatedQueue,
queueError: null, // 에러 초기화
};
}
case types.PROCESS_PANEL_QUEUE: {
console.log('[panelReducer] 🟡 PROCESS_PANEL_QUEUE', {
isProcessing: state.isProcessingQueue,
queueLength: state.panelActionQueue.length,
});
// 이미 처리 중이거나 큐가 비어있으면 아무것도 하지 않음
if (state.isProcessingQueue || state.panelActionQueue.length === 0) {
return state;
}
// 큐의 첫 번째 아이템을 가져옴
const firstQueueItem = state.panelActionQueue[0];
const remainingQueue = state.panelActionQueue.slice(1);
console.log('[panelReducer] 🟡 PROCESSING_QUEUE_ITEM', {
action: firstQueueItem.action,
queueId: firstQueueItem.id,
remainingQueueLength: remainingQueue.length,
});
// 실제 패널 액션을 실행하여 새로운 상태 계산
let newState = state;
const startTime = Date.now();
try {
switch (firstQueueItem.action) {
case 'PUSH_PANEL': {
const mockAction = {
type: types.PUSH_PANEL,
payload: firstQueueItem.panel,
duplicatable: firstQueueItem.duplicatable,
};
newState = panelsReducer(state, mockAction);
break;
}
case 'POP_PANEL': {
const mockAction = {
type: types.POP_PANEL,
payload: firstQueueItem.panelName,
};
newState = panelsReducer(state, mockAction);
break;
}
case 'UPDATE_PANEL': {
const mockAction = {
type: types.UPDATE_PANEL,
payload: firstQueueItem.panelInfo,
};
newState = panelsReducer(state, mockAction);
break;
}
case 'RESET_PANELS': {
const mockAction = {
type: types.RESET_PANELS,
payload: firstQueueItem.panels,
};
newState = panelsReducer(state, mockAction);
break;
}
default:
console.warn('[panelReducer] ⚠️ UNKNOWN_QUEUE_ACTION', firstQueueItem.action);
newState = state;
}
const processingTime = Date.now() - startTime;
const newTotalProcessed = state.queueStats.totalProcessed + 1;
const newAverageTime =
(state.queueStats.averageProcessingTime * state.queueStats.totalProcessed + processingTime) /
newTotalProcessed;
console.log('[panelReducer] ✅ QUEUE_ITEM_PROCESSED', {
action: firstQueueItem.action,
queueId: firstQueueItem.id,
processingTime,
newTotalProcessed,
});
return {
...newState,
panelActionQueue: remainingQueue,
isProcessingQueue: remainingQueue.length > 0,
queueStats: {
...newState.queueStats,
totalProcessed: newTotalProcessed,
averageProcessingTime: Math.round(newAverageTime * 100) / 100,
},
};
} catch (error) {
console.error('[panelReducer] ❌ QUEUE_PROCESSING_ERROR', {
action: firstQueueItem.action,
queueId: firstQueueItem.id,
error: error.message,
});
return {
...state,
panelActionQueue: remainingQueue,
isProcessingQueue: remainingQueue.length > 0,
queueError: {
action: firstQueueItem.action,
queueId: firstQueueItem.id,
error: error.message,
timestamp: Date.now(),
},
queueStats: {
...state.queueStats,
failedCount: state.queueStats.failedCount + 1,
},
};
}
}
case types.CLEAR_PANEL_QUEUE: {
console.log('[panelReducer] 🔵 CLEAR_PANEL_QUEUE', {
currentQueueLength: state.panelActionQueue.length,
});
return {
...state,
panelActionQueue: [],
isProcessingQueue: false,
queueError: null,
};
}
case types.SET_QUEUE_PROCESSING: {
console.log('[panelReducer] 🟣 SET_QUEUE_PROCESSING', {
isProcessing: action.payload.isProcessing,
queueLength: state.panelActionQueue.length,
});
return {
...state,
isProcessingQueue: action.payload.isProcessing,
};
}
// [251106] 비동기 패널 액션 관련 reducer 케이스들
case types.ENQUEUE_ASYNC_PANEL_ACTION: {
console.log('[panelReducer] 🟠 ENQUEUE_ASYNC_PANEL_ACTION', {
actionId: action.payload.id,
timestamp: action.payload.timestamp,
});
return {
...state,
asyncActions: {
...state.asyncActions,
[action.payload.id]: {
...action.payload,
status: 'running',
startTime: Date.now(),
}
},
queueError: null, // 에러 초기화
};
}
case types.COMPLETE_ASYNC_PANEL_ACTION: {
console.log('[panelReducer] ✅ COMPLETE_ASYNC_PANEL_ACTION', {
actionId: action.payload.actionId,
timestamp: action.payload.timestamp,
});
const asyncAction = state.asyncActions[action.payload.actionId];
const executionTime = asyncAction ? Date.now() - asyncAction.startTime : 0;
// 실행 중인 액션에서 제거하고 완료된 액션에 추가
const newAsyncActions = { ...state.asyncActions };
delete newAsyncActions[action.payload.actionId];
return {
...state,
asyncActions: newAsyncActions,
completedAsyncActions: [
...state.completedAsyncActions,
{
actionId: action.payload.actionId,
result: action.payload.result,
executionTime,
completedAt: action.payload.timestamp,
}
].slice(-100), // 최근 100개만 유지
queueError: null,
queueStats: {
...state.queueStats,
totalProcessed: state.queueStats.totalProcessed + 1,
averageProcessingTime: Math.round(
((state.queueStats.averageProcessingTime * state.queueStats.totalProcessed) + executionTime) /
(state.queueStats.totalProcessed + 1) * 100
) / 100,
},
};
}
case types.FAIL_ASYNC_PANEL_ACTION: {
console.log('[panelReducer] ❌ FAIL_ASYNC_PANEL_ACTION', {
actionId: action.payload.actionId,
error: action.payload.error?.message || 'Unknown error',
timestamp: action.payload.timestamp,
});
const asyncAction = state.asyncActions[action.payload.actionId];
const executionTime = asyncAction ? Date.now() - asyncAction.startTime : 0;
// 실행 중인 액션에서 제거하고 실패한 액션에 추가
const newAsyncActions = { ...state.asyncActions };
delete newAsyncActions[action.payload.actionId];
return {
...state,
asyncActions: newAsyncActions,
failedAsyncActions: [
...state.failedAsyncActions,
{
actionId: action.payload.actionId,
error: action.payload.error,
executionTime,
failedAt: action.payload.timestamp,
}
].slice(-100), // 최근 100개만 유지
queueError: {
actionId: action.payload.actionId,
error: action.payload.error,
timestamp: action.payload.timestamp,
},
queueStats: {
...state.queueStats,
failedCount: state.queueStats.failedCount + 1,
},
};
}
default:
return state;
}
};