diff --git a/com.twin.app.shoptime/src/actions/actionTypes.js b/com.twin.app.shoptime/src/actions/actionTypes.js index c088ae3f..ba5f877d 100644 --- a/com.twin.app.shoptime/src/actions/actionTypes.js +++ b/com.twin.app.shoptime/src/actions/actionTypes.js @@ -18,6 +18,11 @@ export const types = { CLEAR_PANEL_QUEUE: 'CLEAR_PANEL_QUEUE', SET_QUEUE_PROCESSING: 'SET_QUEUE_PROCESSING', + // ๐Ÿ”ฝ [251106] ๋น„๋™๊ธฐ ์•ก์…˜ ์™„๋ฃŒ ์ฒ˜๋ฆฌ + ENQUEUE_ASYNC_PANEL_ACTION: 'ENQUEUE_ASYNC_PANEL_ACTION', + COMPLETE_ASYNC_PANEL_ACTION: 'COMPLETE_ASYNC_PANEL_ACTION', + FAIL_ASYNC_PANEL_ACTION: 'FAIL_ASYNC_PANEL_ACTION', + // device actions GET_AUTHENTICATION_CODE: 'GET_AUTHENTICATION_CODE', REGISTER_DEVICE: 'REGISTER_DEVICE', diff --git a/com.twin.app.shoptime/src/actions/queuedPanelActions.js b/com.twin.app.shoptime/src/actions/queuedPanelActions.js index 81c826ef..e0cabb27 100644 --- a/com.twin.app.shoptime/src/actions/queuedPanelActions.js +++ b/com.twin.app.shoptime/src/actions/queuedPanelActions.js @@ -151,4 +151,295 @@ export const createPanelSequence = (sequence) => { 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()}`; + + console.log('[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) { + console.warn('[queuedPanelActions] โš ๏ธ ASYNC_ACTION_NOT_FOUND', actionId); + return; + } + + console.log('[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 => { + console.log('[queuedPanelActions] ๐Ÿ“Š ASYNC_ACTION_RESULT', { + actionId, + success: result.success, + hasError: !!result.error, + errorCode: result.error?.code + }); + + if (result.success) { + // ์„ฑ๊ณต ์ฒ˜๋ฆฌ + console.log('[queuedPanelActions] โœ… ASYNC_ACTION_SUCCESS', actionId); + + // ์‚ฌ์šฉ์ž ์ •์˜ ์„ฑ๊ณต ์ฝœ๋ฐฑ ์‹คํ–‰ + if (asyncAction.onSuccess) { + try { + asyncAction.onSuccess(result.data); + } catch (error) { + console.error('[queuedPanelActions] โŒ USER_ON_SUCCESS_ERROR', error); + } + } + + // ์™„๋ฃŒ ์ฝœ๋ฐฑ ์‹คํ–‰ + if (asyncAction.onFinish) { + try { + asyncAction.onFinish(true, result.data); + } catch (error) { + console.error('[queuedPanelActions] โŒ USER_ON_FINISH_ERROR', error); + } + } + + // Redux ์ƒํƒœ ์—…๋ฐ์ดํŠธ + dispatch({ + type: types.COMPLETE_ASYNC_PANEL_ACTION, + payload: { + actionId, + result: result.data, + timestamp: Date.now() + } + }); + + } else { + // ์‹คํŒจ ์ฒ˜๋ฆฌ + console.error('[queuedPanelActions] โŒ ASYNC_ACTION_FAILED', { + actionId, + error: result.error, + errorCode: result.error?.code + }); + + // ์‚ฌ์šฉ์ž ์ •์˜ ์‹คํŒจ ์ฝœ๋ฐฑ ์‹คํ–‰ + if (asyncAction.onFail) { + try { + asyncAction.onFail(result.error); + } catch (callbackError) { + console.error('[queuedPanelActions] โŒ USER_ON_FAIL_ERROR', callbackError); + } + } + + // ์™„๋ฃŒ ์ฝœ๋ฐฑ ์‹คํ–‰ + if (asyncAction.onFinish) { + try { + asyncAction.onFinish(false, result.error); + } catch (callbackError) { + console.error('[queuedPanelActions] โŒ USER_ON_FINISH_ERROR', callbackError); + } + } + + // Redux ์ƒํƒœ ์—…๋ฐ์ดํŠธ + dispatch({ + type: types.FAIL_ASYNC_PANEL_ACTION, + payload: { + actionId, + error: result.error, + timestamp: Date.now() + } + }); + } + }) + .catch(error => { + console.error('[queuedPanelActions] ๐Ÿ’ฅ ASYNC_ACTION_EXECUTION_ERROR', { actionId, error }); + + // ์น˜๋ช…์ ์ธ ์—๋Ÿฌ ์ฒ˜๋ฆฌ + if (asyncAction.onFail) { + try { + asyncAction.onFail(error); + } catch (callbackError) { + console.error('[queuedPanelActions] โŒ USER_ON_FAIL_ERROR', callbackError); + } + } + + if (asyncAction.onFinish) { + try { + asyncAction.onFinish(false, error); + } catch (callbackError) { + console.error('[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 => { + console.error('[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) => { + console.log('[queuedPanelActions] ๐ŸŒ API_CALL_START'); + config.apiCall(dispatch, getState, onSuccess, onFail); + }, + onSuccess: (response) => { + console.log('[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) => { + console.log('[queuedPanelActions] ๐Ÿšซ API_FAILED', error); + + // API ์‹คํŒจ ์ฝœ๋ฐฑ ์‹คํ–‰ + if (config.onApiFail) { + config.onApiFail(error); + } + }, + onFinish: (isSuccess, result) => { + console.log('[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) { + console.log('[queuedPanelActions] ๐ŸŽŠ ASYNC_SEQUENCE_COMPLETE'); + return; + } + + const config = asyncConfigs[currentIndex]; + console.log('[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 { + console.error('[queuedPanelActions] โ›” ASYNC_SEQUENCE_STOPPED_ON_ERROR', { + index: currentIndex, + error: result + }); + } + } + }; + + dispatch(enqueueAsyncPanelAction(enhancedConfig)); + }; + + // ์ฒซ ๋ฒˆ์งธ ์•ก์…˜ ์‹คํ–‰ + executeNext(); + }; }; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/reducers/panelReducer.js b/com.twin.app.shoptime/src/reducers/panelReducer.js index b9a324a5..50049037 100644 --- a/com.twin.app.shoptime/src/reducers/panelReducer.js +++ b/com.twin.app.shoptime/src/reducers/panelReducer.js @@ -14,7 +14,12 @@ const initialState = { totalProcessed: 0, // ์ด ์ฒ˜๋ฆฌ๋œ ์•ก์…˜ ์ˆ˜ failedCount: 0, // ์‹คํŒจํ•œ ์•ก์…˜ ์ˆ˜ averageProcessingTime: 0 // ํ‰๊ท  ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ - } + }, + + // [251106] ๋น„๋™๊ธฐ ์•ก์…˜ ๊ด€๋ จ ์ƒํƒœ + asyncActions: {}, // ์‹คํ–‰ ์ค‘์ธ ๋น„๋™๊ธฐ ์•ก์…˜๋“ค { actionId: { ... } } + completedAsyncActions: [], // ์™„๋ฃŒ๋œ ๋น„๋™๊ธฐ ์•ก์…˜ ID๋“ค + failedAsyncActions: [], // ์‹คํŒจํ•œ ๋น„๋™๊ธฐ ์•ก์…˜ ID๋“ค }; // last one will be on top @@ -332,6 +337,102 @@ export const panelsReducer = (state = initialState, action) => { }; } + // [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; } diff --git a/com.twin.app.shoptime/src/utils/advancedAsyncPanelExamples.js b/com.twin.app.shoptime/src/utils/advancedAsyncPanelExamples.js new file mode 100644 index 00000000..6a126968 --- /dev/null +++ b/com.twin.app.shoptime/src/utils/advancedAsyncPanelExamples.js @@ -0,0 +1,720 @@ +/** + * [251106] ๊ณ ๊ธ‰ ๋น„๋™๊ธฐ ํŒจ๋„ ์•ก์…˜ ์˜ˆ์‹œ + * + * ์ด ํŒŒ์ผ์€ Promise ๊ธฐ๋ฐ˜์˜ ์ •๊ตํ•œ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์™€ + * ํ”„๋กœ์ ํŠธ ํŠนํ™”๋œ ์„ฑ๊ณต/์‹คํŒจ ๊ธฐ์ค€์„ ์ ์šฉํ•œ ์˜ˆ์‹œ๋“ค์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. + */ + +import { + enqueueAsyncPanelAction, + createApiWithPanelActions, + createAsyncPanelSequence +} from '../actions/queuedPanelActions'; + +import { + pushPanelQueued, + popPanelQueued, + updatePanelQueued, + clearPanelQueue +} from '../actions/queuedPanelActions'; + +import { + fetchApi, + tAxiosToPromise, + wrapAsyncAction, + executeParallelAsyncActions, + withTimeout, + processAsyncResult +} from './asyncActionUtils'; + +import { panel_names } from './Config'; + +/** + * ์˜ˆ์‹œ 1: Promise ๊ธฐ๋ฐ˜ fetch API ์‚ฌ์šฉ + * + * 200 + retCode 0/'0' ์„ฑ๊ณต ๊ธฐ์ค€ ์ ์šฉ + */ +export const promiseBasedFetchExample = (dispatch, getState, searchQuery) => { + console.log('๐ŸŒ Promise Based Fetch Example'); + + dispatch(enqueueAsyncPanelAction({ + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + const result = await fetchApi(`/api/search?q=${encodeURIComponent(searchQuery)}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + // ํ•ญ์ƒ resolve๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ƒ์„ธ ์ œ์–ด + onSuccess(result); + } catch (error) { + onFail(error); + } + }, + + onSuccess: (result) => { + console.log('โœ… Fetch Success:', { + httpStatus: result.response?.status, + retCode: result.data?.retCode, + success: result.success + }); + + // ์„ฑ๊ณต ์‹œ ํŒจ๋„ ์•ก์…˜๋“ค + dispatch(pushPanelQueued({ + name: panel_names.SEARCH_PANEL, + panelInfo: { + query: searchQuery, + results: result.data?.data || [], + isLoading: false + } + })); + }, + + onFail: (error) => { + console.error('โŒ Fetch Failed:', { + code: error.code, + message: error.message, + httpStatus: error.httpStatus + }); + + // ์‹คํŒจ ์‹œ ์—๋Ÿฌ ํŒจ๋„ + dispatch(pushPanelQueued({ + name: panel_names.ERROR_PANEL, + panelInfo: { + error: error.message, + errorCode: error.code, + canRetry: true, + retryAction: () => promiseBasedFetchExample(dispatch, getState, searchQuery) + } + })); + }, + + onFinish: (isSuccess, result) => { + console.log('๐Ÿ Fetch Complete:', { + isSuccess, + hasData: !!result?.data, + errorCode: result?.error?.code + }); + } + })); +}; + +/** + * ์˜ˆ์‹œ 2: TAxios ๋ฐฉ์‹์„ Promise๋กœ ๋ณ€ํ™˜ + * + * ๊ธฐ์กด TAxios ์ฝ”๋“œ์™€ ํ˜ธํ™˜๋˜๋ฉด์„œ Promise ๊ธฐ๋ฐ˜ ์ œ์–ด + */ +export const tAxiosPromiseExample = (dispatch, getState, productId) => { + console.log('๐Ÿ”„ TAxios Promise Example'); + + dispatch(enqueueAsyncPanelAction({ + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + // TAxios ๋™์  import + const { TAxios } = await import('../api/TAxios'); + + const result = await tAxiosToPromise( + TAxios, + dispatch, + getState, + 'get', + '/api/products', + { productId }, // URL params + {}, // request params + { + noTokenRefresh: false, + responseType: 'json' + } + ); + + onSuccess(result); + } catch (error) { + onFail(error); + } + }, + + onSuccess: (result) => { + console.log('โœ… TAxios Success:', { + retCode: result.data?.retCode, + success: result.success, + hasData: !!result.data?.data + }); + + if (result.success) { + dispatch(pushPanelQueued({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + product: result.data?.data, + isLoading: false + } + })); + } else { + // retCode๊ฐ€ 0์ด ์•„๋‹Œ ๊ฒฝ์šฐ ์‹คํŒจ ์ฒ˜๋ฆฌ + dispatch(pushPanelQueued({ + name: panel_names.ERROR_PANEL, + panelInfo: { + error: result.error?.message || '์ œํ’ˆ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค', + errorCode: result.error?.code + } + })); + } + }, + + onFail: (error) => { + console.error('โŒ TAxios Failed:', error); + + dispatch(pushPanelQueued({ + name: panel_names.ERROR_PANEL, + panelInfo: { + error: error.message, + isSystemError: true + } + })); + } + })); +}; + +/** + * ์˜ˆ์‹œ 3: ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ์›Œํฌํ”Œ๋กœ์šฐ (Promise.all) + * + * ์—ฌ๋Ÿฌ API๋ฅผ ๋ณ‘๋ ฌ๋กœ ํ˜ธ์ถœํ•˜๊ณ  ๋ชจ๋‘ ์™„๋ฃŒ๋˜๋ฉด ํŒจ๋„ ์กฐ์ž‘ + */ +export const parallelAsyncWorkflow = (dispatch, getState, userId) => { + console.log('๐Ÿš€ Parallel Async Workflow'); + + dispatch(enqueueAsyncPanelAction({ + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + // ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•  ๋น„๋™๊ธฐ ์•ก์…˜๋“ค + const asyncActions = [ + // ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ๋กœ๋“œ + async (dispatch, getState, success, fail) => { + const result = await fetchApi(`/api/users/${userId}/profile`); + success(result); + }, + + // ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ •๋ณด ๋กœ๋“œ + async (dispatch, getState, success, fail) => { + const result = await fetchApi(`/api/users/${userId}/cart`); + success(result); + }, + + // ์œ„์‹œ๋ฆฌ์ŠคํŠธ ๋กœ๋“œ + async (dispatch, getState, success, fail) => { + const result = await fetchApi(`/api/users/${userId}/wishlist`); + success(result); + } + ]; + + // ๋ณ‘๋ ฌ ์‹คํ–‰ + const results = await executeParallelAsyncActions(asyncActions, { dispatch, getState }); + + // ๋ชจ๋“  ๊ฒฐ๊ณผ ํฌ์žฅ + const combinedResult = { + response: { status: 200 }, + data: { + profile: results[0].data, + cart: results[1].data, + wishlist: results[2].data, + errors: results.filter(r => !r.success).map(r => r.error) + }, + success: results.every(r => r.success), + error: results.some(r => !r.success) ? { + code: 'PARTIAL_FAILURE', + message: '์ผ๋ถ€ ์ •๋ณด ๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค', + errors: results.filter(r => !r.success).map(r => r.error) + } : null + }; + + onSuccess(combinedResult); + } catch (error) { + onFail(error); + } + }, + + onSuccess: (result) => { + console.log('โœ… Parallel Workflow Success:', { + allSuccess: result.success, + hasData: !!result.data, + errorCount: result.data?.errors?.length || 0 + }); + + // ์‚ฌ์šฉ์ž ํŒจ๋„ ํ‘œ์‹œ + dispatch(pushPanelQueued({ + name: panel_names.USER_PANEL, + panelInfo: { + user: result.data.profile?.data, + cartItems: result.data.cart?.data?.items || [], + wishlistItems: result.data.wishlist?.data?.items || [], + hasErrors: !!result.error, + errors: result.data?.errors || [] + } + })); + }, + + onFail: (error) => { + console.error('โŒ Parallel Workflow Failed:', error); + + dispatch(pushPanelQueued({ + name: panel_names.ERROR_PANEL, + panelInfo: { + error: '์‚ฌ์šฉ์ž ์ •๋ณด ๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค', + canRetry: true, + retryAction: () => parallelAsyncWorkflow(dispatch, getState, userId) + } + })); + } + })); +}; + +/** + * ์˜ˆ์‹œ 4: ์ˆœ์ฐจ์  ๋น„๋™๊ธฐ ์ฒด์ด๋‹ + * + * ๊ฐ ๋‹จ๊ณ„์˜ ์„ฑ๊ณต ์—ฌ๋ถ€์— ๋”ฐ๋ผ ๋‹ค์Œ ๋‹จ๊ณ„ ์‹คํ–‰/์ค‘๋‹จ + */ +export const sequentialAsyncChaining = (dispatch, getState, orderId) => { + console.log('๐Ÿ”— Sequential Async Chaining'); + + const asyncSequence = [ + { + name: 'loadOrder', + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + const result = await withTimeout( + fetchApi(`/api/orders/${orderId}`), + 5000, + '์ฃผ๋ฌธ ์ •๋ณด ๋กœ๋“œ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค' + ); + onSuccess(result); + }, + onSuccess: (result) => { + console.log('โœ… Order loaded'); + if (result.success) { + dispatch(pushPanelQueued({ + name: panel_names.ORDER_PANEL, + panelInfo: { + order: result.data?.data, + step: 'loaded' + } + })); + } + }, + onFail: (error) => { + console.error('โŒ Order load failed:', error); + // ์‹คํŒจ ์‹œ ์ฒด์ด๋‹ ์ค‘๋‹จ + dispatch(pushPanelQueued({ + name: panel_names.ERROR_PANEL, + panelInfo: { + error: '์ฃผ๋ฌธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค', + errorCode: error.code + } + })); + } + }, + + { + name: 'loadPayment', + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + const result = await withTimeout( + fetchApi(`/api/orders/${orderId}/payment`), + 3000, + '๊ฒฐ์ œ ์ •๋ณด ๋กœ๋“œ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค' + ); + onSuccess(result); + }, + onSuccess: (result) => { + console.log('โœ… Payment loaded'); + if (result.success) { + dispatch(updatePanelQueued({ + name: panel_names.ORDER_PANEL, + panelInfo: { + payment: result.data?.data, + step: 'payment_loaded' + } + })); + } + }, + onFail: (error) => { + console.error('โŒ Payment load failed:', error); + // ๊ฒฐ์ œ ์ •๋ณด ์‹คํŒจ๋Š” ์ฃผ๋ฌธ์—๋Š” ์˜ํ–ฅ ์—†์Œ + dispatch(updatePanelQueued({ + name: panel_names.ORDER_PANEL, + panelInfo: { + paymentError: error.message, + step: 'payment_failed' + } + })); + } + }, + + { + name: 'loadShipping', + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + const result = await withTimeout( + fetchApi(`/api/orders/${orderId}/shipping`), + 3000, + '๋ฐฐ์†ก ์ •๋ณด ๋กœ๋“œ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค' + ); + onSuccess(result); + }, + onSuccess: (result) => { + console.log('โœ… Shipping loaded'); + if (result.success) { + dispatch(updatePanelQueued({ + name: panel_names.ORDER_PANEL, + panelInfo: { + shipping: result.data?.data, + step: 'completed' + } + })); + } + }, + onFail: (error) => { + console.error('โŒ Shipping load failed:', error); + dispatch(updatePanelQueued({ + name: panel_names.ORDER_PANEL, + panelInfo: { + shippingError: error.message, + step: 'shipping_failed' + } + })); + } + } + ]; + + // ์ˆœ์ฐจ์  ์‹คํ–‰ + dispatch(createAsyncPanelSequence(asyncSequence)); +}; + +/** + * ์˜ˆ์‹œ 5: ์žฌ์‹œ๋„ ๋กœ์ง๊ณผ ์—๋Ÿฌ ๋ณต๊ตฌ + * + * ์‹คํŒจ ์‹œ ์ž๋™ ์žฌ์‹œ๋„ ๋ฐ ํด๋ฐฑ ๋กœ์ง + */ +export const retryAndFallbackExample = (dispatch, getState, productId) => { + console.log('๐Ÿ”„ Retry and Fallback Example'); + + const maxRetries = 3; + let retryCount = 0; + + const attemptLoad = () => { + retryCount++; + console.log(`๐Ÿ“ก Attempt ${retryCount}/${maxRetries}`); + + dispatch(enqueueAsyncPanelAction({ + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + const result = await withTimeout( + fetchApi(`/api/products/${productId}`), + 3000, + '์ œํ’ˆ ์ •๋ณด ๋กœ๋“œ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค' + ); + + // ์„ฑ๊ณต/์‹คํŒจ ๊ด€๊ณ„์—†์ด ํ•ญ์ƒ resolve + onSuccess(result); + } catch (error) { + onSuccess({ + response: null, + data: null, + success: false, + error: { + code: 'NETWORK_ERROR', + message: error.message, + originalError: error + } + }); + } + }, + + onSuccess: (result) => { + if (result.success) { + console.log('โœ… Product loaded successfully'); + + // ์„ฑ๊ณต ์‹œ ํŒจ๋„ ํ‘œ์‹œ + dispatch(pushPanelQueued({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + product: result.data?.data, + source: 'api', + retryCount + } + })); + + } else { + console.error('โŒ Product load failed:', result.error); + + if (retryCount < maxRetries) { + // ์žฌ์‹œ๋„ + console.log(`๐Ÿ”„ Retrying in 1 second... (${retryCount}/${maxRetries})`); + setTimeout(() => attemptLoad(), 1000); + } else { + // ์ตœ๋Œ€ ์žฌ์‹œ๋„ ๋„๋‹ฌ - ํด๋ฐฑ ๋กœ์ง + console.log('๐Ÿ“ฆ Max retries reached, using fallback'); + + // ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๋‚˜ ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ + dispatch(pushPanelQueued({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + product: { + id: productId, + name: '์ œํ’ˆ ์ •๋ณด ๋กœ๋“œ ์‹คํŒจ', + placeholder: true + }, + source: 'fallback', + error: result.error?.message, + retryCount + } + })); + } + } + }, + + timeout: 4000 // ๊ฐ ์‹œ๋„ ํƒ€์ž„์•„์›ƒ + })); + }; + + // ์ฒซ ๋ฒˆ์งธ ์‹œ๋„ + attemptLoad(); +}; + +/** + * ์˜ˆ์‹œ 6: ์‹ค์ œ ํ”„๋กœ์ ํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค - ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๊ฒฐ์ œ ๊ณผ์ • + * + * ์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ์ ์šฉ ๊ฐ€๋Šฅํ•œ ๋ณตํ•ฉ ์˜ˆ์‹œ + */ +export const realWorldCheckoutFlow = (dispatch, getState, cartItems) => { + console.log('๐Ÿ›’ Real World Checkout Flow'); + + dispatch(clearPanelQueue()); // ๊ธฐ์กด ํ ์ •๋ฆฌ + + const checkoutWorkflow = [ + { + name: 'validateCart', + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + const result = await fetchApi('/api/cart/validate', { + method: 'POST', + body: JSON.stringify({ items: cartItems }) + }); + onSuccess(result); + } catch (error) { + onSuccess({ + response: null, + data: null, + success: false, + error: { code: 'VALIDATION_ERROR', message: error.message } + }); + } + }, + onSuccess: (result) => { + if (result.success) { + dispatch(pushPanelQueued({ + name: panel_names.CHECKOUT_PANEL, + panelInfo: { + step: 'validated', + items: cartItems, + validationResult: result.data + } + })); + } else { + dispatch(pushPanelQueued({ + name: panel_names.CART_PANEL, + panelInfo: { + step: 'validation_failed', + items: cartItems, + errors: result.error + } + })); + } + } + }, + + { + name: 'calculateShipping', + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + const result = await fetchApi('/api/shipping/calculate', { + method: 'POST', + body: JSON.stringify({ + items: cartItems, + address: getState().user?.defaultAddress + }) + }); + onSuccess(result); + } catch (error) { + onSuccess({ + response: null, + data: null, + success: false, + error: { code: 'SHIPPING_ERROR', message: error.message } + }); + } + }, + onSuccess: (result) => { + if (result.success) { + dispatch(updatePanelQueued({ + name: panel_names.CHECKOUT_PANEL, + panelInfo: { + step: 'shipping_calculated', + shippingOptions: result.data?.options || [] + } + })); + } else { + dispatch(updatePanelQueued({ + name: panel_names.CHECKOUT_PANEL, + panelInfo: { + step: 'shipping_error', + shippingError: result.error?.message + } + })); + } + } + }, + + { + name: 'processPayment', + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + const result = await fetchApi('/api/payment/process', { + method: 'POST', + body: JSON.stringify({ + items: cartItems, + shipping: getState().panels?.panels?.find(p => p.name === panel_names.CHECKOUT_PANEL)?.panelInfo?.selectedShipping, + paymentMethod: 'credit_card' + }) + }); + onSuccess(result); + } catch (error) { + onSuccess({ + response: null, + data: null, + success: false, + error: { code: 'PAYMENT_ERROR', message: error.message } + }); + } + }, + onSuccess: (result) => { + if (result.success) { + // ๊ฒฐ์ œ ์„ฑ๊ณต - ์ฃผ๋ฌธ ์™„๋ฃŒ ํŒจ๋„ + dispatch(pushPanelQueued({ + name: panel_names.ORDER_COMPLETE_PANEL, + panelInfo: { + orderData: result.data, + step: 'completed' + } + })); + + // ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํŒจ๋„ ์ œ๊ฑฐ + dispatch(popPanelQueued(panel_names.CHECKOUT_PANEL)); + } else { + // ๊ฒฐ์ œ ์‹คํŒจ - ์—๋Ÿฌ ํ‘œ์‹œ + dispatch(updatePanelQueued({ + name: panel_names.CHECKOUT_PANEL, + panelInfo: { + step: 'payment_failed', + paymentError: result.error?.message, + canRetry: true + } + })); + } + } + } + ]; + + // ์ฒดํฌ์•„์›ƒ ์›Œํฌํ”Œ๋กœ์šฐ ์‹คํ–‰ + dispatch(createAsyncPanelSequence(checkoutWorkflow)); +}; + +/** + * ์˜ˆ์‹œ 7: ๊ธฐ์กด ๋ฐฉ์‹๊ณผ ์ƒˆ๋กœ์šด ๋ฐฉ์‹์˜ ๋น„๊ต + */ +export const comparisonExample = (dispatch, getState) => { + console.log('๐Ÿ“Š Comparison Example - Old vs New'); + + // ๊ธฐ์กด ๋ฐฉ์‹ (๋ณต์žกํ•œ ์ฝœ๋ฐฑ ์ฒด์ธ, ์„ฑ๊ณต ๊ธฐ์ค€ ๋ถˆ๋ช…ํ™•) + console.log('โŒ Old way (complex callbacks, unclear success criteria):'); + setTimeout(() => { + // ๋ณต์žกํ•œ ์ฝœ๋ฐฑ ์ฒด์ธ + TAxios( + dispatch, + getState, + 'get', + '/api/user/profile', + {}, + {}, + (response) => { + // ์„ฑ๊ณต ์—ฌ๋ถ€ ๋ถˆ๋ช…ํ™• - ๋‹จ์ˆœํžˆ ์ฝœ๋ฐฑ ํ˜ธ์ถœ๋งŒ ํ™•์ธ + if (response.data && response.data.retCode === 0) { + // ์„ฑ๊ณต ์ฒ˜๋ฆฌ + dispatch({ type: 'PUSH_PANEL', payload: { name: 'USER_PANEL' }}); + + // ๋˜ ๋‹ค๋ฅธ API ํ˜ธ์ถœ - ์ค‘์ฒฉ ์ฝœ๋ฐฑ + TAxios( + dispatch, + getState, + 'get', + '/api/user/cart', + {}, + {}, + (cartResponse) => { + if (cartResponse.data && cartResponse.data.retCode === 0) { + dispatch({ type: 'UPDATE_PANEL', payload: { + name: 'USER_PANEL', + panelInfo: { cart: cartResponse.data.data } + }}); + } + }, + (error) => { + console.error('Cart load failed:', error); + } + ); + } else { + // ์‹คํŒจ ์ฒ˜๋ฆฌ - retCode ํ™•์ธ ํ•„์š” + console.error('Profile load failed, retCode:', response.data?.retCode); + } + }, + (error) => { + console.error('Profile load failed:', error); + dispatch({ type: 'PUSH_PANEL', payload: { name: 'ERROR_PANEL' }}); + } + ); + }, 1000); + + // ์ƒˆ๋กœ์šด ๋ฐฉ์‹ (Promise ๊ธฐ๋ฐ˜, ๋ช…ํ™•ํ•œ ์„ฑ๊ณต ๊ธฐ์ค€) + console.log('โœ… New way (Promise-based, clear success criteria):'); + setTimeout(() => { + const newWayWorkflow = [ + { + name: 'loadProfile', + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + // ์ž๋™ ์„ฑ๊ณต ๊ธฐ์ค€ ์ ์šฉ (200 + retCode 0/'0') + const result = await fetchApi('/api/user/profile'); + onSuccess(result); + }, + onSuccess: (result) => { + if (result.success) { + dispatch(pushPanelQueued({ name: 'USER_PANEL' })); + } + } + }, + { + name: 'loadCart', + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + const result = await fetchApi('/api/user/cart'); + onSuccess(result); + }, + onSuccess: (result) => { + if (result.success) { + dispatch(updatePanelQueued({ + name: 'USER_PANEL', + panelInfo: { cart: result.data?.data } + })); + } + } + } + ]; + + dispatch(createAsyncPanelSequence(newWayWorkflow)); + }, 2000); +}; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/utils/asyncActionUtils.js b/com.twin.app.shoptime/src/utils/asyncActionUtils.js new file mode 100644 index 00000000..19fc076a --- /dev/null +++ b/com.twin.app.shoptime/src/utils/asyncActionUtils.js @@ -0,0 +1,385 @@ +/** + * [251106] ๋น„๋™๊ธฐ ์•ก์…˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ + * + * ์ด ํŒŒ์ผ์€ Promise ๊ธฐ๋ฐ˜์˜ ๋น„๋™๊ธฐ ์•ก์…˜ ์ฒ˜๋ฆฌ์™€ ์ƒ์„ธํ•œ ์„ฑ๊ณต/์‹คํŒจ ๊ธฐ์ค€์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * ํ”„๋กœ์ ํŠธ ํŠนํ™”๋œ ์„ฑ๊ณต ๊ธฐ์ค€๊ณผ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + */ + +import { ERROR_MESSAGES_GROUPS } from './Config'; + +/** + * API ์‘๋‹ต ์„ฑ๊ณต ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋Š” ํ•จ์ˆ˜ + * + * ์„ฑ๊ณต ๊ธฐ์ค€: + * 1. HTTP ์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 200-299 ๋ฒ”์œ„์—ฌ์•ผ ํ•จ + * 2. ์‘๋‹ต ๋ฐ์ดํ„ฐ์˜ retCode๊ฐ€ 0 ๋˜๋Š” '0'์ด์–ด์•ผ ํ•จ + * + * @param {Response} response - fetch API ์‘๋‹ต ๊ฐ์ฒด + * @param {Object} responseData - ํŒŒ์‹ฑ๋œ ์‘๋‹ต ๋ฐ์ดํ„ฐ + * @returns {boolean} ์„ฑ๊ณต ์—ฌ๋ถ€ + */ +export const isApiSuccess = (response, responseData) => { + // HTTP ์ƒํƒœ ์ฝ”๋“œ ํ™•์ธ (200-299 ์„ฑ๊ณต ๋ฒ”์œ„) + if (!response.ok || response.status < 200 || response.status >= 300) { + return false; + } + + // retCode ํ™•์ธ - 0 ๋˜๋Š” '0'์ด์–ด์•ผ ์„ฑ๊ณต + if (responseData && responseData.retCode !== undefined) { + return responseData.retCode === 0 || responseData.retCode === '0'; + } + + // retCode๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ HTTP ์ƒํƒœ ์ฝ”๋“œ๋งŒ์œผ๋กœ ํŒ๋‹จ + return response.ok; +}; + +/** + * API ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜ + * @param {number} code - ์—๋Ÿฌ ์ฝ”๋“œ + * @returns {string} ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + */ +export const getApiErrorMessage = (code) => { + const errorGroup = ERROR_MESSAGES_GROUPS.find(group => + group.codes && group.codes.includes(code) + ); + + return errorGroup ? errorGroup.message : `์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค (์ฝ”๋“œ: ${code})`; +}; + +/** + * Promise ๊ธฐ๋ฐ˜ fetch API ๋ž˜ํผ + * ํ”„๋กœ์ ํŠธ ์„ฑ๊ณต ๊ธฐ์ค€์— ๋งž์ถฐ ์‘๋‹ต์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param {string} url - API URL + * @param {Object} options - fetch ์˜ต์…˜ + * @returns {Promise} ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•œ Promise + */ +export const fetchApi = (url, options = {}) => { + console.log('[asyncActionUtils] ๐ŸŒ FETCH_API_START', { url, method: options.method || 'GET' }); + + return new Promise((resolve) => { + fetch(url, { + headers: { + 'Content-Type': 'application/json', + ...options.headers + }, + ...options + }) + .then(response => { + // JSON ํŒŒ์‹ฑ + return response.json() + .then(responseData => { + console.log('[asyncActionUtils] ๐Ÿ“Š API_RESPONSE', { + status: response.status, + ok: response.ok, + retCode: responseData.retCode, + success: isApiSuccess(response, responseData) + }); + + // ์„ฑ๊ณต/์‹คํŒจ ์—ฌ๋ถ€์™€ ๊ด€๊ณ„์—†์ด ํ•ญ์ƒ resolve + resolve({ + response, + data: responseData, + success: isApiSuccess(response, responseData), + error: !isApiSuccess(response, responseData) ? { + code: responseData.retCode || response.status, + message: responseData.message || getApiErrorMessage(responseData.retCode || response.status), + httpStatus: response.status + } : null + }); + }) + .catch(parseError => { + console.error('[asyncActionUtils] โŒ JSON_PARSE_ERROR', parseError); + + // JSON ํŒŒ์‹ฑ ์‹คํŒจ๋„ resolve๋กœ ์ฒ˜๋ฆฌ + resolve({ + response, + data: null, + success: false, + error: { + code: 'PARSE_ERROR', + message: '์‘๋‹ต ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค', + originalError: parseError + } + }); + }); + }) + .catch(error => { + console.error('[asyncActionUtils] ๐Ÿ’ฅ FETCH_ERROR', error); + + // ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ ๋“ฑ๋„ resolve๋กœ ์ฒ˜๋ฆฌ + resolve({ + response: null, + data: null, + success: false, + error: { + code: 'NETWORK_ERROR', + message: error.message || '๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค', + originalError: error + } + }); + }); + }); +}; + +/** + * TAxios ๋ฐฉ์‹์˜ API ํ˜ธ์ถœ์„ Promise๋กœ ๋ณ€ํ™˜ + * + * @param {Function} TAxios - TAxios ํ•จ์ˆ˜ + * @param {Function} dispatch - Redux dispatch + * @param {Function} getState - Redux getState + * @param {string} method - HTTP ๋ฉ”์†Œ๋“œ + * @param {string} baseUrl - ๊ธฐ๋ณธ URL + * @param {Object} urlParams - URL ํŒŒ๋ผ๋ฏธํ„ฐ + * @param {Object} params - ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ + * @param {Object} options - ์ถ”๊ฐ€ ์˜ต์…˜ + * @returns {Promise} Promise ๊ธฐ๋ฐ˜ ์‘๋‹ต + */ +export const tAxiosToPromise = (TAxios, dispatch, getState, method, baseUrl, urlParams, params, options = {}) => { + return new Promise((resolve) => { + console.log('[asyncActionUtils] ๐Ÿ”„ TAXIOS_TO_PROMISE_START', { method, baseUrl }); + + const enhancedOnSuccess = (response) => { + console.log('[asyncActionUtils] โœ… TAXIOS_SUCCESS', { retCode: response?.data?.retCode }); + + // TAxios ์„ฑ๊ณต ์ฝœ๋ฐฑ๋„ ์„ฑ๊ณต ๊ธฐ์ค€ ์ ์šฉ + const isSuccess = response?.data && ( + response.data.retCode === 0 || + response.data.retCode === '0' + ); + + resolve({ + response, + data: response.data, + success: isSuccess, + error: !isSuccess ? { + code: response.data?.retCode || 'UNKNOWN_ERROR', + message: response.data?.message || getApiErrorMessage(response.data?.retCode || 'UNKNOWN_ERROR') + } : null + }); + }; + + const enhancedOnFail = (error) => { + console.error('[asyncActionUtils] โŒ TAXIOS_FAIL', error); + + resolve({ + response: null, + data: null, + success: false, + error: { + code: error.retCode || 'TAXIOS_ERROR', + message: error.message || 'API ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค', + originalError: error + } + }); + }; + + try { + TAxios( + dispatch, + getState, + method, + baseUrl, + urlParams, + params, + enhancedOnSuccess, + enhancedOnFail, + options.noTokenRefresh || false, + options.responseType + ); + } catch (error) { + console.error('[asyncActionUtils] ๐Ÿ’ฅ TAXIOS_EXECUTION_ERROR', error); + + resolve({ + response: null, + data: null, + success: false, + error: { + code: 'EXECUTION_ERROR', + message: 'API ํ˜ธ์ถœ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค', + originalError: error + } + }); + } + }); +}; + +/** + * ๋น„๋™๊ธฐ ์•ก์…˜์„ Promise ๊ธฐ๋ฐ˜์œผ๋กœ ๋ž˜ํ•‘ํ•˜๋Š” ํ—ฌํผ ํ•จ์ˆ˜ + * ๋ชจ๋“  ๋น„๋™๊ธฐ ์ž‘์—…์ด Promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param {Function} asyncAction - ๋น„๋™๊ธฐ ์•ก์…˜ ํ•จ์ˆ˜ + * @param {Object} context - ์‹คํ–‰ ์ปจํ…์ŠคํŠธ (dispatch, getState ๋“ฑ) + * @returns {Promise} Promise ๊ธฐ๋ฐ˜ ๊ฒฐ๊ณผ + */ +export const wrapAsyncAction = (asyncAction, context = {}) => { + return new Promise((resolve) => { + const { dispatch, getState } = context; + + console.log('[asyncActionUtils] ๐ŸŽฏ WRAP_ASYNC_ACTION_START'); + + // ์„ฑ๊ณต ์ฝœ๋ฐฑ - ํ•ญ์ƒ resolve ํ˜ธ์ถœ + const onSuccess = (result) => { + console.log('[asyncActionUtils] โœ… WRAP_ASYNC_SUCCESS', result); + + // result์˜ ๊ตฌ์กฐ๋ฅผ ํ‘œ์ค€ํ™” + const normalizedResult = { + response: result.response || result, + data: result.data || result, + success: true, + error: null + }; + + resolve(normalizedResult); + }; + + // ์‹คํŒจ ์ฝœ๋ฐฑ - ํ•ญ์ƒ resolve ํ˜ธ์ถœ (reject ํ•˜์ง€ ์•Š์Œ) + const onFail = (error) => { + console.error('[asyncActionUtils] โŒ WRAP_ASYNC_FAIL', error); + + // error ๊ฐ์ฒด๋ฅผ ํ‘œ์ค€ํ™” + const normalizedError = { + response: null, + data: null, + success: false, + error: { + code: error.retCode || error.code || 'ASYNC_ACTION_ERROR', + message: error.message || error.errorMessage || '๋น„๋™๊ธฐ ์ž‘์—…์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค', + originalError: error + } + }; + + resolve(normalizedError); + }; + + try { + // ๋น„๋™๊ธฐ ์•ก์…˜ ์‹คํ–‰ + const result = asyncAction(dispatch, getState, onSuccess, onFail); + + // Promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ๋„ ์ฒ˜๋ฆฌ + if (result && typeof result.then === 'function') { + result + .then(onSuccess) + .catch(onFail); + } + } catch (error) { + console.error('[asyncActionUtils] ๐Ÿ’ฅ WRAP_ASYNC_EXECUTION_ERROR', error); + onFail(error); + } + }); +}; + +/** + * ์—ฌ๋Ÿฌ ๋น„๋™๊ธฐ ์•ก์…˜์„ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•˜๊ณ  ๋ชจ๋“  ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. + * + * @param {Array} asyncActions - ๋น„๋™๊ธฐ ์•ก์…˜ ๋ฐฐ์—ด + * @param {Object} context - ์‹คํ–‰ ์ปจํ…์ŠคํŠธ + * @returns {Promise} ๋ชจ๋“  ๊ฒฐ๊ณผ ๋ฐฐ์—ด + */ +export const executeParallelAsyncActions = (asyncActions, context = {}) => { + console.log('[asyncActionUtils] ๐Ÿš€ EXECUTE_PARALLEL_START', { count: asyncActions.length }); + + const promises = asyncActions.map(action => + wrapAsyncAction(action, context) + ); + + return Promise.all(promises) + .then(results => { + console.log('[asyncActionUtils] โœ… EXECUTE_PARALLEL_SUCCESS', { + successCount: results.filter(r => r.success).length, + failCount: results.filter(r => !r.success).length + }); + return results; + }) + .catch(error => { + console.error('[asyncActionUtils] โŒ EXECUTE_PARALLEL_ERROR', error); + // Promise.all์ด ์‹คํŒจํ•ด๋„ ๋นˆ ๋ฐฐ์—ด ๋ฐ˜ํ™˜ + return []; + }); +}; + +/** + * ๋น„๋™๊ธฐ ์•ก์…˜ ์‹คํ–‰ ๊ฒฐ๊ณผ๋ฅผ ๊ฒ€์ฆํ•˜๊ณ  ์ฒ˜๋ฆฌํ•˜๋Š” ํ—ฌํผ ํ•จ์ˆ˜ + * + * @param {Object} result - ๋น„๋™๊ธฐ ์•ก์…˜ ์‹คํ–‰ ๊ฒฐ๊ณผ + * @param {Object} options - ์ฒ˜๋ฆฌ ์˜ต์…˜ + * @returns {Object} ์ฒ˜๋ฆฌ๋œ ๊ฒฐ๊ณผ + */ +export const processAsyncResult = (result, options = {}) => { + const { + throwOnError = false, + defaultErrorMessage = '์ž‘์—…์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค', + logErrors = true + } = options; + + if (result.success) { + console.log('[asyncActionUtils] ๐ŸŽ‰ PROCESS_RESULT_SUCCESS'); + return { + ...result, + processed: true + }; + } + + const errorMessage = result.error?.message || defaultErrorMessage; + + if (logErrors) { + console.error('[asyncActionUtils] โŒ PROCESS_RESULT_ERROR', { + code: result.error?.code, + message: errorMessage, + originalError: result.error?.originalError + }); + } + + const processedResult = { + ...result, + processed: true, + errorMessage + }; + + if (throwOnError) { + throw new Error(errorMessage); + } + + return processedResult; +}; + +/** + * ํƒ€์ž„์•„์›ƒ์ด ์žˆ๋Š” Promise ๋ž˜ํผ + * + * @param {Promise} promise - ์›๋ณธ Promise + * @param {number} timeoutMs - ํƒ€์ž„์•„์›ƒ ์‹œ๊ฐ„ (ms) + * @param {string} timeoutMessage - ํƒ€์ž„์•„์›ƒ ๋ฉ”์‹œ์ง€ + * @returns {Promise} ํƒ€์ž„์•„์›ƒ ์ ์šฉ๋œ Promise + */ +export const withTimeout = (promise, timeoutMs, timeoutMessage = '์ž‘์—… ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค') => { + return Promise.race([ + promise, + new Promise((resolve) => { + setTimeout(() => { + console.error('[asyncActionUtils] โฐ PROMISE_TIMEOUT', { timeoutMs }); + resolve({ + response: null, + data: null, + success: false, + error: { + code: 'TIMEOUT', + message: timeoutMessage, + timeout: timeoutMs + } + }); + }, timeoutMs); + }) + ]); +}; + +// ๊ธฐ๋ณธ export +export default { + fetchApi, + tAxiosToPromise, + wrapAsyncAction, + executeParallelAsyncActions, + processAsyncResult, + withTimeout, + isApiSuccess, + getApiErrorMessage +}; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/utils/asyncPanelQueueExamples.js b/com.twin.app.shoptime/src/utils/asyncPanelQueueExamples.js new file mode 100644 index 00000000..2b6bdacb --- /dev/null +++ b/com.twin.app.shoptime/src/utils/asyncPanelQueueExamples.js @@ -0,0 +1,488 @@ +/** + * [251106] ๋น„๋™๊ธฐ ํŒจ๋„ ์•ก์…˜ ํ ์‚ฌ์šฉ ์˜ˆ์‹œ + * + * ์ด ํŒŒ์ผ์€ API ํ˜ธ์ถœ์ด๋‚˜ ๋‹ค๋ฅธ ๋น„๋™๊ธฐ ์ž‘์—…์ด ํฌํ•จ๋œ ํŒจ๋„ ์•ก์…˜๋“ค์„ + * ์–ด๋–ป๊ฒŒ ์ˆœ์ฐจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š”์ง€ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. + */ + +import { + enqueueAsyncPanelAction, + createApiWithPanelActions, + createAsyncPanelSequence +} from '../actions/queuedPanelActions'; + +import { + pushPanelQueued, + popPanelQueued, + updatePanelQueued +} from '../actions/queuedPanelActions'; + +import { panel_names } from './Config'; + +/** + * ์˜ˆ์‹œ 1: TAxios API ํ˜ธ์ถœ ํ›„ ํŒจ๋„ ์•ก์…˜ ์‹คํ–‰ + * + * TAxios ๋ฐฉ์‹์˜ API ํ˜ธ์ถœ์„ ํ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + */ +export const tAxiosWithPanelExample = (dispatch, getState) => { + console.log('๐ŸŽฏ TAxios + Panel Actions Example'); + + dispatch(createApiWithPanelActions({ + // TAxios ๋ฐฉ์‹์˜ API ํ˜ธ์ถœ + apiCall: (dispatch, getState, onSuccess, onFail) => { + import('../api/TAxios').then(({ TAxios }) => { + TAxios( + dispatch, + getState, + 'post', // method + '/api/search', // URL + {}, // URL params + { query: 'product search' }, // request params + onSuccess, // ์„ฑ๊ณต ์ฝœ๋ฐฑ + onFail, // ์‹คํŒจ ์ฝœ๋ฐฑ + false, // noTokenRefresh + ); + }); + }, + + // API ์„ฑ๊ณต ํ›„ ์‹คํ–‰ํ•  ํŒจ๋„ ์•ก์…˜๋“ค + panelActions: [ + { type: 'PUSH_PANEL', payload: { name: panel_names.SEARCH_PANEL } }, + (response) => updatePanelQueued({ + name: panel_names.SEARCH_PANEL, + panelInfo: { + searchResults: response.data.results, + query: response.data.query + } + }) + ], + + // API ์„ฑ๊ณต ์ฝœ๋ฐฑ (์„ ํƒ) + onApiSuccess: (response) => { + console.log('๐ŸŽ‰ API Success:', response.data); + }, + + // API ์‹คํŒจ ์ฝœ๋ฐฑ (์„ ํƒ) + onApiFail: (error) => { + console.error('๐Ÿ’ฅ API Failed:', error); + } + })); +}; + +/** + * ์˜ˆ์‹œ 2: Redux Thunk ๋น„๋™๊ธฐ ์•ก์…˜๊ณผ ํŒจ๋„ ์กฐํ•ฉ + * + * Redux Thunk ๋ฐฉ์‹์˜ ๋น„๋™๊ธฐ ์•ก์…˜์„ ํ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + */ +export const thunkWithPanelExample = (dispatch, getState) => { + console.log('๐Ÿ”„ Thunk + Panel Actions Example'); + + dispatch(enqueueAsyncPanelAction({ + // ๋น„๋™๊ธฐ ์•ก์…˜ ์ •์˜ + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + // Redux Thunk ๋ฐฉ์‹์˜ ๋น„๋™๊ธฐ ์ž‘์—… + const response = await fetch('/api/products/123'); + const data = await response.json(); + + if (response.ok) { + onSuccess(data); + } else { + onFail(new Error(data.message || 'API Error')); + } + } catch (error) { + onFail(error); + } + }, + + // ์„ฑ๊ณต ์ฝœ๋ฐฑ - ํŒจ๋„ ์•ก์…˜๋“ค ์‹คํ–‰ + onSuccess: (response) => { + console.log('โœ… Thunk Success:', response); + + // ์„ฑ๊ณต ํ›„ ํŒจ๋„ ์•ก์…˜๋“ค ์ˆœ์ฐจ ์‹คํ–‰ + dispatch(pushPanelQueued({ name: panel_names.DETAIL_PANEL })); + dispatch(updatePanelQueued({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + product: response.product, + isLoading: false + } + })); + }, + + // ์‹คํŒจ ์ฝœ๋ฐฑ + onFail: (error) => { + console.error('โŒ Thunk Failed:', error); + + // ์—๋Ÿฌ ํŒจ๋„ ํ‘œ์‹œ + dispatch(pushPanelQueued({ + name: panel_names.ERROR_PANEL, + panelInfo: { + error: error.message, + retryAction: () => thunkWithPanelExample(dispatch, getState) + } + })); + }, + + // ์™„๋ฃŒ ์ฝœ๋ฐฑ (์„ฑ๊ณต/์‹คํŒจ ๋ชจ๋‘ ํ˜ธ์ถœ) + onFinish: (isSuccess, result) => { + console.log('๐Ÿ Thunk Complete:', { isSuccess, result }); + + if (isSuccess) { + // ์„ฑ๊ณตํ–ˆ์„ ๋•Œ๋งŒ ์ถ”๊ฐ€์ ์ธ ํŒจ๋„ ์•ก์…˜ + dispatch(pushPanelQueued({ + name: panel_names.RECOMMENDATION_PANEL, + panelInfo: { productId: result.product.id } + })); + } + } + })); +}; + +/** + * ์˜ˆ์‹œ 3: ์—ฌ๋Ÿฌ ๋น„๋™๊ธฐ ์•ก์…˜๋“ค์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰ + * + * ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + */ +export const complexAsyncSequenceExample = (dispatch, getState) => { + console.log('๐Ÿ”€ Complex Async Sequence Example'); + + const asyncActions = [ + { + // 1๋‹จ๊ณ„: ์‚ฌ์šฉ์ž ์ •๋ณด ๋กœ๋“œ + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + const response = await fetch('/api/user/profile'); + const data = await response.json(); + + if (response.ok) { + onSuccess(data); + } else { + onFail(new Error('Failed to load user profile')); + } + } catch (error) { + onFail(error); + } + }, + onSuccess: (response) => { + console.log('๐Ÿ‘ค User profile loaded:', response); + dispatch(updatePanelQueued({ + name: panel_names.USER_PANEL, + panelInfo: { user: response.user } + })); + } + }, + + { + // 2๋‹จ๊ณ„: ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ •๋ณด ๋กœ๋“œ (1๋‹จ๊ณ„ ์„ฑ๊ณต ์‹œ์—๋งŒ ์‹คํ–‰) + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + const response = await fetch('/api/cart/items'); + const data = await response.json(); + + if (response.ok) { + onSuccess(data); + } else { + onFail(new Error('Failed to load cart items')); + } + } catch (error) { + onFail(error); + } + }, + onSuccess: (response) => { + console.log('๐Ÿ›’ Cart items loaded:', response); + dispatch(pushPanelQueued({ + name: panel_names.CART_PANEL, + panelInfo: { items: response.items } + })); + } + }, + + { + // 3๋‹จ๊ณ„: ์ถ”์ฒœ ์ƒํ’ˆ ๋กœ๋“œ (2๋‹จ๊ณ„ ์„ฑ๊ณต ์‹œ์—๋งŒ ์‹คํ–‰) + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + const response = await fetch('/api/recommendations'); + const data = await response.json(); + + if (response.ok) { + onSuccess(data); + } else { + onFail(new Error('Failed to load recommendations')); + } + } catch (error) { + onFail(error); + } + }, + onSuccess: (response) => { + console.log('โญ Recommendations loaded:', response); + dispatch(updatePanelQueued({ + name: panel_names.CART_PANEL, + panelInfo: { recommendations: response.recommendations } + })); + } + } + ]; + + // ๋น„๋™๊ธฐ ์‹œํ€€์Šค ์‹คํ–‰ + dispatch(createAsyncPanelSequence(asyncActions)); +}; + +/** + * ์˜ˆ์‹œ 4: ํƒ€์ž„์•„์›ƒ๊ณผ ์—๋Ÿฌ ์ฒ˜๋ฆฌ + * + * ํƒ€์ž„์•„์›ƒ ์„ค์ •๊ณผ ์ƒ์„ธํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. + */ +export const timeoutAndErrorHandlingExample = (dispatch, getState) => { + console.log('โฐ Timeout and Error Handling Example'); + + dispatch(enqueueAsyncPanelAction({ + // 5์ดˆ ํƒ€์ž„์•„์›ƒ ์„ค์ • + timeout: 5000, + + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + // ์˜๋„์ ์œผ๋กœ ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—… ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + await new Promise(resolve => setTimeout(resolve, 3000)); + + // ๋žœ๋ค์œผ๋กœ ์„ฑ๊ณต/์‹คํŒจ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + if (Math.random() > 0.5) { + onSuccess({ message: 'Success after delay' }); + } else { + onFail(new Error('Random failure occurred')); + } + }, + + onSuccess: (response) => { + console.log('๐ŸŽ‰ Success with timeout handling:', response); + dispatch(pushPanelQueued({ + name: panel_names.SUCCESS_PANEL, + panelInfo: { message: response.message } + })); + }, + + onFail: (error) => { + console.error('๐Ÿ’ฅ Error with timeout handling:', error); + dispatch(pushPanelQueued({ + name: panel_names.ERROR_PANEL, + panelInfo: { + error: error.message, + isTimeout: error.message.includes('timeout') + } + })); + }, + + onFinish: (isSuccess, result) => { + console.log('๐Ÿ Operation completed:', { + isSuccess, + result, + timestamp: Date.now() + }); + } + })); +}; + +/** + * ์˜ˆ์‹œ 5: ๊ธฐ์กด ๋ฐฉ์‹๊ณผ ์ƒˆ๋กœ์šด ๋ฐฉ์‹์˜ ๋น„๊ต + * + * ๊ธฐ์กด์˜ callback ๋ฐฉ์‹๊ณผ ์ƒˆ๋กœ์šด ํ ๊ธฐ๋ฐ˜ ๋ฐฉ์‹์„ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + */ +export const comparisonExample = (dispatch, getState) => { + console.log('๐Ÿ“Š Async Comparison Example'); + + // ๊ธฐ์กด ๋ฐฉ์‹ (์ˆœ์„œ ๋ณด์žฅ๋˜์ง€ ์•Š์Œ, ๋ณต์žกํ•œ ์ฝœ๋ฐฑ ์ฒด์ธ) + console.log('โŒ Old way (complex callbacks, no order guarantee):'); + setTimeout(() => { + // API ํ˜ธ์ถœ + TAxios( + dispatch, + getState, + 'get', + '/api/products', + {}, + {}, + (response) => { + // ์„ฑ๊ณต ์ฝœ๋ฐฑ + dispatch({ type: 'PUSH_PANEL', payload: { name: 'PRODUCT_PANEL' } }); + dispatch({ type: 'UPDATE_PANEL', payload: { + name: 'PRODUCT_PANEL', + panelInfo: { products: response.data } + }}); + + // ์ถ”๊ฐ€ API ํ˜ธ์ถœ + TAxios( + dispatch, + getState, + 'get', + '/api/recommendations', + {}, + {}, + (recResponse) => { + // ์ค‘์ฒฉ ์ฝœ๋ฐฑ + dispatch({ type: 'UPDATE_PANEL', payload: { + name: 'PRODUCT_PANEL', + panelInfo: { recommendations: recResponse.data } + }}); + }, + (error) => { + // ์—๋Ÿฌ ์ฒ˜๋ฆฌ + console.error('Recommendation load failed:', error); + } + ); + }, + (error) => { + // ์—๋Ÿฌ ์ฒ˜๋ฆฌ + console.error('Product load failed:', error); + dispatch({ type: 'PUSH_PANEL', payload: { name: 'ERROR_PANEL' }}); + } + ); + }, 1000); + + // ์ƒˆ๋กœ์šด ๋ฐฉ์‹ (์ˆœ์„œ ๋ณด์žฅ, ๊น”๋”ํ•œ ๊ตฌ์กฐ) + console.log('โœ… New way (ordered, clean structure):'); + setTimeout(() => { + const asyncActions = [ + { + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + const response = await fetch('/api/products'); + const data = await response.json(); + onSuccess(data); + } catch (error) { + onFail(error); + } + }, + onSuccess: (response) => { + dispatch(pushPanelQueued({ name: 'PRODUCT_PANEL' })); + dispatch(updatePanelQueued({ + name: 'PRODUCT_PANEL', + panelInfo: { products: response.data } + })); + } + }, + { + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + const response = await fetch('/api/recommendations'); + const data = await response.json(); + onSuccess(data); + } catch (error) { + onFail(error); + } + }, + onSuccess: (response) => { + dispatch(updatePanelQueued({ + name: 'PRODUCT_PANEL', + panelInfo: { recommendations: response.data } + })); + } + } + ]; + + dispatch(createAsyncPanelSequence(asyncActions)); + }, 2000); +}; + +/** + * ์˜ˆ์‹œ 6: ์‹ค์ œ ํ”„๋กœ์ ํŠธ ์ ์šฉ ์˜ˆ์‹œ + * + * ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. + */ +export const realWorldExample = (dispatch, getState, productId) => { + console.log('๐ŸŒ Real World Example - Product Detail Flow'); + + const productDetailFlow = [ + { + // 1. ์ œํ’ˆ ์ƒ์„ธ ์ •๋ณด ๋กœ๋“œ + name: 'loadProductDetail', + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + const response = await fetch(`/api/products/${productId}`); + const data = await response.json(); + + if (response.ok) { + onSuccess(data); + } else { + onFail(new Error(data.message || 'Product not found')); + } + } catch (error) { + onFail(error); + } + }, + onSuccess: (response) => { + dispatch(pushPanelQueued({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + product: response.product, + isLoading: false + } + })); + }, + onFail: (error) => { + dispatch(pushPanelQueued({ + name: panel_names.ERROR_PANEL, + panelInfo: { + error: error.message, + retryCallback: () => realWorldExample(dispatch, getState, productId) + } + })); + } + }, + + { + // 2. ๊ด€๋ จ ์ƒํ’ˆ ๋กœ๋“œ + name: 'loadRelatedProducts', + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + const response = await fetch(`/api/products/${productId}/related`); + const data = await response.json(); + + if (response.ok) { + onSuccess(data); + } else { + onFail(new Error('Failed to load related products')); + } + } catch (error) { + onFail(error); + } + }, + onSuccess: (response) => { + dispatch(updatePanelQueued({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + relatedProducts: response.products + } + })); + } + }, + + { + // 3. ์žฌ๊ณ  ํ™•์ธ + name: 'checkStock', + asyncAction: async (dispatch, getState, onSuccess, onFail) => { + try { + const response = await fetch(`/api/products/${productId}/stock`); + const data = await response.json(); + + if (response.ok) { + onSuccess(data); + } else { + onFail(new Error('Failed to check stock')); + } + } catch (error) { + onFail(error); + } + }, + onSuccess: (response) => { + dispatch(updatePanelQueued({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + stockInfo: response.stock, + canPurchase: response.stock.available > 0 + } + })); + } + } + ]; + + dispatch(createAsyncPanelSequence(productDetailFlow)); +}; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/utils/compatibleAsyncPanelExamples.js b/com.twin.app.shoptime/src/utils/compatibleAsyncPanelExamples.js new file mode 100644 index 00000000..990fc097 --- /dev/null +++ b/com.twin.app.shoptime/src/utils/compatibleAsyncPanelExamples.js @@ -0,0 +1,721 @@ +/** + * [251106] Chrome 68 ํ˜ธํ™˜ ๋น„๋™๊ธฐ ํŒจ๋„ ์•ก์…˜ ์˜ˆ์‹œ + * + * ์ด ํŒŒ์ผ์€ async/await ์—†์ด Promise/.then ์ฒด์ด๋‹๋งŒ ์‚ฌ์šฉํ•˜์—ฌ + * Chrome 68 ํ˜ธํ™˜์„ฑ์„ ์™„๋ฒฝํ•˜๊ฒŒ ๋ณด์žฅํ•˜๋Š” ์˜ˆ์‹œ๋“ค์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. + * TAxios๋Š” ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๋ฉด์„œ ์„ฑ๊ณต ๊ธฐ์ค€(200 + retCode 0/'0')์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + */ + +import { + enqueueAsyncPanelAction, + createApiWithPanelActions, + createAsyncPanelSequence +} from '../actions/queuedPanelActions'; + +import { + pushPanelQueued, + popPanelQueued, + updatePanelQueued, + clearPanelQueue +} from '../actions/queuedPanelActions'; + +import { panel_names } from './Config'; + +/** + * ์˜ˆ์‹œ 1: TAxios Promise ๋ž˜ํ•‘ (Chrome 68 ํ˜ธํ™˜) + * + * ๊ธฐ์กด TAxios๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๋ฉด์„œ Promise ๊ธฐ๋ฐ˜ ์ œ์–ด + */ +export const compatibleTaxiosExample = (dispatch, getState, searchQuery) => { + console.log('๐Ÿ”„ Compatible TAxios Example'); + + dispatch(enqueueAsyncPanelAction({ + asyncAction: (dispatch, getState, onSuccess, onFail) => { + // TAxios๋Š” ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ + import('../api/TAxios').then(({ TAxios }) => { + // TAxios๋ฅผ Promise๋กœ ๋ž˜ํ•‘ํ•˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ์‚ฌ์šฉ + import('../utils/asyncActionUtils').then(({ tAxiosToPromise }) => { + tAxiosToPromise( + TAxios, // ๊ธฐ์กด TAxios (์ˆ˜์ • ์—†์Œ) + dispatch, getState, + 'post', // HTTP ๋ฉ”์†Œ๋“œ + '/api/search', // URL + {}, // URL params + { query: searchQuery }, // request params + { noTokenRefresh: false } // ์˜ต์…˜ + ) + .then(result => { + console.log('[CompatibleExample] ๐Ÿ“Š TAxios Result:', { + retCode: result.data?.retCode, + success: result.success, + hasData: !!result.data?.data + }); + onSuccess(result); + }) + .catch(error => { + // tAxiosToPromise์€ rejectํ•˜์ง€ ์•Š์ง€๋งŒ, ํ˜น์‹œ ๋ชจ๋ฅผ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ + onSuccess({ + response: null, + data: null, + success: false, + error: { + code: 'TAXIOS_WRAPPER_ERROR', + message: error.message || 'TAxios ๋ž˜ํผ ์˜ค๋ฅ˜' + } + }); + }); + }).catch(error => { + // ์œ ํ‹ธ๋ฆฌํ‹ฐ import ์‹คํŒจ + console.error('[CompatibleExample] โŒ Utils Import Error:', error); + onSuccess({ + response: null, + data: null, + success: false, + error: { + code: 'UTILS_IMPORT_ERROR', + message: '์œ ํ‹ธ๋ฆฌํ‹ฐ import ์‹คํŒจ' + } + }); + }); + }).catch(error => { + // TAxios import ์‹คํŒจ + console.error('[CompatibleExample] โŒ TAxios Import Error:', error); + onSuccess({ + response: null, + data: null, + success: false, + error: { + code: 'TAXIOS_IMPORT_ERROR', + message: 'TAxios import ์‹คํŒจ' + } + }); + }); + }, + + onSuccess: (result) => { + if (result.success) { + // HTTP 200 + retCode 0/'0' ์„ฑ๊ณต + console.log('[CompatibleExample] โœ… Search Success:', result.data?.data); + + dispatch(pushPanelQueued({ + name: panel_names.SEARCH_PANEL, + panelInfo: { + query: searchQuery, + results: result.data?.data || [], + isLoading: false + } + })); + + } else { + // retCode๊ฐ€ 0์ด ์•„๋‹Œ ๊ฒฝ์šฐ (504, 6xx, 9xx ๋“ฑ) + console.error('[CompatibleExample] โŒ Search Failed:', { + code: result.error?.code, + message: result.error?.message, + httpStatus: result.error?.httpStatus + }); + + dispatch(pushPanelQueued({ + name: panel_names.ERROR_PANEL, + panelInfo: { + error: result.error?.message || '๊ฒ€์ƒ‰์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค', + errorCode: result.error?.code, + canRetry: true, + retryAction: () => compatibleTaxiosExample(dispatch, getState, searchQuery) + } + })); + } + }, + + onFail: (error) => { + console.error('[CompatibleExample] ๐Ÿ’ฅ Critical Error:', error); + + dispatch(pushPanelQueued({ + name: panel_names.ERROR_PANEL, + panelInfo: { + error: '์น˜๋ช…์ ์ธ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค', + isSystemError: true + } + })); + }, + + timeout: 10000 // 10์ดˆ ํƒ€์ž„์•„์›ƒ + })); +}; + +/** + * ์˜ˆ์‹œ 2: ์ˆœ์ฐจ์  TAxios ํ˜ธ์ถœ ์ฒด์ด๋‹ + * + * ์—ฌ๋Ÿฌ TAxios ํ˜ธ์ถœ์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰ํ•˜๊ณ  ๊ฐ ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ํŒจ๋„ ์กฐ์ž‘ + */ +export const sequentialTaxiosChaining = (dispatch, getState, userId) => { + console.log('๐Ÿ”— Sequential TAxios Chaining'); + + dispatch(clearPanelQueue()); // ํ ์ •๋ฆฌ + + const workflow = [ + { + name: 'loadUserProfile', + asyncAction: (dispatch, getState, onSuccess, onFail) => { + // Promise ์ฒด์ด๋‹์œผ๋กœ ์ˆœ์ฐจ์  ์ฒ˜๋ฆฌ + Promise.all([ + import('../api/TAxios'), + import('../utils/asyncActionUtils') + ]) + .then(([{ TAxios }, { tAxiosToPromise }]) => { + return tAxiosToPromise( + TAxios, dispatch, getState, 'get', + '/api/user/profile', + { userId }, {}, {} + ); + }) + .then(result => { + console.log('[Sequential] ๐Ÿ‘ค Profile Loaded:', { + retCode: result.data?.retCode, + success: result.success + }); + onSuccess(result); + }) + .catch(error => { + console.error('[Sequential] โŒ Profile Load Error:', error); + onSuccess({ + response: null, + data: null, + success: false, + error: { + code: 'PROFILE_LOAD_ERROR', + message: 'ํ”„๋กœํ•„ ๋กœ๋“œ ์‹คํŒจ' + } + }); + }); + }, + + onSuccess: (result) => { + if (result.success) { + dispatch(pushPanelQueued({ + name: panel_names.USER_PANEL, + panelInfo: { + user: result.data?.data, + step: 'profile_loaded' + } + })); + } else { + // ์‹คํŒจ ์‹œ ์ฒด์ด๋‹ ์ค‘๋‹จ + dispatch(pushPanelQueued({ + name: panel_names.ERROR_PANEL, + panelInfo: { + error: result.error?.message || 'ํ”„๋กœํ•„ ๋กœ๋“œ ์‹คํŒจ', + stopChain: true + } + })); + } + } + }, + + { + name: 'loadCartItems', + asyncAction: (dispatch, getState, onSuccess, onFail) => { + Promise.all([ + import('../api/TAxios'), + import('../utils/asyncActionUtils') + ]) + .then(([{ TAxios }, { tAxiosToPromise }]) => { + return tAxiosToPromise( + TAxios, dispatch, getState, 'get', + '/api/user/cart', + { userId }, {}, {} + ); + }) + .then(result => { + console.log('[Sequential] ๐Ÿ›’ Cart Loaded:', { + retCode: result.data?.retCode, + itemCount: result.data?.data?.items?.length + }); + onSuccess(result); + }) + .catch(error => { + console.error('[Sequential] โŒ Cart Load Error:', error); + onSuccess({ + response: null, + data: null, + success: false, + error: { + code: 'CART_LOAD_ERROR', + message: '์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋กœ๋“œ ์‹คํŒจ' + } + }); + }); + }, + + onSuccess: (result) => { + if (result.success) { + dispatch(updatePanelQueued({ + name: panel_names.USER_PANEL, + panelInfo: { + cartItems: result.data?.data?.items || [], + step: 'completed' + } + })); + } else { + // ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋กœ๋“œ ์‹คํŒจ๋Š” ์‚ฌ์šฉ์ž ํŒจ๋„์€ ์œ ์ง€ + dispatch(updatePanelQueued({ + name: panel_names.USER_PANEL, + panelInfo: { + cartError: result.error?.message, + step: 'cart_failed' + } + })); + } + } + } + ]; + + // ์ˆœ์ฐจ์  ์›Œํฌํ”Œ๋กœ์šฐ ์‹คํ–‰ + dispatch(createAsyncPanelSequence(workflow)); +}; + +/** + * ์˜ˆ์‹œ 3: ๋ณ‘๋ ฌ TAxios ํ˜ธ์ถœ + * + * ์—ฌ๋Ÿฌ API๋ฅผ ๋™์‹œ์— ํ˜ธ์ถœํ•˜๊ณ  ๋ชจ๋‘ ์™„๋ฃŒ๋˜๋ฉด ๊ฒฐ๊ณผ๋ฅผ ์ข…ํ•ฉ + */ +export const parallelTaxiosCalls = (dispatch, getState, productId) => { + console.log('๐Ÿš€ Parallel TAxios Calls'); + + dispatch(enqueueAsyncPanelAction({ + asyncAction: (dispatch, getState, onSuccess, onFail) => { + // ํ•„์š”ํ•œ ๋ชจ๋“ˆ import + Promise.all([ + import('../api/TAxios'), + import('../utils/asyncActionUtils') + ]) + .then(([{ TAxios }, { tAxiosToPromise, executeParallelAsyncActions }]) => { + // ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•  TAxios ํ˜ธ์ถœ๋“ค + const parallelActions = [ + // ์ œํ’ˆ ์ƒ์„ธ ์ •๋ณด + (dispatch, getState, success, fail) => { + tAxiosToPromise( + TAxios, dispatch, getState, 'get', + '/api/products/detail', + { productId }, {}, {} + ).then(result => success(result)) + .catch(error => success({ + response: null, + data: null, + success: false, + error: { code: 'DETAIL_ERROR', message: error.message } + })); + }, + + // ๊ด€๋ จ ์ƒํ’ˆ + (dispatch, getState, success, fail) => { + tAxiosToPromise( + TAxios, dispatch, getState, 'get', + '/api/products/related', + { productId }, {}, {} + ).then(result => success(result)) + .catch(error => success({ + response: null, + data: null, + success: false, + error: { code: 'RELATED_ERROR', message: error.message } + })); + }, + + // ๋ฆฌ๋ทฐ ์ •๋ณด + (dispatch, getState, success, fail) => { + tAxiosToPromise( + TAxios, dispatch, getState, 'get', + '/api/products/reviews', + { productId }, {}, {} + ).then(result => success(result)) + .catch(error => success({ + response: null, + data: null, + success: false, + error: { code: 'REVIEWS_ERROR', message: error.message } + })); + } + ]; + + // ๋ณ‘๋ ฌ ์‹คํ–‰ + return executeParallelAsyncActions(parallelActions, { dispatch, getState }); + }) + .then(results => { + console.log('[Parallel] ๐Ÿ“Š All Results:', { + total: results.length, + success: results.filter(r => r.success).length, + failed: results.filter(r => !r.success).length + }); + + // ๊ฒฐ๊ณผ ์ข…ํ•ฉ + const combinedResult = { + response: { status: 200 }, + data: { + detail: results[0].data, + related: results[1].data, + reviews: results[2].data, + errors: results.filter(r => !r.success).map(r => ({ + endpoint: ['detail', 'related', 'reviews'][results.indexOf(r)], + error: r.error + })) + }, + success: results.every(r => r.success), + error: results.some(r => !r.success) ? { + code: 'PARTIAL_FAILURE', + message: '์ผ๋ถ€ ์ •๋ณด ๋กœ๋“œ ์‹คํŒจ' + } : null + }; + + onSuccess(combinedResult); + }) + .catch(error => { + console.error('[Parallel] ๐Ÿ’ฅ Parallel Execution Error:', error); + onSuccess({ + response: null, + data: null, + success: false, + error: { + code: 'PARALLEL_EXECUTION_ERROR', + message: '๋ณ‘๋ ฌ ์‹คํ–‰ ์‹คํŒจ' + } + }); + }); + }, + + onSuccess: (result) => { + if (result.success) { + // ๋ชจ๋“  API ์„ฑ๊ณต + dispatch(pushPanelQueued({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + product: result.data.detail?.data, + relatedProducts: result.data.related?.data?.items || [], + reviews: result.data.reviews?.data?.reviews || [], + isLoading: false + } + })); + + } else { + // ๋ถ€๋ถ„ ์‹คํŒจ - ์„ฑ๊ณตํ•œ ๋ฐ์ดํ„ฐ๋Š” ํ‘œ์‹œ + dispatch(pushPanelQueued({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + product: result.data.detail?.data, + relatedProducts: result.data.related?.data?.items || [], + reviews: result.data.reviews?.data?.reviews || [], + hasErrors: true, + errors: result.data.errors, + isLoading: false + } + })); + } + }, + + timeout: 15000 // 15์ดˆ ํƒ€์ž„์•„์›ƒ (๋ณ‘๋ ฌ ํ˜ธ์ถœ์ด๋ฏ€๋กœ ๋” ๊ธธ๊ฒŒ) + })); +}; + +/** + * ์˜ˆ์‹œ 4: ์žฌ์‹œ๋„ ๋กœ์ง (Promise ์ฒด์ด๋‹) + * + * ์‹คํŒจ ์‹œ ์ž๋™ ์žฌ์‹œ๋„ - Promise ์ฒด์ด๋‹์œผ๋กœ ๊ตฌํ˜„ + */ +export const retryWithPromiseChaining = (dispatch, getState, productId) => { + console.log('๐Ÿ”„ Retry with Promise Chaining'); + + const maxRetries = 3; + let retryCount = 0; + + const attemptLoad = () => { + retryCount++; + console.log(`[Retry] ๐Ÿ“ก Attempt ${retryCount}/${maxRetries}`); + + dispatch(enqueueAsyncPanelAction({ + asyncAction: (dispatch, getState, onSuccess, onFail) => { + Promise.all([ + import('../api/TAxios'), + import('../utils/asyncActionUtils') + ]) + .then(([{ TAxios }, { tAxiosToPromise, withTimeout }]) => { + const apiPromise = tAxiosToPromise( + TAxios, dispatch, getState, 'get', + '/api/products/detail', + { productId }, {}, {} + ); + + // ํƒ€์ž„์•„์›ƒ ์ ์šฉ + return withTimeout(apiPromise, 3000, '์ œํ’ˆ ์ •๋ณด ๋กœ๋“œ ์‹œ๊ฐ„ ์ดˆ๊ณผ'); + }) + .then(result => { + console.log('[Retry] ๐Ÿ“Š Load Result:', { + success: result.success, + errorCode: result.error?.code, + retryCount + }); + + // ํ•ญ์ƒ resolve + onSuccess(result); + }) + .catch(error => { + console.error('[Retry] ๐Ÿ’ฅ Load Error:', error); + + // ์˜ˆ์™ธ๋„ resolve๋กœ ์ฒ˜๋ฆฌ + onSuccess({ + response: null, + data: null, + success: false, + error: { + code: 'LOAD_EXCEPTION', + message: error.message || '๋กœ๋“œ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ' + } + }); + }); + }, + + onSuccess: (result) => { + if (result.success) { + // ์„ฑ๊ณต ์‹œ ํŒจ๋„ ํ‘œ์‹œ + console.log('[Retry] โœ… Load Success after', retryCount, 'attempts'); + + dispatch(pushPanelQueued({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + product: result.data?.data, + source: 'api', + retryCount + } + })); + + } else { + console.error('[Retry] โŒ Load Failed:', result.error); + + if (retryCount < maxRetries) { + // ์žฌ์‹œ๋„ + console.log(`[Retry] ๐Ÿ”„ Retrying in 1 second... (${retryCount}/${maxRetries})`); + + setTimeout(() => { + attemptLoad(); + }, 1000); + + } else { + // ์ตœ๋Œ€ ์žฌ์‹œ๊ณ  ๋„๋‹ฌ - ํด๋ฐฑ + console.log('[Retry] ๐Ÿ“ฆ Max retries reached, using fallback'); + + dispatch(pushPanelQueued({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + product: { + id: productId, + name: '์ œํ’ˆ ์ •๋ณด ๋กœ๋“œ ์‹คํŒจ', + placeholder: true + }, + source: 'fallback', + error: result.error?.message, + retryCount, + canRetry: true, + retryAction: () => { + retryCount = 0; // ์žฌ์‹œ๋„ ์นด์šดํŠธ ์ดˆ๊ธฐํ™” + attemptLoad(); + } + } + })); + } + } + }, + + timeout: 5000 // ๊ฐ ์‹œ๋„ ํƒ€์ž„์•„์›ƒ + })); + }; + + // ์ฒซ ๋ฒˆ์งธ ์‹œ๋„ + attemptLoad(); +}; + +/** + * ์˜ˆ์‹œ 5: ์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ์‹œ๋‚˜๋ฆฌ์˜ค - ์ œํ’ˆ ๊ฒ€์ƒ‰ ๋ฐ ์ƒ์„ธ ๋ณด๊ธฐ + * + * ๊ฒ€์ƒ‰ โ†’ ์ƒ์ œ ์„ ํƒ โ†’ ์ƒ์„ธ ๋กœ๋“œ โ†’ ๊ด€๋ จ ์ƒํ’ˆ ๋กœ๋“œ + */ +export const productSearchToDetailFlow = (dispatch, getState, searchQuery) => { + console.log('๐Ÿ” Product Search to Detail Flow'); + + // 1๋‹จ๊ณ„: ๊ฒ€์ƒ‰ + dispatch(enqueueAsyncPanelAction({ + asyncAction: (dispatch, getState, onSuccess, onFail) => { + Promise.all([ + import('../api/TAxios'), + import('../utils/asyncActionUtils') + ]) + .then(([{ TAxios }, { tAxiosToPromise }]) => { + return tAxiosToPromise( + TAxios, dispatch, getState, 'post', + '/api/search', + {}, {}, + { query: searchQuery, limit: 20 } + ); + }) + .then(result => { + console.log('[SearchFlow] ๐Ÿ” Search Results:', { + retCode: result.data?.retCode, + resultCount: result.data?.data?.results?.length + }); + onSuccess(result); + }) + .catch(error => { + onSuccess({ + response: null, + data: null, + success: false, + error: { code: 'SEARCH_ERROR', message: error.message } + }); + }); + }, + + onSuccess: (result) => { + if (result.success && result.data?.data?.results?.length > 0) { + // ๊ฒ€์ƒ‰ ์„ฑ๊ณต - ๊ฒฐ๊ณผ ํŒจ๋„ ํ‘œ์‹œ + dispatch(pushPanelQueued({ + name: panel_names.SEARCH_RESULTS_PANEL, + panelInfo: { + query: searchQuery, + results: result.data.data.results, + isLoading: false + } + })); + + // ์ฒซ ๋ฒˆ์งธ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ์ƒ์„ธ ์ •๋ณด ์ž๋™ ๋กœ๋“œ + const firstProduct = result.data.data.results[0]; + loadProductDetail(dispatch, getState, firstProduct.id); + + } else { + // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ + dispatch(pushPanelQueued({ + name: panel_names.SEARCH_RESULTS_PANEL, + panelInfo: { + query: searchQuery, + results: [], + isEmpty: true, + message: '๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค' + } + })); + } + }, + + timeout: 8000 + })); +}; + +// ์ œํ’ˆ ์ƒ์„ธ ๋กœ๋“œ ํ•จ์ˆ˜ (Promise ์ฒด์ด๋‹) +const loadProductDetail = (dispatch, getState, productId) => { + console.log('[SearchFlow] ๐Ÿ“ฆ Loading Product Detail:', productId); + + dispatch(enqueueAsyncPanelAction({ + asyncAction: (dispatch, getState, onSuccess, onFail) => { + Promise.all([ + import('../api/TAxios'), + import('../utils/asyncActionUtils') + ]) + .then(([{ TAxios }, { tAxiosToPromise }]) => { + return tAxiosToPromise( + TAxios, dispatch, getState, 'get', + '/api/products/detail', + { productId }, {}, {} + ); + }) + .then(result => { + console.log('[SearchFlow] ๐Ÿ“ฆ Product Detail Loaded:', { + retCode: result.data?.retCode, + success: result.success + }); + onSuccess(result); + }) + .catch(error => { + onSuccess({ + response: null, + data: null, + success: false, + error: { code: 'DETAIL_LOAD_ERROR', message: error.message } + }); + }); + }, + + onSuccess: (result) => { + if (result.success) { + // ์ƒ์„ธ ์ •๋ณด ์„ฑ๊ณต - ์ƒ์„ธ ํŒจ๋„ ํ‘œ์‹œ + dispatch(pushPanelQueued({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + product: result.data?.data, + isLoading: false + } + })); + + // ๊ด€๋ จ ์ƒํ’ˆ ๋กœ๋“œ (๋ฐฐ๊ฒฝ์—์„œ) + loadRelatedProducts(dispatch, getState, productId); + + } else { + // ์ƒ์„ธ ์ •๋ณด ๋กœ๋“œ ์‹คํŒจ + dispatch(updatePanelQueued({ + name: panel_names.SEARCH_RESULTS_PANEL, + panelInfo: { + detailError: result.error?.message, + selectedProductId: productId + } + })); + } + }, + + timeout: 5000 + })); +}; + +// ๊ด€๋ จ ์ƒํ’ˆ ๋กœ๋“œ ํ•จ์ˆ˜ +const loadRelatedProducts = (dispatch, getState, productId) => { + console.log('[SearchFlow] ๐Ÿ”— Loading Related Products:', productId); + + dispatch(enqueueAsyncPanelAction({ + asyncAction: (dispatch, getState, onSuccess, onFail) => { + Promise.all([ + import('../api/TAxios'), + import('../utils/asyncActionUtils') + ]) + .then(([{ TAxios }, { tAxiosToPromise }]) => { + return tAxiosToPromise( + TAxios, dispatch, getState, 'get', + '/api/products/related', + { productId }, {}, {} + ); + }) + .then(result => { + console.log('[SearchFlow] ๐Ÿ”— Related Products Loaded:', { + retCode: result.data?.retCode, + count: result.data?.data?.items?.length + }); + onSuccess(result); + }) + .catch(error => { + onSuccess({ + response: null, + data: null, + success: false, + error: { code: 'RELATED_LOAD_ERROR', message: error.message } + }); + }); + }, + + onSuccess: (result) => { + if (result.success) { + // ๊ด€๋ จ ์ƒํ’ˆ ์„ฑ๊ณต - ์ƒ์„ธ ํŒจ๋„ ์—…๋ฐ์ดํŠธ + dispatch(updatePanelQueued({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + relatedProducts: result.data?.data?.items || [] + } + })); + } + // ๊ด€๋ จ ์ƒํ’ˆ ์‹คํŒจ๋Š” ๋ฌด์‹œ (๋ฉ”์ธ ๊ธฐ๋Šฅ์— ์˜ํ–ฅ ์—†์Œ) + }, + + timeout: 3000 // ๋” ์งง์€ ํƒ€์ž„์•„์›ƒ (๋ถ€๊ฐ€ ๊ธฐ๋Šฅ) + })); +}; \ No newline at end of file