diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..453fabd
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,19 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(curl:*)",
+ "Bash(cat:*)",
+ "Bash(mkdir:*)",
+ "Bash(npm install:*)",
+ "Bash(npm run build:*)",
+ "Bash(npm run dev:*)",
+ "Bash(netstat:*)",
+ "Bash(taskkill:*)",
+ "Bash(ls:*)",
+ "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/API_CHANGES.md b/API_CHANGES.md
new file mode 100644
index 0000000..f54789e
--- /dev/null
+++ b/API_CHANGES.md
@@ -0,0 +1,263 @@
+# Content API ๋ณ๊ฒฝ์ฌํญ
+
+## ๐ ์ฃผ์ ๋ณ๊ฒฝ์ฌํญ ์์ฝ
+
+### 1. **eventDraftId โ eventId ํ์
๋ณ๊ฒฝ**
+
+| ํญ๋ชฉ | ๊ธฐ์กด (Old) | ๋ณ๊ฒฝ (New) |
+|------|-----------|-----------|
+| ํ๋๋ช
| `eventDraftId` | `eventId` |
+| ํ์
| `number` | `string` |
+| ์์ | `7777` | `"7777"` |
+
+---
+
+## ๐ ์ํฅ์ ๋ฐ๋ ์ธํฐํ์ด์ค
+
+### GenerateImagesRequest
+
+**Before:**
+```typescript
+interface GenerateImagesRequest {
+ eventDraftId: number;
+ eventTitle: string;
+ eventDescription: string;
+ industry?: string;
+ location?: string;
+ trends?: string[];
+ styles: ('SIMPLE' | 'FANCY' | 'TRENDY')[];
+ platforms: ('INSTAGRAM' | 'NAVER' | 'KAKAO')[];
+}
+```
+
+**After:**
+```typescript
+interface GenerateImagesRequest {
+ eventId: string; // Changed from eventDraftId: number
+ eventTitle: string;
+ eventDescription: string;
+ industry?: string;
+ location?: string;
+ trends?: string[];
+ styles: ('SIMPLE' | 'FANCY' | 'TRENDY')[];
+ platforms: ('INSTAGRAM' | 'NAVER' | 'KAKAO')[];
+}
+```
+
+### JobInfo
+
+**Before:**
+```typescript
+interface JobInfo {
+ id: string;
+ eventDraftId: number;
+ jobType: string;
+ status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
+ progress: number;
+ resultMessage?: string;
+ errorMessage?: string;
+ createdAt: string;
+ updatedAt: string;
+}
+```
+
+**After:**
+```typescript
+interface JobInfo {
+ id: string;
+ eventId: string; // Changed from eventDraftId: number
+ jobType: string;
+ status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
+ progress: number;
+ resultMessage?: string;
+ errorMessage?: string;
+ createdAt: string;
+ updatedAt: string;
+}
+```
+
+### ImageInfo
+
+**Before:**
+```typescript
+interface ImageInfo {
+ id: number;
+ eventDraftId: number;
+ style: 'SIMPLE' | 'FANCY' | 'TRENDY';
+ platform: 'INSTAGRAM' | 'NAVER' | 'KAKAO';
+ cdnUrl: string;
+ prompt: string;
+ selected: boolean;
+ createdAt: string;
+ updatedAt: string;
+}
+```
+
+**After:**
+```typescript
+interface ImageInfo {
+ id: number;
+ eventId: string; // Changed from eventDraftId: number
+ style: 'SIMPLE' | 'FANCY' | 'TRENDY';
+ platform: 'INSTAGRAM' | 'NAVER' | 'KAKAO';
+ cdnUrl: string;
+ prompt: string;
+ selected: boolean;
+ createdAt: string;
+ updatedAt: string;
+}
+```
+
+### ContentInfo
+
+**Before:**
+```typescript
+interface ContentInfo {
+ id: number;
+ eventDraftId: number;
+ eventTitle: string;
+ eventDescription: string;
+ images: ImageInfo[];
+ createdAt: string;
+ updatedAt: string;
+}
+```
+
+**After:**
+```typescript
+interface ContentInfo {
+ id: number;
+ eventId: string; // Changed from eventDraftId: number
+ eventTitle: string;
+ eventDescription: string;
+ images: ImageInfo[];
+ createdAt: string;
+ updatedAt: string;
+}
+```
+
+---
+
+## ๐ ์์ ๋ ํ์ผ ๋ชฉ๋ก
+
+### 1. Type Definitions
+- โ
`src/shared/api/contentApi.ts`
+ - `GenerateImagesRequest` interface updated
+ - `JobInfo` interface updated
+ - `ImageInfo` interface updated
+ - `ContentInfo` interface updated
+ - API function signatures updated
+
+### 2. Components
+- โ
`src/app/(main)/events/create/steps/ContentPreviewStep.tsx`
+ - `EventCreationData` interface: `eventDraftId: number` โ `eventDraftId: string`
+ - Mock data updated to use string type
+ - API call updated: `eventDraftId` โ `eventId`
+
+### 3. Mock Data Files
+- โ
`public/init-mock-data.html`
+ - `eventDraftId: 7777` โ `eventDraftId: "7777"`
+
+- โ
`MOCK_DATA_SETUP.md`
+ - All mock data examples updated to string type
+ - Documentation notes added about type change
+
+### 4. API Routes (Next.js Proxy)
+- โ
`src/app/api/content/images/generate/route.ts` (no changes needed)
+- โ
`src/app/api/content/images/jobs/[jobId]/route.ts` (no changes needed)
+- โ
`src/app/api/content/events/[eventDraftId]/images/route.ts`
+ - Comment added about eventId parameter
+
+---
+
+## ๐งช ํ
์คํธ ์์
+
+### API ์์ฒญ ์์
+
+**Before:**
+```json
+POST /api/v1/content/images/generate
+{
+ "eventDraftId": 7777,
+ "eventTitle": "๋งฅ์ฃผ ํํฐ ์ด๋ฒคํธ",
+ "eventDescription": "๊ฐ๋จ์์ ์ด๋ฆฌ๋ ์ ๋๋ ๋งฅ์ฃผ ํํฐ",
+ "industry": "์์์ ",
+ "location": "๊ฐ๋จ",
+ "trends": ["ํํฐ", "๋งฅ์ฃผ", "์๋งฅ์ฃผ"],
+ "styles": ["SIMPLE", "FANCY", "TRENDY"],
+ "platforms": ["INSTAGRAM"]
+}
+```
+
+**After:**
+```json
+POST /api/v1/content/images/generate
+{
+ "eventId": "7777",
+ "eventTitle": "๋งฅ์ฃผ ํํฐ ์ด๋ฒคํธ",
+ "eventDescription": "๊ฐ๋จ์์ ์ด๋ฆฌ๋ ์ ๋๋ ๋งฅ์ฃผ ํํฐ",
+ "industry": "์์์ ",
+ "location": "๊ฐ๋จ",
+ "trends": ["ํํฐ", "๋งฅ์ฃผ", "์๋งฅ์ฃผ"],
+ "styles": ["SIMPLE", "FANCY", "TRENDY"],
+ "platforms": ["INSTAGRAM"]
+}
+```
+
+### localStorage Mock ๋ฐ์ดํฐ
+
+**Before:**
+```javascript
+localStorage.setItem('eventCreationData', JSON.stringify({
+ eventDraftId: 7777,
+ eventTitle: "๋งฅ์ฃผ ํํฐ ์ด๋ฒคํธ",
+ // ...
+}));
+```
+
+**After:**
+```javascript
+localStorage.setItem('eventCreationData', JSON.stringify({
+ eventDraftId: "7777", // String type now
+ eventTitle: "๋งฅ์ฃผ ํํฐ ์ด๋ฒคํธ",
+ // ...
+}));
+```
+
+---
+
+## โ
๋ง์ด๊ทธ๋ ์ด์
์ฒดํฌ๋ฆฌ์คํธ
+
+- [x] TypeScript ์ธํฐํ์ด์ค ์
๋ฐ์ดํธ
+- [x] API ํธ์ถ ์ฝ๋ ์์
+- [x] Mock ๋ฐ์ดํฐ ํ์
๋ณ๊ฒฝ
+- [x] ๋ฌธ์ ์
๋ฐ์ดํธ
+- [x] ๋น๋ ์ฑ๊ณต ํ์ธ
+- [ ] ๊ฐ๋ฐ ์๋ฒ ํ
์คํธ
+- [ ] ์ค์ API ์ฐ๋ ํ
์คํธ
+
+---
+
+## ๐ ๊ด๋ จ API ๋ฌธ์
+
+- Swagger UI: http://localhost:8084/swagger-ui/index.html
+- OpenAPI Spec: http://localhost:8084/v3/api-docs
+
+---
+
+## ๐ ์ฃผ์์ฌํญ
+
+1. **ํ์
์ฃผ์**: `eventId`๋ ์ด์ `string` ํ์
์
๋๋ค. ์ซ์๋ก ์ฌ์ฉํ์ง ๋ง์ธ์.
+2. **Mock ๋ฐ์ดํฐ**: localStorage์ ์ ์ฅํ ๋ ๋ฌธ์์ด ํ์
์ผ๋ก ์ ์ฅํด์ผ ํฉ๋๋ค.
+3. **API ํธ์ถ**: ํ๋ก ํธ์๋์์ ๋ฐฑ์๋๋ก ์ ์ก ์ string์ผ๋ก ์ ์ก๋ฉ๋๋ค.
+4. **ํ์ ํธํ์ฑ**: ๊ธฐ์กด number ํ์
๋ฐ์ดํฐ๋ ์๋ํ์ง ์์ผ๋ฏ๋ก localStorage๋ฅผ ์ด๊ธฐํํด์ผ ํฉ๋๋ค.
+
+---
+
+## ๐ ๋กค๋ฐฑ ๋ฐฉ๋ฒ
+
+๋ง์ฝ ์ด์ ๋ฒ์ ์ผ๋ก ๋์๊ฐ์ผ ํ๋ค๋ฉด:
+
+1. `git revert` ๋๋ ํน์ ์ปค๋ฐ์ผ๋ก ๋ณต์
+2. localStorage ์ด๊ธฐํ: `localStorage.removeItem('eventCreationData')`
+3. ๊ฐ๋ฐ ์๋ฒ ์ฌ์์
diff --git a/CORS_FIX.md b/CORS_FIX.md
new file mode 100644
index 0000000..de2857f
--- /dev/null
+++ b/CORS_FIX.md
@@ -0,0 +1,221 @@
+# 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)
diff --git a/FIX_EVENTID_MISMATCH.md b/FIX_EVENTID_MISMATCH.md
new file mode 100644
index 0000000..f10105c
--- /dev/null
+++ b/FIX_EVENTID_MISMATCH.md
@@ -0,0 +1,182 @@
+# EventId ๋ถ์ผ์น ๋ฌธ์ ํด๊ฒฐ
+
+## ๋ฌธ์ ์ํฉ
+
+์ฌ์ฉ์๊ฐ ์ด๋ฏธ์ง ์์ฑ ํ์ด์ง์์ ์คํ์ผ 1 ์นด๋์ ์ด๋ฏธ์ง๊ฐ ํ์๋์ง ์๊ณ ํ๋ ์ด์คํ๋๋ง ๋ณด์ด๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
+
+### ์คํฌ๋ฆฐ์ท ๋ถ์
+- **์คํ์ผ 1 (SIMPLE)**: ํ๋ ์ด์คํ๋๋ง ํ์ (์์ด์ฝ + ์ ๋ชฉ + ๊ฒฝํ)
+- **์คํ์ผ 2 (FANCY)**: ์ค์ ์ด๋ฏธ์ง ํ์ โ
+- **์คํ์ผ 3 (TRENDY)**: ์ค์ ์ด๋ฏธ์ง ํ์ โ
+
+## ๊ทผ๋ณธ ์์ธ
+
+API ๋ถ์ ๊ฒฐ๊ณผ, ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋ ์ด๋ฏธ์ง์ Mock ๋ฐ์ดํฐ์ eventId๊ฐ ์ผ์นํ์ง ์์์ต๋๋ค:
+
+```bash
+# Mock ๋ฐ์ดํฐ eventId
+"7777"
+
+# ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ eventId
+curl http://localhost:8084/api/v1/content/events/7777/images
+โ Response: [] (๋น ๋ฐฐ์ด)
+
+# ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์กด์ฌํ๋ eventId
+- "Tst12131": SIMPLE ์ด๋ฏธ์ง 1๊ฐ
+- "1761634317010": SIMPLE, FANCY, TRENDY ๊ฐ 2๊ฐ์ฉ ์ด 6๊ฐ
+- null: SIMPLE ์ด๋ฏธ์ง 1๊ฐ
+```
+
+**๊ฒฐ๋ก **: Mock ๋ฐ์ดํฐ์ eventId "7777"๋ก๋ ์ด๋ค ์ด๋ฏธ์ง๋ ์กฐํ๋์ง ์์์ต๋๋ค.
+
+## ํด๊ฒฐ ๋ฐฉ๋ฒ
+
+๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ด๋ฏธ ์กด์ฌํ๋ ์ด๋ฏธ์ง๊ฐ ์๋ eventId๋ก Mock ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ์ต๋๋ค.
+
+### ๋ณ๊ฒฝ๋ eventId
+```javascript
+// Before
+eventDraftId: "7777"
+
+// After
+eventDraftId: "1761634317010"
+```
+
+**์ ํ ์ด์ **:
+- SIMPLE, FANCY, TRENDY 3๊ฐ์ง ์คํ์ผ ๋ชจ๋ ์ด๋ฏธ์ง ๋ณด์
+- ๊ฐ ์คํ์ผ๋ณ๋ก 2๊ฐ์ฉ ์ด 6๊ฐ์ ์ด๋ฏธ์ง ์กด์ฌ
+- INSTAGRAM ํ๋ซํผ ์ด๋ฏธ์ง ์กด์ฌ
+
+## ์์ ๋ ํ์ผ
+
+### 1. ContentPreviewStep.tsx
+**์์น**: `src/app/(main)/events/create/steps/ContentPreviewStep.tsx:109`
+
+```typescript
+const mockData: EventCreationData = {
+ eventDraftId: "1761634317010", // Changed from "7777"
+ eventTitle: "๋งฅ์ฃผ ํํฐ ์ด๋ฒคํธ",
+ eventDescription: "๊ฐ๋จ์์ ์ด๋ฆฌ๋ ์ ๋๋ ๋งฅ์ฃผ ํํฐ์ ์ฐธ์ฌํ์ธ์!",
+ industry: "์์์ ",
+ location: "๊ฐ๋จ",
+ trends: ["ํํฐ", "๋งฅ์ฃผ", "์๋งฅ์ฃผ"],
+ prize: "์๋งฅ์ฃผ 1์"
+};
+```
+
+### 2. init-mock-data.html
+**์์น**: `public/init-mock-data.html:121`, `public/init-mock-data.html:168`
+
+```html
+
+1761634317010
+
+
+
+```
+
+### 3. QUICK_TEST.md
+**์์น**: `QUICK_TEST.md` (์ ์ฒด ๋ฌธ์)
+
+- Mock ๋ฐ์ดํฐ ์์์ eventId ๋ณ๊ฒฝ
+- API ํ์ธ ์์์ eventId ๋ณ๊ฒฝ
+- ๋๋ฒ๊น
๋ก๊ทธ ์์ ์
๋ฐ์ดํธ
+
+### 4. MOCK_DATA_SETUP.md
+**์์น**: `MOCK_DATA_SETUP.md` (์ ์ฒด ๋ฌธ์)
+
+- Mock ๋ฐ์ดํฐ ๊ตฌ์กฐ ์์ ์
๋ฐ์ดํธ
+- ํ
์คํธ ์๋๋ฆฌ์ค eventId ๋ณ๊ฒฝ
+- ์ฐธ๊ณ ์ฌํญ ์ถ๊ฐ: "1761634317010์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ด๋ฏธ ์์ฑ๋ ์ด๋ฏธ์ง๊ฐ ์๋ eventId"
+
+## ๋น๋ ๊ฒ์ฆ
+
+```bash
+npm run build
+```
+
+โ
**์ฑ๊ณต**: TypeScript ํ์
๊ฒ์ฆ ํต๊ณผ, ๋น๋ ์๋ฃ
+
+๊ฒฝ๊ณ ์ฌํญ:
+- `loadingProgress`, `setLoadingProgress` ๋ฏธ์ฌ์ฉ ๋ณ์ (๊ธฐ๋ฅ์ ์ํฅ ์์)
+- ๊ธฐํ ESLint ๊ฒฝ๊ณ (๊ธฐ์กด ์ฝ๋, ๊ธ๋ฒ ์์ ๊ณผ ๋ฌด๊ด)
+
+## ํ
์คํธ ๋ฐฉ๋ฒ
+
+### 1. localStorage ์ด๊ธฐํ
+๋ธ๋ผ์ฐ์ ์ฝ์์์ ๊ธฐ์กด ๋ฐ์ดํฐ ์ญ์ :
+```javascript
+localStorage.removeItem('eventCreationData');
+```
+
+### 2. ๊ฐ๋ฐ ์๋ฒ ์คํ
+```bash
+npm run dev
+```
+
+### 3. ํ์ด์ง ์ ์
+```
+http://localhost:3000/events/create?event-creation.step=contentPreview
+```
+
+### 4. ์์ ๊ฒฐ๊ณผ
+- โ
์คํ์ผ 1 (SIMPLE): ์ค์ ์ด๋ฏธ์ง ํ์
+- โ
์คํ์ผ 2 (FANCY): ์ค์ ์ด๋ฏธ์ง ํ์
+- โ
์คํ์ผ 3 (TRENDY): ์ค์ ์ด๋ฏธ์ง ํ์
+- โ
3๊ฐ ์คํ์ผ ๋ชจ๋ "ํฌ๊ฒ๋ณด๊ธฐ" ๋ฒํผ ํ์ฑํ
+
+### 5. ์ฝ์ ๋ก๊ทธ ํ์ธ
+```
+๐ฅ Loading generated images for event: 1761634317010
+โ
Images loaded from API: 6 [...]
+๐ธ Processing image 1: { id: X, style: 'SIMPLE', ... }
+ โ
Selected as latest SIMPLE image
+๐ธ Processing image 2: { id: Y, style: 'FANCY', ... }
+ โ
Selected as latest FANCY image
+๐ธ Processing image 3: { id: Z, style: 'TRENDY', ... }
+ โ
Selected as latest TRENDY image
+๐จ Image map created with entries: { SIMPLE: 'YES โ
', FANCY: 'YES โ
', TRENDY: 'YES โ
', totalSize: 3 }
+โ
์ด๋ฏธ์ง ๋ก๋ ์๋ฃ!
+๐ผ๏ธ Rendering SIMPLE: { hasImage: true, imageDataExists: true, ... }
+โ
SIMPLE image loaded successfully
+```
+
+## ์ถ๊ฐ ์ฐธ๊ณ ์ฌํญ
+
+### ์๋ก์ด ์ด๋ฒคํธ ํ
์คํธ ์
+์๋ก์ด eventId๋ก ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๋ ค๋ฉด:
+
+1. localStorage์ ์๋ก์ด eventId ์ค์
+2. "์ด๋ฏธ์ง ์ฌ์์ฑ" ๋ฒํผ ํด๋ฆญ
+3. ์ฝ 2์ด ํ ์๋์ผ๋ก ์์ฑ๋ ์ด๋ฏธ์ง ๋ก๋
+
+### Mock ๋ฐ์ดํฐ ๋ณ๊ฒฝ ๋ฐฉ๋ฒ
+`public/init-mock-data.html` ํ์ด์ง ์ฌ์ฉ:
+```
+http://localhost:3000/init-mock-data.html
+```
+
+๋๋ ๋ธ๋ผ์ฐ์ ์ฝ์์์ ์ง์ ์ค์ :
+```javascript
+localStorage.setItem('eventCreationData', JSON.stringify({
+ eventDraftId: "1761634317010",
+ eventTitle: "...",
+ // ...
+}));
+```
+
+## ๊ฒฐ๋ก
+
+EventId ๋ถ์ผ์น ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ฌ ๋ชจ๋ ์คํ์ผ ์นด๋์์ ์ค์ ์ด๋ฏธ์ง๊ฐ ์ ์์ ์ผ๋ก ํ์๋ฉ๋๋ค.
+
+**ํต์ฌ ๋ณ๊ฒฝ**: Mock ๋ฐ์ดํฐ์ eventId๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์กด์ฌํ๋ "1761634317010"์ผ๋ก ๋ณ๊ฒฝ
+
+**์ํฅ ๋ฒ์**:
+- ๊ฐ๋ฐ/ํ
์คํธ ํ๊ฒฝ์ Mock ๋ฐ์ดํฐ๋ง ์ํฅ
+- ์ค์ ์ด์ ํ๊ฒฝ์์๋ Channel Step API์์ ์ ๊ณตํ๋ ์ค์ eventId ์ฌ์ฉ
+- ์ฝ๋ ๋ก์ง ๋ณ๊ฒฝ ์์, ๋ฐ์ดํฐ๋ง ๋ณ๊ฒฝ
diff --git a/MOCK_DATA_SETUP.md b/MOCK_DATA_SETUP.md
new file mode 100644
index 0000000..f438436
--- /dev/null
+++ b/MOCK_DATA_SETUP.md
@@ -0,0 +1,149 @@
+# Mock ๋ฐ์ดํฐ ์ค์ ๊ฐ์ด๋
+
+AI ์ด๋ฏธ์ง ์์ฑ ๊ธฐ๋ฅ์ ํ
์คํธํ๊ธฐ ์ํด localStorage์ mock ๋ฐ์ดํฐ๋ฅผ ์ค์ ํ๋ ๋ฐฉ๋ฒ์
๋๋ค.
+
+## ๐ ๋น ๋ฅธ ์์
+
+### ๋ฐฉ๋ฒ 1: ์น ์ธํฐํ์ด์ค ์ฌ์ฉ (๊ถ์ฅ)
+
+1. ๊ฐ๋ฐ ์๋ฒ ์คํ
+```bash
+npm run dev
+```
+
+2. ๋ธ๋ผ์ฐ์ ์์ mock ๋ฐ์ดํฐ ์ด๊ธฐํ ํ์ด์ง ์ด๊ธฐ
+```
+http://localhost:3000/init-mock-data.html
+```
+
+3. "LocalStorage์ ์ ์ฅํ๊ธฐ" ๋ฒํผ ํด๋ฆญ
+
+4. ์ด๋ฏธ์ง ์์ฑ ํ์ด์ง๋ก ์ด๋
+```
+http://localhost:3000/events/create?step=contentPreview
+```
+
+### ๋ฐฉ๋ฒ 2: ๋ธ๋ผ์ฐ์ ์ฝ์ ์ฌ์ฉ
+
+1. ๊ฐ๋ฐ ์๋ฒ ์คํ ํ ๋ธ๋ผ์ฐ์ ์์ ์๋ฌด ํ์ด์ง๋ ์ด๊ธฐ
+
+2. F12 ๋๋ Cmd+Option+I๋ก ๊ฐ๋ฐ์ ๋๊ตฌ ์ด๊ธฐ
+
+3. Console ํญ์์ ๋ค์ ์ฝ๋ ์คํ:
+
+```javascript
+const mockEventData = {
+ eventDraftId: "1761634317010", // String type (existing eventId with images)
+ eventTitle: "๋งฅ์ฃผ ํํฐ ์ด๋ฒคํธ",
+ eventDescription: "๊ฐ๋จ์์ ์ด๋ฆฌ๋ ์ ๋๋ ๋งฅ์ฃผ ํํฐ์ ์ฐธ์ฌํ์ธ์!",
+ industry: "์์์ ",
+ location: "๊ฐ๋จ",
+ trends: ["ํํฐ", "๋งฅ์ฃผ", "์๋งฅ์ฃผ"],
+ prize: "์๋งฅ์ฃผ 1์"
+};
+
+localStorage.setItem('eventCreationData', JSON.stringify(mockEventData));
+console.log('โ
Mock ๋ฐ์ดํฐ ์ ์ฅ ์๋ฃ!');
+```
+
+4. ์ด๋ฏธ์ง ์์ฑ ํ์ด์ง๋ก ์ด๋
+
+### ๋ฐฉ๋ฒ 3: ํ
์คํธ HTML ํ์ผ ์ฌ์ฉ
+
+ํ๋ก์ ํธ ๋ฃจํธ์ `test-localstorage.html` ํ์ผ์ ๋ธ๋ผ์ฐ์ ์์ ์ง์ ์ด๊ธฐ:
+
+```bash
+open test-localstorage.html
+```
+
+## ๐ Mock ๋ฐ์ดํฐ ๊ตฌ์กฐ
+
+```json
+{
+ "eventDraftId": "1761634317010",
+ "eventTitle": "๋งฅ์ฃผ ํํฐ ์ด๋ฒคํธ",
+ "eventDescription": "๊ฐ๋จ์์ ์ด๋ฆฌ๋ ์ ๋๋ ๋งฅ์ฃผ ํํฐ์ ์ฐธ์ฌํ์ธ์!",
+ "industry": "์์์ ",
+ "location": "๊ฐ๋จ",
+ "trends": ["ํํฐ", "๋งฅ์ฃผ", "์๋งฅ์ฃผ"],
+ "prize": "์๋งฅ์ฃผ 1์"
+}
+```
+
+**์ฐธ๊ณ **:
+- `eventDraftId`๋ API ๋ณ๊ฒฝ์ผ๋ก ์ธํด `string` ํ์
์
๋๋ค.
+- `"1761634317010"`์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ด๋ฏธ ์์ฑ๋ ์ด๋ฏธ์ง๊ฐ ์๋ eventId์
๋๋ค.
+
+## ๐งช ํ
์คํธ ์๋๋ฆฌ์ค
+
+### ์๋๋ฆฌ์ค 1: ์ ์ฒด ์ด๋ฏธ์ง ์์ฑ ํ๋ก์ฐ
+
+1. Mock ๋ฐ์ดํฐ ์ค์
+2. `/events/create?step=contentPreview` ์ ์
+3. ์๋์ผ๋ก AI ์ด๋ฏธ์ง ์์ฑ ์์
+4. 3๊ฐ์ง ์คํ์ผ(SIMPLE, FANCY, TRENDY) ํ์ธ
+5. ์คํ์ผ ์ ํ ํ ๋ค์ ๋จ๊ณ ์งํ
+
+### ์๋๋ฆฌ์ค 2: ๋ค์ํ ์ด๋ฒคํธ ๋ฐ์ดํฐ ํ
์คํธ
+
+๋ค๋ฅธ ์
์ข
/์ง์ญ/ํธ๋ ๋๋ก ํ
์คํธ:
+
+```javascript
+// ์นดํ ์ด๋ฒคํธ (์๋ก์ด ์ด๋ฏธ์ง ์์ฑ ํ์)
+localStorage.setItem('eventCreationData', JSON.stringify({
+ eventDraftId: "test-cafe-001",
+ eventTitle: "์ปคํผ ํ ์ธ ์ด๋ฒคํธ",
+ eventDescription: "์ ๋ฉ๋ด ์ถ์ ๊ธฐ๋
30% ํ ์ธ",
+ industry: "์นดํ",
+ location: "ํ๋",
+ trends: ["์ปคํผ", "ํ ์ธ", "์ ๋ฉ๋ด"],
+ prize: "์๋ฉ๋ฆฌ์นด๋
ธ 1์"
+}));
+```
+
+```javascript
+// ๋ทฐํฐ ์ด๋ฒคํธ (์๋ก์ด ์ด๋ฏธ์ง ์์ฑ ํ์)
+localStorage.setItem('eventCreationData', JSON.stringify({
+ eventDraftId: "test-beauty-001",
+ eventTitle: "๋ด๋ง์ด ํผ๋ถ๊ด๋ฆฌ ์ด๋ฒคํธ",
+ eventDescription: "๋ด๋ง์ด ํน๋ณ ์ผ์ด ํ๋ก๊ทธ๋จ",
+ industry: "๋ทฐํฐ",
+ location: "๊ฐ๋จ",
+ trends: ["ํผ๋ถ๊ด๋ฆฌ", "๋ด", "์ผ์ด"],
+ prize: "ํ์ด์
์ผ์ด 1ํ"
+}));
+```
+
+## ๐ ๋๋ฒ๊น
+
+### localStorage ๋ฐ์ดํฐ ํ์ธ
+
+```javascript
+// ํ์ฌ ์ ์ฅ๋ ๋ฐ์ดํฐ ํ์ธ
+const data = localStorage.getItem('eventCreationData');
+console.log(JSON.parse(data));
+```
+
+### localStorage ๋ฐ์ดํฐ ์ญ์
+
+```javascript
+localStorage.removeItem('eventCreationData');
+console.log('โ
๋ฐ์ดํฐ ์ญ์ ์๋ฃ');
+```
+
+## โ ๏ธ ์ฃผ์์ฌํญ
+
+1. **๊ฐ์ ๋๋ฉ์ธ**: localStorage๋ ๋๋ฉ์ธ๋ณ๋ก ๋ถ๋ฆฌ๋๋ฏ๋ก, ๊ฐ์ localhost:3000์์ ์ค์ ํด์ผ ํฉ๋๋ค.
+
+2. **๋ธ๋ผ์ฐ์ ์ ํ**: ์ํฌ๋ฆฟ ๋ชจ๋์์๋ localStorage๊ฐ ์ ํ๋ ์ ์์ต๋๋ค.
+
+3. **๋ฐ์ดํฐ ์ ์ง**: ๋ธ๋ผ์ฐ์ ๋ฅผ ๋ซ์๋ localStorage ๋ฐ์ดํฐ๋ ์ ์ง๋ฉ๋๋ค. ์๋ก์ด ํ
์คํธ ์ ์ญ์ ํ ์งํํ์ธ์.
+
+## ๐ฏ ์ค์ API ์ฐ๋ ํ
+
+Channel Step API๊ฐ ๊ตฌํ๋๋ฉด ์ด mock ๋ฐ์ดํฐ ์ค์ ์ ๋ถํ์ํ๋ฉฐ,
+์ค์ ํ๋ก์ฐ์์ ์๋์ผ๋ก ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋ฉ๋๋ค:
+
+```
+Objective โ Recommendation โ Channel (์ฌ๊ธฐ์ localStorage ์ ์ฅ) โ ContentPreview (์ด๋ฏธ์ง ์์ฑ)
+```
diff --git a/QUICK_TEST.md b/QUICK_TEST.md
new file mode 100644
index 0000000..8a1b748
--- /dev/null
+++ b/QUICK_TEST.md
@@ -0,0 +1,145 @@
+# ๐ AI ์ด๋ฏธ์ง ์์ฑ ๋น ๋ฅธ ํ
์คํธ ๊ฐ์ด๋
+
+## โก ๊ฐ์ฅ ๋น ๋ฅธ ๋ฐฉ๋ฒ (๊ธฐ์กด ์ด๋ฏธ์ง ํ์ธ)
+
+```bash
+# 1. ๊ฐ๋ฐ ์๋ฒ ์คํ
+npm run dev
+
+# 2. ๋ธ๋ผ์ฐ์ ์์ ๋ฐ๋ก ์ ์
+http://localhost:3000/events/create?event-creation.step=contentPreview
+```
+
+โจ **๋!** ์๋์ผ๋ก Mock ๋ฐ์ดํฐ(eventId: "1761634317010")๊ฐ ์์ฑ๋๊ณ ๊ธฐ์กด ์์ฑ๋ ์ด๋ฏธ์ง๋ฅผ ๋ถ๋ฌ์ต๋๋ค.
+
+๐ก **์ด๋ฏธ์ง๊ฐ ์์ ๊ฒฝ์ฐ**: "์ด๋ฏธ์ง ์์ฑํ๊ธฐ" ๋ฒํผ์ ํด๋ฆญํ๋ฉด ์๋ก์ด ์ด๋ฏธ์ง๋ฅผ ์์ฑํฉ๋๋ค.
+
+---
+
+## ๐ ์ปค์คํ
๋ฐ์ดํฐ๋ก ํ
์คํธ (์ ํ์ฌํญ)
+
+### 1๋จ๊ณ: ๊ฐ๋ฐ ์๋ฒ ์คํ
+
+```bash
+npm run dev
+```
+
+### 2๋จ๊ณ: Mock ๋ฐ์ดํฐ ์ค์ (3๊ฐ์ง ๋ฐฉ๋ฒ ์ค ์ ํ)
+
+### โจ ๋ฐฉ๋ฒ A: ์น UI ์ฌ์ฉ (๊ฐ์ฅ ์ฌ์!)
+
+๋ธ๋ผ์ฐ์ ์์ ์ด๊ธฐ:
+```
+http://localhost:3000/init-mock-data.html
+```
+
+"LocalStorage์ ์ ์ฅํ๊ธฐ" ๋ฒํผ ํด๋ฆญ โ ์๋ฃ!
+
+---
+
+### ๋ฐฉ๋ฒ B: ๋ธ๋ผ์ฐ์ ์ฝ์ ์ฌ์ฉ
+
+1. `http://localhost:3000` ์ ์
+2. F12 (๊ฐ๋ฐ์ ๋๊ตฌ) โ Console ํญ
+3. ๋ค์ ์ฝ๋ ๋ณต์ฌ & ๋ถ์ฌ๋ฃ๊ธฐ:
+
+```javascript
+localStorage.setItem('eventCreationData', JSON.stringify({
+ eventDraftId: "1761634317010",
+ eventTitle: "๋งฅ์ฃผ ํํฐ ์ด๋ฒคํธ",
+ eventDescription: "๊ฐ๋จ์์ ์ด๋ฆฌ๋ ์ ๋๋ ๋งฅ์ฃผ ํํฐ์ ์ฐธ์ฌํ์ธ์!",
+ industry: "์์์ ",
+ location: "๊ฐ๋จ",
+ trends: ["ํํฐ", "๋งฅ์ฃผ", "์๋งฅ์ฃผ"],
+ prize: "์๋งฅ์ฃผ 1์"
+}));
+```
+
+---
+
+### ๋ฐฉ๋ฒ C: HTML ํ์ผ ์ง์ ์ด๊ธฐ
+
+```bash
+open test-localstorage.html
+```
+
+## 3๋จ๊ณ: ์ด๋ฏธ์ง ์์ฑ ํ์ด์ง ์ ์
+
+๋ธ๋ผ์ฐ์ ์์ ์ด๊ธฐ:
+```
+http://localhost:3000/events/create?event-creation.step=contentPreview
+```
+
+## 4๋จ๊ณ: ์๋ ์คํ ํ์ธ โ
+
+1. ํ์ด์ง ๋ก๋ฉ๋๋ฉด ์๋์ผ๋ก ์ด๋ฏธ์ง ์์ฑ ์์
+2. ๋ก๋ฉ ์คํผ๋์ ์งํ๋ฅ ํ์ธ
+3. ์ฝ 60์ด ํ 3๊ฐ์ง ์คํ์ผ ์ด๋ฏธ์ง ์์ฑ
+ - ์คํ์ผ 1: ์ฌํ
+ - ์คํ์ผ 2: ํ๋ ค
+ - ์คํ์ผ 3: ํธ๋ ๋
+
+## ์์ ๊ฒฐ๊ณผ
+
+### ์ด๋ฏธ์ง๊ฐ ์๋ ๊ฒฝ์ฐ
+- โ
**์ฆ์ ํ์**: ๋ก๋ฉ ํ ๋ฐ๋ก ์ด๋ฏธ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ๋ฉด
+- โ
**3๊ฐ ์คํ์ผ ์ด๋ฏธ์ง**: SIMPLE, FANCY, TRENDY ๊ฐ๊ฐ ์ต์ ์ด๋ฏธ์ง ํ์
+- โ
**์ด๋ฏธ์ง ์ ํ**: ๋ผ๋์ค ๋ฒํผ์ผ๋ก ์ํ๋ ์คํ์ผ ์ ํ
+- โ
**์ฌ์์ฑ ๋ฒํผ**: "์ด๋ฏธ์ง ์ฌ์์ฑ" ๋ฒํผ์ผ๋ก ์๋ก์ด ์ด๋ฏธ์ง ์์ฑ ๊ฐ๋ฅ
+- โ
**ํฌ๊ฒ๋ณด๊ธฐ**: ๊ฐ ์ด๋ฏธ์ง ํด๋ฆญ ์ ์ ์ฒดํ๋ฉด ๋ฏธ๋ฆฌ๋ณด๊ธฐ
+
+### ์ด๋ฏธ์ง๊ฐ ์๋ ๊ฒฝ์ฐ
+- โ ๏ธ **์๋ฌ ๋ฉ์์ง**: "์์ฑ๋ ์ด๋ฏธ์ง๊ฐ ์์ต๋๋ค. ์ด๋ฏธ์ง๋ฅผ ๋จผ์ ์์ฑํด์ฃผ์ธ์."
+- ๐ **์์ฑ ๋ฒํผ**: "์ด๋ฏธ์ง ์์ฑํ๊ธฐ" ๋ฒํผ ํด๋ฆญ
+- โณ **์์ฑ ๋๊ธฐ**: API ์์ฒญ ํ 2์ด ๋ค ์๋์ผ๋ก ์ด๋ฏธ์ง ์กฐํ
+- โ
**์ด๋ฏธ์ง ํ์**: ์์ฑ ์๋ฃ๋ ์ด๋ฏธ์ง ์๋ ํ์
+
+## ๋ฌธ์ ํด๊ฒฐ
+
+### ~~"์ด๋ฒคํธ ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค" ์๋ฌ~~
+โ โ
**ํด๊ฒฐ๋จ!** ์ด์ ์๋์ผ๋ก Mock ๋ฐ์ดํฐ๊ฐ ์์ฑ๋ฉ๋๋ค.
+
+### ~~Network Error / CORS ์๋ฌ~~
+โ โ
**ํด๊ฒฐ๋จ!** Next.js API proxy๋ฅผ ํตํด CORS ๋ฌธ์ ์ฐํ
+โ ํ๋ก ํธ์๋๊ฐ `/api/content/*` โ ๋ฐฑ์๋ `localhost:8084` ๋ก ์๋ ํ๋ก์
+
+### ์ด๋ฏธ์ง ์์ฑ ์คํจ
+โ Content API (localhost:8084) ์คํ ์ฌ๋ถ ํ์ธ
+โ ํฐ๋ฏธ๋์์ ํ์ธ: `curl http://localhost:8084/api/v1/content/events/7777/images`
+
+### ์ด๋ฏธ์ง๊ฐ ํ์๋์ง ์์
+โ ๋คํธ์ํฌ ํญ์์ CDN URL ๋ก๋ ์ํ ํ์ธ
+โ Azure Blob Storage ์ ๊ทผ ๊ถํ ํ์ธ
+
+## API ํ์ธ
+
+```bash
+# ์ด๋ฒคํธ 1761634317010์ ์ด๋ฏธ์ง ํ์ธ
+curl http://localhost:8084/api/v1/content/events/1761634317010/images
+
+# ํ๋ก ํธ์๋ ํ๋ก์๋ฅผ ํตํ ํ์ธ (๊ฐ๋ฐ ์๋ฒ ์คํ ์ค)
+curl http://localhost:3000/api/content/events/1761634317010/images
+```
+
+## ๋๋ฒ๊น
+
+๋ธ๋ผ์ฐ์ ๊ฐ๋ฐ์ ๋๊ตฌ (F12) โ Console ํญ์์ ๋ค์ ๋ก๊ทธ ํ์ธ:
+
+```
+๐ฅ Loading generated images for event: 1761634317010
+โ
Images loaded from API: 6 [...]
+๐ธ Processing image 1: { id: 1, style: 'SIMPLE', platform: 'INSTAGRAM', ... }
+ โ
Selected as latest SIMPLE image
+๐ธ Processing image 2: { id: 3, style: 'FANCY', platform: 'INSTAGRAM', ... }
+ โ
Selected as latest FANCY image
+๐ธ Processing image 3: { id: 5, style: 'TRENDY', platform: 'INSTAGRAM', ... }
+ โ
Selected as latest TRENDY image
+๐จ Image map created with entries: { SIMPLE: 'YES โ
', FANCY: 'YES โ
', TRENDY: 'YES โ
', totalSize: 3 }
+โ
์ด๋ฏธ์ง ๋ก๋ ์๋ฃ! ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ๋ฉด์ผ๋ก ์ ํํฉ๋๋ค.
+๐ผ๏ธ Rendering SIMPLE: { hasImage: true, imageDataExists: true, cdnUrl: 'https://blob...' }
+โ
SIMPLE image loaded successfully
+```
+
+---
+
+๋ ์์ธํ ๋ด์ฉ์ `MOCK_DATA_SETUP.md` ์ฐธ์กฐ
diff --git a/TEST_URLS.md b/TEST_URLS.md
new file mode 100644
index 0000000..df456ed
--- /dev/null
+++ b/TEST_URLS.md
@@ -0,0 +1,117 @@
+# ๐ AI ์ด๋ฏธ์ง ์์ฑ ํ
์คํธ URL ๊ฐ์ด๋
+
+## โ
์ฌ๋ฐ๋ฅธ URL
+
+### ContentPreview Step ์ง์ ์ ์
+```
+http://localhost:3000/events/create?event-creation.step=contentPreview
+```
+
+๋๋ ๊ฐ๋จํ๊ฒ:
+```
+http://localhost:3000/events/create?step=contentPreview
+```
+
+### Mock ๋ฐ์ดํฐ ์ค์ ํ์ด์ง
+```
+http://localhost:3000/init-mock-data.html
+```
+
+## ๐ URL ํ๋ผ๋ฏธํฐ ์ค๋ช
+
+- `event-creation.step=contentPreview` - Funnel์ step์ contentPreview๋ก ์ค์
+- `step=contentPreview` - ๊ฐ๋จํ ํ์ (funnel id๊ฐ event-creation์ผ ๋)
+
+## ๐ฏ ์ ์ฒด ํ๋ก์ฐ ํ
์คํธ URL
+
+### 1. ์์ (Objective Step)
+```
+http://localhost:3000/events/create
+```
+
+### 2. Channel Step๊น์ง ์งํ ํ
+```
+http://localhost:3000/events/create?event-creation.step=channel
+```
+
+### 3. ContentPreview Step (์ด๋ฏธ์ง ์์ฑ)
+```
+http://localhost:3000/events/create?event-creation.step=contentPreview
+```
+
+## ๐ก ์๋ Mock ๋ฐ์ดํฐ ์์ฑ
+
+์ด์ `contentPreview` ํ์ด์ง์ ์ง์ ์ ์ํ๋ฉด:
+
+1. โ
localStorage ํ์ธ
+2. โ
๋ฐ์ดํฐ ์์ผ๋ฉด ์๋์ผ๋ก Mock ๋ฐ์ดํฐ ์์ฑ
+3. โ
์ฆ์ AI ์ด๋ฏธ์ง ์์ฑ ์์
+
+**๋ ์ด์ ์๋์ผ๋ก Mock ๋ฐ์ดํฐ๋ฅผ ์ค์ ํ ํ์๊ฐ ์์ต๋๋ค!**
+
+## ๐งช ํ
์คํธ ์๋๋ฆฌ์ค
+
+### ์๋๋ฆฌ์ค 1: ๊ฐ์ฅ ๋น ๋ฅธ ํ
์คํธ
+```bash
+# 1. ๊ฐ๋ฐ ์๋ฒ ์คํ
+npm run dev
+
+# 2. ๋ธ๋ผ์ฐ์ ์์ ๋ฐ๋ก ์ ์
+http://localhost:3000/events/create?event-creation.step=contentPreview
+```
+โ ์๋์ผ๋ก Mock ๋ฐ์ดํฐ ์์ฑ & ์ด๋ฏธ์ง ์์ฑ ์์!
+
+### ์๋๋ฆฌ์ค 2: ์ปค์คํ
๋ฐ์ดํฐ๋ก ํ
์คํธ
+```bash
+# 1. Mock ๋ฐ์ดํฐ ์ค์ ํ์ด์ง ์ด๊ธฐ
+http://localhost:3000/init-mock-data.html
+
+# 2. ์ํ๋ ๋ฐ์ดํฐ ์
๋ ฅ ํ ์ ์ฅ
+
+# 3. ContentPreview ์ ์
+http://localhost:3000/events/create?event-creation.step=contentPreview
+```
+
+### ์๋๋ฆฌ์ค 3: ์ ์ฒด ํ๋ก์ฐ ํ
์คํธ
+```bash
+# 1. ์ฒ์๋ถํฐ ์์
+http://localhost:3000/events/create
+
+# 2. Objective ์ ํ
+
+# 3. Recommendation ํ์ธ
+
+# 4. Channel ์ ํ (SNS, ์ฐ๋ฆฌ๋๋คTV, ์ง๋TV ์ค ํ๋)
+
+# 5. ์๋์ผ๋ก ContentPreview๋ก ์ด๋ํ๋ฉฐ ์ด๋ฏธ์ง ์์ฑ ์์
+```
+
+## ๐ ๋ฌธ์ ํด๊ฒฐ
+
+### "์ด๋ฒคํธ ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค" ์๋ฌ
+โ ์ด์ ์ด ์๋ฌ๋ ๋ฐ์ํ์ง ์์ต๋๋ค! ์๋์ผ๋ก Mock ๋ฐ์ดํฐ๊ฐ ์์ฑ๋ฉ๋๋ค.
+
+### ์ด๋ฏธ์ง ์์ฑ ์คํจ
+```bash
+# Content API ์๋ฒ ํ์ธ
+curl http://localhost:8084/api/v1/content/events/7777/images
+
+# API ์๋ฒ๊ฐ ๊บผ์ ธ์๋ค๋ฉด ์คํ ํ์
+```
+
+### ๋ค๋ฅธ ์ด๋ฒคํธ ID๋ก ํ
์คํธํ๊ณ ์ถ์ ๋
+```javascript
+// ๋ธ๋ผ์ฐ์ ์ฝ์์์
+localStorage.setItem('eventCreationData', JSON.stringify({
+ eventDraftId: 8888, // ๋ค๋ฅธ ID
+ eventTitle: "์ปคํผ ํ ์ธ ์ด๋ฒคํธ",
+ eventDescription: "์ ๋ฉ๋ด ์ถ์ ๊ธฐ๋
",
+ industry: "์นดํ",
+ location: "ํ๋",
+ trends: ["์ปคํผ", "ํ ์ธ"],
+ prize: "์๋ฉ๋ฆฌ์นด๋
ธ 1์"
+}));
+
+// ํ์ด์ง ์๋ก๊ณ ์นจ
+location.reload();
+```
diff --git a/next.config.js b/next.config.js
index 03b1343..aabb89d 100644
--- a/next.config.js
+++ b/next.config.js
@@ -6,12 +6,21 @@ const nextConfig = {
emotion: true,
},
images: {
- domains: ['localhost'],
+ domains: ['localhost', 'blobkteventstorage.blob.core.windows.net'],
formats: ['image/webp', 'image/avif'],
},
env: {
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080',
},
+ // CORS ์ฐํ๋ฅผ ์ํ API Proxy ์ค์
+ async rewrites() {
+ return [
+ {
+ source: '/api/proxy/:path*',
+ destination: 'http://localhost:8084/api/:path*',
+ },
+ ]
+ },
}
module.exports = nextConfig
diff --git a/package-lock.json b/package-lock.json
index 9644766..d8292b6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,7 @@
"@tanstack/react-query": "^5.59.16",
"@use-funnel/browser": "^0.0.12",
"@use-funnel/next": "^0.0.12",
- "axios": "^1.7.7",
+ "axios": "^1.13.0",
"chart.js": "^4.5.1",
"dayjs": "^1.11.13",
"next": "^14.2.15",
@@ -2122,9 +2122,9 @@
}
},
"node_modules/axios": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
- "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.0.tgz",
+ "integrity": "sha512-zt40Pz4zcRXra9CVV31KeyofwiNvAbJ5B6YPz9pMJ+yOSLikvPT4Yi5LjfgjRa9CawVYBaD1JQzIVcIvBejKeA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
diff --git a/package.json b/package.json
index b4463e4..89d446c 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"@tanstack/react-query": "^5.59.16",
"@use-funnel/browser": "^0.0.12",
"@use-funnel/next": "^0.0.12",
- "axios": "^1.7.7",
+ "axios": "^1.13.0",
"chart.js": "^4.5.1",
"dayjs": "^1.11.13",
"next": "^14.2.15",
diff --git a/public/init-mock-data.html b/public/init-mock-data.html
new file mode 100644
index 0000000..9546666
--- /dev/null
+++ b/public/init-mock-data.html
@@ -0,0 +1,205 @@
+
+
+
+
+
+ Mock ๋ฐ์ดํฐ ์ด๊ธฐํ
+
+
+
+
+
๐จ Mock ๋ฐ์ดํฐ ์ด๊ธฐํ
+
์ด๋ฒคํธ ์์ฑ ํ
์คํธ๋ฅผ ์ํ ์ํ ๋ฐ์ดํฐ
+
+
+
๐ ์ ์ฅ๋ ๋ฐ์ดํฐ
+
+ ์ด๋ฒคํธ ID:
+ 1761634317010
+
+
+ ์ ๋ชฉ:
+ ๋งฅ์ฃผ ํํฐ ์ด๋ฒคํธ
+
+
+ ์ค๋ช
:
+ ๊ฐ๋จ์์ ์ด๋ฆฌ๋ ์ ๋๋ ๋งฅ์ฃผ ํํฐ
+
+
+ ์
์ข
:
+ ์์์
+
+
+ ์ง์ญ:
+ ๊ฐ๋จ
+
+
+ ํธ๋ ๋:
+ ํํฐ, ๋งฅ์ฃผ, ์๋งฅ์ฃผ
+
+
+ ๊ฒฝํ:
+ ์๋งฅ์ฃผ 1์
+
+
+
+
+
+
+ โ
Mock ๋ฐ์ดํฐ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์ ์ฅ๋์์ต๋๋ค!
+ ์ด์ ์ด๋ฒคํธ ์์ฑ ํ์ด์ง๋ก ์ด๋ํ ์ ์์ต๋๋ค.
+
+
+
+
+
+
+
+
diff --git a/setup-mock-data.js b/setup-mock-data.js
new file mode 100644
index 0000000..ccf8745
--- /dev/null
+++ b/setup-mock-data.js
@@ -0,0 +1,17 @@
+// Mock ๋ฐ์ดํฐ๋ฅผ localStorage์ ์ ์ฅํ๋ ์คํฌ๋ฆฝํธ
+
+const mockEventData = {
+ eventDraftId: 7777,
+ eventTitle: "๋งฅ์ฃผ ํํฐ ์ด๋ฒคํธ",
+ eventDescription: "๊ฐ๋จ์์ ์ด๋ฆฌ๋ ์ ๋๋ ๋งฅ์ฃผ ํํฐ์ ์ฐธ์ฌํ์ธ์!",
+ industry: "์์์ ",
+ location: "๊ฐ๋จ",
+ trends: ["ํํฐ", "๋งฅ์ฃผ", "์๋งฅ์ฃผ"],
+ prize: "์๋งฅ์ฃผ 1์"
+};
+
+// localStorage์ ์ ์ฅ
+localStorage.setItem('eventCreationData', JSON.stringify(mockEventData));
+
+console.log('โ
Mock ๋ฐ์ดํฐ๊ฐ localStorage์ ์ ์ฅ๋์์ต๋๋ค:');
+console.log(JSON.stringify(mockEventData, null, 2));
diff --git a/src/app/(main)/events/[eventId]/draw/page.tsx b/src/app/(main)/events/[eventId]/draw/page.tsx
index e9d09fe..c393d42 100644
--- a/src/app/(main)/events/[eventId]/draw/page.tsx
+++ b/src/app/(main)/events/[eventId]/draw/page.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import {
Box,
@@ -17,6 +17,8 @@ import {
DialogActions,
IconButton,
Grid,
+ Alert,
+ CircularProgress,
} from '@mui/material';
import {
EventNote,
@@ -25,12 +27,13 @@ import {
Download,
Refresh,
Notifications,
- List as ListIcon,
Add,
Remove,
Info,
People,
} from '@mui/icons-material';
+import { drawWinners, getWinners, getParticipants } from '@/shared/api/participation.api';
+import type { DrawWinnersResponse } from '@/shared/types/api.types';
// ๋์์ธ ์์คํ
์์
const colors = {
@@ -50,39 +53,19 @@ const colors = {
},
};
-// Mock ๋ฐ์ดํฐ
-const mockEventData = {
- name: '์ ๊ท๊ณ ๊ฐ ์ ์น ์ด๋ฒคํธ',
- totalParticipants: 127,
- participants: [
- { id: '00042', name: '๊น**', phone: '010-****-1234', channel: '์ฐ๋ฆฌ๋๋คTV', hasBonus: true },
- { id: '00089', name: '์ด**', phone: '010-****-5678', channel: 'SNS', hasBonus: false },
- { id: '00103', name: '๋ฐ**', phone: '010-****-9012', channel: '๋ง๊ณ ๋น์ฆ', hasBonus: true },
- { id: '00012', name: '์ต**', phone: '010-****-3456', channel: 'SNS', hasBonus: false },
- { id: '00067', name: '์ **', phone: '010-****-7890', channel: '์ฐ๋ฆฌ๋๋คTV', hasBonus: false },
- { id: '00025', name: '๊ฐ**', phone: '010-****-2468', channel: '๋ง๊ณ ๋น์ฆ', hasBonus: true },
- { id: '00078', name: '์กฐ**', phone: '010-****-1357', channel: 'SNS', hasBonus: false },
- ],
-};
-
-const mockDrawingHistory = [
- { date: '2025-01-15 14:30', winnerCount: 5, isRedraw: false },
- { date: '2025-01-15 14:25', winnerCount: 5, isRedraw: true },
-];
-
interface Winner {
- id: string;
+ participantId: string;
name: string;
- phone: string;
- channel: string;
- hasBonus: boolean;
+ phoneNumber: string;
+ rank: number;
+ channel?: string;
+ storeVisited: boolean;
}
export default function DrawPage() {
const params = useParams();
const router = useRouter();
- // eventId will be used for API calls in future
- const _eventId = params.eventId as string;
+ const eventId = params.eventId as string;
// State
const [winnerCount, setWinnerCount] = useState(5);
@@ -95,8 +78,60 @@ export default function DrawPage() {
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const [redrawDialogOpen, setRedrawDialogOpen] = useState(false);
const [notifyDialogOpen, setNotifyDialogOpen] = useState(false);
- const [historyDetailOpen, setHistoryDetailOpen] = useState(false);
- const [selectedHistory, setSelectedHistory] = useState(null);
+
+ // API ๊ด๋ จ ์ํ
+ const [totalParticipants, setTotalParticipants] = useState(0);
+ const [eventName] = useState('์ด๋ฒคํธ');
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [drawResult, setDrawResult] = useState(null);
+
+ // ์ด๊ธฐ ๋ฐ์ดํฐ ๋ก๋
+ useEffect(() => {
+ const loadInitialData = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ // ์ฐธ์ฌ์ ์ด ์ ์กฐํ
+ const participantsResponse = await getParticipants({
+ eventId,
+ page: 0,
+ size: 1,
+ });
+
+ setTotalParticipants(participantsResponse.data.totalElements);
+
+ // ๊ธฐ์กด ๋น์ฒจ์๊ฐ ์๋์ง ํ์ธ
+ try {
+ const winnersResponse = await getWinners(eventId, 0, 100);
+ if (winnersResponse.data.content.length > 0) {
+ // ๋น์ฒจ์๊ฐ ์์ผ๋ฉด ๊ฒฐ๊ณผ ํ๋ฉด ํ์
+ const winnerList: Winner[] = winnersResponse.data.content.map((p) => ({
+ participantId: p.participantId,
+ name: p.name,
+ phoneNumber: p.phoneNumber,
+ rank: 0, // rank๋ ์์๋๋ก
+ channel: p.channel,
+ storeVisited: p.storeVisited,
+ }));
+ setWinners(winnerList);
+ setShowResults(true);
+ }
+ } catch {
+ // ๋น์ฒจ์๊ฐ ์์ผ๋ฉด ๋ฌด์
+ console.log('No winners yet');
+ }
+ } catch (err) {
+ console.error('Failed to load initial data:', err);
+ setError('๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋๋ฐ ์คํจํ์ต๋๋ค.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadInitialData();
+ }, [eventId]);
const handleDecrease = () => {
if (winnerCount > 1) {
@@ -105,7 +140,7 @@ export default function DrawPage() {
};
const handleIncrease = () => {
- if (winnerCount < 100 && winnerCount < mockEventData.totalParticipants) {
+ if (winnerCount < 100 && winnerCount < totalParticipants) {
setWinnerCount(winnerCount + 1);
}
};
@@ -114,34 +149,54 @@ export default function DrawPage() {
setConfirmDialogOpen(true);
};
- const executeDrawing = () => {
+ const executeDrawing = async () => {
setConfirmDialogOpen(false);
setIsDrawing(true);
+ setError(null);
- // Phase 1: ๋์ ์์ฑ ์ค (1 second)
- setTimeout(() => {
- setAnimationText('๋น์ฒจ์ ์ ์ ์ค...');
- setAnimationSubtext('๊ณต์ ํ ์ถ์ฒจ์ ์งํํ๊ณ ์์ต๋๋ค');
- }, 1000);
+ try {
+ // Phase 1: ๋์ ์์ฑ ์ค (1 second)
+ setTimeout(() => {
+ setAnimationText('๋น์ฒจ์ ์ ์ ์ค...');
+ setAnimationSubtext('๊ณต์ ํ ์ถ์ฒจ์ ์งํํ๊ณ ์์ต๋๋ค');
+ }, 1000);
- // Phase 2: ์๋ฃ (2 seconds)
- setTimeout(() => {
- setAnimationText('์๋ฃ!');
- setAnimationSubtext('์ถ์ฒจ์ด ์๋ฃ๋์์ต๋๋ค');
- }, 2000);
+ // ์ค์ API ํธ์ถ
+ const response = await drawWinners(eventId, winnerCount, storeBonus);
+ setDrawResult(response.data);
- // Phase 3: Show results (3 seconds)
- setTimeout(() => {
+ // Phase 2: ์๋ฃ (2 seconds)
+ setTimeout(() => {
+ setAnimationText('์๋ฃ!');
+ setAnimationSubtext('์ถ์ฒจ์ด ์๋ฃ๋์์ต๋๋ค');
+ }, 2000);
+
+ // Phase 3: ๋น์ฒจ์ ๋ชฉ๋ก ๋ณํ ๋ฐ ํ์
+ setTimeout(() => {
+ const winnerList: Winner[] = response.data.winners.map((w) => ({
+ participantId: w.participantId,
+ name: w.name,
+ phoneNumber: w.phoneNumber,
+ rank: w.rank,
+ storeVisited: false, // API ์๋ต์ ํฌํจ๋์ง ์์
+ }));
+
+ setWinners(winnerList);
+ setIsDrawing(false);
+ setShowResults(true);
+ }, 3000);
+ } catch (err) {
+ console.error('Draw failed:', err);
setIsDrawing(false);
-
- // Select random winners
- const shuffled = [...mockEventData.participants].sort(() => Math.random() - 0.5);
- setWinners(shuffled.slice(0, winnerCount));
- setShowResults(true);
- }, 3000);
+ const errorMessage =
+ err && typeof err === 'object' && 'response' in err
+ ? (err as { response?: { data?: { message?: string } } }).response?.data?.message
+ : undefined;
+ setError(errorMessage || '์ถ์ฒจ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์.');
+ }
};
- const handleRedraw = () => {
+ const handleRedraw = async () => {
setRedrawDialogOpen(false);
setShowResults(false);
setWinners([]);
@@ -161,7 +216,7 @@ export default function DrawPage() {
const handleDownload = () => {
const now = new Date();
const dateStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}`;
- const filename = `๋น์ฒจ์๋ชฉ๋ก_${mockEventData.name}_${dateStr}.xlsx`;
+ const filename = `๋น์ฒจ์๋ชฉ๋ก_${eventName}_${dateStr}.xlsx`;
alert(`${filename} ๋ค์ด๋ก๋๋ฅผ ์์ํฉ๋๋ค`);
};
@@ -169,11 +224,6 @@ export default function DrawPage() {
router.push('/events');
};
- const handleHistoryDetail = (history: typeof mockDrawingHistory[0]) => {
- setSelectedHistory(history);
- setHistoryDetailOpen(true);
- };
-
const getRankClass = (rank: number) => {
if (rank === 1) return 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)';
if (rank === 2) return 'linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%)';
@@ -181,9 +231,32 @@ export default function DrawPage() {
return '#e0e0e0';
};
+ // ๋ก๋ฉ ์ํ
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
return (
+ {/* ์๋ฌ ๋ฉ์์ง */}
+ {error && (
+ setError(null)}>
+ {error}
+
+ )}
+
{/* Setup View (Before Drawing) */}
{!showResults && (
<>
@@ -214,7 +287,7 @@ export default function DrawPage() {
์ด๋ฒคํธ๋ช
- {mockEventData.name}
+ {eventName}
@@ -234,7 +307,7 @@ export default function DrawPage() {
์ด ์ฐธ์ฌ์
- {mockEventData.totalParticipants}๋ช
+ {totalParticipants}๋ช
@@ -372,65 +445,6 @@ export default function DrawPage() {
์ถ์ฒจ ์์
- {/* Drawing History */}
-
-
- ๐ ์ถ์ฒจ ์ด๋ ฅ
-
- {mockDrawingHistory.length === 0 ? (
-
-
-
-
- ์ถ์ฒจ ์ด๋ ฅ์ด ์์ต๋๋ค
-
-
-
- ) : (
-
- {mockDrawingHistory.slice(0, 3).map((history, index) => (
-
-
-
-
-
- {history.date} {history.isRedraw && '(์ฌ์ถ์ฒจ)'}
-
-
- ๋น์ฒจ์ {history.winnerCount}๋ช
-
-
-
-
-
-
- ))}
-
- )}
-
>
)}
@@ -443,8 +457,13 @@ export default function DrawPage() {
๐ ์ถ์ฒจ ์๋ฃ!
- ์ด {mockEventData.totalParticipants}๋ช
์ค {winnerCount}๋ช
๋น์ฒจ
+ ์ด {totalParticipants}๋ช
์ค {winners.length}๋ช
๋น์ฒจ
+ {drawResult && (
+
+ ์ถ์ฒจ ์ผ์: {new Date(drawResult.drawnAt).toLocaleString('ko-KR')}
+
+ )}
{/* Winner List */}
@@ -453,11 +472,10 @@ export default function DrawPage() {
๐ ๋น์ฒจ์ ๋ชฉ๋ก
- {winners.map((winner, index) => {
- const rank = index + 1;
+ {winners.map((winner) => {
return (
- {rank}์
+ {winner.rank}์
- ์๋ชจ๋ฒํธ: #{winner.id}
+ ์ฐธ์ฌ์ ID: {winner.participantId}
- {winner.name} ({winner.phone})
-
-
- ์ฐธ์ฌ: {winner.channel}{' '}
- {winner.hasBonus && storeBonus && '๐'}
+ {winner.name} ({winner.phoneNumber})
+ {winner.channel && (
+
+ ์ฐธ์ฌ: {winner.channel}{' '}
+ {winner.storeVisited && storeBonus && '๐'}
+
+ )}
@@ -671,8 +691,13 @@ export default function DrawPage() {
- ์ด {mockEventData.totalParticipants}๋ช
์ค {winnerCount}๋ช
์ ์ถ์ฒจํ์๊ฒ ์ต๋๊น?
+ ์ด {totalParticipants}๋ช
์ค {winnerCount}๋ช
์ ์ถ์ฒจํ์๊ฒ ์ต๋๊น?
+ {storeBonus && (
+
+ ๋งค์ฅ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ์๊ฒ 1.5๋ฐฐ ๊ฐ์ฐ์ ์ด ์ ์ฉ๋ฉ๋๋ค.
+
+ )}
- {/* Participants Count */}
+ {/* Participants Count Info */}
-
- ํ์ฌ {mockEventData.participants}๋ช
์ด
- ์ฐธ์ฌํ์ต๋๋ค
-
-
+
๋น์ฒจ์ ๋ฐํ: {mockEventData.announcementDate}
@@ -307,7 +353,7 @@ export default function EventParticipatePage() {
1. ์์งํ๋ ๊ฐ์ธ์ ๋ณด ํญ๋ชฉ
-
- ์ด๋ฆ, ์ ํ๋ฒํธ
+
- ์ด๋ฆ, ์ ํ๋ฒํธ, ์ด๋ฉ์ผ(์ ํ)
2. ๊ฐ์ธ์ ๋ณด์ ์์ง ๋ฐ ์ด์ฉ๋ชฉ์
diff --git a/src/app/(main)/events/create/page.tsx b/src/app/(main)/events/create/page.tsx
index 1595442..1fa7006 100644
--- a/src/app/(main)/events/create/page.tsx
+++ b/src/app/(main)/events/create/page.tsx
@@ -15,19 +15,50 @@ export type BudgetLevel = 'low' | 'medium' | 'high';
export type EventMethod = 'online' | 'offline';
export interface EventData {
+ eventDraftId?: number;
objective?: EventObjective;
recommendation?: {
- budget: BudgetLevel;
- method: EventMethod;
- title: string;
- prize: string;
- participationMethod: string;
- expectedParticipants: number;
- estimatedCost: number;
- roi: number;
+ recommendation: {
+ optionNumber: number;
+ concept: string;
+ title: string;
+ description: string;
+ targetAudience: string;
+ duration: {
+ recommendedDays: number;
+ recommendedPeriod?: string;
+ };
+ mechanics: {
+ type: 'DISCOUNT' | 'GIFT' | 'STAMP' | 'EXPERIENCE' | 'LOTTERY' | 'COMBO';
+ details: string;
+ };
+ promotionChannels: string[];
+ estimatedCost: {
+ min: number;
+ max: number;
+ breakdown?: {
+ material?: number;
+ promotion?: number;
+ discount?: number;
+ };
+ };
+ expectedMetrics: {
+ newCustomers: { min: number; max: number };
+ repeatVisits?: { min: number; max: number };
+ revenueIncrease: { min: number; max: number };
+ roi: { min: number; max: number };
+ socialEngagement?: {
+ estimatedPosts: number;
+ estimatedReach: number;
+ };
+ };
+ differentiator: string;
+ };
+ eventId: string;
};
contentPreview?: {
imageStyle: string;
+ images?: any[];
};
contentEdit?: {
title: string;
@@ -89,6 +120,18 @@ export default function EventCreatePage() {
);
if (needsContent) {
+ // localStorage์ ์ด๋ฒคํธ ์ ๋ณด ์ ์ฅ
+ const eventData = {
+ eventDraftId: context.recommendation?.eventId || String(Date.now()), // eventId ์ฌ์ฉ
+ eventTitle: context.recommendation?.recommendation.title || '',
+ eventDescription: context.recommendation?.recommendation.description || '',
+ industry: '',
+ location: '',
+ trends: context.recommendation?.recommendation.promotionChannels || [],
+ prize: '',
+ };
+ localStorage.setItem('eventCreationData', JSON.stringify(eventData));
+
history.push('contentPreview', { ...context, channels });
} else {
history.push('approval', { ...context, channels });
@@ -101,12 +144,13 @@ export default function EventCreatePage() {
)}
contentPreview={({ context, history }) => (
{
+ eventId={context.recommendation?.eventId}
+ eventTitle={context.recommendation?.recommendation.title}
+ eventDescription={context.recommendation?.recommendation.description}
+ onNext={(imageStyle, images) => {
history.push('contentEdit', {
...context,
- contentPreview: { imageStyle },
+ contentPreview: { imageStyle, images },
});
}}
onSkip={() => {
@@ -119,8 +163,8 @@ export default function EventCreatePage() {
)}
contentEdit={({ context, history }) => (
{
history.push('approval', { ...context, contentEdit });
}}
diff --git a/src/app/(main)/events/create/steps/ApprovalStep.tsx b/src/app/(main)/events/create/steps/ApprovalStep.tsx
index 465029e..2a349db 100644
--- a/src/app/(main)/events/create/steps/ApprovalStep.tsx
+++ b/src/app/(main)/events/create/steps/ApprovalStep.tsx
@@ -120,7 +120,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}
>
- {eventData.recommendation?.title || '์ด๋ฒคํธ ์ ๋ชฉ'}
+ {eventData.recommendation?.recommendation.title || '์ด๋ฒคํธ ์ ๋ชฉ'}
@@ -158,7 +158,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}
>
- {eventData.recommendation?.expectedParticipants || 0}
+ {eventData.recommendation?.recommendation.expectedMetrics.newCustomers.max || 0}
- {((eventData.recommendation?.estimatedCost || 0) / 10000).toFixed(0)}
+ {((eventData.recommendation?.recommendation.estimatedCost.max || 0) / 10000).toFixed(0)}
- {eventData.recommendation?.roi || 0}%
+ {eventData.recommendation?.recommendation.expectedMetrics.roi.max || 0}%
@@ -270,7 +270,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
์ด๋ฒคํธ ์ ๋ชฉ
- {eventData.recommendation?.title}
+ {eventData.recommendation?.recommendation.title}
@@ -288,7 +288,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
๊ฒฝํ
- {eventData.recommendation?.prize}
+ {eventData.recommendation?.recommendation.mechanics.details || ''}
@@ -306,7 +306,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
์ฐธ์ฌ ๋ฐฉ๋ฒ
- {eventData.recommendation?.participationMethod}
+ {eventData.recommendation?.recommendation.mechanics.details || ''}
diff --git a/src/app/(main)/events/create/steps/ContentPreviewStep.tsx b/src/app/(main)/events/create/steps/ContentPreviewStep.tsx
index 5a2f7df..ddead7a 100644
--- a/src/app/(main)/events/create/steps/ContentPreviewStep.tsx
+++ b/src/app/(main)/events/create/steps/ContentPreviewStep.tsx
@@ -12,8 +12,11 @@ import {
IconButton,
Dialog,
Grid,
+ Alert,
} from '@mui/material';
-import { ArrowBack, ZoomIn, Psychology } from '@mui/icons-material';
+import { ArrowBack, ZoomIn, Psychology, Refresh } from '@mui/icons-material';
+import { contentApi, ImageInfo } from '@/shared/api/contentApi';
+import Image from 'next/image';
// ๋์์ธ ์์คํ
์์
const colors = {
@@ -34,7 +37,7 @@ const colors = {
};
interface ImageStyle {
- id: string;
+ id: 'SIMPLE' | 'FANCY' | 'TRENDY';
name: string;
gradient?: string;
icon: string;
@@ -43,19 +46,19 @@ interface ImageStyle {
const imageStyles: ImageStyle[] = [
{
- id: 'simple',
+ id: 'SIMPLE',
name: '์คํ์ผ 1: ์ฌํ',
icon: 'celebration',
},
{
- id: 'fancy',
+ id: 'FANCY',
name: '์คํ์ผ 2: ํ๋ ค',
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
icon: 'auto_awesome',
textColor: 'white',
},
{
- id: 'trendy',
+ id: 'TRENDY',
name: '์คํ์ผ 3: ํธ๋ ๋',
gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
icon: 'trending_up',
@@ -64,50 +67,246 @@ const imageStyles: ImageStyle[] = [
];
interface ContentPreviewStepProps {
- title: string;
- prize: string;
- onNext: (imageStyle: string) => void;
+ eventId?: string;
+ eventTitle?: string;
+ eventDescription?: string;
+ onNext: (imageStyle: string, images: ImageInfo[]) => void;
onSkip: () => void;
onBack: () => void;
}
+interface EventCreationData {
+ eventDraftId: string;
+ eventTitle: string;
+ eventDescription: string;
+ industry: string;
+ location: string;
+ trends: string[];
+ prize: string;
+}
+
export default function ContentPreviewStep({
- title,
- prize,
+ eventId: propsEventId,
+ eventTitle: propsEventTitle,
+ eventDescription: propsEventDescription,
onNext,
onSkip,
onBack,
}: ContentPreviewStepProps) {
const [loading, setLoading] = useState(true);
- const [selectedStyle, setSelectedStyle] = useState(null);
+ const [selectedStyle, setSelectedStyle] = useState<'SIMPLE' | 'FANCY' | 'TRENDY' | null>(null);
const [fullscreenOpen, setFullscreenOpen] = useState(false);
- const [fullscreenStyle, setFullscreenStyle] = useState(null);
+ const [fullscreenImage, setFullscreenImage] = useState(null);
+ const [generatedImages, setGeneratedImages] = useState
- {fullscreenStyle && (
+ {fullscreenImage && (
-
- {fullscreenStyle.icon}
-
-
- {title}
-
-
- {prize}
-
+
)}
diff --git a/src/app/(main)/events/create/steps/RecommendationStep.tsx b/src/app/(main)/events/create/steps/RecommendationStep.tsx
index a201950..e3472b8 100644
--- a/src/app/(main)/events/create/steps/RecommendationStep.tsx
+++ b/src/app/(main)/events/create/steps/RecommendationStep.tsx
@@ -1,4 +1,6 @@
-import { useState } from 'react';
+'use client';
+
+import { useState, useEffect } from 'react';
import {
Box,
Container,
@@ -13,11 +15,12 @@ import {
RadioGroup,
FormControlLabel,
IconButton,
- Tabs,
- Tab,
+ CircularProgress,
+ Alert,
} from '@mui/material';
import { ArrowBack, Edit, Insights } from '@mui/icons-material';
import { EventObjective, BudgetLevel, EventMethod } from '../page';
+import { aiApi, eventApi, AIRecommendationResult, EventRecommendation } from '@/shared/api';
// ๋์์ธ ์์คํ
์์
const colors = {
@@ -37,130 +40,288 @@ const colors = {
},
};
-interface Recommendation {
- id: string;
- budget: BudgetLevel;
- method: EventMethod;
- title: string;
- prize: string;
- participationMethod: string;
- expectedParticipants: number;
- estimatedCost: number;
- roi: number;
-}
-
-// Mock ์ถ์ฒ ๋ฐ์ดํฐ
-const mockRecommendations: Recommendation[] = [
- // ์ ๋น์ฉ
- {
- id: 'low-online',
- budget: 'low',
- method: 'online',
- title: 'SNS ํ๋ก์ฐ ์ด๋ฒคํธ',
- prize: '์ปคํผ ์ฟ ํฐ',
- participationMethod: 'SNS ํ๋ก์ฐ',
- expectedParticipants: 180,
- estimatedCost: 250000,
- roi: 520,
- },
- {
- id: 'low-offline',
- budget: 'low',
- method: 'offline',
- title: '์ ํ๋ฒํธ ๋ฑ๋ก ์ด๋ฒคํธ',
- prize: '์ปคํผ ์ฟ ํฐ',
- participationMethod: '๋ฐฉ๋ฌธ ์ ์ ํ๋ฒํธ ๋ฑ๋ก',
- expectedParticipants: 120,
- estimatedCost: 300000,
- roi: 380,
- },
- // ์ค๋น์ฉ
- {
- id: 'medium-online',
- budget: 'medium',
- method: 'online',
- title: '๋ฆฌ๋ทฐ ์์ฑ ์ด๋ฒคํธ',
- prize: '์ํ๊ถ 5๋ง์',
- participationMethod: '๋ค์ด๋ฒ ๋ฆฌ๋ทฐ ์์ฑ',
- expectedParticipants: 250,
- estimatedCost: 800000,
- roi: 450,
- },
- {
- id: 'medium-offline',
- budget: 'medium',
- method: 'offline',
- title: '์คํฌํ ์ ๋ฆฝ ์ด๋ฒคํธ',
- prize: '์ํ๊ถ 5๋ง์',
- participationMethod: '3ํ ๋ฐฉ๋ฌธ ์ ์คํฌํ',
- expectedParticipants: 200,
- estimatedCost: 1000000,
- roi: 380,
- },
- // ๊ณ ๋น์ฉ
- {
- id: 'high-online',
- budget: 'high',
- method: 'online',
- title: '์ธํ๋ฃจ์ธ์ ํ์
์ด๋ฒคํธ',
- prize: '์ ํ ์์ดํ',
- participationMethod: '๊ฒ์๋ฌผ ๊ณต์ ๋ฐ ๋๊ธ',
- expectedParticipants: 500,
- estimatedCost: 2000000,
- roi: 380,
- },
- {
- id: 'high-offline',
- budget: 'high',
- method: 'offline',
- title: 'VIP ๊ณ ๊ฐ ์ด๋ ์ด๋ฒคํธ',
- prize: '์ ํ ์์ดํ',
- participationMethod: '๋์ 10ํ ๋ฐฉ๋ฌธ',
- expectedParticipants: 300,
- estimatedCost: 2500000,
- roi: 320,
- },
-];
-
interface RecommendationStepProps {
objective?: EventObjective;
- onNext: (data: Recommendation) => void;
+ eventId?: string; // ์ด์ ๋จ๊ณ์์ ์์ฑ๋ eventId
+ onNext: (data: {
+ recommendation: EventRecommendation;
+ eventId: string;
+ }) => void;
onBack: () => void;
}
-export default function RecommendationStep({ onNext, onBack }: RecommendationStepProps) {
- const [selectedBudget, setSelectedBudget] = useState('low');
- const [selected, setSelected] = useState(null);
- const [editedData, setEditedData] = useState>({});
+export default function RecommendationStep({
+ objective,
+ eventId: initialEventId,
+ onNext,
+ onBack
+}: RecommendationStepProps) {
+ const [eventId, setEventId] = useState(initialEventId || null);
+ const [jobId, setJobId] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [polling, setPolling] = useState(false);
+ const [error, setError] = useState(null);
- const budgetRecommendations = mockRecommendations.filter((r) => r.budget === selectedBudget);
+ const [aiResult, setAiResult] = useState(null);
+ const [selected, setSelected] = useState(null);
+ const [editedData, setEditedData] = useState>({});
- const handleNext = () => {
- const selectedRec = mockRecommendations.find((r) => r.id === selected);
- if (selectedRec && selected) {
- const edited = editedData[selected];
- onNext({
- ...selectedRec,
- title: edited?.title || selectedRec.title,
- prize: edited?.prize || selectedRec.prize,
- });
+ // ์ปดํฌ๋ํธ ๋ง์ดํธ ์ AI ์ถ์ฒ ์์ฒญ
+ useEffect(() => {
+ if (!eventId && objective) {
+ // Step 1: ์ด๋ฒคํธ ์์ฑ
+ createEventAndRequestAI();
+ } else if (eventId) {
+ // ์ด๋ฏธ eventId๊ฐ ์์ผ๋ฉด AI ์ถ์ฒ ์์ฒญ
+ requestAIRecommendations(eventId);
+ }
+ }, []);
+
+ const createEventAndRequestAI = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ // Step 1: ์ด๋ฒคํธ ๋ชฉ์ ์ ํ ๋ฐ ์์ฑ
+ const eventResponse = await eventApi.selectObjective(objective || '์ ๊ท ๊ณ ๊ฐ ์ ์น');
+ const newEventId = eventResponse.eventId;
+ setEventId(newEventId);
+
+ // Step 2: AI ์ถ์ฒ ์์ฒญ
+ await requestAIRecommendations(newEventId);
+ } catch (err: any) {
+ console.error('์ด๋ฒคํธ ์์ฑ ์คํจ:', err);
+ setError(err.response?.data?.message || '์ด๋ฒคํธ ์์ฑ์ ์คํจํ์ต๋๋ค');
+ setLoading(false);
}
};
- const handleEditTitle = (id: string, title: string) => {
+ const requestAIRecommendations = async (evtId: string) => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ // ์ฌ์ฉ์ ์ ๋ณด์์ ๋งค์ฅ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
+ const userProfile = JSON.parse(localStorage.getItem('userProfile') || '{}');
+ const storeInfo = {
+ storeId: userProfile.storeId || '1',
+ storeName: userProfile.storeName || '๋ด ๋งค์ฅ',
+ category: userProfile.industry || '์์์ ',
+ description: userProfile.businessHours || '',
+ };
+
+ // AI ์ถ์ฒ ์์ฒญ
+ const jobResponse = await eventApi.requestAiRecommendations(evtId, storeInfo);
+ setJobId(jobResponse.jobId);
+
+ // Job ํด๋ง ์์
+ pollJobStatus(jobResponse.jobId, evtId);
+ } catch (err: any) {
+ console.error('AI ์ถ์ฒ ์์ฒญ ์คํจ:', err);
+ setError(err.response?.data?.message || 'AI ์ถ์ฒ ์์ฒญ์ ์คํจํ์ต๋๋ค');
+ setLoading(false);
+ }
+ };
+
+ const pollJobStatus = async (jId: string, evtId: string) => {
+ setPolling(true);
+ const maxAttempts = 60; // ์ต๋ 5๋ถ (5์ด ๊ฐ๊ฒฉ)
+ let attempts = 0;
+
+ const poll = async () => {
+ try {
+ const status = await eventApi.getJobStatus(jId);
+ console.log('Job ์ํ:', status);
+
+ if (status.status === 'COMPLETED') {
+ // AI ์ถ์ฒ ๊ฒฐ๊ณผ ์กฐํ
+ const recommendations = await aiApi.getRecommendations(evtId);
+ setAiResult(recommendations);
+ setLoading(false);
+ setPolling(false);
+ return;
+ } else if (status.status === 'FAILED') {
+ setError(status.errorMessage || 'AI ์ถ์ฒ ์์ฑ์ ์คํจํ์ต๋๋ค');
+ setLoading(false);
+ setPolling(false);
+ return;
+ }
+
+ // ๊ณ์ ํด๋ง
+ attempts++;
+ if (attempts < maxAttempts) {
+ setTimeout(poll, 5000); // 5์ด ํ ์ฌ์๋
+ } else {
+ setError('AI ์ถ์ฒ ์์ฑ ์๊ฐ์ด ์ด๊ณผ๋์์ต๋๋ค');
+ setLoading(false);
+ setPolling(false);
+ }
+ } catch (err: any) {
+ console.error('Job ์ํ ์กฐํ ์คํจ:', err);
+ setError(err.response?.data?.message || 'Job ์ํ ์กฐํ์ ์คํจํ์ต๋๋ค');
+ setLoading(false);
+ setPolling(false);
+ }
+ };
+
+ poll();
+ };
+
+ const handleNext = async () => {
+ if (selected === null || !aiResult || !eventId) return;
+
+ const selectedRec = aiResult.recommendations[selected - 1];
+ const edited = editedData[selected];
+
+ try {
+ setLoading(true);
+
+ // AI ์ถ์ฒ ์ ํ API ํธ์ถ
+ await eventApi.selectRecommendation(eventId, {
+ recommendationId: `${eventId}-opt${selected}`,
+ customizations: {
+ eventName: edited?.title || selectedRec.title,
+ description: edited?.description || selectedRec.description,
+ },
+ });
+
+ // ๋ค์ ๋จ๊ณ๋ก ์ด๋
+ onNext({
+ recommendation: {
+ ...selectedRec,
+ title: edited?.title || selectedRec.title,
+ description: edited?.description || selectedRec.description,
+ },
+ eventId,
+ });
+ } catch (err: any) {
+ console.error('์ถ์ฒ ์ ํ ์คํจ:', err);
+ setError(err.response?.data?.message || '์ถ์ฒ ์ ํ์ ์คํจํ์ต๋๋ค');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleEditTitle = (optionNumber: number, title: string) => {
setEditedData((prev) => ({
...prev,
- [id]: { ...prev[id], title },
+ [optionNumber]: {
+ ...prev[optionNumber],
+ title
+ },
}));
};
- const handleEditPrize = (id: string, prize: string) => {
+ const handleEditDescription = (optionNumber: number, description: string) => {
setEditedData((prev) => ({
...prev,
- [id]: { ...prev[id], prize },
+ [optionNumber]: {
+ ...prev[optionNumber],
+ description
+ },
}));
};
+ // ๋ก๋ฉ ์ํ ํ์
+ if (loading || polling) {
+ return (
+
+
+
+
+
+
+
+ AI ์ด๋ฒคํธ ์ถ์ฒ
+
+
+
+
+
+
+ AI๊ฐ ์ต์ ์ ์ด๋ฒคํธ๋ฅผ ์์ฑํ๊ณ ์์ต๋๋ค...
+
+
+ ์
์ข
, ์ง์ญ, ์์ฆ ํธ๋ ๋๋ฅผ ๋ถ์ํ์ฌ ๋ง์ถคํ ์ด๋ฒคํธ๋ฅผ ์ถ์ฒํฉ๋๋ค
+
+
+
+
+ );
+ }
+
+ // ์๋ฌ ์ํ ํ์
+ if (error) {
+ return (
+
+
+
+
+
+
+
+ AI ์ด๋ฒคํธ ์ถ์ฒ
+
+
+
+
+ {error}
+
+
+
+
+ ์ด์
+
+ {
+ setError(null);
+ if (eventId) {
+ requestAIRecommendations(eventId);
+ } else {
+ createEventAndRequestAI();
+ }
+ }}
+ sx={{
+ py: 3,
+ borderRadius: 3,
+ fontSize: '1rem',
+ fontWeight: 700,
+ background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
+ }}
+ >
+ ๋ค์ ์๋
+
+
+
+
+ );
+ }
+
+ // AI ๊ฒฐ๊ณผ๊ฐ ์์ผ๋ฉด ๋ก๋ฉ ํ์
+ if (!aiResult) {
+ return (
+
+
+
+
+
+ );
+ }
+
return (
@@ -195,158 +356,159 @@ export default function RecommendationStep({ onNext, onBack }: RecommendationSte
๐ ์
์ข
ํธ๋ ๋
-
- ์์์ ์
์ ๋
ํ๋ก๋ชจ์
ํธ๋ ๋
-
+ {aiResult.trendAnalysis.industryTrends.slice(0, 3).map((trend, idx) => (
+
+ โข {trend.description}
+
+ ))}
๐บ๏ธ ์ง์ญ ํธ๋ ๋
-
- ๊ฐ๋จ๊ตฌ ์์์ ํ ์ธ ์ด๋ฒคํธ ์ฆ๊ฐ
-
+ {aiResult.trendAnalysis.regionalTrends.slice(0, 3).map((trend, idx) => (
+
+ โข {trend.description}
+
+ ))}
โ๏ธ ์์ฆ ํธ๋ ๋
-
- ์ค ์ฐํด ํน์ ๋๋น ๊ณ ๊ฐ ์ ์น ์ ๋ต
-
+ {aiResult.trendAnalysis.seasonalTrends.slice(0, 3).map((trend, idx) => (
+
+ โข {trend.description}
+
+ ))}
- {/* Budget Selection */}
+ {/* AI Recommendations */}
- ์์ฐ๋ณ ์ถ์ฒ ์ด๋ฒคํธ
+ AI ์ถ์ฒ ์ด๋ฒคํธ ({aiResult.recommendations.length}๊ฐ์ง ์ต์
)
- ๊ฐ ์์ฐ๋ณ 2๊ฐ์ง ๋ฐฉ์ (์จ๋ผ์ธ 1๊ฐ, ์คํ๋ผ์ธ 1๊ฐ)์ ์ถ์ฒํฉ๋๋ค
+ ๊ฐ ์ต์
์ ์ฐจ๋ณํ๋ ์ปจ์
์ผ๋ก ๊ตฌ์ฑ๋์ด ์์ต๋๋ค. ์ํ์๋ ์ต์
์ ์ ํํ๊ณ ์์ ํ ์ ์์ต๋๋ค.
- setSelectedBudget(value)}
- variant="fullWidth"
- sx={{ mb: 8 }}
- >
-
-
-
-
{/* Recommendations */}
- setSelected(e.target.value)}>
+ setSelected(Number(e.target.value))}>
- {budgetRecommendations.map((rec) => (
-
+ {aiResult.recommendations.map((rec) => (
+
setSelected(rec.id)}
+ onClick={() => setSelected(rec.optionNumber)}
>
-
+
+
+
+ }
+ label=""
+ sx={{ m: 0 }}
/>
- } label="" sx={{ m: 0 }} />
handleEditTitle(rec.id, e.target.value)}
+ value={editedData[rec.optionNumber]?.title || rec.title}
+ onChange={(e) => handleEditTitle(rec.optionNumber, e.target.value)}
onClick={(e) => e.stopPropagation()}
sx={{ mb: 4 }}
InputProps={{
endAdornment: ,
- sx: { fontSize: '1rem', py: 2 },
+ sx: { fontSize: '1.1rem', fontWeight: 600, py: 2 },
}}
/>
-
-
- ๊ฒฝํ
-
- handleEditPrize(rec.id, e.target.value)}
- onClick={(e) => e.stopPropagation()}
- InputProps={{
- endAdornment: ,
- sx: { fontSize: '1rem' },
- }}
- />
-
+ handleEditDescription(rec.optionNumber, e.target.value)}
+ onClick={(e) => e.stopPropagation()}
+ sx={{ mb: 4 }}
+ InputProps={{
+ sx: { fontSize: '1rem' },
+ }}
+ />
-
-
+
+
- ์ฐธ์ฌ ๋ฐฉ๋ฒ
+ ํ๊ฒ ๊ณ ๊ฐ
- {rec.participationMethod}
+ {rec.targetAudience}
-
-
- ์์ ์ฐธ์ฌ
-
-
- {rec.expectedParticipants}๋ช
-
-
-
+
์์ ๋น์ฉ
- {(rec.estimatedCost / 10000).toFixed(0)}๋ง์
+ {(rec.estimatedCost.min / 10000).toFixed(0)}~{(rec.estimatedCost.max / 10000).toFixed(0)}๋ง์
-
+
- ํฌ์๋๋น์์ต๋ฅ
+ ์์ ์ ๊ท ๊ณ ๊ฐ
+
+
+ {rec.expectedMetrics.newCustomers.min}~{rec.expectedMetrics.newCustomers.max}๋ช
+
+
+
+
+ ROI
- {rec.roi}%
+ {rec.expectedMetrics.roi.min}~{rec.expectedMetrics.roi.max}%
+
+
+
+
+ ์ฐจ๋ณ์
+
+
+ {rec.differentiator}
@@ -381,7 +543,7 @@ export default function RecommendationStep({ onNext, onBack }: RecommendationSte
fullWidth
variant="contained"
size="large"
- disabled={!selected}
+ disabled={selected === null || loading}
onClick={handleNext}
sx={{
py: 3,
@@ -398,7 +560,7 @@ export default function RecommendationStep({ onNext, onBack }: RecommendationSte
},
}}
>
- ๋ค์
+ {loading ? : '๋ค์'}
diff --git a/src/app/api/content/events/[eventDraftId]/images/route.ts b/src/app/api/content/events/[eventDraftId]/images/route.ts
new file mode 100644
index 0000000..77e12cf
--- /dev/null
+++ b/src/app/api/content/events/[eventDraftId]/images/route.ts
@@ -0,0 +1,52 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+const CONTENT_API_BASE_URL = process.env.NEXT_PUBLIC_CONTENT_API_URL || 'http://localhost:8084';
+
+export async function GET(
+ request: NextRequest,
+ context: { params: Promise<{ eventDraftId: string }> }
+) {
+ try {
+ const { eventDraftId } = await context.params;
+ const { searchParams } = new URL(request.url);
+ const style = searchParams.get('style');
+ const platform = searchParams.get('platform');
+
+ // eventDraftId is now eventId in the API
+ let url = `${CONTENT_API_BASE_URL}/api/v1/content/events/${eventDraftId}/images`;
+ const queryParams = [];
+ if (style) queryParams.push(`style=${style}`);
+ if (platform) queryParams.push(`platform=${platform}`);
+ if (queryParams.length > 0) {
+ url += `?${queryParams.join('&')}`;
+ }
+
+ console.log('๐ Proxying images request to Content API:', { url });
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error('โ Content API error:', response.status, errorText);
+ return NextResponse.json(
+ { error: 'Failed to get images', details: errorText },
+ { status: response.status }
+ );
+ }
+
+ const data = await response.json();
+
+ return NextResponse.json(data);
+ } catch (error) {
+ console.error('โ Proxy error:', error);
+ return NextResponse.json(
+ { error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/src/app/api/content/images/generate/route.ts b/src/app/api/content/images/generate/route.ts
new file mode 100644
index 0000000..98a6e84
--- /dev/null
+++ b/src/app/api/content/images/generate/route.ts
@@ -0,0 +1,42 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+const CONTENT_API_BASE_URL = process.env.NEXT_PUBLIC_CONTENT_API_URL || 'http://localhost:8084';
+
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+
+ console.log('๐ Proxying image generation request to Content API:', {
+ url: `${CONTENT_API_BASE_URL}/api/v1/content/images/generate`,
+ body,
+ });
+
+ const response = await fetch(`${CONTENT_API_BASE_URL}/api/v1/content/images/generate`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error('โ Content API error:', response.status, errorText);
+ return NextResponse.json(
+ { error: 'Failed to generate images', details: errorText },
+ { status: response.status }
+ );
+ }
+
+ const data = await response.json();
+ console.log('โ
Image generation job created:', data);
+
+ return NextResponse.json(data);
+ } catch (error) {
+ console.error('โ Proxy error:', error);
+ return NextResponse.json(
+ { error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/src/app/api/content/images/jobs/[jobId]/route.ts b/src/app/api/content/images/jobs/[jobId]/route.ts
new file mode 100644
index 0000000..fbfde54
--- /dev/null
+++ b/src/app/api/content/images/jobs/[jobId]/route.ts
@@ -0,0 +1,42 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+const CONTENT_API_BASE_URL = process.env.NEXT_PUBLIC_CONTENT_API_URL || 'http://localhost:8084';
+
+export async function GET(
+ request: NextRequest,
+ context: { params: Promise<{ jobId: string }> }
+) {
+ try {
+ const { jobId } = await context.params;
+
+ console.log('๐ Proxying job status request to Content API:', {
+ url: `${CONTENT_API_BASE_URL}/api/v1/content/images/jobs/${jobId}`,
+ });
+
+ const response = await fetch(`${CONTENT_API_BASE_URL}/api/v1/content/images/jobs/${jobId}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error('โ Content API error:', response.status, errorText);
+ return NextResponse.json(
+ { error: 'Failed to get job status', details: errorText },
+ { status: response.status }
+ );
+ }
+
+ const data = await response.json();
+
+ return NextResponse.json(data);
+ } catch (error) {
+ console.error('โ Proxy error:', error);
+ return NextResponse.json(
+ { error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/src/shared/api/aiApi.ts b/src/shared/api/aiApi.ts
new file mode 100644
index 0000000..c541eb8
--- /dev/null
+++ b/src/shared/api/aiApi.ts
@@ -0,0 +1,178 @@
+import axios, { AxiosInstance } from 'axios';
+
+// AI Service API ํด๋ผ์ด์ธํธ
+const AI_API_BASE_URL = process.env.NEXT_PUBLIC_AI_HOST || 'http://localhost:8083';
+
+export const aiApiClient: AxiosInstance = axios.create({
+ baseURL: AI_API_BASE_URL,
+ timeout: 300000, // AI ์์ฑ์ ์ต๋ 5๋ถ
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// Request interceptor
+aiApiClient.interceptors.request.use(
+ (config) => {
+ console.log('๐ค AI 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}`;
+ }
+ return config;
+ },
+ (error) => {
+ console.error('โ AI API Request Error:', error);
+ return Promise.reject(error);
+ }
+);
+
+// Response interceptor
+aiApiClient.interceptors.response.use(
+ (response) => {
+ console.log('โ
AI API Response:', {
+ status: response.status,
+ url: response.config.url,
+ data: response.data,
+ });
+ return response;
+ },
+ (error) => {
+ console.error('โ AI API Error:', {
+ message: error.message,
+ status: error.response?.status,
+ url: error.config?.url,
+ data: error.response?.data,
+ });
+ return Promise.reject(error);
+ }
+);
+
+// Types
+export interface TrendKeyword {
+ keyword: string;
+ relevance: number;
+ description: string;
+}
+
+export interface TrendAnalysis {
+ industryTrends: TrendKeyword[];
+ regionalTrends: TrendKeyword[];
+ seasonalTrends: TrendKeyword[];
+}
+
+export interface ExpectedMetrics {
+ newCustomers: {
+ min: number;
+ max: number;
+ };
+ repeatVisits?: {
+ min: number;
+ max: number;
+ };
+ revenueIncrease: {
+ min: number;
+ max: number;
+ };
+ roi: {
+ min: number;
+ max: number;
+ };
+ socialEngagement?: {
+ estimatedPosts: number;
+ estimatedReach: number;
+ };
+}
+
+export interface EventRecommendation {
+ optionNumber: number;
+ concept: string;
+ title: string;
+ description: string;
+ targetAudience: string;
+ duration: {
+ recommendedDays: number;
+ recommendedPeriod?: string;
+ };
+ mechanics: {
+ type: 'DISCOUNT' | 'GIFT' | 'STAMP' | 'EXPERIENCE' | 'LOTTERY' | 'COMBO';
+ details: string;
+ };
+ promotionChannels: string[];
+ estimatedCost: {
+ min: number;
+ max: number;
+ breakdown?: {
+ material?: number;
+ promotion?: number;
+ discount?: number;
+ };
+ };
+ expectedMetrics: ExpectedMetrics;
+ differentiator: string;
+}
+
+export interface AIRecommendationResult {
+ eventId: string;
+ trendAnalysis: TrendAnalysis;
+ recommendations: EventRecommendation[];
+ generatedAt: string;
+ expiresAt: string;
+ aiProvider: 'CLAUDE' | 'GPT4';
+}
+
+export interface JobStatusResponse {
+ jobId: string;
+ status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
+ progress: number;
+ message: string;
+ eventId?: string;
+ createdAt: string;
+ startedAt?: string;
+ completedAt?: string;
+ failedAt?: string;
+ errorMessage?: string;
+ retryCount?: number;
+ processingTimeMs?: number;
+}
+
+export interface HealthCheckResponse {
+ status: 'UP' | 'DOWN' | 'DEGRADED';
+ timestamp: string;
+ services: {
+ kafka: 'UP' | 'DOWN';
+ redis: 'UP' | 'DOWN';
+ claude_api: 'UP' | 'DOWN' | 'CIRCUIT_OPEN';
+ gpt4_api?: 'UP' | 'DOWN' | 'CIRCUIT_OPEN';
+ circuit_breaker: 'CLOSED' | 'OPEN' | 'HALF_OPEN';
+ };
+}
+
+// API Functions
+export const aiApi = {
+ // ํฌ์ค์ฒดํฌ
+ healthCheck: async (): Promise => {
+ const response = await aiApiClient.get('/health');
+ return response.data;
+ },
+
+ // Job ์ํ ์กฐํ (Internal API)
+ getJobStatus: async (jobId: string): Promise => {
+ const response = await aiApiClient.get(`/internal/jobs/${jobId}/status`);
+ return response.data;
+ },
+
+ // AI ์ถ์ฒ ๊ฒฐ๊ณผ ์กฐํ (Internal API)
+ getRecommendations: async (eventId: string): Promise => {
+ const response = await aiApiClient.get(`/internal/recommendations/${eventId}`);
+ return response.data;
+ },
+};
+
+export default aiApi;
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/contentApi.ts b/src/shared/api/contentApi.ts
new file mode 100644
index 0000000..8a9cc15
--- /dev/null
+++ b/src/shared/api/contentApi.ts
@@ -0,0 +1,160 @@
+import axios, { AxiosInstance } from 'axios';
+
+// Use Next.js API proxy to bypass CORS issues
+const CONTENT_API_BASE_URL = '/api/content';
+
+export const contentApiClient: AxiosInstance = axios.create({
+ baseURL: CONTENT_API_BASE_URL,
+ timeout: 120000, // ์ด๋ฏธ์ง ์์ฑ์ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆด ์ ์์ผ๋ฏ๋ก 120์ด
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// Request interceptor
+contentApiClient.interceptors.request.use(
+ (config) => {
+ console.log('๐จ Content 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}`;
+ }
+ return config;
+ },
+ (error) => {
+ console.error('โ Content API Request Error:', error);
+ return Promise.reject(error);
+ }
+);
+
+// Response interceptor
+contentApiClient.interceptors.response.use(
+ (response) => {
+ console.log('โ
Content API Response:', {
+ status: response.status,
+ url: response.config.url,
+ data: response.data,
+ });
+ return response;
+ },
+ (error) => {
+ console.error('โ Content API Error:', {
+ message: error.message,
+ status: error.response?.status,
+ url: error.config?.url,
+ data: error.response?.data,
+ });
+ return Promise.reject(error);
+ }
+);
+
+// Types
+export interface GenerateImagesRequest {
+ eventId: string;
+ eventTitle: string;
+ eventDescription: string;
+ industry?: string;
+ location?: string;
+ trends?: string[];
+ styles: ('SIMPLE' | 'FANCY' | 'TRENDY')[];
+ platforms: ('INSTAGRAM' | 'NAVER' | 'KAKAO')[];
+}
+
+export interface JobInfo {
+ id: string;
+ eventId: string;
+ jobType: string;
+ status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
+ progress: number;
+ resultMessage?: string;
+ errorMessage?: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface ImageInfo {
+ id: number;
+ eventId: string;
+ style: 'SIMPLE' | 'FANCY' | 'TRENDY';
+ platform: 'INSTAGRAM' | 'NAVER' | 'KAKAO';
+ cdnUrl: string;
+ prompt: string;
+ selected: boolean;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface ContentInfo {
+ id: number;
+ eventId: string;
+ eventTitle: string;
+ eventDescription: string;
+ images: ImageInfo[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+// API Functions
+export const contentApi = {
+ // ์ด๋ฏธ์ง ์์ฑ (Next.js API proxy ์ฌ์ฉ)
+ generateImages: async (request: GenerateImagesRequest): Promise => {
+ const response = await contentApiClient.post('/images/generate', request);
+ return response.data;
+ },
+
+ // Job ์ํ ์กฐํ (Next.js API proxy ์ฌ์ฉ)
+ getJobStatus: async (jobId: string): Promise => {
+ const response = await contentApiClient.get(`/images/jobs/${jobId}`);
+ return response.data;
+ },
+
+ // ์ด๋ฒคํธ๋ณ ์ฝํ
์ธ ์กฐํ
+ getContentByEventId: async (eventId: string): Promise => {
+ const response = await contentApiClient.get(`/events/${eventId}`);
+ return response.data;
+ },
+
+ // ์ด๋ฏธ์ง ๋ชฉ๋ก ์กฐํ (Next.js API proxy ์ฌ์ฉ)
+ getImages: async (
+ eventId: string,
+ style?: 'SIMPLE' | 'FANCY' | 'TRENDY',
+ platform?: 'INSTAGRAM' | 'NAVER' | 'KAKAO'
+ ): Promise => {
+ const params = new URLSearchParams();
+ if (style) params.append('style', style);
+ if (platform) params.append('platform', platform);
+
+ const response = await contentApiClient.get(
+ `/events/${eventId}/images${params.toString() ? `?${params.toString()}` : ''}`
+ );
+ return response.data;
+ },
+
+ // ํน์ ์ด๋ฏธ์ง ์กฐํ
+ getImageById: async (imageId: number): Promise => {
+ const response = await contentApiClient.get(`/api/v1/content/images/${imageId}`);
+ return response.data;
+ },
+
+ // ์ด๋ฏธ์ง ์ญ์
+ deleteImage: async (imageId: number): Promise => {
+ await contentApiClient.delete(`/api/v1/content/images/${imageId}`);
+ },
+
+ // ์ด๋ฏธ์ง ์ฌ์์ฑ
+ regenerateImage: async (imageId: number, newPrompt?: string): Promise => {
+ const response = await contentApiClient.post(
+ `/api/v1/content/images/${imageId}/regenerate`,
+ { imageId, newPrompt }
+ );
+ return response.data;
+ },
+};
+
+export default contentApi;
diff --git a/src/shared/api/eventApi.ts b/src/shared/api/eventApi.ts
new file mode 100644
index 0000000..4e9465f
--- /dev/null
+++ b/src/shared/api/eventApi.ts
@@ -0,0 +1,329 @@
+import axios, { AxiosInstance } from 'axios';
+
+// Event Service API ํด๋ผ์ด์ธํธ
+const EVENT_API_BASE_URL = process.env.NEXT_PUBLIC_EVENT_HOST || 'http://localhost:8080';
+const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'api';
+
+export const eventApiClient: AxiosInstance = axios.create({
+ baseURL: `${EVENT_API_BASE_URL}/${API_VERSION}`,
+ timeout: 30000, // Job ํด๋ง ๊ณ ๋ ค
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// Request interceptor
+eventApiClient.interceptors.request.use(
+ (config) => {
+ console.log('๐
Event 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}`;
+ }
+ return config;
+ },
+ (error) => {
+ console.error('โ Event API Request Error:', error);
+ return Promise.reject(error);
+ }
+);
+
+// Response interceptor
+eventApiClient.interceptors.response.use(
+ (response) => {
+ console.log('โ
Event API Response:', {
+ status: response.status,
+ url: response.config.url,
+ data: response.data,
+ });
+ return response;
+ },
+ (error) => {
+ console.error('โ Event API Error:', {
+ message: error.message,
+ status: error.response?.status,
+ url: error.config?.url,
+ data: error.response?.data,
+ });
+ return Promise.reject(error);
+ }
+);
+
+// Types
+export interface EventObjectiveRequest {
+ objective: string; // "์ ๊ท ๊ณ ๊ฐ ์ ์น", "์ฌ๋ฐฉ๋ฌธ ์ ๋", "๋งค์ถ ์ฆ๋", "๋ธ๋๋ ์ธ์ง๋ ํฅ์"
+}
+
+export interface EventCreatedResponse {
+ eventId: string;
+ status: 'DRAFT' | 'PUBLISHED' | 'ENDED';
+ objective: string;
+ createdAt: string;
+}
+
+export interface AiRecommendationRequest {
+ storeInfo: {
+ storeId: string;
+ storeName: string;
+ category: string;
+ description?: string;
+ };
+}
+
+export interface JobAcceptedResponse {
+ jobId: string;
+ status: 'PENDING';
+ message: string;
+}
+
+export interface EventJobStatusResponse {
+ jobId: string;
+ jobType: 'AI_RECOMMENDATION' | 'IMAGE_GENERATION';
+ status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
+ progress: number;
+ resultKey?: string;
+ errorMessage?: string;
+ createdAt: string;
+ completedAt?: string;
+}
+
+export interface SelectRecommendationRequest {
+ recommendationId: string;
+ customizations?: {
+ eventName?: string;
+ description?: string;
+ startDate?: string;
+ endDate?: string;
+ discountRate?: number;
+ };
+}
+
+export interface ImageGenerationRequest {
+ eventInfo: {
+ eventName: string;
+ description: string;
+ promotionType: string;
+ };
+ imageCount?: number;
+}
+
+export interface SelectChannelsRequest {
+ channels: ('WEBSITE' | 'KAKAO' | 'INSTAGRAM' | 'FACEBOOK' | 'NAVER_BLOG')[];
+}
+
+export interface ChannelDistributionResult {
+ channel: string;
+ success: boolean;
+ url?: string;
+ message: string;
+}
+
+export interface EventPublishedResponse {
+ eventId: string;
+ status: 'PUBLISHED';
+ publishedAt: string;
+ channels: string[];
+ distributionResults: ChannelDistributionResult[];
+}
+
+export interface EventSummary {
+ eventId: string;
+ eventName: string;
+ objective: string;
+ status: 'DRAFT' | 'PUBLISHED' | 'ENDED';
+ startDate: string;
+ endDate: string;
+ thumbnailUrl?: string;
+ createdAt: string;
+}
+
+export interface PageInfo {
+ page: number;
+ size: number;
+ totalElements: number;
+ totalPages: number;
+}
+
+export interface EventListResponse {
+ content: EventSummary[];
+ page: PageInfo;
+}
+
+export interface GeneratedImage {
+ imageId: string;
+ imageUrl: string;
+ isSelected: boolean;
+ createdAt: string;
+}
+
+export interface AiRecommendation {
+ recommendationId: string;
+ eventName: string;
+ description: string;
+ promotionType: string;
+ targetAudience: string;
+ isSelected: boolean;
+}
+
+export interface EventDetailResponse {
+ eventId: string;
+ userId: string;
+ storeId: string;
+ eventName: string;
+ objective: string;
+ description: string;
+ targetAudience: string;
+ promotionType: string;
+ discountRate?: number;
+ startDate: string;
+ endDate: string;
+ status: 'DRAFT' | 'PUBLISHED' | 'ENDED';
+ selectedImageId?: string;
+ selectedImageUrl?: string;
+ generatedImages?: GeneratedImage[];
+ channels?: string[];
+ aiRecommendations?: AiRecommendation[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface UpdateEventRequest {
+ eventName?: string;
+ description?: string;
+ startDate?: string;
+ endDate?: string;
+ discountRate?: number;
+}
+
+export interface EndEventRequest {
+ reason: string;
+}
+
+// API Functions
+export const eventApi = {
+ // Step 1: ๋ชฉ์ ์ ํ ๋ฐ ์ด๋ฒคํธ ์์ฑ
+ selectObjective: async (objective: string): Promise => {
+ const response = await eventApiClient.post('/events/objectives', {
+ objective,
+ });
+ return response.data;
+ },
+
+ // Step 2: AI ์ถ์ฒ ์์ฒญ
+ requestAiRecommendations: async (
+ eventId: string,
+ storeInfo: AiRecommendationRequest['storeInfo']
+ ): Promise => {
+ const response = await eventApiClient.post(
+ `/events/${eventId}/ai-recommendations`,
+ { storeInfo }
+ );
+ return response.data;
+ },
+
+ // Job ์ํ ํด๋ง
+ getJobStatus: async (jobId: string): Promise => {
+ const response = await eventApiClient.get(`/jobs/${jobId}`);
+ return response.data;
+ },
+
+ // AI ์ถ์ฒ ์ ํ
+ selectRecommendation: async (
+ eventId: string,
+ request: SelectRecommendationRequest
+ ): Promise => {
+ const response = await eventApiClient.put(
+ `/events/${eventId}/recommendations`,
+ request
+ );
+ return response.data;
+ },
+
+ // Step 3: ์ด๋ฏธ์ง ์์ฑ ์์ฒญ
+ requestImageGeneration: async (
+ eventId: string,
+ request: ImageGenerationRequest
+ ): Promise => {
+ const response = await eventApiClient.post(`/events/${eventId}/images`, request);
+ return response.data;
+ },
+
+ // ์ด๋ฏธ์ง ์ ํ
+ selectImage: async (eventId: string, imageId: string): Promise => {
+ const response = await eventApiClient.put(
+ `/events/${eventId}/images/${imageId}/select`
+ );
+ return response.data;
+ },
+
+ // Step 4: ์ด๋ฏธ์ง ํธ์ง
+ editImage: async (
+ eventId: string,
+ imageId: string,
+ editRequest: any
+ ): Promise<{ imageId: string; imageUrl: string; editedAt: string }> => {
+ const response = await eventApiClient.put(`/events/${eventId}/images/${imageId}/edit`, editRequest);
+ return response.data;
+ },
+
+ // Step 5: ๋ฐฐํฌ ์ฑ๋ ์ ํ
+ selectChannels: async (eventId: string, channels: string[]): Promise => {
+ const response = await eventApiClient.put(`/events/${eventId}/channels`, {
+ channels,
+ });
+ return response.data;
+ },
+
+ // Step 6: ์ต์ข
๋ฐฐํฌ
+ publishEvent: async (eventId: string): Promise => {
+ const response = await eventApiClient.post(`/events/${eventId}/publish`);
+ return response.data;
+ },
+
+ // ์ด๋ฒคํธ ๋ชฉ๋ก ์กฐํ
+ getEvents: async (params?: {
+ status?: 'DRAFT' | 'PUBLISHED' | 'ENDED';
+ objective?: string;
+ search?: string;
+ page?: number;
+ size?: number;
+ sort?: string;
+ order?: 'asc' | 'desc';
+ }): Promise => {
+ const response = await eventApiClient.get('/events', { params });
+ return response.data;
+ },
+
+ // ์ด๋ฒคํธ ์์ธ ์กฐํ
+ getEventDetail: async (eventId: string): Promise => {
+ const response = await eventApiClient.get(`/events/${eventId}`);
+ return response.data;
+ },
+
+ // ์ด๋ฒคํธ ์์
+ updateEvent: async (eventId: string, request: UpdateEventRequest): Promise => {
+ const response = await eventApiClient.put(`/events/${eventId}`, request);
+ return response.data;
+ },
+
+ // ์ด๋ฒคํธ ์ญ์
+ deleteEvent: async (eventId: string): Promise => {
+ await eventApiClient.delete(`/events/${eventId}`);
+ },
+
+ // ์ด๋ฒคํธ ์กฐ๊ธฐ ์ข
๋ฃ
+ endEvent: async (eventId: string, reason: string): Promise => {
+ const response = await eventApiClient.post(`/events/${eventId}/end`, {
+ reason,
+ });
+ return response.data;
+ },
+};
+
+export default eventApi;
diff --git a/src/shared/api/index.ts b/src/shared/api/index.ts
index 51e397f..2afee3f 100644
--- a/src/shared/api/index.ts
+++ b/src/shared/api/index.ts
@@ -1,2 +1,6 @@
-export { apiClient } from './client';
+export { apiClient, participationClient } from './client';
export type { ApiError } from './types';
+export * from './contentApi';
+export * from './aiApi';
+export * from './eventApi';
+export * from './participation.api';
diff --git a/src/shared/api/participation.api.ts b/src/shared/api/participation.api.ts
new file mode 100644
index 0000000..3eeb37b
--- /dev/null
+++ b/src/shared/api/participation.api.ts
@@ -0,0 +1,153 @@
+import { participationClient } from './client';
+import type {
+ ApiResponse,
+ PageResponse,
+ ParticipationRequest,
+ ParticipationResponse,
+ GetParticipantsParams,
+} from '../types/api.types';
+
+/**
+ * Participation API Service
+ * ์ด๋ฒคํธ ์ฐธ์ฌ ๊ด๋ จ API ํจ์๋ค
+ */
+
+/**
+ * ์ด๋ฒคํธ ์ฐธ์ฌ ์ ์ฒญ
+ * POST /v1/events/{eventId}/participate
+ */
+export const participate = async (
+ eventId: string,
+ data: ParticipationRequest
+): Promise> => {
+ const response = await participationClient.post>(
+ `/v1/events/${eventId}/participate`,
+ data
+ );
+ return response.data;
+};
+
+/**
+ * ์ฐธ์ฌ์ ๋ชฉ๋ก ์กฐํ (ํ์ด์ง)
+ * GET /v1/events/{eventId}/participants
+ */
+export const getParticipants = async (
+ params: GetParticipantsParams
+): Promise>> => {
+ const { eventId, storeVisited, page = 0, size = 20, sort = ['createdAt,DESC'] } = params;
+
+ const response = await participationClient.get>>(
+ `/v1/events/${eventId}/participants`,
+ {
+ params: {
+ storeVisited,
+ page,
+ size,
+ sort,
+ },
+ }
+ );
+ return response.data;
+};
+
+/**
+ * ํน์ ์ฐธ์ฌ์ ์ ๋ณด ์กฐํ
+ * GET /v1/events/{eventId}/participants/{participantId}
+ */
+export const getParticipant = async (
+ eventId: string,
+ participantId: string
+): Promise> => {
+ const response = await participationClient.get>(
+ `/v1/events/${eventId}/participants/${participantId}`
+ );
+ return response.data;
+};
+
+/**
+ * ์ฐธ์ฌ์ ๊ฒ์ (ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ํํฐ๋ง์ฉ ํฌํผ)
+ * ์ค์ API๋ ์๋ฒ ์ฌ์ด๋ ๊ฒ์์ ์ง์ํ์ง ์์ผ๋ฏ๋ก,
+ * ์ ์ฒด ๋ชฉ๋ก์ ๊ฐ์ ธ์์ ํด๋ผ์ด์ธํธ์์ ํํฐ๋ง
+ */
+export const searchParticipants = async (
+ eventId: string,
+ searchTerm: string,
+ storeVisited?: boolean
+): Promise => {
+ // ๋ชจ๋ ํ์ด์ง ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
+ let allParticipants: ParticipationResponse[] = [];
+ let currentPage = 0;
+ let hasMore = true;
+
+ while (hasMore) {
+ const response = await getParticipants({
+ eventId,
+ storeVisited,
+ page: currentPage,
+ size: 100, // ํ ๋ฒ์ ๋ง์ด ๊ฐ์ ธ์ค๊ธฐ
+ });
+
+ allParticipants = [...allParticipants, ...response.data.content];
+ hasMore = !response.data.last;
+ currentPage++;
+ }
+
+ // ๊ฒ์์ด๋ก ํํฐ๋ง
+ if (searchTerm) {
+ const term = searchTerm.toLowerCase();
+ return allParticipants.filter(
+ (p) =>
+ p.name.toLowerCase().includes(term) ||
+ p.phoneNumber.includes(term) ||
+ p.email?.toLowerCase().includes(term)
+ );
+ }
+
+ return allParticipants;
+};
+
+// ===================================
+// Winner API Functions
+// ===================================
+
+/**
+ * ๋น์ฒจ์ ์ถ์ฒจ
+ * POST /v1/events/{eventId}/draw-winners
+ */
+export const drawWinners = async (
+ eventId: string,
+ winnerCount: number,
+ applyStoreVisitBonus?: boolean
+): Promise> => {
+ const response = await participationClient.post>(
+ `/v1/events/${eventId}/draw-winners`,
+ {
+ winnerCount,
+ applyStoreVisitBonus,
+ }
+ );
+ return response.data;
+};
+
+/**
+ * ๋น์ฒจ์ ๋ชฉ๋ก ์กฐํ
+ * GET /v1/events/{eventId}/winners
+ */
+export const getWinners = async (
+ eventId: string,
+ page = 0,
+ size = 20,
+ sort: string[] = ['winnerRank,ASC']
+): Promise>> => {
+ const response = await participationClient.get>>(
+ `/v1/events/${eventId}/winners`,
+ {
+ params: {
+ page,
+ size,
+ sort,
+ },
+ }
+ );
+ return response.data;
+};
diff --git a/src/shared/types/api.types.ts b/src/shared/types/api.types.ts
new file mode 100644
index 0000000..b57f9ff
--- /dev/null
+++ b/src/shared/types/api.types.ts
@@ -0,0 +1,151 @@
+// ===================================
+// Common API Response Types
+// ===================================
+
+/**
+ * ๊ณตํต API ์๋ต ๋ํผ
+ */
+export interface ApiResponse {
+ success: boolean;
+ data: T;
+ errorCode?: string;
+ message?: string;
+ timestamp: string;
+}
+
+/**
+ * ํ์ด์ง ์๋ต
+ */
+export interface PageResponse {
+ content: T[];
+ page: number;
+ size: number;
+ totalElements: number;
+ totalPages: number;
+ first: boolean;
+ last: boolean;
+}
+
+// ===================================
+// Participation API Types
+// ===================================
+
+/**
+ * ์ด๋ฒคํธ ์ฐธ์ฌ ์์ฒญ
+ */
+export interface ParticipationRequest {
+ /** ์ด๋ฆ (2-50์, ํ์) */
+ name: string;
+ /** ์ ํ๋ฒํธ (ํจํด: ^\d{3}-\d{3,4}-\d{4}$, ํ์) */
+ phoneNumber: string;
+ /** ์ด๋ฉ์ผ (์ ํ) */
+ email?: string;
+ /** ์ฐธ์ฌ ๊ฒฝ๋ก (์ ํ) */
+ channel?: string;
+ /** ๋ง์ผํ
๋์ (์ ํ) */
+ agreeMarketing?: boolean;
+ /** ๊ฐ์ธ์ ๋ณด ์์ง ๋์ (ํ์) */
+ agreePrivacy: boolean;
+ /** ๋งค์ฅ ๋ฐฉ๋ฌธ ์ฌ๋ถ (์ ํ) */
+ storeVisited?: boolean;
+}
+
+/**
+ * ์ฐธ์ฌ์ ์ ๋ณด ์๋ต
+ */
+export interface ParticipationResponse {
+ /** ์ฐธ์ฌ์ ID */
+ participantId: string;
+ /** ์ด๋ฒคํธ ID */
+ eventId: string;
+ /** ์ด๋ฆ */
+ name: string;
+ /** ์ ํ๋ฒํธ */
+ phoneNumber: string;
+ /** ์ด๋ฉ์ผ */
+ email?: string;
+ /** ์ฐธ์ฌ ๊ฒฝ๋ก */
+ channel?: string;
+ /** ์ฐธ์ฌ ์ผ์ */
+ participatedAt: string;
+ /** ๋งค์ฅ ๋ฐฉ๋ฌธ ์ฌ๋ถ */
+ storeVisited: boolean;
+ /** ๋ณด๋์ค ์๋ชจ๊ถ ์ */
+ bonusEntries: number;
+ /** ๋น์ฒจ ์ฌ๋ถ */
+ isWinner: boolean;
+}
+
+/**
+ * ์ฐธ์ฌ์ ๋ชฉ๋ก ์กฐํ ํ๋ผ๋ฏธํฐ
+ */
+export interface GetParticipantsParams {
+ /** ์ด๋ฒคํธ ID */
+ eventId: string;
+ /** ๋งค์ฅ ๋ฐฉ๋ฌธ ์ฌ๋ถ ํํฐ (true: ๋ฐฉ๋ฌธ์๋ง, false: ๋ฏธ๋ฐฉ๋ฌธ์๋ง, null: ์ ์ฒด) */
+ storeVisited?: boolean;
+ /** ํ์ด์ง ๋ฒํธ (0๋ถํฐ ์์) */
+ page?: number;
+ /** ํ์ด์ง ํฌ๊ธฐ */
+ size?: number;
+ /** ์ ๋ ฌ ๊ธฐ์ค (์: createdAt,DESC) */
+ sort?: string[];
+}
+
+// ===================================
+// Winner API Types
+// ===================================
+
+/**
+ * ๋น์ฒจ์ ์ถ์ฒจ ์์ฒญ
+ */
+export interface DrawWinnersRequest {
+ /** ๋น์ฒจ์ ์ (์ต์ 1) */
+ winnerCount: number;
+ /** ๋งค์ฅ ๋ฐฉ๋ฌธ ๋ณด๋์ค ์ ์ฉ ์ฌ๋ถ */
+ applyStoreVisitBonus?: boolean;
+}
+
+/**
+ * ๋น์ฒจ์ ์์ฝ ์ ๋ณด
+ */
+export interface WinnerSummary {
+ /** ์ฐธ์ฌ์ ID */
+ participantId: string;
+ /** ์ด๋ฆ */
+ name: string;
+ /** ์ ํ๋ฒํธ */
+ phoneNumber: string;
+ /** ๋ฑ์ */
+ rank: number;
+}
+
+/**
+ * ๋น์ฒจ์ ์ถ์ฒจ ์๋ต
+ */
+export interface DrawWinnersResponse {
+ /** ์ด๋ฒคํธ ID */
+ eventId: string;
+ /** ์ ์ฒด ์ฐธ์ฌ์ ์ */
+ totalParticipants: number;
+ /** ๋น์ฒจ์ ์ */
+ winnerCount: number;
+ /** ์ถ์ฒจ ์ผ์ */
+ drawnAt: string;
+ /** ๋น์ฒจ์ ๋ชฉ๋ก */
+ winners: WinnerSummary[];
+}
+
+/**
+ * ๋น์ฒจ์ ๋ชฉ๋ก ์กฐํ ํ๋ผ๋ฏธํฐ
+ */
+export interface GetWinnersParams {
+ /** ์ด๋ฒคํธ ID */
+ eventId: string;
+ /** ํ์ด์ง ๋ฒํธ (0๋ถํฐ ์์) */
+ page?: number;
+ /** ํ์ด์ง ํฌ๊ธฐ */
+ size?: number;
+ /** ์ ๋ ฌ ๊ธฐ์ค (์: winnerRank,ASC) */
+ sort?: string[];
+}
diff --git a/test-images.html b/test-images.html
new file mode 100644
index 0000000..134520d
--- /dev/null
+++ b/test-images.html
@@ -0,0 +1,124 @@
+
+
+
+
+
+ Event 7777 - AI Generated Images Test
+
+
+
+ ๐จ Event 7777 - AI Generated Images
+ Loading images...
+
+
+
+
diff --git a/test-localstorage.html b/test-localstorage.html
new file mode 100644
index 0000000..253f696
--- /dev/null
+++ b/test-localstorage.html
@@ -0,0 +1,182 @@
+
+
+
+
+
+ LocalStorage ํ
์คํธ
+
+
+
+
+
๐จ ์ด๋ฒคํธ ์์ฑ ๋ฐ์ดํฐ ์ค์
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ๐พ LocalStorage์ ์ ์ฅ
+ ๐๏ธ ์ญ์
+ ๐ ํ์ฌ ๋ฐ์ดํฐ ๋ณด๊ธฐ
+
+
+
+ โ
localStorage์ ์ ์ฅ๋์์ต๋๋ค!
+ ์ด์ ์ด๋ฒคํธ ์์ฑ ํ๋ก์ฐ์์ channel โ contentPreview๋ก ์ด๋ํ๋ฉด
+ ์๋์ผ๋ก AI ์ด๋ฏธ์ง ์์ฑ์ด ์์๋ฉ๋๋ค.
+
+
+
+
ํ์ฌ localStorage ๋ฐ์ดํฐ:
+
์์
+
+
+
+
+
+