diff --git a/ROUTES.md b/ROUTES.md new file mode 100644 index 0000000..96296ce --- /dev/null +++ b/ROUTES.md @@ -0,0 +1,247 @@ +# πŸ“ KT 이벀트 λ§ˆμΌ€νŒ… - 전체 νŽ˜μ΄μ§€ 라우트 λͺ©λ‘ + +## πŸ” 인증 라우트 (auth) +**Route Group**: `(auth)` - AuthGuard 미적용 + +| 라우트 | 파일 경둜 | μ„€λͺ… | 320px μ΅œμ ν™” | +|--------|-----------|------|-------------| +| `/login` | `(auth)/login/page.tsx` | 둜그인 νŽ˜μ΄μ§€ | βœ… μ™„λ£Œ | +| `/register` | `(auth)/register/page.tsx` | νšŒμ›κ°€μž… νŽ˜μ΄μ§€ | βœ… μ™„λ£Œ | +| `/logout` | `(auth)/logout/page.tsx` | λ‘œκ·Έμ•„μ›ƒ νŽ˜μ΄μ§€ | βœ… μ™„λ£Œ | + +--- + +## 🏠 메인 μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 라우트 (main) +**Route Group**: `(main)` - AuthGuard 적용, BottomNavigation 포함 + +### λŒ€μ‹œλ³΄λ“œ & 뢄석 +| 라우트 | 파일 경둜 | μ„€λͺ… | 320px μ΅œμ ν™” | +|--------|-----------|------|-------------| +| `/` | `(main)/page.tsx` | 메인 λŒ€μ‹œλ³΄λ“œ | βœ… μ™„λ£Œ | +| `/analytics` | `(main)/analytics/page.tsx` | 전체 뢄석 νŽ˜μ΄μ§€ | βœ… μ™„λ£Œ | +| `/profile` | `(main)/profile/page.tsx` | μ‚¬μš©μž ν”„λ‘œν•„ | βœ… μ™„λ£Œ | + +### 이벀트 관리 +| 라우트 | 파일 경둜 | μ„€λͺ… | 320px μ΅œμ ν™” | +|--------|-----------|------|-------------| +| `/events` | `(main)/events/page.tsx` | 이벀트 λͺ©λ‘ | βœ… μ™„λ£Œ | +| `/events/create` | `(main)/events/create/page.tsx` | 이벀트 생성 (Funnel 방식) | βœ… μ™„λ£Œ | +| `/events/create/content-test` | `(main)/events/create/content-test/page.tsx` | μ½˜ν…μΈ  ν…ŒμŠ€νŠΈ νŽ˜μ΄μ§€ | βœ… μ™„λ£Œ | + +### 이벀트 상세 (동적 라우트) +| 라우트 | 파일 경둜 | μ„€λͺ… | 320px μ΅œμ ν™” | +|--------|-----------|------|-------------| +| `/events/[eventId]` | `(main)/events/[eventId]/page.tsx` | 이벀트 상세 νŽ˜μ΄μ§€ | βœ… μ™„λ£Œ | +| `/events/[eventId]/participate` | `(main)/events/[eventId]/participate/page.tsx` | 이벀트 μ°Έμ—¬ νŽ˜μ΄μ§€ | βœ… μ™„λ£Œ | +| `/events/[eventId]/participants` | `(main)/events/[eventId]/participants/page.tsx` | μ°Έμ—¬μž 관리 νŽ˜μ΄μ§€ | βœ… μ™„λ£Œ | +| `/events/[eventId]/draw` | `(main)/events/[eventId]/draw/page.tsx` | 좔첨 νŽ˜μ΄μ§€ | βœ… μ™„λ£Œ | + +### ν…ŒμŠ€νŠΈ νŽ˜μ΄μ§€ +| 라우트 | 파일 경둜 | μ„€λͺ… | 320px μ΅œμ ν™” | +|--------|-----------|------|-------------| +| `/test/analytics/[eventId]` | `(main)/test/analytics/[eventId]/page.tsx` | 뢄석 ν…ŒμŠ€νŠΈ νŽ˜μ΄μ§€ | βœ… μ™„λ£Œ | + +--- + +## πŸ”Œ API 라우트 + +### User API (v1) +| 라우트 | 파일 경둜 | HTTP Method | +|--------|-----------|-------------| +| `/api/v1/users/login` | `api/v1/users/login/route.ts` | POST | +| `/api/v1/users/logout` | `api/v1/users/logout/route.ts` | POST | +| `/api/v1/users/register` | `api/v1/users/register/route.ts` | POST | +| `/api/v1/users/profile` | `api/v1/users/profile/route.ts` | GET, PUT | +| `/api/v1/users/password` | `api/v1/users/password/route.ts` | PUT | + +### Event API (v1) +| 라우트 | 파일 경둜 | HTTP Method | +|--------|-----------|-------------| +| `/api/v1/events/objectives` | `api/v1/events/objectives/route.ts` | POST | +| `/api/v1/events/[eventId]/participate` | `api/v1/events/[eventId]/participate/route.ts` | POST | +| `/api/v1/events/[eventId]/participants` | `api/v1/events/[eventId]/participants/route.ts` | GET | +| `/api/v1/events/[eventId]/participants/[participantId]` | `api/v1/events/[eventId]/participants/[participantId]/route.ts` | GET, PATCH | +| `/api/v1/events/[eventId]/draw-winners` | `api/v1/events/[eventId]/draw-winners/route.ts` | POST | +| `/api/v1/events/[eventId]/winners` | `api/v1/events/[eventId]/winners/route.ts` | GET | + +### Participation API (ν”„λ‘μ‹œ) +λ°±μ—”λ“œ Participation Service둜 ν”„λ‘μ‹œν•˜λŠ” API 라우트 + +| 라우트 | 파일 경둜 | HTTP Method | +|--------|-----------|-------------| +| `/api/participations/[eventId]/participate` | `api/participations/[eventId]/participate/route.ts` | POST | +| `/api/participations/[eventId]/participants` | `api/participations/[eventId]/participants/route.ts` | GET | +| `/api/participations/[eventId]/participants/[participantId]` | `api/participations/[eventId]/participants/[participantId]/route.ts` | GET, PATCH | +| `/api/participations/[eventId]/draw-winners` | `api/participations/[eventId]/draw-winners/route.ts` | POST | +| `/api/participations/[eventId]/winners` | `api/participations/[eventId]/winners/route.ts` | GET | + +### Distribution API +| 라우트 | 파일 경둜 | HTTP Method | +|--------|-----------|-------------| +| `/api/distribution/[eventId]/status` | `api/distribution/[eventId]/status/route.ts` | GET | + +--- + +## πŸ“Š 이벀트 생성 Funnel Steps + +`/events/create` νŽ˜μ΄μ§€λŠ” `@use-funnel/browser`λ₯Ό μ‚¬μš©ν•œ Funnel λ°©μ‹μœΌλ‘œ λ‹€μŒ 단계듀을 ν¬ν•¨ν•©λ‹ˆλ‹€: + +### Step 흐름 +``` +objective β†’ recommendation β†’ channel β†’ contentPreview/contentEdit β†’ approval +``` + +### 각 단계 상세 + +| Step | Component | URL Query | μ„€λͺ… | +|------|-----------|-----------|------| +| 1 | `ObjectiveStep` | `?event-creation.step=objective` | 이벀트 λͺ©μ  선택 (μ‹ κ·œκ³ κ°/재방문/맀좜/인지도) | +| 2 | `RecommendationStep` | `?event-creation.step=recommendation` | AI μΆ”μ²œ 이벀트 3κ°€μ§€ μ˜΅μ…˜ μ œμ‹œ 및 선택 | +| 3 | `ChannelStep` | `?event-creation.step=channel` | 배포 채널 선택 (μš°λ¦¬λ™λ„€TV, μ§€λ‹ˆTV, SNS λ“±) | +| 4a | `ContentPreviewStep` | `?event-creation.step=contentPreview` | μ½˜ν…μΈ  미리보기 (채널에 μ½˜ν…μΈ  ν•„μš” μ‹œ) | +| 4b | `ContentEditStep` | `?event-creation.step=contentEdit` | μ½˜ν…μΈ  νŽΈμ§‘ (μ‚¬μš©μžκ°€ μˆ˜μ • 선택 μ‹œ) | +| 5 | `ApprovalStep` | `?event-creation.step=approval` | μ΅œμ’… 이벀트 κ²€ν†  및 승인 | + +### Step λΆ„κΈ° 둜직 +- **μš°λ¦¬λ™λ„€TV, μ§€λ‹ˆTV, SNS** 채널 선택 μ‹œ β†’ `contentPreview` λ‹¨κ³„λ‘œ 이동 +- 기타 μ±„λ„λ§Œ 선택 μ‹œ β†’ `approval` λ‹¨κ³„λ‘œ λ°”λ‘œ 이동 +- `contentPreview`μ—μ„œ "μˆ˜μ •ν•˜κΈ°" 선택 μ‹œ β†’ `contentEdit` λ‹¨κ³„λ‘œ 이동 + +--- + +## πŸ”’ 인증 보호 μ •μ±… + +### AuthGuard 적용 +- **(main)** Route Group: **AuthGuard 적용** + - 둜그인 ν•„μˆ˜ + - `isAuthenticated && user` 체크 + - 미인증 μ‹œ μžλ™μœΌλ‘œ `/login`으둜 λ¦¬λ‹€μ΄λ ‰νŠΈ + - BottomNavigation 포함 + +### AuthGuard 미적용 +- **(auth)** Route Group: 인증 없이 μ ‘κ·Ό κ°€λŠ₯ + - `/login`, `/register`, `/logout` + +### κ΅¬ν˜„ μœ„μΉ˜ +- AuthGuard μ»΄ν¬λ„ŒνŠΈ: `src/features/auth/ui/AuthGuard.tsx` +- Layout 적용: `src/app/(main)/layout.tsx` + +--- + +## πŸ—‚οΈ ν”„λ‘œμ νŠΈ ꡬ쑰 + +``` +src/app/ +β”œβ”€β”€ (auth)/ # 인증 κ΄€λ ¨ νŽ˜μ΄μ§€ (AuthGuard 미적용) +β”‚ β”œβ”€β”€ login/ +β”‚ β”œβ”€β”€ register/ +β”‚ └── logout/ +β”œβ”€β”€ (main)/ # 메인 μ• ν”Œλ¦¬μΌ€μ΄μ…˜ (AuthGuard 적용) +β”‚ β”œβ”€β”€ page.tsx # λŒ€μ‹œλ³΄λ“œ +β”‚ β”œβ”€β”€ layout.tsx # AuthGuard + BottomNavigation +β”‚ β”œβ”€β”€ analytics/ +β”‚ β”œβ”€β”€ profile/ +β”‚ β”œβ”€β”€ events/ +β”‚ β”‚ β”œβ”€β”€ page.tsx +β”‚ β”‚ β”œβ”€β”€ create/ +β”‚ β”‚ β”‚ β”œβ”€β”€ page.tsx # Funnel 메인 +β”‚ β”‚ β”‚ β”œβ”€β”€ steps/ # Funnel 각 단계 μ»΄ν¬λ„ŒνŠΈ +β”‚ β”‚ β”‚ └── content-test/ +β”‚ β”‚ └── [eventId]/ +β”‚ β”‚ β”œβ”€β”€ page.tsx +β”‚ β”‚ β”œβ”€β”€ participate/ +β”‚ β”‚ β”œβ”€β”€ participants/ +β”‚ β”‚ └── draw/ +β”‚ └── test/ +β”‚ └── analytics/[eventId]/ +└── api/ # API 라우트 (ν”„λ‘μ‹œ) + β”œβ”€β”€ v1/ + β”‚ β”œβ”€β”€ users/ + β”‚ └── events/ + β”œβ”€β”€ participations/ + └── distribution/ +``` + +--- + +## πŸ“ 참고사항 + +### Next.js 14 App Router νŠΉμ§• +- Route Groups: `(auth)`, `(main)` - URL에 ν¬ν•¨λ˜μ§€ μ•ŠλŠ” 논리적 κ·Έλ£Ή +- Dynamic Routes: `[eventId]`, `[participantId]` - 동적 μ„Έκ·Έλ¨ΌνŠΈ +- API Routes: `route.ts` 파일둜 API μ—”λ“œν¬μΈνŠΈ κ΅¬ν˜„ +- Layouts: 각 라우트 그룹별 곡톡 λ ˆμ΄μ•„μ›ƒ 적용 + +### 졜근 변경사항 +- βœ… AI μΆ”μ²œ 둜직 λ³€κ²½: Job 폴링 방식 β†’ POST `/events/{eventId}/ai-recommendations` 직접 호좜 +- βœ… AuthGuard 적용: (main) κ·Έλ£Ή 전체에 둜그인 ν•„μˆ˜ 적용 +- βœ… 둜그인 νŽ˜μ΄μ§€: μ§€μ›ν•˜μ§€ μ•ŠλŠ” κΈ°λŠ₯ Toast β†’ Modal λ³€κ²½ +- βœ… 320px λͺ¨λ°”일 μ΅œμ ν™”: λͺ¨λ“  νŽ˜μ΄μ§€ μ™„λ£Œ (2025-10-30) + +--- + +## πŸ“± 320px λͺ¨λ°”일 μ΅œμ ν™” λ‚΄μ—­ + +### μ΅œμ ν™” λ‚΄μš© +1. **Container Padding μ‘°μ •**: `px: { xs: 6 }` β†’ `px: { xs: 2 }` + - 320px ν™”λ©΄μ—μ„œ 쒌우 νŒ¨λ”© 48px β†’ 16px둜 쀄여 μ½˜ν…μΈ  μ˜μ—­ 확보 + - μ‹€μ œ μ½˜ν…μΈ  μ˜μ—­: 224px β†’ 288px (29% 증가) + +2. **Grid Spacing μ‘°μ •**: 큰 spacing 값을 λ°˜μ‘ν˜•μœΌλ‘œ μ‘°μ • + - `spacing={6}` β†’ `spacing={{ xs: 2, sm: 6 }}` + +3. **Typography 크기 μ‘°μ •**: μž‘μ€ ν™”λ©΄μ—μ„œ μ μ ˆν•œ 폰트 크기 적용 + - 제λͺ©: `fontSize: '2rem'` β†’ `fontSize: { xs: '1.5rem', sm: '2rem' }` + - λ³Έλ¬Έ: `fontSize: '1rem'` β†’ `fontSize: { xs: '0.875rem', sm: '1rem' }` + +4. **Vertical Spacing λŒ€ν­ μΆ•μ†Œ**: 320pxμ—μ„œ κ³Όλ„ν•œ μƒν•˜ μ—¬λ°± 문제 ν•΄κ²° + - **Container Padding**: `pt: 8, pb: 8` β†’ `pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }` (64px β†’ 32px) + - **Box Padding Bottom**: `pb: 10` β†’ `pb: { xs: 4, sm: 10 }` (80px β†’ 32px) + - **Large Margin Bottom**: `mb: 10` β†’ `mb: { xs: 4, sm: 10 }` (80px β†’ 32px) + - **Medium Margin Bottom**: `mb: 8` β†’ `mb: { xs: 3, sm: 8 }` (64px β†’ 24px) + - **CardContent Padding**: + - `p: 8` β†’ `p: { xs: 3, sm: 8 }` (64px β†’ 24px) + - `p: 6` β†’ `p: { xs: 3, sm: 6 }` (48px β†’ 24px) + - `p: 5` β†’ `p: { xs: 2.5, sm: 5 }` (40px β†’ 20px) + - `py: 6` β†’ `pt: { xs: 3, sm: 6 }, pb: { xs: 3, sm: 6 }` (μƒν•˜ 각 48px β†’ 24px) + - `py: { xs: 4, sm: 6 }` β†’ `pt: { xs: 2, sm: 6 }, pb: { xs: 2, sm: 6 }` (μƒν•˜ 각 32px β†’ 16px) + - `py: { xs: 2, sm: 6 }` β†’ `pt: { xs: 1.5, sm: 6 }, pb: { xs: 1.5, sm: 6 }` (μƒν•˜ 각 16px β†’ 12px) + - **총 효과**: μƒν•˜ 여백이 평균 50-60% κ°μ†Œν•˜μ—¬ 320px ν™”λ©΄μ—μ„œ μ½˜ν…μΈ  밀도 ν–₯상 + +5. **Margin/Gap μ‘°μ •**: λΆˆν•„μš”ν•˜κ²Œ 큰 μ—¬λ°± μ€„μž„ + - `gap: 3` β†’ `gap: { xs: 2, sm: 3 }` + +### 영ν–₯받은 νŽ˜μ΄μ§€ +- λͺ¨λ“  인증 νŽ˜μ΄μ§€ (login, register, logout) +- λͺ¨λ“  이벀트 생성 단계 (ObjectiveStep, RecommendationStep, ChannelStep, ContentPreviewStep, ContentEditStep, ApprovalStep) +- 이벀트 상세 νŽ˜μ΄μ§€λ“€ (detail, participate, participants, draw) +- λŒ€μ‹œλ³΄λ“œ 및 ν”„λ‘œν•„ νŽ˜μ΄μ§€ + +### μ°Έμ—¬μž 관리 νŽ˜μ΄μ§€ μΆ”κ°€ μ΅œμ ν™” (2025-10-30) +**검색 및 ν•„ν„° UI μ΅œμ ν™”**: +- Search TextField: μ•„μ΄μ½˜ 18px, 폰트 0.75rem, νŒ¨λ”© 8px +- FilterList μ•„μ΄μ½˜: 28px β†’ 20px +- FormControl: minWidth 100px/90px, 폰트 0.75rem, νŒ¨λ”© 8px +- MenuItem: 폰트 0.75rem + +**λ²„νŠΌ μ΅œμ ν™”**: +- μ•„μ΄μ½˜: 20px β†’ 16px +- 폰트: 0.875rem β†’ 0.7rem +- νŒ¨λ”©: px 4β†’1.5, py 1.5β†’0.75 +- ν…μŠ€νŠΈ 단좕: "μ—‘μ…€ λ‹€μš΄λ‘œλ“œ"β†’"μ—‘μ…€", "λ‹Ήμ²¨μž 좔첨"β†’"좔첨" (320px만) + +**Pagination μ΅œμ ν™”**: +- 폰트: 1rem β†’ 0.75rem +- λ²„νŠΌ 크기: 32px β†’ 26px +- μ•„μ΄μ½˜: 1.5rem β†’ 1rem +- μ—¬λ°±: 4px β†’ 2px + +**톡계 μΉ΄λ“œ**: +- Grid: xs={6} β†’ xs={4} (ν•œ 쀄에 3개 ν‘œμ‹œ) +- μΉ΄λ“œ λ‚΄λΆ€ μš”μ†Œ 크기 μΆ•μ†Œ (μ•„μ΄μ½˜, 폰트, νŒ¨λ”©) + +### λΉŒλ“œ 확인 +```bash +npm run build +# βœ… λΉŒλ“œ 성곡 (2025-10-30) +``` diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 60116f1..4c9632f 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -104,8 +104,8 @@ export default function LoginPage() { flexDirection: 'column', justifyContent: 'center', alignItems: 'center', - px: 3, - py: 8, + px: { xs: 2, sm: 3 }, // 320px: 16px, 600px+: 24px + py: { xs: 4, sm: 8 }, // 320px: 32px, 600px+: 64px background: 'linear-gradient(135deg, #FFF 0%, #F5F5F5 100%)', }} > @@ -114,38 +114,49 @@ export default function LoginPage() { sx={{ width: '100%', maxWidth: 440, - p: { xs: 4, sm: 6 }, + p: { xs: 2.5, sm: 4, md: 6 }, // 320px: 20px, 600px: 32px, 960px+: 48px borderRadius: 3, boxShadow: '0 8px 32px rgba(0,0,0,0.08)', }} > {/* 둜고 및 타이틀 */} - + {/* 320px: 32px, 600px+: 48px */} - πŸŽ‰ + πŸŽ‰ {/* 320px: 28px, 600px+: 32px */} - + KT 이벀트 νŒŒνŠΈλ„ˆ - + μ†Œμƒκ³΅μΈμ„ μœ„ν•œ λ§ˆμΌ€νŒ… μ–΄μ‹œμŠ€ν„΄νŠΈ {/* 둜그인 폼 */}
- + {/* 320px: 16px, 600px+: 24px */} - + {/* 320px: 20px, 600px+: 24px */} ), }} + sx={{ + '& .MuiInputBase-input': { + fontSize: { xs: '0.875rem', sm: '1rem' } // 320px: 14px, 600px+: 16px + } + }} /> )} /> - + {/* 320px: 12px, 600px+: 16px */} - + ), endAdornment: ( @@ -201,20 +217,29 @@ export default function LoginPage() { ), }} + sx={{ + '& .MuiInputBase-input': { + fontSize: { xs: '0.875rem', sm: '1rem' } + } + }} /> )} /> - + {/* 320px: 16px, 600px+: 24px */} ( } + control={} label={ - + 둜그인 μƒνƒœ μœ μ§€ } @@ -230,26 +255,41 @@ export default function LoginPage() { size="large" sx={{ mb: 2, - py: { xs: 1.5, sm: 1.75 }, - fontSize: { xs: 15, sm: 16 }, + py: { xs: 1.25, sm: 1.5, md: 1.75 }, // 320px: 10px, 600px: 12px, 960px+: 14px + fontSize: { xs: '0.938rem', sm: 15, md: 16 }, // 320px: 15px, 600px: 15px, 960px+: 16px ...getGradientButtonStyle('primary'), }} > 둜그인 - + λΉ„λ°€λ²ˆν˜Έ μ°ΎκΈ° - + | νšŒμ›κ°€μž… @@ -266,20 +310,32 @@ export default function LoginPage() { {/* μ†Œμ…œ 둜그인 */} - - + {/* 320px: 16px, 600px+: 24px */} + λ˜λŠ” - + @@ -300,7 +356,8 @@ export default function LoginPage() { size="large" onClick={handleUnavailableFeature} sx={{ - py: 1.5, + py: { xs: 1.25, sm: 1.5 }, + fontSize: { xs: '0.875rem', sm: '0.938rem' }, borderColor: '#03C75A', bgcolor: '#03C75A', color: '#FFFFFF', @@ -313,8 +370,8 @@ export default function LoginPage() { startIcon={ N @@ -337,14 +394,22 @@ export default function LoginPage() { νšŒμ›κ°€μž… μ‹œ{' '} μ΄μš©μ•½κ΄€ {' '} @@ -353,7 +418,10 @@ export default function LoginPage() { href="#" onClick={handleUnavailableFeature} underline="hover" - sx={{ color: 'text.secondary' }} + sx={{ + color: 'text.secondary', + fontSize: 'inherit' + }} > κ°œμΈμ •λ³΄μ²˜λ¦¬λ°©μΉ¨ @@ -370,8 +438,9 @@ export default function LoginPage() { PaperProps={{ sx: { borderRadius: 3, - px: 2, - py: 1, + px: { xs: 1.5, sm: 2 }, // 320px: 12px, 600px+: 16px + py: { xs: 0.5, sm: 1 }, // 320px: 4px, 600px+: 8px + m: { xs: 2, sm: 3 }, // 320px: 16px, 600px+: 24px }, }} > @@ -379,27 +448,42 @@ export default function LoginPage() { sx={{ display: 'flex', alignItems: 'center', - gap: 1.5, - pb: 2, + gap: { xs: 1, sm: 1.5 }, // 320px: 8px, 600px+: 12px + pb: { xs: 1.5, sm: 2 }, // 320px: 12px, 600px+: 16px + px: { xs: 1, sm: 2 }, // 320px: 8px, 600px+: 16px }} > - - + {/* 320px: 24px, 600px+: 28px */} + μ•ˆλ‚΄ - - + {/* 320px: 16px, 600px+: 24px */} + ν˜„μž¬ μ§€μ›ν•˜μ§€ μ•ŠλŠ” κΈ°λŠ₯μž…λ‹ˆλ‹€. - + {/* 320px: 16px, 600px+: 24px */} @@ -391,7 +419,7 @@ export default function ParticipantsPage() { {/* Participant List */} {!loading && filteredParticipants.length > 0 && ( <> - + {filteredParticipants.map((participant) => ( handleParticipantClick(participant)} > - + {/* Header */} - + - + - + #{participant.participantId} - + {participant.name} - + {participant.phoneNumber} @@ -449,7 +479,7 @@ export default function ParticipantsPage() { label={getStatusText(participant.isWinner)} color={getStatusColor(participant.isWinner) as any} size="medium" - sx={{ fontWeight: 600, px: 2, py: 2.5 }} + sx={{ fontWeight: 600, px: { xs: 1.5, sm: 2 }, py: { xs: 2, sm: 2.5 }, fontSize: { xs: '0.75rem', sm: '0.875rem' } }} /> @@ -458,15 +488,15 @@ export default function ParticipantsPage() { sx={{ borderTop: '1px solid', borderColor: colors.gray[100], - pt: 4, + pt: { xs: 2, sm: 4 }, display: 'flex', flexDirection: 'column', - gap: 2, + gap: { xs: 1.5, sm: 2 }, }} > {participant.channel && ( - - + + μ°Έμ—¬ 경둜 )} - - + + μ°Έμ—¬ μΌμ‹œ - + {new Date(participant.participatedAt).toLocaleString('ko-KR')} {participant.storeVisited && ( - - + + λ§€μž₯ λ°©λ¬Έ - + )} @@ -505,7 +536,7 @@ export default function ParticipantsPage() { {/* Pagination */} {totalPages > 1 && ( - + @@ -558,7 +595,7 @@ export default function ParticipantsPage() { mb: 3, }} > - + {selectedParticipant.name} diff --git a/src/app/(main)/events/create/steps/ApprovalStep.tsx b/src/app/(main)/events/create/steps/ApprovalStep.tsx index 5e4e816..ee59caf 100644 --- a/src/app/(main)/events/create/steps/ApprovalStep.tsx +++ b/src/app/(main)/events/create/steps/ApprovalStep.tsx @@ -156,10 +156,10 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS }; return ( - - + + {/* Header */} - + @@ -169,7 +169,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS {/* Title Section */} - + 이벀트λ₯Ό ν™•μΈν•΄μ£Όμ„Έμš” @@ -180,7 +180,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS {/* Event Summary Statistics */} - + - + @@ -412,7 +412,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS 배포 채널 - + {getChannelNames(eventData.channels).map((channel) => ( @@ -445,7 +445,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS {/* Terms Agreement */} - + 배포 μ™„λ£Œ! - + μ΄λ²€νŠΈκ°€ μ„±κ³΅μ μœΌλ‘œ λ°°ν¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
μ‹€μ‹œκ°„μœΌλ‘œ μ°Έμ—¬μžλ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. diff --git a/src/app/(main)/events/create/steps/ChannelStep.tsx b/src/app/(main)/events/create/steps/ChannelStep.tsx index 39e29e7..377fd01 100644 --- a/src/app/(main)/events/create/steps/ChannelStep.tsx +++ b/src/app/(main)/events/create/steps/ChannelStep.tsx @@ -108,9 +108,9 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) { return ( - + {/* Header */} - + @@ -119,7 +119,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
- + (μ΅œμ†Œ 1개 이상) @@ -136,7 +136,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) { transition: 'all 0.3s', }} > - + - + - + - + - + 총 μ˜ˆμƒ λΉ„μš© diff --git a/src/app/(main)/events/create/steps/ContentEditStep.tsx b/src/app/(main)/events/create/steps/ContentEditStep.tsx index 5f8f659..321b980 100644 --- a/src/app/(main)/events/create/steps/ContentEditStep.tsx +++ b/src/app/(main)/events/create/steps/ContentEditStep.tsx @@ -40,10 +40,10 @@ export default function ContentEditStep({ }; return ( - - + + {/* Header */} - + diff --git a/src/app/(main)/events/create/steps/ContentPreviewStep.tsx b/src/app/(main)/events/create/steps/ContentPreviewStep.tsx index ddead7a..3bb6d24 100644 --- a/src/app/(main)/events/create/steps/ContentPreviewStep.tsx +++ b/src/app/(main)/events/create/steps/ContentPreviewStep.tsx @@ -310,8 +310,8 @@ export default function ContentPreviewStep({ if (loading) { return ( - - + + @@ -442,9 +442,9 @@ export default function ContentPreviewStep({ }, }} > - + {/* Header */} - + @@ -453,7 +453,7 @@ export default function ContentPreviewStep({ - + {generatedImages.size > 0 && ( ✨ μƒμ„±λœ 이미지λ₯Ό ν™•μΈν•˜κ³  μŠ€νƒ€μΌμ„ μ„ νƒν•˜μ„Έμš” @@ -477,12 +477,12 @@ export default function ContentPreviewStep({ - + μ΄λ²€νŠΈμ— μ–΄μšΈλ¦¬λŠ” μŠ€νƒ€μΌμ„ μ„ νƒν•˜μ„Έμš” handleStyleSelect(e.target.value as 'SIMPLE' | 'FANCY' | 'TRENDY')}> - + {imageStyles.map((style) => ( - + {/* Title Section */} - - - + + + 이벀트 λͺ©μ μ„ μ„ νƒν•΄μ£Όμ„Έμš” - + AIκ°€ λͺ©μ μ— λ§žλŠ” 졜적의 이벀트λ₯Ό μΆ”μ²œν•΄λ“œλ¦½λ‹ˆλ‹€ {/* Purpose Options */} setSelected(e.target.value as EventObjective)}> - + {objectives.map((objective) => ( setSelected(objective.id)} > - - + + {objective.icon} - + {objective.title} - + {objective.description} @@ -191,15 +191,15 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) { - - - + + + μ„ νƒν•˜μ‹  λͺ©μ μ— 따라 AIκ°€ μ—…μ’…, μ§€μ—­, κ³„μ ˆ νŠΈλ Œλ“œλ₯Ό λΆ„μ„ν•˜μ—¬ κ°€μž₯ 효과적인 이벀트λ₯Ό μΆ”μ²œν•©λ‹ˆλ‹€. @@ -214,9 +214,9 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) { disabled={!selected} onClick={handleNext} sx={{ - py: 3, + py: { xs: 2, sm: 3 }, borderRadius: 3, - fontSize: '1rem', + fontSize: { xs: '0.875rem', sm: '1rem' }, fontWeight: 700, background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`, '&:hover': { diff --git a/src/app/(main)/events/create/steps/RecommendationStep.tsx b/src/app/(main)/events/create/steps/RecommendationStep.tsx index 844e54d..5d583cf 100644 --- a/src/app/(main)/events/create/steps/RecommendationStep.tsx +++ b/src/app/(main)/events/create/steps/RecommendationStep.tsx @@ -182,24 +182,24 @@ export default function RecommendationStep({ if (loading) { return ( - - + + - + AI 이벀트 μΆ”μ²œ - + AIκ°€ 졜적의 이벀트λ₯Ό μƒμ„±ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€... - + μ—…μ’…, μ§€μ—­, μ‹œμ¦Œ νŠΈλ Œλ“œλ₯Ό λΆ„μ„ν•˜μ—¬ λ§žμΆ€ν˜• 이벀트λ₯Ό μΆ”μ²œν•©λ‹ˆλ‹€ @@ -212,8 +212,8 @@ export default function RecommendationStep({ if (error) { return ( - - + + @@ -276,7 +276,7 @@ export default function RecommendationStep({ if (!aiResult) { return ( - + @@ -285,9 +285,9 @@ export default function RecommendationStep({ return ( - + {/* Header */} - + @@ -300,12 +300,12 @@ export default function RecommendationStep({ - + @@ -363,7 +363,7 @@ export default function RecommendationStep({ {/* AI Recommendations */} - + AI μΆ”μ²œ 이벀트 ({aiResult.recommendations.length}κ°€μ§€ μ˜΅μ…˜) @@ -375,7 +375,7 @@ export default function RecommendationStep({ {/* Recommendations */} setSelected(Number(e.target.value))}> - + {aiResult.recommendations.map((rec) => ( setSelected(rec.optionNumber)} > - + (null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - // KPI 계산 - const activeEvents = mockEvents.filter((e) => e.status === '진행쀑'); - const totalParticipants = mockEvents.reduce((sum, e) => sum + e.participants, 0); - const avgROI = - mockEvents.length > 0 - ? Math.round(mockEvents.reduce((sum, e) => sum + e.roi, 0) / mockEvents.length) - : 0; + // Analytics API 호좜 + useEffect(() => { + const fetchAnalytics = async () => { + if (!user?.userId) { + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + const data = await analyticsApi.getUserAnalytics(user.userId, { refresh: false }); + setAnalyticsData(data); + } catch (err: any) { + console.error('Failed to fetch analytics:', err); + + // 404 λ˜λŠ” 400 μ—λŸ¬λŠ” 아직 Analytics 데이터가 μ—†λŠ” 경우 + if (err.response?.status === 404 || err.response?.status === 400) { + console.log('ℹ️ Analytics 데이터가 아직 μƒμ„±λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.'); + setError('아직 뢄석 데이터가 μ—†μŠ΅λ‹ˆλ‹€. 이벀트λ₯Ό μƒμ„±ν•˜κ³  μ°Έμ—¬μžκ°€ 생기면 μžλ™μœΌλ‘œ μƒμ„±λ©λ‹ˆλ‹€.'); + } else { + setError('뢄석 데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); + } + } finally { + setLoading(false); + } + }; + + fetchAnalytics(); + }, [user?.userId]); + + // KPI 계산 - Analytics API 데이터 μ‚¬μš© + const activeEventsCount = analyticsData?.activeEvents ?? 0; + const totalParticipants = analyticsData?.overallSummary?.participants ?? 0; + const avgROI = Math.round((analyticsData?.overallRoi?.roi ?? 0) * 100) / 100; + const eventPerformances = analyticsData?.eventPerformances ?? []; const handleCreateEvent = () => { router.push('/events/create'); @@ -83,7 +92,7 @@ export default function HomePage() { - μ•ˆλ…•ν•˜μ„Έμš”, {mockUser.name}λ‹˜! πŸ‘‹ + μ•ˆλ…•ν•˜μ„Έμš”, {user?.userName || 'μ‚¬μš©μž'}λ‹˜! πŸ‘‹ 이벀트 ν˜„ν™©μ„ ν•œλˆˆμ— ν™•μΈν•˜κ³  μ„±κ³Όλ₯Ό λΆ„μ„ν•΄λ³΄μ„Έμš” + {/* Error Alert */} + {error && ( + + {error} + + )} + + {/* Loading State */} + {loading && ( + + + + )} + {/* KPI Cards */} - - + + - + @@ -142,8 +165,9 @@ export default function HomePage() { mb: 0.5, color: colors.gray[700], fontWeight: 500, - fontSize: { xs: '0.75rem', sm: '0.875rem' }, + fontSize: { xs: '0.6875rem', sm: '0.875rem' }, textShadow: '0px 1px 2px rgba(0,0,0,0.1)', + lineHeight: 1.2, }} > μ§„ν–‰ 쀑인 이벀트 @@ -153,16 +177,16 @@ export default function HomePage() { sx={{ fontWeight: 700, color: colors.gray[900], - fontSize: { xs: '1.5rem', sm: '2.25rem' }, + fontSize: { xs: '1.375rem', sm: '2.25rem' }, textShadow: '0px 2px 4px rgba(0,0,0,0.15)', }} > - {activeEvents.length} + {activeEventsCount}
- + - + @@ -197,8 +221,9 @@ export default function HomePage() { mb: 0.5, color: colors.gray[700], fontWeight: 500, - fontSize: { xs: '0.75rem', sm: '0.875rem' }, + fontSize: { xs: '0.6875rem', sm: '0.875rem' }, textShadow: '0px 1px 2px rgba(0,0,0,0.1)', + lineHeight: 1.2, }} > 총 μ°Έμ—¬μž @@ -208,7 +233,7 @@ export default function HomePage() { sx={{ fontWeight: 700, color: colors.gray[900], - fontSize: { xs: '1.5rem', sm: '2.25rem' }, + fontSize: { xs: '1.375rem', sm: '2.25rem' }, textShadow: '0px 2px 4px rgba(0,0,0,0.15)', }} > @@ -217,7 +242,7 @@ export default function HomePage() { - + - + @@ -252,8 +277,9 @@ export default function HomePage() { mb: 0.5, color: colors.gray[700], fontWeight: 500, - fontSize: { xs: '0.75rem', sm: '0.875rem' }, + fontSize: { xs: '0.6875rem', sm: '0.875rem' }, textShadow: '0px 1px 2px rgba(0,0,0,0.1)', + lineHeight: 1.2, }} > 평균 ROI @@ -263,7 +289,7 @@ export default function HomePage() { sx={{ fontWeight: 700, color: colors.gray[900], - fontSize: { xs: '1.5rem', sm: '2.25rem' }, + fontSize: { xs: '1.375rem', sm: '2.25rem' }, textShadow: '0px 2px 4px rgba(0,0,0,0.15)', }} > @@ -288,7 +314,7 @@ export default function HomePage() { }} onClick={handleCreateEvent} > - + - + - {activeEvents.length === 0 ? ( + {!loading && eventPerformances.length === 0 ? ( - ) : ( + ) : !loading && ( - {activeEvents.map((event) => ( + {eventPerformances.slice(0, 2).map((event) => ( handleEventClick(event.id)} + onClick={() => handleEventClick(event.eventId)} > - {event.title} + {event.eventTitle} - - πŸ“… - - {event.startDate} ~ {event.endDate} - - - + + + + 쑰회수 + + + {event.views.toLocaleString()} + + 회 + + + - {event.roi}% + {Math.round(event.roi * 100) / 100}% diff --git a/src/app/(main)/profile/page.tsx b/src/app/(main)/profile/page.tsx index 5e8a9fb..ba6fa43 100644 --- a/src/app/(main)/profile/page.tsx +++ b/src/app/(main)/profile/page.tsx @@ -242,14 +242,14 @@ export default function ProfilePage() { - + {/* μ‚¬μš©μž 정보 μ„Ήμ…˜ */} - + {/* κΈ°λ³Έ 정보 */} - + κΈ°λ³Έ 정보 @@ -338,7 +338,7 @@ export default function ProfilePage() { {/* λ§€μž₯ 정보 */} - + λ§€μž₯ 정보 @@ -458,12 +458,12 @@ export default function ProfilePage() { }, }} > - - - + + + μ €μž₯ μ™„λ£Œ - + ν”„λ‘œν•„ 정보가 μ—…λ°μ΄νŠΈλ˜μ—ˆμŠ΅λ‹ˆλ‹€. diff --git a/src/app/(main)/test/analytics/[eventId]/page.tsx b/src/app/(main)/test/analytics/[eventId]/page.tsx index dd9f42b..5e6f736 100644 --- a/src/app/(main)/test/analytics/[eventId]/page.tsx +++ b/src/app/(main)/test/analytics/[eventId]/page.tsx @@ -519,7 +519,7 @@ export default function TestAnalyticsPage() { > @@ -544,7 +544,7 @@ export default function TestAnalyticsPage() { > @@ -569,7 +569,7 @@ export default function TestAnalyticsPage() { > @@ -594,7 +594,7 @@ export default function TestAnalyticsPage() { > diff --git a/src/app/api/analytics/events/[eventId]/channels/route.ts b/src/app/api/analytics/events/[eventId]/channels/route.ts new file mode 100644 index 0000000..4015949 --- /dev/null +++ b/src/app/api/analytics/events/[eventId]/channels/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const ANALYTICS_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io'; + +export async function GET( + request: NextRequest, + { params }: { params: { eventId: string } } +) { + try { + const { eventId } = params; + const token = request.headers.get('Authorization'); + const { searchParams } = new URL(request.url); + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = token; + } + + const queryString = searchParams.toString(); + const url = `${ANALYTICS_HOST}/api/v1/analytics/events/${eventId}/analytics/channels${queryString ? `?${queryString}` : ''}`; + + const response = await fetch(url, { + method: 'GET', + headers, + }); + + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data); + } catch (error) { + console.error('❌ [Analytics Proxy] Event channels error:', error); + return NextResponse.json( + { message: 'Event Channels 데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/analytics/events/[eventId]/roi/route.ts b/src/app/api/analytics/events/[eventId]/roi/route.ts new file mode 100644 index 0000000..577d426 --- /dev/null +++ b/src/app/api/analytics/events/[eventId]/roi/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const ANALYTICS_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io'; + +export async function GET( + request: NextRequest, + { params }: { params: { eventId: string } } +) { + try { + const { eventId } = params; + const token = request.headers.get('Authorization'); + const { searchParams } = new URL(request.url); + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = token; + } + + const queryString = searchParams.toString(); + const url = `${ANALYTICS_HOST}/api/v1/analytics/events/${eventId}/analytics/roi${queryString ? `?${queryString}` : ''}`; + + const response = await fetch(url, { + method: 'GET', + headers, + }); + + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data); + } catch (error) { + console.error('❌ [Analytics Proxy] Event ROI error:', error); + return NextResponse.json( + { message: 'Event ROI 데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/analytics/events/[eventId]/route.ts b/src/app/api/analytics/events/[eventId]/route.ts new file mode 100644 index 0000000..de7b140 --- /dev/null +++ b/src/app/api/analytics/events/[eventId]/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const ANALYTICS_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io'; + +export async function GET( + request: NextRequest, + { params }: { params: { eventId: string } } +) { + try { + const { eventId } = params; + const token = request.headers.get('Authorization'); + const { searchParams } = new URL(request.url); + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = token; + } + + const queryString = searchParams.toString(); + const url = `${ANALYTICS_HOST}/api/v1/analytics/events/${eventId}/analytics${queryString ? `?${queryString}` : ''}`; + + const response = await fetch(url, { + method: 'GET', + headers, + }); + + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data); + } catch (error) { + console.error('❌ [Analytics Proxy] Event analytics error:', error); + return NextResponse.json( + { message: 'Event Analytics 데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/analytics/events/[eventId]/timeline/route.ts b/src/app/api/analytics/events/[eventId]/timeline/route.ts new file mode 100644 index 0000000..6cfcfb1 --- /dev/null +++ b/src/app/api/analytics/events/[eventId]/timeline/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const ANALYTICS_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io'; + +export async function GET( + request: NextRequest, + { params }: { params: { eventId: string } } +) { + try { + const { eventId } = params; + const token = request.headers.get('Authorization'); + const { searchParams } = new URL(request.url); + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = token; + } + + const queryString = searchParams.toString(); + const url = `${ANALYTICS_HOST}/api/v1/analytics/events/${eventId}/analytics/timeline${queryString ? `?${queryString}` : ''}`; + + const response = await fetch(url, { + method: 'GET', + headers, + }); + + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data); + } catch (error) { + console.error('❌ [Analytics Proxy] Event timeline error:', error); + return NextResponse.json( + { message: 'Event Timeline 데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/analytics/users/[userId]/channels/route.ts b/src/app/api/analytics/users/[userId]/channels/route.ts new file mode 100644 index 0000000..00b1ae5 --- /dev/null +++ b/src/app/api/analytics/users/[userId]/channels/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const ANALYTICS_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io'; + +export async function GET( + request: NextRequest, + { params }: { params: { userId: string } } +) { + try { + const { userId } = params; + const token = request.headers.get('Authorization'); + const { searchParams } = new URL(request.url); + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = token; + } + + const queryString = searchParams.toString(); + const url = `${ANALYTICS_HOST}/api/v1/analytics/users/${userId}/analytics/channels${queryString ? `?${queryString}` : ''}`; + + const response = await fetch(url, { + method: 'GET', + headers, + }); + + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data); + } catch (error) { + console.error('❌ [Analytics Proxy] User channels error:', error); + return NextResponse.json( + { message: 'Channels 데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/analytics/users/[userId]/roi/route.ts b/src/app/api/analytics/users/[userId]/roi/route.ts new file mode 100644 index 0000000..bfe71a7 --- /dev/null +++ b/src/app/api/analytics/users/[userId]/roi/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const ANALYTICS_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io'; + +export async function GET( + request: NextRequest, + { params }: { params: { userId: string } } +) { + try { + const { userId } = params; + const token = request.headers.get('Authorization'); + const { searchParams } = new URL(request.url); + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = token; + } + + const queryString = searchParams.toString(); + const url = `${ANALYTICS_HOST}/api/v1/analytics/users/${userId}/analytics/roi${queryString ? `?${queryString}` : ''}`; + + const response = await fetch(url, { + method: 'GET', + headers, + }); + + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data); + } catch (error) { + console.error('❌ [Analytics Proxy] User ROI error:', error); + return NextResponse.json( + { message: 'ROI 데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/analytics/users/[userId]/route.ts b/src/app/api/analytics/users/[userId]/route.ts new file mode 100644 index 0000000..b46d5ff --- /dev/null +++ b/src/app/api/analytics/users/[userId]/route.ts @@ -0,0 +1,59 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const ANALYTICS_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io'; + +/** + * GET /api/analytics/users/{userId} + * Proxy for User Analytics Dashboard + */ +export async function GET( + request: NextRequest, + { params }: { params: { userId: string } } +) { + try { + const { userId } = params; + const token = request.headers.get('Authorization'); + const { searchParams } = new URL(request.url); + + console.log('πŸ“Š [Analytics Proxy] Get user analytics request:', { + userId, + hasToken: !!token, + params: Object.fromEntries(searchParams), + }); + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = token; + } + + const queryString = searchParams.toString(); + const url = `${ANALYTICS_HOST}/api/v1/analytics/users/${userId}/analytics${queryString ? `?${queryString}` : ''}`; + + const response = await fetch(url, { + method: 'GET', + headers, + }); + + const data = await response.json(); + + console.log('βœ… [Analytics Proxy] User analytics response:', { + status: response.status, + success: response.ok, + }); + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data); + } catch (error) { + console.error('❌ [Analytics Proxy] User analytics error:', error); + return NextResponse.json( + { message: 'Analytics 데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/analytics/users/[userId]/timeline/route.ts b/src/app/api/analytics/users/[userId]/timeline/route.ts new file mode 100644 index 0000000..63bd695 --- /dev/null +++ b/src/app/api/analytics/users/[userId]/timeline/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const ANALYTICS_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io'; + +export async function GET( + request: NextRequest, + { params }: { params: { userId: string } } +) { + try { + const { userId } = params; + const token = request.headers.get('Authorization'); + const { searchParams } = new URL(request.url); + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = token; + } + + const queryString = searchParams.toString(); + const url = `${ANALYTICS_HOST}/api/v1/analytics/users/${userId}/analytics/timeline${queryString ? `?${queryString}` : ''}`; + + const response = await fetch(url, { + method: 'GET', + headers, + }); + + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data); + } catch (error) { + console.error('❌ [Analytics Proxy] User timeline error:', error); + return NextResponse.json( + { message: 'Timeline 데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.' }, + { status: 500 } + ); + } +} diff --git a/src/entities/analytics/api/analyticsApi.ts b/src/entities/analytics/api/analyticsApi.ts index 702339e..edbc4e4 100644 --- a/src/entities/analytics/api/analyticsApi.ts +++ b/src/entities/analytics/api/analyticsApi.ts @@ -15,11 +15,11 @@ import type { RoiQueryParams, } from '../model/types'; -const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'v1'; - /** * Analytics API Service * μ‹€μ‹œκ°„ 효과 μΈ‘μ • 및 톡합 λŒ€μ‹œλ³΄λ“œ API + * + * Note: Proxy routes handle /api/v1 prefix, so paths start with /users or /events */ export const analyticsApi = { // ============= User Analytics (μ‚¬μš©μž 전체 이벀트 톡합) ============= @@ -32,7 +32,7 @@ export const analyticsApi = { params?: AnalyticsQueryParams ): Promise => { const response = await analyticsClient.get>( - `/api/${API_VERSION}/users/${userId}/analytics`, + `/users/${userId}`, { params } ); return response.data.data; @@ -46,7 +46,7 @@ export const analyticsApi = { params?: TimelineQueryParams ): Promise => { const response = await analyticsClient.get>( - `/api/${API_VERSION}/users/${userId}/analytics/timeline`, + `/users/${userId}/timeline`, { params } ); return response.data.data; @@ -60,7 +60,7 @@ export const analyticsApi = { params?: AnalyticsQueryParams & RoiQueryParams ): Promise => { const response = await analyticsClient.get>( - `/api/${API_VERSION}/users/${userId}/analytics/roi`, + `/users/${userId}/roi`, { params } ); return response.data.data; @@ -74,7 +74,7 @@ export const analyticsApi = { params?: ChannelQueryParams ): Promise => { const response = await analyticsClient.get>( - `/api/${API_VERSION}/users/${userId}/analytics/channels`, + `/users/${userId}/channels`, { params } ); return response.data.data; @@ -90,7 +90,7 @@ export const analyticsApi = { params?: AnalyticsQueryParams ): Promise => { const response = await analyticsClient.get>( - `/api/${API_VERSION}/events/${eventId}/analytics`, + `/events/${eventId}`, { params } ); return response.data.data; @@ -104,7 +104,7 @@ export const analyticsApi = { params?: TimelineQueryParams ): Promise => { const response = await analyticsClient.get>( - `/api/${API_VERSION}/events/${eventId}/analytics/timeline`, + `/events/${eventId}/timeline`, { params } ); return response.data.data; @@ -118,7 +118,7 @@ export const analyticsApi = { params?: RoiQueryParams ): Promise => { const response = await analyticsClient.get>( - `/api/${API_VERSION}/events/${eventId}/analytics/roi`, + `/events/${eventId}/roi`, { params } ); return response.data.data; @@ -132,7 +132,7 @@ export const analyticsApi = { params?: ChannelQueryParams ): Promise => { const response = await analyticsClient.get>( - `/api/${API_VERSION}/events/${eventId}/analytics/channels`, + `/events/${eventId}/channels`, { params } ); return response.data.data; diff --git a/src/entities/analytics/api/analyticsClient.ts b/src/entities/analytics/api/analyticsClient.ts index 84d7211..dd3656b 100644 --- a/src/entities/analytics/api/analyticsClient.ts +++ b/src/entities/analytics/api/analyticsClient.ts @@ -1,9 +1,10 @@ import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'; -const ANALYTICS_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io'; +// Next.js API Proxyλ₯Ό 톡해 Analytics API 호좜 (CORS νšŒν”Ό) +const ANALYTICS_PROXY_BASE = '/api/analytics'; export const analyticsClient: AxiosInstance = axios.create({ - baseURL: ANALYTICS_HOST, + baseURL: ANALYTICS_PROXY_BASE, timeout: 30000, headers: { 'Content-Type': 'application/json',