-
Notifications
You must be signed in to change notification settings - Fork 1
[Refactor] Axios Instance 구조 수정 #386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cecc82e
efe96dc
b13b9f9
119af60
480a244
70c396e
77c3ea9
1620d7b
0bce8cb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import axios from 'axios'; | ||
|
|
||
| import { getAccessToken } from '@/lib/auth/token'; | ||
| import { CommonErrorResponse } from '@/types/service/common'; | ||
|
|
||
| import { createApiHelper } from '../lib/apiHelper'; | ||
|
|
||
| const authInstance = axios.create({ | ||
| baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, | ||
| timeout: 20000, | ||
| withCredentials: true, | ||
| }); | ||
|
|
||
| authInstance.interceptors.request.use(async (config) => { | ||
| const isServer = typeof window === 'undefined'; | ||
|
|
||
| if (isServer) { | ||
| const { cookies } = await import('next/headers'); | ||
| const cookieStore = await cookies(); | ||
| const refreshToken = cookieStore.get('refreshToken')?.value; | ||
| if (refreshToken) { | ||
| config.headers.Cookie = `refreshToken=${refreshToken}`; | ||
| } | ||
| } | ||
|
|
||
| const accessToken = await getAccessToken(); | ||
| if (accessToken && config.headers) { | ||
| config.headers.Authorization = `Bearer ${accessToken}`; | ||
| } | ||
|
|
||
| return config; | ||
| }); | ||
|
|
||
| authInstance.interceptors.response.use( | ||
| (response) => { | ||
| return response; | ||
| }, | ||
| async (error) => { | ||
| return new CommonErrorResponse(error.response?.data); | ||
| }, | ||
| ); | ||
|
Comment on lines
+34
to
+41
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [심각] 에러 인터셉터에서
const response = await axios.post<CommonSuccessResponse<T>>(url, data, config);
// response = CommonErrorResponse 객체 (AxiosResponse가 아님)
return response.data.data;
// response.data = undefined → TypeError: Cannot read properties of undefined즉, 🐛 수정 제안 authInstance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
- return new CommonErrorResponse(error.response?.data);
+ return Promise.reject(new CommonErrorResponse(error.response?.data));
},
);🤖 Prompt for AI Agents |
||
|
|
||
| export const authAPI = createApiHelper(authInstance); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||||||||||||||||||||
| import axios from 'axios'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| import { getAccessToken } from '@/lib/auth/token'; | ||||||||||||||||||||||||
| import { CommonErrorResponse } from '@/types/service/common'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| import { API } from '../..'; | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check the structure of src/api directory
echo "=== Directory structure ==="
find src/api -type f -name "*.ts" | head -20
echo ""
echo "=== Content of src/api/index.ts ==="
cat src/api/index.ts 2>/dev/null || echo "File not found"
echo ""
echo "=== Content of src/api/core/index.ts ==="
cat src/api/core/index.ts 2>/dev/null || echo "File not found"
echo ""
echo "=== All imports in src/api/core/base/index.ts ==="
head -20 src/api/core/base/index.ts
echo ""
echo "=== Check if authAPI exists ==="
fd -t f 'auth' src/api --type f | grep -E "\.(ts|js)$"Repository: WeGo-Together/WeGo_FrontEnd Length of output: 1923 🏁 Script executed: #!/bin/bash
echo "=== Content of src/api/service/auth-service/index.ts ==="
cat src/api/service/auth-service/index.ts
echo ""
echo "=== Content of src/api/service/index.ts ==="
cat src/api/service/index.ts
echo ""
echo "=== Check imports in auth-service ==="
head -15 src/api/service/auth-service/index.tsRepository: WeGo-Together/WeGo_FrontEnd Length of output: 2233 🏁 Script executed: #!/bin/bash
echo "=== Content of src/api/core/auth/index.ts ==="
cat src/api/core/auth/index.ts
echo ""
echo "=== Full content of src/api/core/base/index.ts to see line 34 context ==="
cat -n src/api/core/base/index.tsRepository: WeGo-Together/WeGo_FrontEnd Length of output: 3264 순환 의존성 제거 필요
추가 이슈: 40-42번 라인
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| import { createApiHelper } from '../lib/apiHelper'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const baseInstance = axios.create({ | ||||||||||||||||||||||||
| baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, | ||||||||||||||||||||||||
| timeout: 20000, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| baseInstance.interceptors.request.use(async (config) => { | ||||||||||||||||||||||||
| const token = await getAccessToken(); | ||||||||||||||||||||||||
| if (token && config.headers) { | ||||||||||||||||||||||||
| config.headers.Authorization = `Bearer ${token}`; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| return config; | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| baseInstance.interceptors.response.use( | ||||||||||||||||||||||||
| (response) => response, | ||||||||||||||||||||||||
| async (error) => { | ||||||||||||||||||||||||
| const isServer = typeof window === 'undefined'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const errorResponse = new CommonErrorResponse(error.response?.data); | ||||||||||||||||||||||||
| const status = errorResponse.status; | ||||||||||||||||||||||||
| const originalRequest = error.config; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (status === 401 && !originalRequest._retry) { | ||||||||||||||||||||||||
| originalRequest._retry = true; | ||||||||||||||||||||||||
|
Comment on lines
+31
to
+32
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for _retry usage in the codebase
echo "=== Searching for _retry usage ==="
rg -rn '_retry' --type=ts
echo -e "\n=== Checking for axios type augmentations ==="
rg -rn 'declare module.*axios' --type=ts
echo -e "\n=== Checking for .d.ts files in types directory ==="
fd -t f '\.d\.ts$' | head -20Repository: WeGo-Together/WeGo_FrontEnd Length of output: 375 🏁 Script executed: #!/bin/bash
# Read the actual file to see context around lines 31-32
echo "=== Checking src/api/core/base/index.ts ==="
if [ -f "src/api/core/base/index.ts" ]; then
cat -n "src/api/core/base/index.ts" | sed -n '25,50p'
else
echo "File not found at src/api/core/base/index.ts"
fd -t f 'index.ts' | grep -E '(api|core|base)' | head -5
fiRepository: WeGo-Together/WeGo_FrontEnd Length of output: 1148 🏁 Script executed: #!/bin/bash
# Check if tsconfig.json has strict mode enabled
echo "=== Checking TypeScript configuration ==="
if [ -f "tsconfig.json" ]; then
cat tsconfig.json | jq '.compilerOptions | {strict, noImplicitAny, strictNullChecks}' 2>/dev/null || cat tsconfig.json
fiRepository: WeGo-Together/WeGo_FrontEnd Length of output: 186 🏁 Script executed: #!/bin/bash
# Check if the code has any type errors related to the n property
echo "=== Checking if there's an `@ts-ignore` or similar suppression ==="
rg -B2 -A2 'originalRequest\.n' src/api/core/base/index.ts
echo -e "\n=== Checking if InternalAxiosRequestConfig is imported ==="
rg 'InternalAxiosRequestConfig|AxiosRequestConfig' src/api/core/base/index.tsRepository: WeGo-Together/WeGo_FrontEnd Length of output: 195 현재 코드의 커스텀 속성
타입 확장 예시// src/types/axios.d.ts
import 'axios';
declare module 'axios' {
export interface InternalAxiosRequestConfig {
_retry?: boolean;
}
}🤖 Prompt for AI Agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. originalRequest.m이 어디있나요? 저는 originalRequest._retry 밖에 사용하지 않았습니다.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
코드를 다시 확인해보니 현재 코드는 이미 만약 TypeScript 컴파일 시 에러가 발생하지 않는다면 현재 설정으로도 충분합니다. 에러가 발생한다면 앞서 제안드린 타입 확장 방법을 적용하시면 됩니다. 혼란을 드려 죄송합니다! 🙏 |
||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| await API.authService.refresh(); | ||||||||||||||||||||||||
| return baseInstance(originalRequest); | ||||||||||||||||||||||||
| } catch (refreshError) { | ||||||||||||||||||||||||
| if (isServer) { | ||||||||||||||||||||||||
| throw refreshError; | ||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||
| const currentPath = window.location.pathname + window.location.search; | ||||||||||||||||||||||||
| window.location.href = `/login?error=unauthorized&path=${encodeURIComponent(currentPath)}`; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
Comment on lines
+39
to
+43
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 클라이언트 측 refresh 실패 시
제안된 수정 window.location.href = `/login?error=unauthorized&path=${encodeURIComponent(currentPath)}`;
+ return new Promise(() => {}); // 리다이렉트 중 Promise를 pending 상태로 유지
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if (status === 404) { | ||||||||||||||||||||||||
| if (isServer) { | ||||||||||||||||||||||||
| const { notFound } = await import('next/navigation'); | ||||||||||||||||||||||||
| notFound(); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| throw errorResponse; | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| export const baseAPI = createApiHelper(baseInstance); | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,124 +1,2 @@ | ||
| import axios from 'axios'; | ||
|
|
||
| import { CommonErrorResponse, CommonSuccessResponse } from '@/types/service/common'; | ||
|
|
||
| import { API } from '..'; | ||
|
|
||
| export const baseAPI = axios.create({ | ||
| baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, | ||
| timeout: 20000, | ||
| }); | ||
|
|
||
| baseAPI.interceptors.request.use(async (config) => { | ||
| const isServer = typeof window === 'undefined'; | ||
|
|
||
| if (isServer) { | ||
| // // Server 환경 | ||
| const { cookies } = await import('next/headers'); | ||
| const cookieStore = await cookies(); | ||
| const token = cookieStore.get('accessToken')?.value; | ||
| if (token && config.headers) { | ||
| config.headers.Authorization = `Bearer ${token}`; | ||
| } | ||
| } else { | ||
| // Client 환경 | ||
| const match = document.cookie.match(new RegExp('(^| )accessToken=([^;]+)')); | ||
| const token = match ? decodeURIComponent(match[2]) : undefined; | ||
| if (token && config.headers) { | ||
| config.headers.Authorization = `Bearer ${token}`; | ||
| } | ||
| } | ||
|
|
||
| return config; | ||
| }); | ||
|
|
||
| baseAPI.interceptors.response.use( | ||
| (response) => { | ||
| return response; | ||
| }, | ||
| async (error) => { | ||
| const errorResponse: CommonErrorResponse = error.response?.data || { | ||
| type: 'about:blank', | ||
| title: 'Network Error', | ||
| status: 0, | ||
| detail: '서버와 연결할 수 없습니다.', | ||
| instance: error.config?.url || '', | ||
| errorCode: 'NETWORK_ERROR', | ||
| }; | ||
|
|
||
| const status = error.response?.status ?? errorResponse.status; | ||
| const isServer = typeof window === 'undefined'; | ||
| const originalRequest = error.config; | ||
|
|
||
| // skipAuthRedirect flag가 지정되어있지 않으면 항상 redirect 되도록 | ||
| if (originalRequest.skipAuthRedirect === undefined) { | ||
| originalRequest.skipAuthRedirect = true; | ||
| } | ||
|
|
||
| if (status === 401 && !originalRequest._retry) { | ||
| originalRequest._retry = true; | ||
| try { | ||
| // refresh - set cookie는 클라이언트 요청만 동작함 | ||
| if (!isServer) { | ||
| await API.authService.refresh(originalRequest.skipAuthRedirect); | ||
| } | ||
| return baseAPI(originalRequest); | ||
| } catch (refreshError) { | ||
| if (!originalRequest.skipAuthRedirect) throw refreshError; | ||
| if (isServer) { | ||
| const { redirect } = await import('next/navigation'); | ||
| redirect('/login'); | ||
| } else { | ||
| if (window.location.pathname === '/login') { | ||
| throw errorResponse; | ||
| } | ||
| const currentPath = window.location.pathname + window.location.search; | ||
| window.location.href = `/login?error=unauthorized&path=${encodeURIComponent(currentPath)}`; | ||
| } | ||
| } | ||
| } | ||
| if (status === 404) { | ||
| if (isServer) { | ||
| const { notFound } = await import('next/navigation'); | ||
| notFound(); | ||
| } | ||
| } | ||
|
|
||
| throw errorResponse; | ||
| }, | ||
| ); | ||
|
|
||
| type ApiVersionType = 'v1' | 'v2'; | ||
|
|
||
| // 공통 응답 형식 처리를 위한 api 헬퍼 | ||
| const apiHelper = (v: ApiVersionType = 'v1') => ({ | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| get: async <T>(url: string, config?: any): Promise<T> => { | ||
| const response = await baseAPI.get<CommonSuccessResponse<T>>(`/api/${v}${url}`, config); | ||
| return response.data.data; | ||
| }, | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| post: async <T>(url: string, data?: any, config?: any): Promise<T> => { | ||
| const response = await baseAPI.post<CommonSuccessResponse<T>>(`/api/${v}${url}`, data, config); | ||
| return response.data.data; | ||
| }, | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| put: async <T>(url: string, data?: any, config?: any): Promise<T> => { | ||
| const response = await baseAPI.put<CommonSuccessResponse<T>>(`/api/${v}${url}`, data, config); | ||
| return response.data.data; | ||
| }, | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| delete: async <T>(url: string, config?: any): Promise<T> => { | ||
| const response = await baseAPI.delete<CommonSuccessResponse<T>>(`/api/${v}${url}`, config); | ||
| return response.data.data; | ||
| }, | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| patch: async <T>(url: string, data?: any, config?: any): Promise<T> => { | ||
| const response = await baseAPI.patch<CommonSuccessResponse<T>>(`/api/${v}${url}`, data, config); | ||
| return response.data.data; | ||
| }, | ||
| }); | ||
|
|
||
| export const api = apiHelper('v1'); // breaking change 방지용 | ||
| export const apiV1 = apiHelper('v1'); | ||
| export const apiV2 = apiHelper('v2'); | ||
| export { authAPI } from './auth'; | ||
| export { baseAPI } from './base'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { AxiosInstance, AxiosRequestConfig } from 'axios'; | ||
|
|
||
| import { CommonSuccessResponse } from '@/types/service/common'; | ||
|
|
||
| export const createApiHelper = (axios: AxiosInstance) => ({ | ||
| get: async <T>(url: string, config?: AxiosRequestConfig): Promise<T> => { | ||
| const response = await axios.get<CommonSuccessResponse<T>>(url, config); | ||
| return response.data.data; | ||
| }, | ||
| post: async <T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> => { | ||
| const response = await axios.post<CommonSuccessResponse<T>>(url, data, config); | ||
| return response.data.data; | ||
| }, | ||
| put: async <T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> => { | ||
| const response = await axios.put<CommonSuccessResponse<T>>(url, data, config); | ||
| return response.data.data; | ||
| }, | ||
| delete: async <T>(url: string, config?: AxiosRequestConfig): Promise<T> => { | ||
| const response = await axios.delete<CommonSuccessResponse<T>>(url, config); | ||
| return response.data.data; | ||
| }, | ||
| patch: async <T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> => { | ||
| const response = await axios.patch<CommonSuccessResponse<T>>(url, data, config); | ||
| return response.data.data; | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import { api } from '@/api/core'; | ||
| import { authAPI } from '@/api/core'; | ||
| import { clearAccessToken, setAccessToken } from '@/lib/auth/token'; | ||
| import { | ||
| GoogleOAuthExchangeRequest, | ||
|
|
@@ -13,47 +13,42 @@ import { | |
| export const authServiceRemote = () => ({ | ||
| // 로그인 | ||
| login: async (payload: LoginRequest) => { | ||
| const data = await api.post<LoginResponse>('/auth/login', payload, { withCredentials: true }); | ||
| const data = await authAPI.post<LoginResponse>('/api/v1/auth/login', payload); | ||
|
|
||
| setAccessToken(data.accessToken, data.expiresIn); | ||
| return data; | ||
| }, | ||
|
|
||
| // 회원가입 | ||
| signup: (payload: SignupRequest) => api.post<SignupResponse>(`/auth/signup`, payload), | ||
| signup: async (payload: SignupRequest) => { | ||
| return authAPI.post<SignupResponse>(`/api/v1/auth/signup`, payload); | ||
| }, | ||
|
|
||
| // 로그아웃 | ||
| logout: async () => { | ||
| await api.post<void>('/auth/logout', null, { withCredentials: true }); | ||
| await authAPI.post<void>('/api/v1/auth/logout', null); | ||
| clearAccessToken(); | ||
| }, | ||
|
|
||
| // 액세스 토큰 재발급 | ||
| refresh: async (redirect: boolean = true) => { | ||
| const data = await api.post<RefreshResponse>( | ||
| '/auth/refresh', | ||
| {}, | ||
| { _retry: true, withCredentials: true, skipAuthRedirect: redirect }, | ||
| ); | ||
|
|
||
| refresh: async () => { | ||
| //prettier-ignore | ||
| const data = await authAPI.post<RefreshResponse>('/api/v1/auth/refresh', {}); | ||
| setAccessToken(data.accessToken, data.expiresIn); | ||
| return data; | ||
| }, | ||
|
|
||
| // 회원 탈퇴 | ||
| withdraw: async () => { | ||
| await api.delete<void>('/auth/withdraw', { withCredentials: true }); | ||
| await authAPI.delete<void>('/api/v1/auth/withdraw'); | ||
| clearAccessToken(); | ||
| }, | ||
|
Comment on lines
28
to
45
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, locate the auth service files
find . -type f -name "index.ts" | grep -E "(auth|service)" | head -20Repository: WeGo-Together/WeGo_FrontEnd Length of output: 732 🏁 Script executed: # Check the structure of the api directory
ls -la src/api/ 2>/dev/null || echo "Directory not found"Repository: WeGo-Together/WeGo_FrontEnd Length of output: 350 🏁 Script executed: # Find the auth-related files
find . -path "*api/core/auth*" -o -path "*api/service/auth-service*" | grep -v node_modulesRepository: WeGo-Together/WeGo_FrontEnd Length of output: 190 🏁 Script executed: # Read the authAPI error interceptor implementation
cat -n src/api/core/auth/index.tsRepository: WeGo-Together/WeGo_FrontEnd Length of output: 1481 🏁 Script executed: # Read the auth-service implementation to see the full context
cat -n src/api/service/auth-service/index.tsRepository: WeGo-Together/WeGo_FrontEnd Length of output: 1874
🤖 Prompt for AI Agents |
||
|
|
||
| // 구글 OAuth 코드 교환 | ||
| exchangeGoogleCode: async (payload: GoogleOAuthExchangeRequest) => { | ||
| const data = await api.post<GoogleOAuthExchangeResponse>('/auth/google', payload, { | ||
| withCredentials: true, | ||
| }); | ||
| const data = await authAPI.post<GoogleOAuthExchangeResponse>('/api/v1/auth/google', payload); | ||
|
|
||
| setAccessToken(data.accessToken, data.expiresIn); | ||
|
|
||
| return data; | ||
| }, | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Next.js 16.0.10에 적용되는 보안 취약점 — 즉시 업그레이드 필요
프로젝트가 사용 중인 Next.js 16.0.x에는 다음 CVE가 존재합니다:
현재 최신 안정 버전은 Next.js 16.1이며, Turbopack 파일 시스템 캐싱(안정화) 등의 개선이 포함되어 있습니다.
🤖 Prompt for AI Agents