-
Notifications
You must be signed in to change notification settings - Fork 0
[API] axios API 클라이언트 및 토큰 관리 유틸 추가 #68
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
a4d1079
fac5f78
a7fdea2
d13e70e
e65b876
13e2742
8712bb4
262da2b
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 |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ node_modules | |
| dist | ||
| dist-ssr | ||
| *.local | ||
| .env.local | ||
|
|
||
| # Editor directories and files | ||
| .vscode/* | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import { apiClient } from '../index'; | ||
| import type { PostData } from '@/types/post'; | ||
|
|
||
| // API 응답 데이터 타입 | ||
| export interface MyFeedData { | ||
| feedList: PostData[]; | ||
| nextCursor: string; | ||
| isLast: boolean; | ||
| } | ||
|
|
||
| // API 응답 타입 | ||
| export interface MyFeedResponse { | ||
| success: boolean; | ||
| code: number; | ||
| message: string; | ||
| data: MyFeedData; | ||
| } | ||
|
|
||
| // 요청 파라미터 타입 | ||
| export interface GetMyFeedParams { | ||
| cursor?: string; // 첫 페이지는 null 또는 없음, 다음 페이지부터는 nextCursor 값 사용 | ||
| } | ||
|
|
||
| // 내 피드 조회 API 함수 | ||
| export const getMyFeeds = async (params?: GetMyFeedParams): Promise<MyFeedResponse> => { | ||
| const queryParams = new URLSearchParams(); | ||
|
|
||
| // cursor가 있을 때만 쿼리 파라미터에 추가 | ||
| if (params?.cursor) { | ||
| queryParams.append('cursor', params.cursor); | ||
| } | ||
|
|
||
| const url = `/feeds/mine${queryParams.toString() ? `?${queryParams.toString()}` : ''}`; | ||
|
|
||
| const response = await apiClient.get<MyFeedResponse>(url); | ||
| return response.data; | ||
| }; | ||
|
|
||
| /* | ||
| // 첫 페이지 조회 | ||
| const firstPage = await getMyFeeds(); | ||
|
|
||
| // 다음 페이지 조회 (nextCursor 사용) | ||
| const nextPage = await getMyFeeds({ | ||
| cursor: firstPage.data.nextCursor | ||
| }); | ||
|
|
||
| // 마지막 페이지인지 확인 | ||
| if (firstPage.data.isLast) { | ||
| console.log('더 이상 불러올 데이터가 없습니다.'); | ||
| } | ||
| */ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import { apiClient } from '../index'; | ||
| import type { PostData } from '@/types/post'; | ||
|
|
||
| // API 응답 데이터 타입 | ||
| export interface TotalFeedData { | ||
| feedList: PostData[]; | ||
| nextCursor: string; | ||
| isLast: boolean; | ||
| } | ||
|
|
||
| // API 응답 타입 | ||
| export interface TotalFeedResponse { | ||
| success: boolean; | ||
| code: number; | ||
| message: string; | ||
| data: TotalFeedData; | ||
| } | ||
|
|
||
| // 요청 파라미터 타입 | ||
| export interface GetTotalFeedParams { | ||
| cursor?: string; // 첫 페이지는 null 또는 없음, 다음 페이지부터는 nextCursor 값 사용 | ||
| } | ||
|
|
||
| export const getTotalFeeds = async (params?: GetTotalFeedParams): Promise<TotalFeedResponse> => { | ||
| const queryParams = new URLSearchParams(); | ||
|
|
||
| // cursor가 있을 때만 쿼리 파라미터에 추가 | ||
| if (params?.cursor) { | ||
| queryParams.append('cursor', params.cursor); | ||
| } | ||
|
|
||
| const url = `/feeds${queryParams.toString() ? `?${queryParams.toString()}` : ''}`; | ||
|
|
||
| const response = await apiClient.get<TotalFeedResponse>(url); | ||
| return response.data; | ||
| }; | ||
|
Comment on lines
+24
to
+36
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. 🛠️ Refactor suggestion 코드 중복을 개선할 수 있습니다.
// api/feeds/common.ts
interface FeedApiParams {
cursor?: string;
}
interface FeedResponse<T> {
success: boolean;
code: number;
message: string;
data: T;
}
const createFeedApi = <T>(endpoint: string) => {
return async (params?: FeedApiParams): Promise<FeedResponse<T>> => {
const queryParams = new URLSearchParams();
if (params?.cursor) {
queryParams.append('cursor', params.cursor);
}
const url = `${endpoint}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
const response = await apiClient.get<FeedResponse<T>>(url);
return response.data;
};
};🤖 Prompt for AI Agents |
||
|
|
||
| /* | ||
| 사용 방법: | ||
|
|
||
| // 첫 페이지 조회 | ||
| const firstPage = await getTotalFeeds(); | ||
|
|
||
| // 다음 페이지 조회 (nextCursor 사용) | ||
| const nextPage = await getTotalFeeds({ | ||
| cursor: firstPage.data.nextCursor | ||
| }); | ||
|
|
||
| // 마지막 페이지인지 확인 | ||
| if (firstPage.data.isLast) { | ||
| console.log('더 이상 불러올 데이터가 없습니다.'); | ||
| } | ||
| */ | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,87 @@ | ||||||||||||
| import axios, { type AxiosResponse, type AxiosError } from 'axios'; | ||||||||||||
|
|
||||||||||||
| // 하드코딩된 액세스 토큰 | ||||||||||||
| const ACCESS_TOKEN = | ||||||||||||
| 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDIwMTY4OCwiZXhwIjoxNzU2NzkzNjg4fQ.oOyJ7JI_t2-Xq1-gfAv4ZaYNrbyplvqdxhCk76-Txe4'; | ||||||||||||
|
Comment on lines
+3
to
+5
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 ACCESS_TOKEN =
- 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDIwMTY4OCwiZXhwIjoxNzU2NzkzNjg4fQ.oOyJ7JI_t2-Xq1-gfAv4ZaYNrbyplvqdxhCk76-Txe4';
+// 개발용 기본 토큰 (환경변수로 관리)
+const ACCESS_TOKEN = import.meta.env.VITE_DEV_ACCESS_TOKEN || '';📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
| // 토큰 관리 유틸리티 | ||||||||||||
| export const TokenManager = { | ||||||||||||
| setAccessToken: (token: string) => localStorage.setItem('accessToken', token), | ||||||||||||
| getAccessToken: (): string | null => localStorage.getItem('accessToken'), | ||||||||||||
| // setRefreshToken: (token: string) => localStorage.setItem('refreshToken', token), | ||||||||||||
| // getRefreshToken: (): string | null => localStorage.getItem('refreshToken'), | ||||||||||||
| clearTokens: () => { | ||||||||||||
| localStorage.removeItem('accessToken'); | ||||||||||||
| // localStorage.removeItem('refreshToken'); | ||||||||||||
| }, | ||||||||||||
| hasValidToken: (): boolean => !!localStorage.getItem('accessToken'), | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| // API 기본 설정 | ||||||||||||
| const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; | ||||||||||||
|
|
||||||||||||
| // 환경변수 확인용 | ||||||||||||
| console.log('API_BASE_URL:', API_BASE_URL); | ||||||||||||
|
|
||||||||||||
| // axios 인스턴스 생성 | ||||||||||||
| export const apiClient = axios.create({ | ||||||||||||
| baseURL: API_BASE_URL, | ||||||||||||
| timeout: 10000, | ||||||||||||
| headers: { | ||||||||||||
| 'Content-Type': 'application/json', | ||||||||||||
| }, | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| // 요청 인터셉터 | ||||||||||||
| apiClient.interceptors.request.use( | ||||||||||||
| config => { | ||||||||||||
| // 로컬스토리지에서 토큰 먼저 확인 | ||||||||||||
| const token = TokenManager.getAccessToken(); | ||||||||||||
| if (token) { | ||||||||||||
| config.headers.Authorization = `Bearer ${token}`; | ||||||||||||
| } else { | ||||||||||||
| // 토큰이 없으면 하드코딩된 토큰 사용 (개발용) | ||||||||||||
| config.headers.Authorization = ACCESS_TOKEN; | ||||||||||||
| } | ||||||||||||
| return config; | ||||||||||||
| }, | ||||||||||||
| error => Promise.reject(error), | ||||||||||||
| ); | ||||||||||||
|
|
||||||||||||
| // 응답 인터셉터 - 토큰 만료 처리 및 에러 처리 | ||||||||||||
| apiClient.interceptors.response.use( | ||||||||||||
| (response: AxiosResponse) => response, | ||||||||||||
| (error: AxiosError) => { | ||||||||||||
| const { status } = error.response || {}; | ||||||||||||
|
|
||||||||||||
| // 에러 로깅 | ||||||||||||
| console.error('API Error:', status, error.message); | ||||||||||||
|
|
||||||||||||
| // 토큰 만료 또는 인증 실패 시 로그인 페이지로 리다이렉트 | ||||||||||||
| if (status === 401) { | ||||||||||||
| // alert('토큰이 만료되었거나 유효하지 않습니다. 로그인 페이지로 이동합니다.'); | ||||||||||||
|
|
||||||||||||
| // 현재 페이지가 로그인 페이지가 아닌 경우에만 리다이렉트 | ||||||||||||
| if (window.location.pathname !== '/') { | ||||||||||||
| // alert('로그인이 필요합니다. 로그인 페이지로 이동합니다.'); | ||||||||||||
| window.location.href = '/'; | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // 권한 없음 (403) 에러 처리 | ||||||||||||
| if (status === 403) { | ||||||||||||
| console.warn('접근 권한이 없습니다.'); | ||||||||||||
| alert('접근 권한이 없습니다.'); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // 서버 에러 (500번대) 처리 | ||||||||||||
| if (status && status >= 500) { | ||||||||||||
| console.error('서버 오류가 발생했습니다.'); | ||||||||||||
| alert('서버에 문제가 발생했습니다. 잠시 후 다시 시도해주세요.'); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| return Promise.reject(error); | ||||||||||||
| }, | ||||||||||||
| ); | ||||||||||||
|
|
||||||||||||
| export default apiClient; | ||||||||||||
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.
💡 Verification agent
🧩 Analysis chain
코드 중복 가능성을 확인해보세요.
getTotalFeed.ts와 거의 동일한 구조를 가지고 있습니다. 공통 유틸리티 함수나 제네릭 함수로 중복을 제거할 수 있을지 검토해보세요.다음 스크립트로 두 API 함수 간의 유사성을 확인할 수 있습니다:
🏁 Script executed:
Length of output: 3173
중복 피드 API 함수 리팩토링 제안
getMyFeeds(/feeds/mine)와getTotalFeeds(/feeds)는 URLSearchParams 처리, 쿼리 문자열 생성,apiClient.get호출 등 거의 동일한 로직을 갖고 있습니다. 아래 파일들을 점검하여 공통 유틸 함수 또는 제네릭 헬퍼로 추출해 중복을 제거해 보세요.수정 대상:
리팩토링 예시:
🤖 Prompt for AI Agents