Participation API를 client.ts로 통합 및 환경변수 설정 개선

- api-client.ts 삭제하고 client.ts의 participationClient 사용
- 마이크로서비스별 호스트 환경변수 지원 추가
- API_VERSION 환경변수로 api prefix 관리
- .env.local 파일 생성 (개발 환경 설정)
- CORS 해결을 위해 백엔드에서 직접 호출하는 방식으로 단순화

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jhbkjh 2025-10-28 17:49:20 +09:00
parent bace9476b1
commit 37e5a76c50
5 changed files with 87 additions and 150 deletions

View File

@ -10,7 +10,8 @@
"Bash(netstat:*)",
"Bash(taskkill:*)",
"Bash(ls:*)",
"Bash(git add:*)"
"Bash(git add:*)",
"Bash(git commit:*)"
],
"deny": [],
"ask": []

View File

@ -7,5 +7,5 @@ NEXT_PUBLIC_PARTICIPATION_HOST=http://localhost:8084
NEXT_PUBLIC_DISTRIBUTION_HOST=http://localhost:8085
NEXT_PUBLIC_ANALYTICS_HOST=http://localhost:8086
# API Version
NEXT_PUBLIC_API_VERSION=v1
# API Version prefix
NEXT_PUBLIC_API_VERSION=api

View File

