mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2025-12-06 23:26:25 +00:00
주요 변경사항: - Step flow 통합: localStorage 기반 eventId 사용 - 자동 이미지 생성: 이미지 없을 시 자동 생성 트리거 - 진행률 바 추가: 0-100% 진행률 표시 - 동적 로딩 메시지: 단계별 메시지 업데이트 - Next.js 15 API routes 수정: params를 Promise로 처리 - 실제 배포 API 연동: Content API 서버 URL 설정 기술 세부사항: - API proxy routes 추가 (CORS 우회) - 2초 폴링 메커니즘 (최대 60초) - 환경변수: NEXT_PUBLIC_CONTENT_API_URL 설정 - CDN URL 디버그 오버레이 제거 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
222 lines
6.0 KiB
Markdown
222 lines
6.0 KiB
Markdown
# CORS 문제 해결 방법
|
|
|
|
## 문제 상황
|
|
|
|
프론트엔드(`http://localhost:3000`)에서 백엔드 Content API(`http://localhost:8084`)를 직접 호출하면 **CORS(Cross-Origin Resource Sharing)** 에러가 발생했습니다.
|
|
|
|
### 에러 메시지
|
|
```
|
|
Network Error
|
|
AxiosError: Network Error
|
|
code: "ERR_NETWORK"
|
|
```
|
|
|
|
### 원인 분석
|
|
```bash
|
|
# CORS preflight 요청 테스트
|
|
curl -X OPTIONS http://localhost:8084/api/v1/content/images/generate \
|
|
-H 'Origin: http://localhost:3000' \
|
|
-H 'Access-Control-Request-Method: POST' \
|
|
-H 'Access-Control-Request-Headers: content-type'
|
|
|
|
# 결과: HTTP/1.1 403 Forbidden
|
|
# Invalid CORS request
|
|
```
|
|
|
|
백엔드 서버가 `http://localhost:3000` origin에서의 CORS 요청을 허용하지 않음.
|
|
|
|
---
|
|
|
|
## 해결 방법: Next.js API Proxy
|
|
|
|
백엔드 CORS 설정을 수정하는 대신, **Next.js API Routes를 프록시로 사용**하여 CORS 문제를 우회했습니다.
|
|
|
|
### 아키텍처
|
|
|
|
```
|
|
[Browser]
|
|
↓ (Same-Origin Request)
|
|
[Next.js Frontend: localhost:3000]
|
|
↓ [Next.js API Proxy: /api/content/*]
|
|
↓ (Server-to-Server Request, CORS 무관)
|
|
[Content API Backend: localhost:8084]
|
|
```
|
|
|
|
### 구현 파일
|
|
|
|
#### 1. **이미지 생성 프록시** (`/api/content/images/generate/route.ts`)
|
|
|
|
```typescript
|
|
export async function POST(request: NextRequest) {
|
|
const body = await request.json();
|
|
|
|
const response = await fetch('http://localhost:8084/api/v1/content/images/generate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
return NextResponse.json(await response.json());
|
|
}
|
|
```
|
|
|
|
**URL 매핑**:
|
|
- Frontend: `POST /api/content/images/generate`
|
|
- Backend: `POST http://localhost:8084/api/v1/content/images/generate`
|
|
|
|
#### 2. **Job 상태 조회 프록시** (`/api/content/images/jobs/[jobId]/route.ts`)
|
|
|
|
```typescript
|
|
export async function GET(request: NextRequest, { params }: { params: { jobId: string } }) {
|
|
const { jobId } = params;
|
|
|
|
const response = await fetch(`http://localhost:8084/api/v1/content/images/jobs/${jobId}`, {
|
|
method: 'GET',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
|
|
return NextResponse.json(await response.json());
|
|
}
|
|
```
|
|
|
|
**URL 매핑**:
|
|
- Frontend: `GET /api/content/images/jobs/{jobId}`
|
|
- Backend: `GET http://localhost:8084/api/v1/content/images/jobs/{jobId}`
|
|
|
|
#### 3. **이미지 목록 조회 프록시** (`/api/content/events/[eventDraftId]/images/route.ts`)
|
|
|
|
```typescript
|
|
export async function GET(request: NextRequest, { params }: { params: { eventDraftId: string } }) {
|
|
const { eventDraftId } = params;
|
|
const { searchParams } = new URL(request.url);
|
|
|
|
let url = `http://localhost:8084/api/v1/content/events/${eventDraftId}/images`;
|
|
if (searchParams.get('style')) url += `?style=${searchParams.get('style')}`;
|
|
if (searchParams.get('platform')) url += `&platform=${searchParams.get('platform')}`;
|
|
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
|
|
return NextResponse.json(await response.json());
|
|
}
|
|
```
|
|
|
|
**URL 매핑**:
|
|
- Frontend: `GET /api/content/events/{eventDraftId}/images?style=SIMPLE&platform=INSTAGRAM`
|
|
- Backend: `GET http://localhost:8084/api/v1/content/events/{eventDraftId}/images?style=SIMPLE&platform=INSTAGRAM`
|
|
|
|
---
|
|
|
|
## 클라이언트 코드 변경
|
|
|
|
### Before (직접 백엔드 호출 - CORS 에러 발생)
|
|
|
|
```typescript
|
|
const CONTENT_API_BASE_URL = 'http://localhost:8084';
|
|
|
|
export const contentApiClient = axios.create({
|
|
baseURL: CONTENT_API_BASE_URL,
|
|
});
|
|
|
|
// ❌ CORS Error
|
|
await contentApiClient.post('/api/v1/content/images/generate', request);
|
|
```
|
|
|
|
### After (Next.js API Proxy 사용 - CORS 우회)
|
|
|
|
```typescript
|
|
const CONTENT_API_BASE_URL = '/api/content'; // Same-origin request
|
|
|
|
export const contentApiClient = axios.create({
|
|
baseURL: CONTENT_API_BASE_URL,
|
|
});
|
|
|
|
// ✅ Works! (Same-origin → Server-side proxy → Backend)
|
|
await contentApiClient.post('/images/generate', request);
|
|
```
|
|
|
|
---
|
|
|
|
## 장점
|
|
|
|
✅ **프론트엔드 수정만으로 해결**: 백엔드 CORS 설정 변경 불필요
|
|
✅ **Same-Origin 정책 준수**: 브라우저는 같은 도메인으로 인식
|
|
✅ **서버 간 통신**: Next.js 서버에서 백엔드 호출 (CORS 무관)
|
|
✅ **보안 강화**: 백엔드 URL을 클라이언트에 노출하지 않음
|
|
✅ **환경 변수 활용**: `NEXT_PUBLIC_CONTENT_API_URL`로 배포 환경 대응
|
|
|
|
---
|
|
|
|
## 프로덕션 배포 시 고려사항
|
|
|
|
### 환경 변수 설정
|
|
|
|
```bash
|
|
# .env.local (개발 환경)
|
|
NEXT_PUBLIC_CONTENT_API_URL=http://localhost:8084
|
|
|
|
# .env.production (프로덕션 환경)
|
|
NEXT_PUBLIC_CONTENT_API_URL=https://api.production.com
|
|
```
|
|
|
|
### 프록시 코드에 적용
|
|
|
|
```typescript
|
|
const CONTENT_API_BASE_URL = process.env.NEXT_PUBLIC_CONTENT_API_URL || 'http://localhost:8084';
|
|
|
|
const response = await fetch(`${CONTENT_API_BASE_URL}/api/v1/content/images/generate`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
```
|
|
|
|
### 타임아웃 설정
|
|
|
|
```typescript
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
signal: AbortSignal.timeout(120000), // 120초 타임아웃
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 테스트 방법
|
|
|
|
### 1. 개발 서버 실행
|
|
|
|
```bash
|
|
npm run dev
|
|
```
|
|
|
|
### 2. 브라우저에서 테스트
|
|
|
|
```
|
|
http://localhost:3000/events/create?event-creation.step=contentPreview
|
|
```
|
|
|
|
### 3. 네트워크 탭 확인
|
|
|
|
브라우저 개발자 도구 → Network 탭에서 다음 요청 확인:
|
|
|
|
```
|
|
POST http://localhost:3000/api/content/images/generate (Status: 202)
|
|
GET http://localhost:3000/api/content/images/jobs/job-xxxxx (Status: 200)
|
|
GET http://localhost:3000/api/content/events/7777/images (Status: 200)
|
|
```
|
|
|
|
모두 **Same-Origin** 요청이므로 CORS 에러 없음!
|
|
|
|
---
|
|
|
|
## 참고 자료
|
|
|
|
- [Next.js API Routes](https://nextjs.org/docs/app/building-your-application/routing/route-handlers)
|
|
- [CORS (MDN)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
|
- [Proxy Pattern](https://en.wikipedia.org/wiki/Proxy_pattern)
|