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