pricelees 5658f6c31f [#34] 회원 / 인증 도메인 재정의 (#43)
<!-- 제목 양식 -->
<!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) -->

## 📝 관련 이슈 및 PR

**PR과 관련된 이슈 번호**
- #34

##  작업 내용
<!-- 어떤 작업을 했는지 알려주세요! -->
- 회원 테이블과 관리자 테이블 분리 및 관리자 계정의 예약 기능 제거
- API 인증을 모두(Public) / 회원 전용(UserOnly) / 관리자 전용(AdminOnly) / 회원 + 관리자(Authenticated) 로 세분화해서 구분
- 관리자의 경우 API 접근 권한 세분화 등 인증 로직 개선
- 전체 리팩터링이 완료되어 레거시 코드 제거

## 🧪 테스트
<!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! -->
<img width="750" alt="스크린샷 2025-09-13 19.11.44.png" src="attachments/11e1a79c-9723-4843-839d-be6158d94130">

- 추가 & 변경된 모든 API에 대한 통합 테스트 진행

## 📚 참고 자료 및 기타
<!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! -->

Reviewed-on: #43
Co-authored-by: pricelees <priceelees@gmail.com>
Co-committed-by: pricelees <priceelees@gmail.com>
2025-09-13 10:13:45 +00:00

102 lines
3.1 KiB
TypeScript

import axios, {type AxiosError, type AxiosRequestConfig, type Method} from 'axios';
import JSONbig from 'json-bigint';
// Create a JSONbig instance that stores big integers as strings
const JSONbigString = JSONbig({ storeAsString: true });
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000,
// transformResponse is used to parse JSON with big integers (Long type from backend) as strings.
// This prevents precision loss in JavaScript.
transformResponse: [(data) => {
// Do not transform if data is not a string or is empty
if (!data || typeof data !== 'string') {
return data;
}
try {
// Use the configured JSONbig instance to parse the data
return JSONbigString.parse(data);
} catch (e) {
// If parsing fails, it might not be JSON, so return original data
// This is the default behavior of axios if parsing fails
return data;
}
}],
});
export const isLoginRequiredError = (error: any): boolean => {
if (!axios.isAxiosError(error) || !error.response) {
return false;
}
const LOGIN_REQUIRED_ERROR_CODE = ['A001', 'A002', 'A003', 'A006'];
const code = error.response?.data?.code;
return code && LOGIN_REQUIRED_ERROR_CODE.includes(code);
}
async function request<T>(
method: Method,
endpoint: string,
data: object = {},
isRequiredAuth: boolean = false
): Promise<T> {
const config: AxiosRequestConfig = {
method,
url: endpoint,
headers: {
'Content-Type': 'application/json'
},
};
const accessToken = localStorage.getItem('accessToken');
if (accessToken) {
if (!config.headers) {
config.headers = {};
}
config.headers['Authorization'] = `Bearer ${accessToken}`;
}
if (method.toUpperCase() !== 'GET') {
config.data = data;
}
try {
const response = await apiClient.request(config);
return response.data.data;
} catch (error: unknown) {
const axiosError = error as AxiosError<{ code: string, message: string }>;
console.error('API 요청 실패:', axiosError);
throw axiosError;
}
}
async function get<T>(endpoint: string, isRequiredAuth: boolean = false): Promise<T> {
return request<T>('GET', endpoint, {}, isRequiredAuth);
}
async function post<T>(endpoint: string, data: object = {}, isRequiredAuth: boolean = false): Promise<T> {
return request<T>('POST', endpoint, data, isRequiredAuth);
}
async function put<T>(endpoint: string, data: object = {}, isRequiredAuth: boolean = false): Promise<T> {
return request<T>('PUT', endpoint, data, isRequiredAuth);
}
async function patch<T>(endpoint: string, data: object = {}, isRequiredAuth: boolean = false): Promise<T> {
return request<T>('PATCH', endpoint, data, isRequiredAuth);
}
async function del<T>(endpoint: string, isRequiredAuth: boolean = false): Promise<T> {
return request<T>('DELETE', endpoint, {}, isRequiredAuth);
}
export default {
get,
post,
put,
patch,
del
};