@ -1,92 +0,0 @@
import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
/**
* API URL
* 환경: Next.js CORS (/api/proxy -> localhost:8084)
* 프로덕션: 직접
*/
const API_BASE_URL = process.env.NODE_ENV === 'production'
? process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8084'
: '/api/proxy';
/**
* Axios
*/
const apiClient: AxiosInstance = axios.create({
baseURL: API_BASE_URL,
timeout: 30000, // 30초
headers: {
'Content-Type': 'application/json',
},
});
/**
*
* - (: 인증 )
*/
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// TODO: 필요 시 인증 토큰 추가
// const token = localStorage.getItem('accessToken');
// if (token && config.headers) {
// config.headers.Authorization = `Bearer ${token}`;
// }
console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`);
return config;
},
(error: AxiosError) => {
console.error('[API Request Error]', error);
return Promise.reject(error);
}
);
/**
*
* - (: 에러 )
*/
apiClient.interceptors.response.use(
(response: AxiosResponse) => {
console.log(`[API Response] ${response.config.method?.toUpperCase()} ${response.config.url}`, response.status);
return response;
},
(error: AxiosError) => {
// 에러 응답 처리
if (error.response) {
const { status, data } = error.response;
console.error(`[API Error] ${status}:`, data);
// 상태 코드별 처리
switch (status) {
case 400:
console.error('잘못된 요청입니다.');
break;
case 401:
console.error('인증이 필요합니다.');
// TODO: 로그인 페이지로 리다이렉트
break;
case 403:
console.error('접근 권한이 없습니다.');
break;
case 404:
console.error('요청한 리소스를 찾을 수 없습니다.');
break;
case 500:
console.error('서버 오류가 발생했습니다.');
break;
default:
console.error('알 수 없는 오류가 발생했습니다.');
}
} else if (error.request) {
// 요청은 전송되었으나 응답을 받지 못한 경우
console.error('[API Network Error]', error.message);
} else {
// 요청 설정 중 오류 발생
console.error('[API Setup Error]', error.message);
}
return Promise.reject(error);
}
);
export default apiClient;

View File

@ -1,67 +1,95 @@
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://20.196.65.160:8081';
// 마이크로서비스별 호스트 설정
const API_HOSTS = {
user: process.env.NEXT_PUBLIC_USER_HOST || 'http://localhost:8081',
event: process.env.NEXT_PUBLIC_EVENT_HOST || 'http://localhost:8080',
content: process.env.NEXT_PUBLIC_CONTENT_HOST || 'http://localhost:8082',
ai: process.env.NEXT_PUBLIC_AI_HOST || 'http://localhost:8083',
participation: process.env.NEXT_PUBLIC_PARTICIPATION_HOST || 'http://localhost:8084',
distribution: process.env.NEXT_PUBLIC_DISTRIBUTION_HOST || 'http://localhost:8085',
analytics: process.env.NEXT_PUBLIC_ANALYTICS_HOST || 'http://localhost:8086',
};
const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'api';
// 기본 User API 클라이언트 (기존 호환성 유지)
const API_BASE_URL = API_HOSTS.user;
export const apiClient: AxiosInstance = axios.create({
baseURL: API_BASE_URL,
timeout: 90000, // 30초로 증가
timeout: 90000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor - JWT 토큰 추가
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
console.log('🚀 API Request:', {
method: config.method?.toUpperCase(),
url: config.url,
baseURL: config.baseURL,
data: config.data,
});
const token = localStorage.getItem('accessToken');
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
console.log('🔑 Token added to request');
}
return config;
// Participation API 전용 클라이언트
export const participationClient: AxiosInstance = axios.create({
baseURL: `${API_HOSTS.participation}/${API_VERSION}`,
timeout: 90000,
headers: {
'Content-Type': 'application/json',
},
(error: AxiosError) => {
console.error('❌ Request Error:', error);
return Promise.reject(error);
});
// 공통 Request interceptor 함수
const requestInterceptor = (config: InternalAxiosRequestConfig) => {
console.log('🚀 API Request:', {
method: config.method?.toUpperCase(),
url: config.url,
baseURL: config.baseURL,
data: config.data,
});
const token = localStorage.getItem('accessToken');
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
console.log('🔑 Token added to request');
}
);
return config;
};
// Response interceptor - 에러 처리
apiClient.interceptors.response.use(
(response) => {
console.log('✅ API Response:', {
status: response.status,
url: response.config.url,
data: response.data,
});
return response;
},
(error: AxiosError) => {
console.error('❌ API Error:', {
message: error.message,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
data: error.response?.data,
});
const requestErrorInterceptor = (error: AxiosError) => {
console.error('❌ Request Error:', error);
return Promise.reject(error);
};
if (error.response?.status === 401) {
console.warn('🔒 401 Unauthorized - Redirecting to login');
// 인증 실패 시 토큰 삭제 및 로그인 페이지로 리다이렉트
localStorage.removeItem('accessToken');
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
// 공통 Response interceptor 함수
const responseInterceptor = (response: any) => {
console.log('✅ API Response:', {
status: response.status,
url: response.config.url,
data: response.data,
});
return response;
};
const responseErrorInterceptor = (error: AxiosError) => {
console.error('❌ API Error:', {
message: error.message,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
data: error.response?.data,
});
if (error.response?.status === 401) {
console.warn('🔒 401 Unauthorized - Redirecting to login');
localStorage.removeItem('accessToken');
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
return Promise.reject(error);
}
);
return Promise.reject(error);
};
// User API Client 인터셉터 적용
apiClient.interceptors.request.use(requestInterceptor, requestErrorInterceptor);
apiClient.interceptors.response.use(responseInterceptor, responseErrorInterceptor);
// Participation API Client 인터셉터 적용
participationClient.interceptors.request.use(requestInterceptor, requestErrorInterceptor);
participationClient.interceptors.response.use(responseInterceptor, responseErrorInterceptor);
export default apiClient;

View File

@ -1,4 +1,4 @@
import apiClient from './api-client';
import { participationClient } from './client';
import type {
ApiResponse,
PageResponse,
@ -20,7 +20,7 @@ export const participate = async (
eventId: string,
data: ParticipationRequest
): Promise<ApiResponse<ParticipationResponse>> => {
const response = await apiClient.post<ApiResponse<ParticipationResponse>>(
const response = await participationClient.post<ApiResponse<ParticipationResponse>>(
`/v1/events/${eventId}/participate`,
data
);
@ -36,7 +36,7 @@ export const getParticipants = async (
): Promise<ApiResponse<PageResponse<ParticipationResponse>>> => {
const { eventId, storeVisited, page = 0, size = 20, sort = ['createdAt,DESC'] } = params;
const response = await apiClient.get<ApiResponse<PageResponse<ParticipationResponse>>>(
const response = await participationClient.get<ApiResponse<PageResponse<ParticipationResponse>>>(
`/v1/events/${eventId}/participants`,
{
params: {
@ -58,7 +58,7 @@ export const getParticipant = async (
eventId: string,
participantId: string
): Promise<ApiResponse<ParticipationResponse>> => {
const response = await apiClient.get<ApiResponse<ParticipationResponse>>(
const response = await participationClient.get<ApiResponse<ParticipationResponse>>(
`/v1/events/${eventId}/participants/${participantId}`
);
return response.data;
@ -119,7 +119,7 @@ export const drawWinners = async (
winnerCount: number,
applyStoreVisitBonus?: boolean
): Promise<ApiResponse<import('../types/api.types').DrawWinnersResponse>> => {
const response = await apiClient.post<ApiResponse<import('../types/api.types').DrawWinnersResponse>>(
const response = await participationClient.post<ApiResponse<import('../types/api.types').DrawWinnersResponse>>(
`/v1/events/${eventId}/draw-winners`,
{
winnerCount,
@ -139,7 +139,7 @@ export const getWinners = async (
size = 20,
sort: string[] = ['winnerRank,ASC']
): Promise<ApiResponse<PageResponse<ParticipationResponse>>> => {
const response = await apiClient.get<ApiResponse<PageResponse<ParticipationResponse>>>(
const response = await participationClient.get<ApiResponse<PageResponse<ParticipationResponse>>>(
`/v1/events/${eventId}/winners`,
{
params: {