Files
shoptime/com.twin.app.shoptime/src/api/TAxios.js
optrader 18c3ac3ad5 [251209] fix: 백화현상 디버깅-1
🕐 커밋 시간: 2025. 12. 09. 18:18:51

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +28줄
  • 삭제: -4줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/homeActions.js
  ~ com.twin.app.shoptime/src/api/TAxios.js

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • API 서비스 레이어 개선
  • 소규모 기능 개선
2025-12-09 18:18:51 +09:00

595 lines
17 KiB
JavaScript

import axios from 'axios';
import Spotlight from '@enact/spotlight';
import { useDispatch } from 'react-redux';
import { useState } from 'react';
import { fetchCurrentUserHomeTermsSafe } from '../actions/homeActions';
import { types } from '../actions/actionTypes';
import {
changeAppStatus,
changeLocalSettings,
setShowPopup,
setSystemNotice,
setSystemTermination,
showError,
} from '../actions/commonActions';
import { getAuthenticationCode, getReAuthenticationCode } from '../actions/deviceActions';
import { pushPanel, resetPanels } from '../actions/panelActions';
import * as Config from '../utils/Config';
import { ACTIVE_POPUP } from '../utils/Config';
import * as HelperMethods from '../utils/helperMethods';
import { getUrl, URLS } from './apiConfig';
let tokenRefreshing = false;
const axiosQueue = [];
export const setTokenRefreshing = (value) => {
// console.log('TAxios setTokenRefreshing ', value);
tokenRefreshing = value;
};
export const runDelayedAction = (dispatch, getState) => {
// console.log('runDelayedAction axiosQueue size', axiosQueue.length);
while (axiosQueue.length > 0) {
const requestConfig = axiosQueue.shift(); // queue에서 요청을 하나씩 shift
TAxios(
dispatch,
getState,
requestConfig.type,
requestConfig.baseUrl,
requestConfig.urlParams,
requestConfig.params,
requestConfig.onSuccess,
requestConfig.onFail,
false, // noTokenRefresh
requestConfig.responseType // ⭐ responseType 전달
);
}
};
export const TAxios = (
dispatch,
getState,
type,
baseUrl,
urlParams = {},
params = {},
onSuccess,
onFail,
noTokenRefresh = false,
responseType = undefined // ⭐ 선택적 파라미터 추가 (기존 코드 영향 없음)
) => {
const pushQueue = () => {
if (!noTokenRefresh) {
axiosQueue.push({ type, baseUrl, urlParams, params, onSuccess, onFail, responseType });
}
};
const decodeMessage = (apiSysMessage) => {
try {
const decodedBase64 = atob(apiSysMessage);
const decodedText = new TextDecoder('utf-8').decode(
new Uint8Array(decodedBase64.split('').map((c) => c.charCodeAt(0)))
);
return decodedText;
} catch (error) {
console.error('Decoding error:', error);
return apiSysMessage;
}
};
const executeRequest = (accessToken) => {
const httpHeader = getState().common.httpHeader;
const { mbr_no, deviceId } = getState().common.appStatus;
const refreshToken = getState().localSettings.refreshToken;
const AUTHORIZATION = { headers: { ...httpHeader } };
if (accessToken) {
AUTHORIZATION.headers['lgsp_auth'] = accessToken;
}
AUTHORIZATION.headers['dvc_id'] = deviceId;
AUTHORIZATION.headers['refresh_token'] = refreshToken;
// ⭐ responseType 옵션 추가 (이미지, PDF 등 바이너리 데이터용)
if (responseType) {
AUTHORIZATION.responseType = responseType;
}
if (typeof window === 'object') {
let url = Array.isArray(baseUrl) ? getUrl(getState, baseUrl[0]) : getUrl(getState, baseUrl);
if (!url) {
//todo error page
return;
}
if (type === 'get') {
const _urlparams = HelperMethods.createQueryString(urlParams);
url += _urlparams ? `?${_urlparams}` : '';
}
let axiosInstance;
switch (type) {
case 'get':
axiosInstance = axios.get(url, AUTHORIZATION);
break;
case 'post':
axiosInstance = axios.post(url, params, AUTHORIZATION);
break;
// TODO: 다른 HTTP 메소드 있다면 처리 (chw)
}
if (axiosInstance) {
axiosInstance
.then((res) => {
// console.log('TAxios response', url, res);
const apiSysStatus = res.headers['api-sys-status'];
const apiSysMessage = res.headers['api-sys-message'];
const { systemNotice, systemTermination, appStatus } = getState().common;
const isInitialLoad = !appStatus.loadingComplete;
if (apiSysStatus === '800' && !systemNotice) {
dispatch(setSystemNotice());
} else if (apiSysStatus === '900' && !systemTermination) {
const decodedMessage = decodeMessage(apiSysMessage);
dispatch(setSystemTermination(isInitialLoad));
dispatch(resetPanels());
dispatch(
pushPanel({
name: Config.panel_names.ERROR_PANEL,
panelInfo: {
apiSysMessage: decodedMessage,
},
})
);
} else if (apiSysStatus === '901' && !systemTermination) {
const decodedMessage = decodeMessage(apiSysMessage);
dispatch(setSystemTermination(isInitialLoad));
dispatch(resetPanels());
dispatch(
pushPanel({
name: Config.panel_names.ERROR_PANEL,
panelInfo: {
apiSysMessage: decodedMessage,
},
})
);
}
if (baseUrl === URLS.GET_AUTHENTICATION_CODE) {
if (res?.data?.retCode !== 0) {
console.error('accessToken failed', res.data.retCode);
dispatch(
showError(res.data.retCode, res.data.retMsg, false, res.data.retDetailCode)
);
return;
}
}
// if(res?.data?.retCode === 501){
// //약관 비동의
// dispatch(changeLocalSettings({accessToken: null}));
// dispatch({type: types.GET_TERMS_AGREE_YN, payload: {}});
// return;
// }
// AccessToken 만료
if (res?.data?.retCode === 401) {
if (!tokenRefreshing) {
dispatch(getReAuthenticationCode());
}
pushQueue();
if (onFail) onFail(res);
return;
}
// 약관 미동의(501): 토큰 재발급 큐에 넣지 않고 바로 실패 처리
if (res?.data?.retCode === 501) {
if (onFail) onFail(res);
return;
}
// RefreshToken 만료
if (res?.data?.retCode === 402) {
if (baseUrl === URLS.GET_RE_AUTHENTICATION_CODE) {
dispatch(getAuthenticationCode());
} else {
if (!tokenRefreshing) {
dispatch(getAuthenticationCode());
}
pushQueue();
}
if (onFail) onFail(res);
return;
}
// 602 요청 국가 불일치
if (res?.data?.retCode === 602) {
dispatch(
setShowPopup(ACTIVE_POPUP.changeCountyPopup, {
data: res.data.retCode,
})
);
return;
}
// 603 서비스 국가 아님
if (res?.data?.retCode === 603) {
dispatch(
setShowPopup(ACTIVE_POPUP.unSupportedCountryPopup, {
data: res.data.retCode,
})
);
return;
}
if (res?.data?.retCode === 604) {
//todo "NotServiceLanguage"
return;
}
if (onSuccess) onSuccess(res);
})
.catch((error) => {
console.error('TAxios ', url, error);
if (onFail) onFail(error);
});
}
}
};
const accessToken = getState().localSettings.accessToken;
if (noTokenRefresh || (!tokenRefreshing && accessToken)) {
executeRequest(accessToken);
} else {
if (!accessToken && !tokenRefreshing) {
dispatch(getAuthenticationCode());
}
pushQueue();
}
};
// 안전한 에러 처리를 위한 TAxiosPromise
export const TAxiosPromise = (
dispatch,
getState,
type,
baseUrl,
urlParams = {},
params = {},
noTokenRefresh = false
) => {
return new Promise((resolve, reject) => {
TAxios(
dispatch,
getState,
type,
baseUrl,
urlParams,
params,
// onSuccess - 항상 성공 객체로 반환
(response) => {
resolve({
success: true,
data: response.data,
response: response,
error: null,
});
},
// onFail - 에러도 resolve로 처리하여 throw 방지
(error) => {
console.error(`TAxiosPromise failed for ${baseUrl}:`, error);
resolve({
success: false,
data: null,
response: null,
error: error,
});
},
noTokenRefresh
);
});
};
// 더 세밀한 제어를 위한 확장된 Promise 버전
export const TAxiosAdvancedPromise = (
dispatch,
getState,
type,
baseUrl,
urlParams = {},
params = {},
options = {}
) => {
const {
noTokenRefresh = false,
timeout = 30000,
retries = 0,
retryDelay = 1000,
throwOnError = false, // 개발자가 명시적으로 원할 때만 throw
} = options;
return new Promise((resolve, reject) => {
let attempts = 0;
const maxAttempts = retries + 1;
const attemptRequest = () => {
attempts++;
// console.log(`TAxiosPromise attempt ${attempts}/${maxAttempts} for ${baseUrl}`);
const timeoutId = setTimeout(() => {
const timeoutError = new Error(`Request timeout after ${timeout}ms for ${baseUrl}`);
if (throwOnError) {
reject(timeoutError);
} else {
resolve({
success: false,
data: null,
response: null,
error: timeoutError,
});
}
}, timeout);
TAxios(
dispatch,
getState,
type,
baseUrl,
urlParams,
params,
// onSuccess
(response) => {
clearTimeout(timeoutId);
// console.log(`TAxiosPromise success on attempt ${attempts} for ${baseUrl}`);
resolve({
success: true,
data: response.data,
response: response,
error: null,
});
},
// onFail
(error) => {
clearTimeout(timeoutId);
console.error(`TAxiosPromise error on attempt ${attempts} for ${baseUrl}:`, error);
// Check if the error is due to token expiration
// TAxios already handles token refresh and queueing for 401/402 (501은 제외)
// So we should NOT retry immediately in this loop, but let TAxios handle it.
const retCode = error?.data?.retCode;
const isTokenError = retCode === 401 || retCode === 402;
// 재시도 로직
if (attempts < maxAttempts && !isTokenError) {
console.log(`Retrying in ${retryDelay}ms... (${attempts}/${maxAttempts})`);
setTimeout(() => {
attemptRequest();
}, retryDelay);
} else {
// 최종 실패 시에도 resolve로 처리 (throwOnError가 false인 경우)
if (throwOnError) {
reject(error);
} else {
resolve({
success: false,
data: null,
response: null,
error: error,
});
}
}
},
noTokenRefresh
);
};
attemptRequest();
});
};
// HTTP 메소드별 편의 함수들 (안전한 버전)
export const TAxiosGet = async (dispatch, getState, baseUrl, urlParams = {}, options = {}) => {
return await TAxiosPromise(
dispatch,
getState,
'get',
baseUrl,
urlParams,
{},
options.noTokenRefresh
);
};
export const TAxiosPost = async (dispatch, getState, baseUrl, params = {}, options = {}) => {
return await TAxiosPromise(
dispatch,
getState,
'post',
baseUrl,
{},
params,
options.noTokenRefresh
);
};
// 안전한 다중 요청 처리
export const TAxiosAll = async (requests) => {
try {
const results = await Promise.all(requests);
// 모든 결과를 안전하게 처리
const successResults = [];
const failedResults = [];
results.forEach((result, index) => {
if (result.success) {
successResults.push({ index, result: result.data });
} else {
failedResults.push({ index, error: result.error });
}
});
return {
success: failedResults.length === 0,
successResults,
failedResults,
allResults: results,
};
} catch (error) {
console.error('TAxiosAll unexpected error:', error);
return {
success: false,
successResults: [],
failedResults: [{ index: -1, error }],
allResults: [],
};
}
};
// 순차 요청 처리 (안전 버전)
export const TAxiosSequential = async (requests) => {
const results = [];
const errors = [];
for (let i = 0; i < requests.length; i++) {
try {
const result = await requests[i];
if (result.success) {
results.push({ index: i, data: result.data });
} else {
errors.push({ index: i, error: result.error });
console.error(`TAxiosSequential failed at request ${i}:`, result.error);
}
} catch (error) {
errors.push({ index: i, error });
console.error(`TAxiosSequential unexpected error at request ${i}:`, error);
}
}
return {
success: errors.length === 0,
results,
errors,
};
};
// 안전한 Redux Thunk 헬퍼
export const createSafeApiThunk = (apiCall) => {
return (...args) =>
async (dispatch, getState) => {
try {
const result = await apiCall(dispatch, getState, ...args);
return result; // 이미 안전한 형태로 반환됨
} catch (error) {
console.error('API thunk unexpected error:', error);
return {
success: false,
data: null,
response: null,
error,
};
}
};
};
// 실제 사용 예시들 (안전한 버전)
export const safeUsageExamples = {
// 1. 기본 안전한 사용법
basicSafeUsage: async (dispatch, getState) => {
const result = await TAxiosPromise(dispatch, getState, 'get', URLS.GET_HOME_TERMS, {
trmsTpCdList: 'MST00401, MST00402',
mbrNo: '12345',
});
if (result.success) {
// console.log('Success:', result.data);
return result.data;
} else {
console.error('API call failed:', result.error);
// 에러를 throw하지 않고 기본값 반환하거나 사용자에게 안내
return null;
}
},
// 2. retCode 체크를 포함한 안전한 처리
safeWithRetCodeCheck: async (dispatch, getState) => {
const result = await TAxiosGet(dispatch, getState, URLS.GET_HOME_TERMS, {
trmsTpCdList: 'MST00401, MST00402',
mbrNo: '12345',
});
if (!result.success) {
console.error('Network error:', result.error);
return { success: false, message: '네트워크 오류가 발생했습니다.' };
}
if (result.data.retCode !== 0) {
console.error('API error:', result.data.retCode, result.data.retMsg);
return {
success: false,
message: result.data.retMsg || 'API 오류가 발생했습니다.',
};
}
return { success: true, data: result.data };
},
// 3. 여러 요청의 안전한 처리
safeParallelRequests: async (dispatch, getState) => {
const requests = [
TAxiosGet(dispatch, getState, URLS.GET_HOME_TERMS, { mbrNo: '12345' }),
TAxiosGet(dispatch, getState, URLS.GET_USER_INFO, { mbrNo: '12345' }),
TAxiosPost(dispatch, getState, URLS.UPDATE_SETTINGS, { setting: 'value' }),
];
const result = await TAxiosAll(requests);
if (result.success) {
// console.log('All requests succeeded');
return result.successResults.map((item) => item.result);
} else {
console.error('Some requests failed:', result.failedResults);
// 부분적 성공도 처리 가능
return {
successData: result.successResults.map((item) => item.result),
errors: result.failedResults,
};
}
},
};
// 컴포넌트에서의 안전한 사용법
export const ComponentUsageExample = () => {
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleFetchTerms = async () => {
setLoading(true);
setError(null);
const result = await dispatch(fetchCurrentUserHomeTermsSafe());
setLoading(false);
if (result.success) {
// console.log('Terms fetched successfully');
// 성공 처리 (예: 성공 토스트 표시)
} else {
console.error('Failed to fetch terms:', result.message);
setError(result.message);
// 에러 처리 (예: 에러 토스트 표시)
}
};
return (
<div>
<button onClick={handleFetchTerms} disabled={loading}>
{loading ? '로딩 중...' : '약관 정보 가져오기'}
</button>
{error && <div className="error-message">{error}</div>}
</div>
);
};