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; } // RefreshToken 만료 if (res?.data?.retCode === 402 || res?.data?.retCode === 501) { 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); // 재시도 로직 if (attempts < maxAttempts) { 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 (