[251106] fix: Dispatch Queue implementation
🕐 커밋 시간: 2025. 11. 06. 20:46:35 📊 변경 통계: • 총 파일: 7개 • 추가: +398줄 • 삭제: -1줄 📁 추가된 파일: + com.twin.app.shoptime/src/utils/advancedAsyncPanelExamples.js + com.twin.app.shoptime/src/utils/asyncActionUtils.js + com.twin.app.shoptime/src/utils/asyncPanelQueueExamples.js + com.twin.app.shoptime/src/utils/compatibleAsyncPanelExamples.js 📝 수정된 파일: ~ com.twin.app.shoptime/src/actions/actionTypes.js ~ com.twin.app.shoptime/src/actions/queuedPanelActions.js ~ com.twin.app.shoptime/src/reducers/panelReducer.js 🔧 주요 변경 내용: • 타입 시스템 안정성 강화 • 핵심 비즈니스 로직 개선 • 공통 유틸리티 함수 최적화 • 대규모 기능 개발 • 모듈 구조 개선
This commit is contained in:
@@ -18,6 +18,11 @@ export const types = {
|
|||||||
CLEAR_PANEL_QUEUE: 'CLEAR_PANEL_QUEUE',
|
CLEAR_PANEL_QUEUE: 'CLEAR_PANEL_QUEUE',
|
||||||
SET_QUEUE_PROCESSING: 'SET_QUEUE_PROCESSING',
|
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
|
// device actions
|
||||||
GET_AUTHENTICATION_CODE: 'GET_AUTHENTICATION_CODE',
|
GET_AUTHENTICATION_CODE: 'GET_AUTHENTICATION_CODE',
|
||||||
REGISTER_DEVICE: 'REGISTER_DEVICE',
|
REGISTER_DEVICE: 'REGISTER_DEVICE',
|
||||||
|
|||||||
@@ -151,4 +151,295 @@ export const createPanelSequence = (sequence) => {
|
|||||||
|
|
||||||
dispatch(enqueueMultiplePanelActions(queuedActions));
|
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();
|
||||||
|
};
|
||||||
};
|
};
|
||||||
@@ -14,7 +14,12 @@ const initialState = {
|
|||||||
totalProcessed: 0, // 총 처리된 액션 수
|
totalProcessed: 0, // 총 처리된 액션 수
|
||||||
failedCount: 0, // 실패한 액션 수
|
failedCount: 0, // 실패한 액션 수
|
||||||
averageProcessingTime: 0 // 평균 처리 시간
|
averageProcessingTime: 0 // 평균 처리 시간
|
||||||
}
|
},
|
||||||
|
|
||||||
|
// [251106] 비동기 액션 관련 상태
|
||||||
|
asyncActions: {}, // 실행 중인 비동기 액션들 { actionId: { ... } }
|
||||||
|
completedAsyncActions: [], // 완료된 비동기 액션 ID들
|
||||||
|
failedAsyncActions: [], // 실패한 비동기 액션 ID들
|
||||||
};
|
};
|
||||||
|
|
||||||
// last one will be on top
|
// 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:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
720
com.twin.app.shoptime/src/utils/advancedAsyncPanelExamples.js
Normal file
720
com.twin.app.shoptime/src/utils/advancedAsyncPanelExamples.js
Normal file
@@ -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);
|
||||||
|
};
|
||||||
385
com.twin.app.shoptime/src/utils/asyncActionUtils.js
Normal file
385
com.twin.app.shoptime/src/utils/asyncActionUtils.js
Normal file
@@ -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<Object>} 응답 데이터를 포함한 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<Object>} 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<Object>} 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<Array>} 모든 결과 배열
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
};
|
||||||
488
com.twin.app.shoptime/src/utils/asyncPanelQueueExamples.js
Normal file
488
com.twin.app.shoptime/src/utils/asyncPanelQueueExamples.js
Normal file
@@ -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));
|
||||||
|
};
|
||||||
721
com.twin.app.shoptime/src/utils/compatibleAsyncPanelExamples.js
Normal file
721
com.twin.app.shoptime/src/utils/compatibleAsyncPanelExamples.js
Normal file
@@ -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 // 더 짧은 타임아웃 (부가 기능)
|
||||||
|
}));
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user