2026-06-12 23:40:28 +09:00
|
|
|
// ============================================================================
|
|
|
|
|
// [공통] HTTP 통신 기반 모듈 — axios 인스턴스 + 응답 래퍼
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
|
// 이 파일은 모든 API 모듈(commission.ts, settle.ts 등)이 공유하는 공통 토대다.
|
|
|
|
|
// - 백엔드는 항상 { success, code, message, data } 형태의 JSON(ApiResponse)으로 응답한다.
|
|
|
|
|
// - 신입 가이드: 다른 *.ts 의 함수들은 여기서 만든 api 인스턴스와 unwrap() 을 import 해 사용한다.
|
|
|
|
|
// ============================================================================
|
2026-05-09 22:00:53 +09:00
|
|
|
import axios, { AxiosError } from 'axios';
|
|
|
|
|
import { message } from 'antd';
|
|
|
|
|
|
2026-06-12 23:40:28 +09:00
|
|
|
// 백엔드 표준 응답 봉투(envelope). 실제 데이터는 data 필드 안에 들어 있다.
|
|
|
|
|
// - success: 성공 여부(false 면 비즈니스 오류)
|
|
|
|
|
// - code : 응답/오류 코드 문자열
|
|
|
|
|
// - message: 사용자에게 보여줄 메시지
|
|
|
|
|
// - data : 실제 페이로드(제네릭 T)
|
|
|
|
|
// - timestamp: 서버 응답 시각
|
2026-05-09 22:00:53 +09:00
|
|
|
export interface ApiResponse<T> {
|
|
|
|
|
success: boolean;
|
|
|
|
|
code: string;
|
|
|
|
|
message: string;
|
|
|
|
|
data: T;
|
|
|
|
|
timestamp: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 23:40:28 +09:00
|
|
|
// 목록 조회용 페이지네이션 응답. list(현재 페이지 데이터) + 페이지 메타.
|
|
|
|
|
// - pageNum: 현재 페이지 번호, pageSize: 페이지당 건수
|
|
|
|
|
// - total : 전체 건수, pages: 전체 페이지 수
|
2026-05-09 22:00:53 +09:00
|
|
|
export interface PageResponse<T> {
|
|
|
|
|
list: T[];
|
|
|
|
|
pageNum: number;
|
|
|
|
|
pageSize: number;
|
|
|
|
|
total: number;
|
|
|
|
|
pages: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 23:40:28 +09:00
|
|
|
// axios 인스턴스 생성. baseURL 은 환경변수(VITE_API_URL), 타임아웃 60초.
|
2026-05-09 22:00:53 +09:00
|
|
|
const api = axios.create({
|
|
|
|
|
baseURL: import.meta.env.VITE_API_URL ?? '',
|
|
|
|
|
timeout: 60_000,
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-12 23:40:28 +09:00
|
|
|
// [요청 인터셉터] 모든 요청에 로그인 토큰(JWT)을 Authorization 헤더로 자동 첨부.
|
2026-05-09 22:00:53 +09:00
|
|
|
api.interceptors.request.use((config) => {
|
|
|
|
|
const token = localStorage.getItem('accessToken');
|
|
|
|
|
if (token) config.headers.Authorization = `Bearer ${token}`;
|
|
|
|
|
return config;
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-12 23:40:28 +09:00
|
|
|
// [응답 인터셉터] 성공 응답이라도 success=false 면 비즈니스 오류로 간주해 토스트 표시.
|
2026-05-09 22:00:53 +09:00
|
|
|
api.interceptors.response.use(
|
|
|
|
|
(res) => {
|
|
|
|
|
const body = res.data;
|
|
|
|
|
if (body && typeof body === 'object' && 'success' in body && body.success === false) {
|
|
|
|
|
message.error(body.message ?? '오류가 발생했습니다');
|
|
|
|
|
return Promise.reject(body);
|
|
|
|
|
}
|
|
|
|
|
return res;
|
|
|
|
|
},
|
2026-06-12 23:40:28 +09:00
|
|
|
// [HTTP 오류 처리] 401: 토큰 만료/미인증 → 로그인 화면으로. 403: 권한 없음. 그 외: 서버 오류.
|
2026-05-09 22:00:53 +09:00
|
|
|
(err: AxiosError<ApiResponse<unknown>>) => {
|
|
|
|
|
const status = err.response?.status;
|
|
|
|
|
const body = err.response?.data;
|
|
|
|
|
if (status === 401) {
|
|
|
|
|
localStorage.removeItem('accessToken');
|
|
|
|
|
window.location.href = '/login';
|
|
|
|
|
} else if (status === 403) {
|
|
|
|
|
message.error(body?.message ?? '권한이 없습니다');
|
|
|
|
|
} else {
|
|
|
|
|
message.error(body?.message ?? err.message ?? '서버 오류');
|
|
|
|
|
}
|
|
|
|
|
return Promise.reject(err);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export default api;
|
|
|
|
|
|
|
|
|
|
/** ApiResponse<T> 의 data 만 풀어서 반환 */
|
|
|
|
|
export async function unwrap<T>(p: Promise<{ data: ApiResponse<T> }>): Promise<T> {
|
|
|
|
|
const res = await p;
|
|
|
|
|
return res.data.data;
|
|
|
|
|
}
|