🕐 커밋 시간: 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 🔧 주요 변경 내용: • 타입 시스템 안정성 강화 • 핵심 비즈니스 로직 개선 • 공통 유틸리티 함수 최적화 • 대규모 기능 개발 • 모듈 구조 개선
385 lines
11 KiB
JavaScript
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
|
|
}; |