From 37e5a76c508561eaed25759f405146255bc1c224 Mon Sep 17 00:00:00 2001 From: jhbkjh Date: Tue, 28 Oct 2025 17:49:20 +0900 Subject: [PATCH] =?UTF-8?q?Participation=20API=EB=A5=BC=20client.ts?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=ED=95=A9=20=EB=B0=8F=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=84=A4=EC=A0=95=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .claude/settings.local.json | 3 +- .env.example | 4 +- src/shared/api/api-client.ts | 92 -------------------- src/shared/api/client.ts | 126 +++++++++++++++++----------- src/shared/api/participation.api.ts | 12 +-- 5 files changed, 87 insertions(+), 150 deletions(-) delete mode 100644 src/shared/api/api-client.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d676d7f..453fabd 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,8 @@ "Bash(netstat:*)", "Bash(taskkill:*)", "Bash(ls:*)", - "Bash(git add:*)" + "Bash(git add:*)", + "Bash(git commit:*)" ], "deny": [], "ask": [] diff --git a/.env.example b/.env.example index 9455646..1cf6290 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/src/shared/api/api-client.ts b/src/shared/api/api-client.ts deleted file mode 100644 index 270a5dc..0000000 --- a/src/shared/api/api-client.ts +++ /dev/null @@ -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; diff --git a/src/shared/api/client.ts b/src/shared/api/client.ts index 16aefe9..5709573 100644 --- a/src/shared/api/client.ts +++ b/src/shared/api/client.ts @@ -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; diff --git a/src/shared/api/participation.api.ts b/src/shared/api/participation.api.ts index b52082d..3eeb37b 100644 --- a/src/shared/api/participation.api.ts +++ b/src/shared/api/participation.api.ts @@ -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> => { - const response = await apiClient.post>( + const response = await participationClient.post>( `/v1/events/${eventId}/participate`, data ); @@ -36,7 +36,7 @@ export const getParticipants = async ( ): Promise>> => { const { eventId, storeVisited, page = 0, size = 20, sort = ['createdAt,DESC'] } = params; - const response = await apiClient.get>>( + const response = await participationClient.get>>( `/v1/events/${eventId}/participants`, { params: { @@ -58,7 +58,7 @@ export const getParticipant = async ( eventId: string, participantId: string ): Promise> => { - const response = await apiClient.get>( + const response = await participationClient.get>( `/v1/events/${eventId}/participants/${participantId}` ); return response.data; @@ -119,7 +119,7 @@ export const drawWinners = async ( winnerCount: number, applyStoreVisitBonus?: boolean ): Promise> => { - const response = await apiClient.post>( + const response = await participationClient.post>( `/v1/events/${eventId}/draw-winners`, { winnerCount, @@ -139,7 +139,7 @@ export const getWinners = async ( size = 20, sort: string[] = ['winnerRank,ASC'] ): Promise>> => { - const response = await apiClient.get>>( + const response = await participationClient.get>>( `/v1/events/${eventId}/winners`, { params: {