useAxios.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import { useLoadingStore } from '@/stores/loading'
  2. import { useAuthStore } from '@/stores/auth'
  3. import axios from 'axios'
  4. let instance = null
  5. let isRefreshing = false;
  6. let failedQueue = [];
  7. function processQueue(error, token = null) {
  8. failedQueue.forEach(prom => {
  9. if (error) {
  10. prom.reject(error);
  11. } else {
  12. prom.resolve(token);
  13. }
  14. });
  15. failedQueue = [];
  16. }
  17. // interceptor가 없는 별도의 axios 인스턴스 생성
  18. const refreshAxios = axios.create({
  19. baseURL: import.meta.env.VITE_APP_API_URL, // 최종 API URL
  20. withCredentials: false,
  21. timeout: 60 * 1000,
  22. responseType: 'json',
  23. responseEncoding: 'utf8',
  24. xsrfHeaderName: 'X-XSRF-TOKEN',
  25. progress: false,
  26. });
  27. const useAxios = () => {
  28. /************************************************************************
  29. | Axios
  30. ************************************************************************/
  31. const { $log } = useNuxtApp()
  32. const store = useLoadingStore()
  33. if (!instance) {
  34. // 환경 변수에서 API URL과 포트를 가져옵니다
  35. const apiBaseUrl = import.meta.env.VITE_APP_API_URL
  36. const apiPort = import.meta.env.VITE_APP_API_PORT
  37. const fullApiUrl = `${apiBaseUrl}:${apiPort}`
  38. let loadingPassUrl = [
  39. '/p5g/fm/eventViewer'
  40. ]
  41. instance = axios.create({
  42. baseURL: apiBaseUrl, // 최종 API URL
  43. withCredentials: false,
  44. timeout: 60 * 1000,
  45. responseType: 'json',
  46. responseEncoding: 'utf8',
  47. xsrfHeaderName: 'X-XSRF-TOKEN',
  48. progress: false,
  49. });
  50. /**
  51. * 요청 인터셉터
  52. */
  53. instance.interceptors.request.use(function (config) {
  54. $log.debug("[REQ]" + config.url)
  55. let accessToken = useAuthStore().getAccessToken;
  56. // 개발 모드일 때는 env에서 VITE_APP_DEV_TOKEN 사용
  57. if (import.meta.env.MODE === 'development' && import.meta.env.VITE_APP_DEV_TOKEN) {
  58. accessToken = import.meta.env.VITE_APP_DEV_TOKEN;
  59. }
  60. config.headers = {
  61. ...config.headers, // 기존 헤더 유지
  62. 'Accept': 'application/json',
  63. 'Access-Token': accessToken ? accessToken : '', // 동적으로 토큰 세팅
  64. };
  65. // 멀티파트 요청이면 Content-Type을 자동으로 설정하지 않음
  66. if (config.headers['Content-Type'] !== 'multipart/form-data') {
  67. if (!config.headers['Content-Type']) {
  68. config.headers['Content-Type'] = 'application/json;charset=UTF-8';
  69. }
  70. }
  71. if(!loadingPassUrl.includes(config.url)) {
  72. store.plusCount()
  73. }
  74. return config
  75. },
  76. function (error) {
  77. $log.error("[REQ][ERR]" + error)
  78. if (!loadingPassUrl.includes(config.url)) {
  79. store.minusCount()
  80. }
  81. // 요청 에러에도 로딩카운트 감소
  82. if (error.config && !loadingPassUrl.includes(error.config.url)) {
  83. store.minusCount()
  84. }
  85. return Promise.reject(error)
  86. }
  87. )
  88. /**
  89. * 응답 인터셉터
  90. */
  91. instance.interceptors.response.use(
  92. response => {
  93. if(!loadingPassUrl.includes(response.config.url)) {
  94. store.minusCount()
  95. }
  96. return response;
  97. },
  98. async error => {
  99. // 응답 에러에도 로딩카운트 감소(최대한 항상 호출)
  100. if (error.config && !loadingPassUrl.includes(error.config.url)) {
  101. store.minusCount()
  102. }
  103. if (error.response && error.response.status === 401) {
  104. const authStore = useAuthStore();
  105. const originalRequest = error.config;
  106. // refreshToken이 있고, 재발급 시도가 아닌 경우
  107. if (authStore.getRefreshToken && !originalRequest._retry) {
  108. if (isRefreshing) {
  109. // 이미 재발급 중이면 큐에 쌓았다가 처리
  110. return new Promise(function(resolve, reject) {
  111. failedQueue.push({resolve, reject});
  112. }).then(token => {
  113. originalRequest.headers['Access-Token'] = `${token}`;
  114. return instance(originalRequest);
  115. }).catch(err => {
  116. return Promise.reject(err);
  117. });
  118. }
  119. originalRequest._retry = true;
  120. isRefreshing = true;
  121. store.plusCount(); // refreshToken 요청 로딩 시작
  122. try {
  123. let __REQ = {
  124. refreshToken: authStore.getRefreshToken
  125. }
  126. // refreshAxios로 refreshToken 요청
  127. const res = await refreshAxios.post('/roulette/refreshToken', __REQ);
  128. // 다양한 응답 구조에서 accessToken 추출
  129. let newAccessToken = res.data.accessToken;
  130. if (!newAccessToken && res.data.data && res.data.data.accessToken) {
  131. newAccessToken = res.data.data.accessToken;
  132. }
  133. if (!newAccessToken && res.data.token) {
  134. newAccessToken = res.data.token;
  135. }
  136. if (!newAccessToken) {
  137. if (typeof window !== 'undefined') {
  138. alert('세션이 만료되었습니다. 다시 로그인 해주세요.');
  139. window.location.href = '/';
  140. }
  141. authStore.setLogout();
  142. throw new Error('No accessToken in refreshToken response');
  143. }
  144. authStore.setAccessToken(newAccessToken);
  145. processQueue(null, newAccessToken);
  146. isRefreshing = false;
  147. originalRequest.headers['Access-Token'] = `${newAccessToken}`;
  148. store.minusCount();
  149. // instance로 원래 요청 재시도
  150. return instance(originalRequest);
  151. } catch (refreshError) {
  152. processQueue(refreshError, null);
  153. isRefreshing = false;
  154. store.minusCount(); // refreshToken 요청 로딩 끝
  155. // refreshToken 만료(401, 403)만 로그아웃, 그 외는 안내
  156. if (refreshError.response && (refreshError.response.status === 401 || refreshError.response.status === 403)) {
  157. authStore.setLogout();
  158. if (typeof window !== 'undefined') {
  159. alert('로그인 세션이 만료되었습니다. 다시 로그인 해주세요.');
  160. window.location.href = '/';
  161. }
  162. } else {
  163. if (typeof window !== 'undefined') {
  164. alert('일시적인 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.');
  165. //window.location.href = '/';
  166. }
  167. }
  168. return Promise.reject(refreshError);
  169. }
  170. } else {
  171. if(!error.response.data.messages.errorCode){
  172. authStore.setLogout();
  173. if (typeof window !== 'undefined') {
  174. window.location.href = '/';
  175. }
  176. }
  177. }
  178. }
  179. return Promise.reject(error);
  180. }
  181. );
  182. }
  183. return instance
  184. }
  185. export default useAxios