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(netstat:*)",
"Bash(taskkill:*)", "Bash(taskkill:*)",
"Bash(ls:*)", "Bash(ls:*)",
"Bash(git add:*)" "Bash(git add:*)",
"Bash(git commit:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@ -7,5 +7,5 @@ NEXT_PUBLIC_PARTICIPATION_HOST=http://localhost:8084
NEXT_PUBLIC_DISTRIBUTION_HOST=http://localhost:8085 NEXT_PUBLIC_DISTRIBUTION_HOST=http://localhost:8085
NEXT_PUBLIC_ANALYTICS_HOST=http://localhost:8086 NEXT_PUBLIC_ANALYTICS_HOST=http://localhost:8086
# API Version # API Version prefix
NEXT_PUBLIC_API_VERSION=v1 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,18 +1,40 @@
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'; 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({ export const apiClient: AxiosInstance = axios.create({
baseURL: API_BASE_URL, baseURL: API_BASE_URL,
timeout: 90000, // 30초로 증가 timeout: 90000,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); });
// Request interceptor - JWT 토큰 추가 // Participation API 전용 클라이언트
apiClient.interceptors.request.use( export const participationClient: AxiosInstance = axios.create({
(config: InternalAxiosRequestConfig) => { baseURL: `${API_HOSTS.participation}/${API_VERSION}`,
timeout: 90000,
headers: {
'Content-Type': 'application/json',
},
});
// 공통 Request interceptor 함수
const requestInterceptor = (config: InternalAxiosRequestConfig) => {
console.log('🚀 API Request:', { console.log('🚀 API Request:', {
method: config.method?.toUpperCase(), method: config.method?.toUpperCase(),
url: config.url, url: config.url,
@ -26,24 +48,24 @@ apiClient.interceptors.request.use(
console.log('🔑 Token added to request'); console.log('🔑 Token added to request');
} }
return config; return config;
}, };
(error: AxiosError) => {
const requestErrorInterceptor = (error: AxiosError) => {
console.error('❌ Request Error:', error); console.error('❌ Request Error:', error);
return Promise.reject(error); return Promise.reject(error);
} };
);
// Response interceptor - 에러 처리 // 공통 Response interceptor 함수
apiClient.interceptors.response.use( const responseInterceptor = (response: any) => {
(response) => {
console.log('✅ API Response:', { console.log('✅ API Response:', {
status: response.status, status: response.status,
url: response.config.url, url: response.config.url,
data: response.data, data: response.data,
}); });
return response; return response;
}, };
(error: AxiosError) => {
const responseErrorInterceptor = (error: AxiosError) => {
console.error('❌ API Error:', { console.error('❌ API Error:', {
message: error.message, message: error.message,
status: error.response?.status, status: error.response?.status,
@ -54,14 +76,20 @@ apiClient.interceptors.response.use(
if (error.response?.status === 401) { if (error.response?.status === 401) {
console.warn('🔒 401 Unauthorized - Redirecting to login'); console.warn('🔒 401 Unauthorized - Redirecting to login');
// 인증 실패 시 토큰 삭제 및 로그인 페이지로 리다이렉트
localStorage.removeItem('accessToken'); localStorage.removeItem('accessToken');
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.location.href = '/login'; 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; export default apiClient;

View File

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