🕐 커밋 시간: 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 서비스 레이어 개선 • 소규모 기능 개선
595 lines
17 KiB
JavaScript
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>
|
|
);
|
|
};
|