260 lines
8.2 KiB
TypeScript
260 lines
8.2 KiB
TypeScript
import axios, { AxiosInstance, AxiosResponse, AxiosError, AxiosRequestConfig } from 'axios';
|
|
import { ApiResponse, ApiErrorResponse, FileUploadProgress, FileUploadResponse } from '@/types';
|
|
import { tokenManager } from './tokenManager';
|
|
import config from '@/config';
|
|
import { useAppBridge } from '@/hooks';
|
|
|
|
|
|
class ApiClient {
|
|
private instance: AxiosInstance;
|
|
private isRefreshing = false;
|
|
private failedQueue: Array<{
|
|
resolve: (value?: unknown) => void;
|
|
reject: (error?: unknown) => void;
|
|
}> = [];
|
|
private isNativeEnvironment: any;
|
|
private requestRefreshToken: any;
|
|
|
|
constructor() {
|
|
this.instance = axios.create({
|
|
baseURL: config.api.baseURL,
|
|
timeout: config.api.timeout,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
},
|
|
});
|
|
|
|
this.setupInterceptors();
|
|
}
|
|
|
|
private setupInterceptors(): void {
|
|
// 요청 인터셉터
|
|
this.instance.interceptors.request.use(
|
|
(config) => {
|
|
const token = tokenManager.getAccessToken();
|
|
console.log('setupInterceptors request ==> ', token)
|
|
|
|
if(token){
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
},
|
|
(error) => Promise.reject(error)
|
|
);
|
|
|
|
// 응답 인터셉터
|
|
this.instance.interceptors.response.use(
|
|
(response: AxiosResponse) => response,
|
|
async (error: AxiosError) => {
|
|
console.log(' this.instance.interceptors.response' );
|
|
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
|
|
console.log('originalRequest ==> ', JSON.stringify(originalRequest));
|
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
if (this.isRefreshing) {
|
|
return new Promise((resolve, reject) => {
|
|
this.failedQueue.push({ resolve, reject });
|
|
}).then(token => {
|
|
originalRequest.headers!.Authorization = `Bearer ${token}`;
|
|
return this.instance(originalRequest);
|
|
}).catch(err => {
|
|
return Promise.reject(err);
|
|
});
|
|
}
|
|
|
|
originalRequest._retry = true;
|
|
this.isRefreshing = true;
|
|
|
|
console.log('refreshToken!!');
|
|
/*
|
|
this.requestRefreshToken1().then((token) => {
|
|
console.log('requestRefreshToken +[' + JSON.stringify(token) + ']' );
|
|
});
|
|
|
|
|
|
try {
|
|
const newToken = await this.refreshAccessToken();
|
|
this.processQueue(null, newToken);
|
|
originalRequest.headers!.Authorization = `Bearer ${newToken}`;
|
|
return this.instance(originalRequest);
|
|
} catch (refreshError) {
|
|
this.processQueue(refreshError, null);
|
|
tokenManager.clearTokens();
|
|
// 로그인 페이지로 리디렉션 또는 로그아웃 처리
|
|
window.location.href = '/login';
|
|
return Promise.reject(refreshError);
|
|
} finally {
|
|
this.isRefreshing = false;
|
|
}
|
|
*/
|
|
}
|
|
|
|
return Promise.reject(this.handleError(error));
|
|
}
|
|
);
|
|
}
|
|
|
|
private processQueue(error: unknown, token: string | null): void {
|
|
this.failedQueue.forEach(({ resolve, reject }) => {
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
resolve(token);
|
|
}
|
|
});
|
|
|
|
this.failedQueue = [];
|
|
}
|
|
/*
|
|
private async requestRefreshAccessToken(): Promise<any> {
|
|
const {
|
|
isNativeEnvironment,
|
|
requestRefreshToken
|
|
} = useAppBridge();
|
|
|
|
|
|
}
|
|
*/
|
|
|
|
private async refreshAccessToken(): Promise<string> {
|
|
const refreshToken = tokenManager.getRefreshToken();
|
|
if (!refreshToken) {
|
|
throw new Error('No refresh token available');
|
|
}
|
|
|
|
try {
|
|
const response = await axios.post(`${config.api.baseURL}/auth/refresh`, {
|
|
refresh_token: refreshToken
|
|
});
|
|
|
|
const { access_token, refresh_token: newRefreshToken } = response.data.data;
|
|
tokenManager.setTokens(access_token, newRefreshToken);
|
|
return access_token;
|
|
} catch {
|
|
throw new Error('Token refresh failed');
|
|
}
|
|
}
|
|
|
|
private handleError(error: AxiosError): ApiErrorResponse {
|
|
const errorResponse: ApiErrorResponse = {
|
|
success: false,
|
|
error: {
|
|
code: 'UNKNOWN_ERROR',
|
|
message: '알 수 없는 오류가 발생했습니다.',
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
if (error.response) {
|
|
// 서버 응답이 있는 경우
|
|
const { status, data } = error.response;
|
|
errorResponse.error.code = `HTTP_${status}`;
|
|
|
|
if (data && typeof data === 'object' && 'message' in data) {
|
|
errorResponse.error.message = (data as { message: string }).message;
|
|
} else {
|
|
errorResponse.error.message = this.getErrorMessage(status);
|
|
}
|
|
|
|
errorResponse.error.details = data;
|
|
} else if (error.request) {
|
|
// 네트워크 오류
|
|
errorResponse.error.code = 'NETWORK_ERROR';
|
|
errorResponse.error.message = '네트워크 연결을 확인해주세요.';
|
|
} else {
|
|
// 요청 설정 오류
|
|
errorResponse.error.code = 'REQUEST_ERROR';
|
|
errorResponse.error.message = '요청 처리 중 오류가 발생했습니다.';
|
|
}
|
|
|
|
return errorResponse;
|
|
}
|
|
|
|
private getErrorMessage(status: number): string {
|
|
const messages: Record<number, string> = {
|
|
400: '잘못된 요청입니다.',
|
|
401: '인증이 필요합니다.',
|
|
403: '접근 권한이 없습니다.',
|
|
404: '요청한 리소스를 찾을 수 없습니다.',
|
|
409: '데이터 충돌이 발생했습니다.',
|
|
422: '입력 데이터를 확인해주세요.',
|
|
500: '서버 오류가 발생했습니다.',
|
|
502: '서버 연결에 문제가 있습니다.',
|
|
503: '서비스를 일시적으로 사용할 수 없습니다.',
|
|
};
|
|
|
|
return messages[status] || '알 수 없는 오류가 발생했습니다.';
|
|
}
|
|
|
|
async get<T>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
const response = await this.instance.get(url, config);
|
|
return response.data;
|
|
}
|
|
|
|
async post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
const response = await this.instance.post(url, data, config);
|
|
return response.data;
|
|
}
|
|
|
|
async put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
const response = await this.instance.put(url, data, config);
|
|
return response.data;
|
|
}
|
|
|
|
async patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
const response = await this.instance.patch(url, data, config);
|
|
return response.data;
|
|
}
|
|
|
|
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
const response = await this.instance.delete(url, config);
|
|
return response.data;
|
|
}
|
|
|
|
async uploadFile(
|
|
url: string,
|
|
file: File,
|
|
onProgress?: (progress: FileUploadProgress) => void
|
|
): Promise<ApiResponse<FileUploadResponse>> {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const config: AxiosRequestConfig = {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
},
|
|
onUploadProgress: (progressEvent) => {
|
|
if (onProgress && progressEvent.total) {
|
|
const progress: FileUploadProgress = {
|
|
loaded: progressEvent.loaded,
|
|
total: progressEvent.total,
|
|
percentage: Math.round((progressEvent.loaded * 100) / progressEvent.total),
|
|
};
|
|
onProgress(progress);
|
|
}
|
|
},
|
|
};
|
|
|
|
const response = await this.instance.post(url, formData, config);
|
|
return response.data;
|
|
}
|
|
|
|
async downloadFile(url: string, filename: string): Promise<void> {
|
|
const response = await this.instance.get(url, {
|
|
responseType: 'blob',
|
|
});
|
|
|
|
const blob = new Blob([response.data]);
|
|
const downloadUrl = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = downloadUrl;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
window.URL.revokeObjectURL(downloadUrl);
|
|
}
|
|
}
|
|
|
|
export const apiClient = new ApiClient();
|
|
export default apiClient; |