import { types } from './actionTypes'; import { createDebugHelpers } from '../utils/debug'; // 디버그 헬퍼 설정 const DEBUG_MODE = false; const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE); /** * [251106] 큐 기반 패널 액션들 * * 이 액션들은 패널 액션 큐를 통해 순차적으로 실행되어, * 비동기 dispatch로 인한 순서 문제를 해결합니다. * * 기존의 pushPanel, popPanel 등과 완전히 호환되며, * 새로운 큐 시스템이 필요한 경우에만 사용합니다. */ // 고유 ID 생성을 위한 카운터 let queueItemId = 0; /** * 패널을 큐에 추가하여 순차적으로 실행합니다. * @param {Object} panel - 패널 정보 * @param {boolean} duplicatable - 중복 허용 여부 * @returns {Object} Redux action */ export const pushPanelQueued = (panel, duplicatable = false) => ({ type: types.ENQUEUE_PANEL_ACTION, payload: { id: `queue_item_${++queueItemId}_${Date.now()}`, action: 'PUSH_PANEL', panel, duplicatable, timestamp: Date.now(), }, }); /** * 큐를 통해 패널을 제거합니다. * @param {string} panelName - 제거할 패널 이름 (optional, 없으면 마지막 패널 제거) * @returns {Object} Redux action */ export const popPanelQueued = (panelName = null) => ({ type: types.ENQUEUE_PANEL_ACTION, payload: { id: `queue_item_${++queueItemId}_${Date.now()}`, action: 'POP_PANEL', panelName, timestamp: Date.now(), }, }); /** * 큐를 통해 패널 정보를 업데이트합니다. * @param {Object} panelInfo - 업데이트할 패널 정보 * @returns {Object} Redux action */ export const updatePanelQueued = (panelInfo) => ({ type: types.ENQUEUE_PANEL_ACTION, payload: { id: `queue_item_${++queueItemId}_${Date.now()}`, action: 'UPDATE_PANEL', panelInfo, timestamp: Date.now(), }, }); /** * 큐를 통해 패널들을 초기화합니다. * @param {Array} panels - 초기화할 패널 배열 (optional) * @returns {Object} Redux action */ export const resetPanelsQueued = (panels = null) => ({ type: types.ENQUEUE_PANEL_ACTION, payload: { id: `queue_item_${++queueItemId}_${Date.now()}`, action: 'RESET_PANELS', panels, timestamp: Date.now(), }, }); /** * 패널 액션 큐를 즉시 비웁니다. * @returns {Object} Redux action */ export const clearPanelQueue = () => ({ type: types.CLEAR_PANEL_QUEUE, payload: { timestamp: Date.now(), }, }); /** * 패널 액션 큐 처리를 시작합니다. * (주로 내부 사용용) * @returns {Object} Redux action */ export const processPanelQueue = () => ({ type: types.PROCESS_PANEL_QUEUE, payload: { timestamp: Date.now(), }, }); /** * 큐 처리 상태를 설정합니다. * (주로 내부 사용용) * @param {boolean} isProcessing - 처리 중 상태 * @returns {Object} Redux action */ export const setQueueProcessing = (isProcessing) => ({ type: types.SET_QUEUE_PROCESSING, payload: { isProcessing, timestamp: Date.now(), }, }); /** * 여러 패널 액션들을 한 번에 큐에 추가합니다. * @param {Array} actions - 패널 액션 배열 * @returns {Function} dispatch 함수 */ export const enqueueMultiplePanelActions = (actions) => { return (dispatch) => { actions.forEach((action) => { dispatch(action); }); // 마지막에 큐 처리 시작 dispatch(processPanelQueue()); }; }; /** * 패널 시퀀스를 큐에 추가하는 헬퍼 함수 * @param {Array} sequence - 패널 액션 시퀀스 * @returns {Function} dispatch 함수 */ export const createPanelSequence = (sequence) => { return (dispatch) => { const queuedActions = sequence .map((item) => { switch (item.type) { case 'push': return pushPanelQueued(item.panel, item.duplicatable); case 'pop': return popPanelQueued(item.panelName); case 'update': return updatePanelQueued(item.panelInfo); case 'reset': return resetPanelsQueued(item.panels); default: return null; } }) .filter(Boolean); dispatch(enqueueMultiplePanelActions(queuedActions)); }; }; // =================================================================== // 🔽 [251106] 비동기 액션 완료 처리 기능 // =================================================================== /** * 비동기 패널 액션을 큐에 추가합니다. * API 호출이나 다른 비동기 작업이 포함된 패널 액션을 순차적으로 처리합니다. * * @param {Object} config - 비동기 액션 설정 * @param {string} config.id - 액션 고유 ID * @param {Function} config.asyncAction - 비동기 액션 (dispatch, getState, onSuccess, onFail) => void * @param {Function} config.onSuccess - 성공 콜백 (response) => void * @param {Function} config.onFail - 실패 콜백 (error) => void * @param {Function} config.onFinish - 완료 콜백 (isSuccess, result) => void * @param {number} config.timeout - 타임아웃 (ms, 기본값: 10000) * @returns {Function} Redux thunk */ export const enqueueAsyncPanelAction = (config) => { return (dispatch, getState) => { const actionId = config.id || `async_action_${++queueItemId}_${Date.now()}`; dlog('[queuedPanelActions] 🔄 ENQUEUE_ASYNC_PANEL_ACTION', { actionId, timestamp: Date.now(), }); dispatch({ type: types.ENQUEUE_ASYNC_PANEL_ACTION, payload: { id: actionId, asyncAction: config.asyncAction, onSuccess: config.onSuccess, onFail: config.onFail, onFinish: config.onFinish, timeout: config.timeout || 10000, timestamp: Date.now(), status: 'pending', }, }); // 비동기 액션 실행 executeAsyncAction(dispatch, getState, actionId); }; }; /** * 비동기 액션을 실행하고 완료 처리를 합니다. * @param {Function} dispatch - Redux dispatch * @param {Function} getState - Redux getState * @param {string} actionId - 액션 ID */ const executeAsyncAction = (dispatch, getState, actionId) => { const state = getState(); const asyncAction = state.panels?.panelActionQueue?.find((item) => item.id === actionId); if (!asyncAction) { dwarn('[queuedPanelActions] ⚠️ ASYNC_ACTION_NOT_FOUND', actionId); return; } dlog('[queuedPanelActions] ⚡ EXECUTING_ASYNC_ACTION', actionId); // 비동기 액션을 Promise로 래핑하여 실행 import('../utils/asyncActionUtils') .then(({ wrapAsyncAction, withTimeout }) => { const actionPromise = wrapAsyncAction(asyncAction.asyncAction, { dispatch, getState }); const timeoutPromise = withTimeout(actionPromise, asyncAction.timeout); timeoutPromise .then((result) => { dlog('[queuedPanelActions] 📊 ASYNC_ACTION_RESULT', { actionId, success: result.success, hasError: !!result.error, errorCode: result.error?.code, }); if (result.success) { // 성공 처리 dlog('[queuedPanelActions] ✅ ASYNC_ACTION_SUCCESS', actionId); // 사용자 정의 성공 콜백 실행 if (asyncAction.onSuccess) { try { asyncAction.onSuccess(result.data); } catch (error) { derror('[queuedPanelActions] ❌ USER_ON_SUCCESS_ERROR', error); } } // 완료 콜백 실행 if (asyncAction.onFinish) { try { asyncAction.onFinish(true, result.data); } catch (error) { derror('[queuedPanelActions] ❌ USER_ON_FINISH_ERROR', error); } } // Redux 상태 업데이트 dispatch({ type: types.COMPLETE_ASYNC_PANEL_ACTION, payload: { actionId, result: result.data, timestamp: Date.now(), }, }); } else { // 실패 처리 derror('[queuedPanelActions] ❌ ASYNC_ACTION_FAILED', { actionId, error: result.error, errorCode: result.error?.code, }); // 사용자 정의 실패 콜백 실행 if (asyncAction.onFail) { try { asyncAction.onFail(result.error); } catch (callbackError) { derror('[queuedPanelActions] ❌ USER_ON_FAIL_ERROR', callbackError); } } // 완료 콜백 실행 if (asyncAction.onFinish) { try { asyncAction.onFinish(false, result.error); } catch (callbackError) { derror('[queuedPanelActions] ❌ USER_ON_FINISH_ERROR', callbackError); } } // Redux 상태 업데이트 dispatch({ type: types.FAIL_ASYNC_PANEL_ACTION, payload: { actionId, error: result.error, timestamp: Date.now(), }, }); } }) .catch((error) => { derror('[queuedPanelActions] 💥 ASYNC_ACTION_EXECUTION_ERROR', { actionId, error }); // 치명적인 에러 처리 if (asyncAction.onFail) { try { asyncAction.onFail(error); } catch (callbackError) { derror('[queuedPanelActions] ❌ USER_ON_FAIL_ERROR', callbackError); } } if (asyncAction.onFinish) { try { asyncAction.onFinish(false, error); } catch (callbackError) { derror('[queuedPanelActions] ❌ USER_ON_FINISH_ERROR', callbackError); } } dispatch({ type: types.FAIL_ASYNC_PANEL_ACTION, payload: { actionId, error: { code: 'EXECUTION_ERROR', message: error.message || '비동기 액션 실행 중 치명적인 오류 발생', }, timestamp: Date.now(), }, }); }); }) .catch((error) => { derror('[queuedPanelActions] 💥 ASYNC_UTILS_IMPORT_ERROR', error); // 유틸리티 import 실패 시 기본 처리 if (asyncAction.onFail) { asyncAction.onFail(error); } if (asyncAction.onFinish) { asyncAction.onFinish(false, error); } }); }; /** * API 호출 후 패널 액션을 실행하는 헬퍼 함수 * @param {Object} config - 설정 * @param {Function} config.apiCall - API 호출 함수 (dispatch, getState, onSuccess, onFail) => void * @param {Array} config.panelActions - API 성공 후 실행할 패널 액션들 * @param {Function} config.onApiSuccess - API 성공 콜백 (선택) * @param {Function} config.onApiFail - API 실패 콜백 (선택) * @returns {Function} Redux thunk */ export const createApiWithPanelActions = (config) => { return enqueueAsyncPanelAction({ asyncAction: (dispatch, getState, onSuccess, onFail) => { dlog('[queuedPanelActions] 🌐 API_CALL_START'); config.apiCall(dispatch, getState, onSuccess, onFail); }, onSuccess: (response) => { dlog('[queuedPanelActions] 🎯 API_SUCCESS_EXECUTING_PANELS'); // API 성공 콜백 실행 if (config.onApiSuccess) { config.onApiSuccess(response); } // 패널 액션들 순차 실행 if (config.panelActions && config.panelActions.length > 0) { config.panelActions.forEach((panelAction, index) => { setTimeout(() => { if (typeof panelAction === 'function') { dispatch(panelAction(response)); } else { dispatch(panelAction); } }, index * 10); // 10ms 간격으로 실행 }); } }, onFail: (error) => { dlog('[queuedPanelActions] 🚫 API_FAILED', error); // API 실패 콜백 실행 if (config.onApiFail) { config.onApiFail(error); } }, onFinish: (isSuccess, result) => { dlog('[queuedPanelActions] 🏁 API_WITH_PANELS_COMPLETE', { isSuccess }); }, }); }; /** * 여러 비동기 액션들을 순차적으로 실행합니다. * @param {Array} asyncConfigs - 비동기 액션 설정 배열 * @returns {Function} Redux thunk */ export const createAsyncPanelSequence = (asyncConfigs) => { return (dispatch, getState) => { let currentIndex = 0; const executeNext = () => { if (currentIndex >= asyncConfigs.length) { dlog('[queuedPanelActions] 🎊 ASYNC_SEQUENCE_COMPLETE'); return; } const config = asyncConfigs[currentIndex]; dlog('[queuedPanelActions] 📋 EXECUTING_ASYNC_SEQUENCE_ITEM', { index: currentIndex, total: asyncConfigs.length, }); // 현재 액션에 다음 액션 실행 로직 추가 const enhancedConfig = { ...config, onFinish: (isSuccess, result) => { // 원래 onFinish 콜백 실행 if (config.onFinish) { config.onFinish(isSuccess, result); } // 성공한 경우에만 다음 액션 실행 if (isSuccess) { currentIndex++; setTimeout(executeNext, 50); // 50ms 후 다음 액션 실행 } else { derror('[queuedPanelActions] ⛔ ASYNC_SEQUENCE_STOPPED_ON_ERROR', { index: currentIndex, error: result, }); } }, }; dispatch(enqueueAsyncPanelAction(enhancedConfig)); }; // 첫 번째 액션 실행 executeNext(); }; };