Files
shoptime/com.twin.app.shoptime/src/utils/asyncActionUtils.js
optrader f9290a12ba [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

🔧 주요 변경 내용:
  • 타입 시스템 안정성 강화
  • 핵심 비즈니스 로직 개선
  • 공통 유틸리티 함수 최적화
  • 대규모 기능 개발
  • 모듈 구조 개선
2025-11-06 20:46:36 +09:00

385 lines
11 KiB
JavaScript

/**
* [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
};