522 lines
17 KiB
JavaScript
522 lines
17 KiB
JavaScript
import { types } from '../actions/actionTypes';
|
|
import { panel_names } from '../utils/Config';
|
|
import { createDebugHelpers } from '../utils/debug';
|
|
|
|
// 디버그 헬퍼 설정
|
|
const DEBUG_MODE = false;
|
|
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
|
|
|
const initialState = {
|
|
// 기존 상태 - 완전히 호환됨
|
|
panels: [],
|
|
lastPanelAction: '', //"", "push", "pop", "update", "reset", "previewPush", "previewPop", "previewUpdate"
|
|
lastFocusTarget: null, // [251114] 마지막 포커스 대상 (panelName + elementId)
|
|
|
|
// [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: {
|
|
dlog('[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';
|
|
}
|
|
}
|
|
|
|
dlog('[panelReducer] 🔵 PUSH_PANEL END', {
|
|
resultPanels: newState.map((p) => p.name),
|
|
lastAction,
|
|
});
|
|
|
|
return {
|
|
...state,
|
|
panels: newState,
|
|
lastPanelAction: lastAction,
|
|
};
|
|
}
|
|
|
|
case types.POP_PANEL: {
|
|
dlog('[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);
|
|
}
|
|
|
|
dlog('[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';
|
|
const hasDetailPanel = state.panels.some((p) => p.name === panel_names.DETAIL_PANEL);
|
|
const isPlayerPanel =
|
|
action.payload.name === panel_names.PLAYER_PANEL ||
|
|
action.payload.name === panel_names.PLAYER_PANEL_NEW;
|
|
const existingPanel = state.panels.find((p) => p.name === action.payload.name);
|
|
let nextPanelInfo = action.payload.panelInfo || {};
|
|
|
|
// lockModalFalse 플래그 처리: DetailPanel이 스택에 있거나 lock이 이미 true면 modal=true 업데이트를 차단
|
|
if (isPlayerPanel && existingPanel) {
|
|
const lockFlag =
|
|
existingPanel.panelInfo?.lockModalFalse === true || nextPanelInfo.lockModalFalse === true;
|
|
|
|
// unlock 명시 시 그대로 진행
|
|
if (nextPanelInfo.lockModalFalse === false) {
|
|
// do nothing
|
|
} else if (lockFlag && nextPanelInfo.modal === true) {
|
|
nextPanelInfo = {
|
|
...nextPanelInfo,
|
|
modal: false,
|
|
modalContainerId: undefined,
|
|
lockModalFalse: true,
|
|
modalStyle: undefined,
|
|
modalScale: undefined,
|
|
shouldShrinkTo1px: false,
|
|
isHidden: false,
|
|
};
|
|
} else if (lockFlag && nextPanelInfo.modal === undefined && hasDetailPanel) {
|
|
nextPanelInfo = {
|
|
...nextPanelInfo,
|
|
modal:
|
|
existingPanel.panelInfo?.modal === true ? false : existingPanel.panelInfo?.modal,
|
|
modalContainerId:
|
|
existingPanel.panelInfo?.modal === true
|
|
? undefined
|
|
: existingPanel.panelInfo?.modalContainerId,
|
|
lockModalFalse: true,
|
|
modalStyle: existingPanel.panelInfo?.modal === true ? undefined : nextPanelInfo.modalStyle,
|
|
modalScale: existingPanel.panelInfo?.modal === true ? undefined : nextPanelInfo.modalScale,
|
|
shouldShrinkTo1px: false,
|
|
isHidden: false,
|
|
};
|
|
} else if (hasDetailPanel && nextPanelInfo.modal === true) {
|
|
// DetailPanel 존재 시 modal=true 업데이트 차단
|
|
nextPanelInfo = {
|
|
...nextPanelInfo,
|
|
modal: false,
|
|
modalContainerId: undefined,
|
|
modalStyle: undefined,
|
|
modalScale: undefined,
|
|
shouldShrinkTo1px: false,
|
|
isHidden: false,
|
|
};
|
|
}
|
|
}
|
|
// 배열의 끝에서부터 시작하여 조건에 맞는 마지막 인덱스 찾기
|
|
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, ...nextPanelInfo },
|
|
}
|
|
: 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: {
|
|
dlog('[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 || {},
|
|
}))
|
|
: [];
|
|
|
|
dlog('[panelReducer] 🟢 RESET_PANELS END', {
|
|
resultPanels: updatedPanels.map((p) => p.name),
|
|
});
|
|
|
|
return {
|
|
...state,
|
|
panels: updatedPanels,
|
|
lastPanelAction: 'reset',
|
|
};
|
|
}
|
|
|
|
// [251106] 패널 액션 큐 관련 reducer 케이스들
|
|
case types.ENQUEUE_PANEL_ACTION: {
|
|
dlog('[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: {
|
|
dlog('[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);
|
|
|
|
dlog('[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:
|
|
dwarn('[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;
|
|
|
|
dlog('[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) {
|
|
derror('[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: {
|
|
dlog('[panelReducer] 🔵 CLEAR_PANEL_QUEUE', {
|
|
currentQueueLength: state.panelActionQueue.length,
|
|
});
|
|
|
|
return {
|
|
...state,
|
|
panelActionQueue: [],
|
|
isProcessingQueue: false,
|
|
queueError: null,
|
|
};
|
|
}
|
|
|
|
case types.SET_QUEUE_PROCESSING: {
|
|
dlog('[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: {
|
|
dlog('[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: {
|
|
dlog('[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: {
|
|
derror('[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,
|
|
},
|
|
};
|
|
}
|
|
|
|
// [251114] 명시적 포커스 이동
|
|
case types.FOCUS_PANEL: {
|
|
dlog('[panelReducer] 🎯 FOCUS_PANEL', {
|
|
panelName: action.payload.panelName,
|
|
focusTarget: action.payload.focusTarget,
|
|
timestamp: action.payload.timestamp,
|
|
});
|
|
|
|
return {
|
|
...state,
|
|
lastFocusTarget: {
|
|
panelName: action.payload.panelName,
|
|
focusTarget: action.payload.focusTarget,
|
|
timestamp: action.payload.timestamp,
|
|
},
|
|
};
|
|
}
|
|
|
|
default:
|
|
return state;
|
|
}
|
|
};
|