Analytics API 프록시 라우트 구현 및 CORS 오류 해결

- Next.js API 프록시 라우트 8개 생성 (User/Event Analytics)
- analyticsClient baseURL을 프록시 경로로 변경
- analyticsApi 경로에서 /api/v1 접두사 제거
- 404/400 에러에 대한 사용자 친화적 에러 처리 추가
- Dashboard, Event Detail, Analytics 페이지 에러 핸들링 개선

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
cherry2250 2025-10-31 00:34:20 +09:00
parent a58ca4ece1
commit 6331ab3fde
26 changed files with 1148 additions and 379 deletions

247
ROUTES.md Normal file
View File

@ -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)
```

View File

@ -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)',
}}
>
{/* 로고 및 타이틀 */}
<Box sx={{ textAlign: 'center', mb: 6 }}>
<Box sx={{ textAlign: 'center', mb: { xs: 4, sm: 6 } }}> {/* 320px: 32px, 600px+: 48px */}
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 64,
height: 64,
width: { xs: 56, sm: 64 }, // 320px: 56px, 600px+: 64px
height: { xs: 56, sm: 64 },
borderRadius: '50%',
bgcolor: 'primary.main',
mb: 3,
mb: { xs: 2, sm: 3 }, // 320px: 16px, 600px+: 24px
}}
>
<Typography sx={{ fontSize: 32 }}>🎉</Typography>
<Typography sx={{ fontSize: { xs: 28, sm: 32 } }}>🎉</Typography> {/* 320px: 28px, 600px+: 32px */}
</Box>
<Typography variant="h4" sx={{ ...responsiveText.h2, mb: 1 }}>
<Typography
variant="h4"
sx={{
...responsiveText.h2,
mb: 1,
fontSize: { xs: '1.25rem', sm: '1.5rem' } // 320px: 20px, 600px+: 24px
}}
>
KT
</Typography>
<Typography variant="body2" color="text.secondary">
<Typography
variant="body2"
color="text.secondary"
sx={{ fontSize: { xs: '0.813rem', sm: '0.875rem' } }} // 320px: 13px, 600px+: 14px
>
</Typography>
</Box>
{/* 로그인 폼 */}
<form onSubmit={handleSubmit(onSubmit)}>
<Box sx={{ mb: 3 }}>
<Box sx={{ mb: { xs: 2, sm: 3 } }}> {/* 320px: 16px, 600px+: 24px */}
<Controller
name="email"
control={control}
@ -161,16 +172,21 @@ export default function LoginPage() {
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Email color="action" />
<Email color="action" sx={{ fontSize: { xs: 20, sm: 24 } }} /> {/* 320px: 20px, 600px+: 24px */}
</InputAdornment>
),
}}
sx={{
'& .MuiInputBase-input': {
fontSize: { xs: '0.875rem', sm: '1rem' } // 320px: 14px, 600px+: 16px
}
}}
/>
)}
/>
</Box>
<Box sx={{ mb: 2 }}>
<Box sx={{ mb: { xs: 1.5, sm: 2 } }}> {/* 320px: 12px, 600px+: 16px */}
<Controller
name="password"
control={control}
@ -186,7 +202,7 @@ export default function LoginPage() {
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Lock color="action" />
<Lock color="action" sx={{ fontSize: { xs: 20, sm: 24 } }} />
</InputAdornment>
),
endAdornment: (
@ -201,20 +217,29 @@ export default function LoginPage() {
</InputAdornment>
),
}}
sx={{
'& .MuiInputBase-input': {
fontSize: { xs: '0.875rem', sm: '1rem' }
}
}}
/>
)}
/>
</Box>
<Box sx={{ mb: 3 }}>
<Box sx={{ mb: { xs: 2, sm: 3 } }}> {/* 320px: 16px, 600px+: 24px */}
<Controller
name="rememberMe"
control={control}
render={({ field }) => (
<FormControlLabel
control={<Checkbox {...field} checked={field.value} />}
control={<Checkbox {...field} checked={field.value} size="small" />}
label={
<Typography variant="body2" color="text.secondary">
<Typography
variant="body2"
color="text.secondary"
sx={{ fontSize: { xs: '0.813rem', sm: '0.875rem' } }} // 320px: 13px, 600px+: 14px
>
</Typography>
}
@ -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'),
}}
>
</Button>
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 2, mb: 4 }}>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
gap: { xs: 1.5, sm: 2 }, // 320px: 12px, 600px+: 16px
mb: { xs: 3, sm: 4 }, // 320px: 24px, 600px+: 32px
flexWrap: 'wrap'
}}
>
<Link
href="#"
onClick={handleUnavailableFeature}
variant="body2"
color="text.secondary"
underline="hover"
sx={{ cursor: 'pointer' }}
sx={{
cursor: 'pointer',
fontSize: { xs: '0.813rem', sm: '0.875rem' } // 320px: 13px, 600px+: 14px
}}
>
</Link>
<Typography variant="body2" color="text.secondary">
<Typography
variant="body2"
color="text.secondary"
sx={{ fontSize: { xs: '0.813rem', sm: '0.875rem' } }}
>
|
</Typography>
<Link
@ -258,7 +298,11 @@ export default function LoginPage() {
variant="body2"
color="primary"
underline="hover"
sx={{ cursor: 'pointer', fontWeight: 600 }}
sx={{
cursor: 'pointer',
fontWeight: 600,
fontSize: { xs: '0.813rem', sm: '0.875rem' }
}}
>
</Link>
@ -266,20 +310,32 @@ export default function LoginPage() {
</form>
{/* 소셜 로그인 */}
<Divider sx={{ mb: 3 }}>
<Typography variant="body2" color="text.secondary">
<Divider sx={{ mb: { xs: 2, sm: 3 } }}> {/* 320px: 16px, 600px+: 24px */}
<Typography
variant="body2"
color="text.secondary"
sx={{ fontSize: { xs: '0.813rem', sm: '0.875rem' } }} // 320px: 13px, 600px+: 14px
>
</Typography>
</Divider>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: { xs: 1.5, sm: 2 }, // 320px: 12px, 600px+: 16px
mb: { xs: 2, sm: 3 } // 320px: 16px, 600px+: 24px
}}
>
<Button
fullWidth
variant="outlined"
size="large"
onClick={handleUnavailableFeature}
sx={{
py: 1.5,
py: { xs: 1.25, sm: 1.5 }, // 320px: 10px, 600px+: 12px
fontSize: { xs: '0.875rem', sm: '0.938rem' }, // 320px: 14px, 600px+: 15px
borderColor: '#FEE500',
bgcolor: '#FEE500',
color: '#000000',
@ -289,7 +345,7 @@ export default function LoginPage() {
borderColor: '#FDD835',
},
}}
startIcon={<ChatBubble />}
startIcon={<ChatBubble sx={{ fontSize: { xs: 20, sm: 24 } }} />}
>
</Button>
@ -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={
<Box
sx={{
width: 20,
height: 20,
width: { xs: 18, sm: 20 }, // 320px: 18px, 600px+: 20px
height: { xs: 18, sm: 20 },
borderRadius: '50%',
bgcolor: 'white',
display: 'flex',
@ -322,7 +379,7 @@ export default function LoginPage() {
justifyContent: 'center',
fontWeight: 700,
color: '#03C75A',
fontSize: 14,
fontSize: { xs: 12, sm: 14 }, // 320px: 12px, 600px+: 14px
}}
>
N
@ -337,14 +394,22 @@ export default function LoginPage() {
<Typography
variant="caption"
color="text.secondary"
sx={{ textAlign: 'center', display: 'block' }}
sx={{
textAlign: 'center',
display: 'block',
fontSize: { xs: '0.75rem', sm: '0.813rem' }, // 320px: 12px, 600px+: 13px
lineHeight: 1.5
}}
>
{' '}
<Link
href="#"
onClick={handleUnavailableFeature}
underline="hover"
sx={{ color: 'text.secondary' }}
sx={{
color: 'text.secondary',
fontSize: 'inherit'
}}
>
</Link>{' '}
@ -353,7 +418,10 @@ export default function LoginPage() {
href="#"
onClick={handleUnavailableFeature}
underline="hover"
sx={{ color: 'text.secondary' }}
sx={{
color: 'text.secondary',
fontSize: 'inherit'
}}
>
</Link>
@ -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
}}
>
<Info color="info" sx={{ fontSize: 28 }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
<Info color="info" sx={{ fontSize: { xs: 24, sm: 28 } }} /> {/* 320px: 24px, 600px+: 28px */}
<Typography
variant="h6"
sx={{
fontWeight: 600,
fontSize: { xs: '1rem', sm: '1.125rem' } // 320px: 16px, 600px+: 18px
}}
>
</Typography>
</DialogTitle>
<DialogContent>
<Typography variant="body1" sx={{ color: 'text.secondary', lineHeight: 1.7 }}>
<DialogContent sx={{ px: { xs: 2, sm: 3 } }}> {/* 320px: 16px, 600px+: 24px */}
<Typography
variant="body1"
sx={{
color: 'text.secondary',
lineHeight: 1.7,
fontSize: { xs: '0.875rem', sm: '1rem' } // 320px: 14px, 600px+: 16px
}}
>
.
</Typography>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 3 }}>
<DialogActions sx={{ px: { xs: 2, sm: 3 }, pb: { xs: 2, sm: 3 } }}> {/* 320px: 16px, 600px+: 24px */}
<Button
onClick={handleCloseModal}
variant="contained"
fullWidth
sx={{
py: 1.5,
py: { xs: 1.25, sm: 1.5 }, // 320px: 10px, 600px+: 12px
fontSize: { xs: '0.875rem', sm: '1rem' }, // 320px: 14px, 600px+: 16px
...getGradientButtonStyle('primary'),
}}
>

View File

@ -97,7 +97,15 @@ export default function AnalyticsPage() {
setLastUpdate(new Date());
} catch (error: any) {
console.error('❌ Analytics 데이터 로드 실패:', error);
// 에러 발생 시에도 로딩 상태 해제
// 404 또는 400 에러는 아직 Analytics 데이터가 없는 경우
if (error.response?.status === 404 || error.response?.status === 400) {
console.log(' Analytics 데이터가 아직 생성되지 않았습니다.');
// 에러 상태를 설정하지 않고 빈 데이터로 표시
} else {
// 다른 에러는 에러로 처리
console.error('❌ 예상치 못한 에러:', error);
}
} finally {
setLoading(false);
setRefreshing(false);
@ -145,7 +153,7 @@ export default function AnalyticsPage() {
<Box
sx={{
pt: { xs: 7, sm: 8 },
pb: 10,
pb: { xs: 4, sm: 10 },
bgcolor: colors.gray[50],
minHeight: '100vh',
display: 'flex',
@ -167,7 +175,7 @@ export default function AnalyticsPage() {
<Box
sx={{
pt: { xs: 7, sm: 8 },
pb: 10,
pb: { xs: 4, sm: 10 },
bgcolor: colors.gray[50],
minHeight: '100vh',
display: 'flex',
@ -326,7 +334,7 @@ export default function AnalyticsPage() {
<Box
sx={{
pt: { xs: 7, sm: 8 },
pb: 10,
pb: { xs: 4, sm: 10 },
bgcolor: colors.gray[50],
minHeight: '100vh',
}}
@ -603,7 +611,7 @@ export default function AnalyticsPage() {
}}
/>
) : (
<Box sx={{ textAlign: 'center', py: { xs: 4, sm: 6 } }}>
<Box sx={{ textAlign: 'center', pt: { xs: 2, sm: 6 }, pb: { xs: 2, sm: 6 } }}>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>
.
</Typography>

View File

@ -248,8 +248,8 @@ export default function DrawPage() {
}
return (
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 10 }}>
<Container maxWidth="lg" sx={{ pt: 8, pb: 8, px: { xs: 6, sm: 8, md: 10 } }}>
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: { xs: 4, sm: 10 } }}>
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 8, md: 10 } }}>
{/* 에러 메시지 */}
{error && (
<Alert severity="error" sx={{ mb: 4, borderRadius: 3 }} onClose={() => setError(null)}>
@ -261,7 +261,7 @@ export default function DrawPage() {
{!showResults && (
<>
{/* Page Header */}
<Box sx={{ mb: 8 }}>
<Box sx={{ mb: { xs: 3, sm: 8 } }}>
<Typography variant="h4" sx={{ fontWeight: 700, fontSize: '2rem', mb: 2 }}>
🎲
</Typography>
@ -271,7 +271,7 @@ export default function DrawPage() {
</Box>
{/* Event Info Summary Cards */}
<Grid container spacing={6} sx={{ mb: 10 }}>
<Grid container spacing={6} sx={{ mb: { xs: 4, sm: 10 } }}>
<Grid item xs={6} md={6}>
<Card
elevation={0}
@ -281,12 +281,12 @@ export default function DrawPage() {
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.purpleLight} 100%)`,
}}
>
<CardContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
<EventNote sx={{ fontSize: 40, mb: 2, color: 'white' }} />
<Typography variant="caption" display="block" sx={{ mb: 2, color: 'rgba(255,255,255,0.9)' }}>
<CardContent sx={{ textAlign: 'center', py: { xs: 3, sm: 6 }, px: { xs: 2, sm: 4 } }}>
<EventNote sx={{ fontSize: { xs: 32, sm: 40 }, mb: { xs: 1, sm: 2 }, color: 'white' }} />
<Typography variant="caption" display="block" sx={{ mb: { xs: 1, sm: 2 }, color: 'rgba(255,255,255,0.9)', fontSize: { xs: '0.625rem', sm: '0.75rem' } }}>
</Typography>
<Typography variant="h6" sx={{ fontWeight: 700, color: 'white' }}>
<Typography variant="h6" sx={{ fontWeight: 700, color: 'white', fontSize: { xs: '0.875rem', sm: '1.25rem' } }}>
{eventName}
</Typography>
</CardContent>
@ -301,12 +301,12 @@ export default function DrawPage() {
background: `linear-gradient(135deg, ${colors.blue} 0%, #93C5FD 100%)`,
}}
>
<CardContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
<People sx={{ fontSize: 40, mb: 2, color: 'white' }} />
<Typography variant="caption" display="block" sx={{ mb: 2, color: 'rgba(255,255,255,0.9)' }}>
<CardContent sx={{ textAlign: 'center', py: { xs: 3, sm: 6 }, px: { xs: 2, sm: 4 } }}>
<People sx={{ fontSize: { xs: 32, sm: 40 }, mb: { xs: 1, sm: 2 }, color: 'white' }} />
<Typography variant="caption" display="block" sx={{ mb: { xs: 1, sm: 2 }, color: 'rgba(255,255,255,0.9)', fontSize: { xs: '0.625rem', sm: '0.75rem' } }}>
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: '1.75rem' }}>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: { xs: '1rem', sm: '1.75rem' } }}>
{totalParticipants}
</Typography>
</CardContent>
@ -315,8 +315,8 @@ export default function DrawPage() {
</Grid>
{/* Drawing Settings */}
<Card elevation={0} sx={{ mb: 10, borderRadius: 4, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)' }}>
<CardContent sx={{ p: 6 }}>
<Card elevation={0} sx={{ mb: { xs: 4, sm: 10 }, borderRadius: 4, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)' }}>
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 6 }}>
<Tune sx={{ fontSize: 32, color: colors.pink }} />
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: '1.5rem' }}>
@ -428,7 +428,7 @@ export default function DrawPage() {
startIcon={<Casino sx={{ fontSize: 28 }} />}
onClick={handleStartDrawing}
sx={{
mb: 10,
mb: { xs: 4, sm: 10 },
py: 3,
borderRadius: 4,
fontWeight: 700,
@ -452,7 +452,7 @@ export default function DrawPage() {
{showResults && (
<>
{/* Results Header */}
<Box sx={{ textAlign: 'center', mb: 10 }}>
<Box sx={{ textAlign: 'center', mb: { xs: 4, sm: 10 } }}>
<Typography variant="h4" sx={{ fontWeight: 700, mb: 4, fontSize: '2rem' }}>
🎉 !
</Typography>
@ -467,7 +467,7 @@ export default function DrawPage() {
</Box>
{/* Winner List */}
<Box sx={{ mb: 10 }}>
<Box sx={{ mb: { xs: 4, sm: 10 } }}>
<Typography variant="h5" sx={{ fontWeight: 700, mb: 6, fontSize: '1.5rem' }}>
🏆
</Typography>
@ -482,7 +482,7 @@ export default function DrawPage() {
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
}}
>
<CardContent sx={{ p: 5 }}>
<CardContent sx={{ p: { xs: 2.5, sm: 5 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Box
sx={{

View File

@ -155,74 +155,64 @@ export default function EventDetailPage() {
const [error, setError] = useState<string | null>(null);
const [analyticsData, setAnalyticsData] = useState<any>(null);
// Analytics API 호출 (임시 주석처리 - 서버 이슈)
// Analytics API 호출
const fetchAnalytics = async (forceRefresh = false) => {
try {
if (forceRefresh) {
console.log('🔄 Mock 데이터 새로고침...');
console.log('🔄 Analytics 데이터 새로고침...');
setRefreshing(true);
} else {
console.log('📊 Mock Analytics 데이터 로딩...');
console.log('📊 Analytics 데이터 로딩...');
setLoading(true);
}
setError(null);
// TODO: Analytics API 서버 이슈 해결 후 주석 해제
// Event Analytics API 병렬 호출
// const [dashboard, timeline, roi, channels] = await Promise.all([
// analyticsApi.getEventAnalytics(eventId, { refresh: forceRefresh }),
// analyticsApi.getEventTimelineAnalytics(eventId, {
// interval: chartPeriod === '7d' ? 'daily' : chartPeriod === '30d' ? 'daily' : 'daily',
// refresh: forceRefresh
// }),
// analyticsApi.getEventRoiAnalytics(eventId, { includeProjection: true, refresh: forceRefresh }),
// analyticsApi.getEventChannelAnalytics(eventId, { refresh: forceRefresh }),
// ]);
const [dashboard, timeline, roi, channels] = await Promise.all([
analyticsApi.getEventAnalytics(eventId, { refresh: forceRefresh }),
analyticsApi.getEventTimelineAnalytics(eventId, {
interval: chartPeriod === '7d' ? 'daily' : chartPeriod === '30d' ? 'daily' : 'daily',
}),
analyticsApi.getEventRoiAnalytics(eventId, { includeProjection: true }),
analyticsApi.getEventChannelAnalytics(eventId, {}),
]);
// 임시 Mock 데이터
await new Promise(resolve => setTimeout(resolve, 500));
const mockAnalyticsData = {
dashboard: {
summary: {
participants: mockEventData.participants,
totalViews: mockEventData.views,
conversionRate: mockEventData.conversion / 100,
},
roi: {
roi: mockEventData.roi,
},
},
timeline: {
participations: [
{ date: '2025-01-15', count: 12 },
{ date: '2025-01-16', count: 18 },
{ date: '2025-01-17', count: 25 },
{ date: '2025-01-18', count: 31 },
{ date: '2025-01-19', count: 22 },
{ date: '2025-01-20', count: 20 },
],
},
roi: {
currentRoi: mockEventData.roi,
projectedRoi: mockEventData.roi + 50,
},
channels: {
distribution: [
{ channel: '우리동네TV', participants: 45 },
{ channel: '링고비즈', participants: 38 },
{ channel: 'SNS', participants: 45 },
],
},
};
console.log('✅ Dashboard 데이터:', dashboard);
console.log('✅ Timeline 데이터:', timeline);
console.log('✅ ROI 데이터:', roi);
console.log('✅ Channels 데이터:', channels);
// Analytics 데이터 저장
setAnalyticsData(mockAnalyticsData);
const formattedAnalyticsData = {
dashboard,
timeline,
roi,
channels,
};
console.log('✅ Mock Analytics 데이터 로딩 완료');
setAnalyticsData(formattedAnalyticsData);
// Event 객체 업데이트 - Analytics 데이터 반영
setEvent(prev => ({
...prev,
participants: dashboard.summary.participants,
views: dashboard.summary.totalViews,
conversion: dashboard.summary.conversionRate * 100,
roi: dashboard.roi.roi,
title: dashboard.eventTitle,
}));
console.log('✅ Analytics 데이터 로딩 완료');
} catch (err: any) {
console.error('❌ Analytics 데이터 로딩 실패:', err);
setError(err.message || 'Analytics 데이터를 불러오는데 실패했습니다.');
// 404 또는 400 에러는 아직 Analytics 데이터가 없는 경우
if (err.response?.status === 404 || err.response?.status === 400) {
console.log(' Analytics 데이터가 아직 생성되지 않았습니다.');
setError('이벤트의 Analytics 데이터가 아직 생성되지 않았습니다. 참여자가 생기면 자동으로 생성됩니다.');
} else {
setError(err.message || 'Analytics 데이터를 불러오는데 실패했습니다.');
}
} finally {
setLoading(false);
setRefreshing(false);
@ -579,7 +569,8 @@ export default function EventDetailPage() {
>
<CardContent sx={{
textAlign: 'center',
py: { xs: 2, sm: 6 },
pt: { xs: 1.5, sm: 6 },
pb: { xs: 1.5, sm: 6 },
px: { xs: 1, sm: 4 },
display: 'flex',
flexDirection: 'column',
@ -612,7 +603,8 @@ export default function EventDetailPage() {
>
<CardContent sx={{
textAlign: 'center',
py: { xs: 2, sm: 6 },
pt: { xs: 1.5, sm: 6 },
pb: { xs: 1.5, sm: 6 },
px: { xs: 1, sm: 4 },
display: 'flex',
flexDirection: 'column',
@ -642,7 +634,8 @@ export default function EventDetailPage() {
>
<CardContent sx={{
textAlign: 'center',
py: { xs: 2, sm: 6 },
pt: { xs: 1.5, sm: 6 },
pb: { xs: 1.5, sm: 6 },
px: { xs: 1, sm: 4 },
display: 'flex',
flexDirection: 'column',
@ -672,7 +665,8 @@ export default function EventDetailPage() {
>
<CardContent sx={{
textAlign: 'center',
py: { xs: 2, sm: 6 },
pt: { xs: 1.5, sm: 6 },
pb: { xs: 1.5, sm: 6 },
px: { xs: 1, sm: 4 },
display: 'flex',
flexDirection: 'column',

View File

@ -168,82 +168,82 @@ export default function ParticipantsPage() {
};
return (
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 10 }}>
<Container maxWidth="lg" sx={{ pt: 8, pb: 8, px: { xs: 6, sm: 8, md: 10 } }}>
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: { xs: 4, sm: 10 } }}>
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 8, md: 10 } }}>
{/* Page Header */}
<Box sx={{ mb: 8 }}>
<Typography variant="h4" sx={{ fontWeight: 700, fontSize: '2rem', mb: 2 }}>
<Box sx={{ mb: { xs: 4, sm: 8 } }}>
<Typography variant="h4" sx={{ fontWeight: 700, fontSize: { xs: '1.5rem', sm: '2rem' }, mb: 2 }}>
👥
</Typography>
<Typography variant="body1" color="text.secondary">
<Typography variant="body1" color="text.secondary" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>
</Typography>
</Box>
{/* Error Alert */}
{error && (
<Alert severity="error" sx={{ mb: 6 }} icon={<ErrorIcon />} onClose={() => setError('')}>
<Alert severity="error" sx={{ mb: { xs: 3, sm: 6 } }} icon={<ErrorIcon />} onClose={() => setError('')}>
{error}
</Alert>
)}
{/* Statistics Cards */}
<Grid container spacing={6} sx={{ mb: 10 }}>
<Grid item xs={6} md={4}>
<Grid container spacing={{ xs: 1, sm: 6 }} sx={{ mb: { xs: 4, sm: 10 } }}>
<Grid item xs={4} md={4}>
<Card
elevation={0}
sx={{
borderRadius: 4,
borderRadius: { xs: 2, sm: 4 },
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.purpleLight} 100%)`,
}}
>
<CardContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
<People sx={{ fontSize: 40, mb: 2, color: 'white' }} />
<Typography variant="caption" display="block" sx={{ mb: 2, color: 'rgba(255,255,255,0.9)' }}>
<CardContent sx={{ textAlign: 'center', py: { xs: 1.5, sm: 6 }, px: { xs: 0.5, sm: 4 } }}>
<People sx={{ fontSize: { xs: 24, sm: 40 }, mb: { xs: 0.5, sm: 2 }, color: 'white' }} />
<Typography variant="caption" display="block" sx={{ mb: { xs: 0.5, sm: 2 }, color: 'rgba(255,255,255,0.9)', fontSize: { xs: '0.5rem', sm: '0.75rem' }, lineHeight: 1.2 }}>
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: '1.75rem' }}>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: { xs: '0.875rem', sm: '1.75rem' } }}>
{loading ? '...' : stats.total}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={4}>
<Grid item xs={4} md={4}>
<Card
elevation={0}
sx={{
borderRadius: 4,
borderRadius: { xs: 2, sm: 4 },
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
background: `linear-gradient(135deg, ${colors.yellow} 0%, #FCD34D 100%)`,
}}
>
<CardContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
<AccessTime sx={{ fontSize: 40, mb: 2, color: 'white' }} />
<Typography variant="caption" display="block" sx={{ mb: 2, color: 'rgba(255,255,255,0.9)' }}>
<CardContent sx={{ textAlign: 'center', py: { xs: 1.5, sm: 6 }, px: { xs: 0.5, sm: 4 } }}>
<AccessTime sx={{ fontSize: { xs: 24, sm: 40 }, mb: { xs: 0.5, sm: 2 }, color: 'white' }} />
<Typography variant="caption" display="block" sx={{ mb: { xs: 0.5, sm: 2 }, color: 'rgba(255,255,255,0.9)', fontSize: { xs: '0.5rem', sm: '0.75rem' }, lineHeight: 1.2 }}>
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: '1.75rem' }}>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: { xs: '0.875rem', sm: '1.75rem' } }}>
{loading ? '...' : stats.waiting}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={4}>
<Grid item xs={4} md={4}>
<Card
elevation={0}
sx={{
borderRadius: 4,
borderRadius: { xs: 2, sm: 4 },
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
background: `linear-gradient(135deg, ${colors.mint} 0%, #6EE7B7 100%)`,
}}
>
<CardContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
<TrendingUp sx={{ fontSize: 40, mb: 2, color: 'white' }} />
<Typography variant="caption" display="block" sx={{ mb: 2, color: 'rgba(255,255,255,0.9)' }}>
<CardContent sx={{ textAlign: 'center', py: { xs: 1.5, sm: 6 }, px: { xs: 0.5, sm: 4 } }}>
<TrendingUp sx={{ fontSize: { xs: 24, sm: 40 }, mb: { xs: 0.5, sm: 2 }, color: 'white' }} />
<Typography variant="caption" display="block" sx={{ mb: { xs: 0.5, sm: 2 }, color: 'rgba(255,255,255,0.9)', fontSize: { xs: '0.5rem', sm: '0.75rem' }, lineHeight: 1.2 }}>
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: '1.75rem' }}>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: { xs: '0.875rem', sm: '1.75rem' } }}>
{loading ? '...' : stats.winner}
</Typography>
</CardContent>
@ -252,7 +252,7 @@ export default function ParticipantsPage() {
</Grid>
{/* Search Section */}
<Box sx={{ mb: 6 }}>
<Box sx={{ mb: { xs: 3, sm: 6 } }}>
<TextField
fullWidth
placeholder="이름, 전화번호 또는 이메일 검색..."
@ -261,7 +261,7 @@ export default function ParticipantsPage() {
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search />
<Search sx={{ fontSize: { xs: 18, sm: 24 } }} />
</InputAdornment>
),
}}
@ -269,6 +269,10 @@ export default function ParticipantsPage() {
'& .MuiOutlinedInput-root': {
borderRadius: 3,
bgcolor: 'white',
fontSize: { xs: '0.75rem', sm: '1rem' },
},
'& .MuiOutlinedInput-input': {
padding: { xs: '8px 14px', sm: '16.5px 14px' },
},
}}
disabled={loading}
@ -276,11 +280,11 @@ export default function ParticipantsPage() {
</Box>
{/* Filters */}
<Box sx={{ mb: 8 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, flexWrap: 'wrap' }}>
<FilterList sx={{ fontSize: 28, color: colors.pink }} />
<FormControl sx={{ flex: 1, minWidth: 160 }}>
<InputLabel> </InputLabel>
<Box sx={{ mb: { xs: 4, sm: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, flexWrap: 'wrap' }}>
<FilterList sx={{ fontSize: { xs: 20, sm: 28 }, color: colors.pink }} />
<FormControl sx={{ flex: 1, minWidth: { xs: 100, sm: 160 } }}>
<InputLabel sx={{ fontSize: { xs: '0.75rem', sm: '1rem' } }}> </InputLabel>
<Select
value={storeVisitedFilter === undefined ? 'all' : storeVisitedFilter ? 'visited' : 'not_visited'}
label="매장 방문"
@ -291,47 +295,60 @@ export default function ParticipantsPage() {
);
setCurrentPage(1); // 필터 변경 시 첫 페이지로
}}
sx={{ borderRadius: 2 }}
sx={{
borderRadius: 2,
fontSize: { xs: '0.75rem', sm: '1rem' },
'& .MuiSelect-select': {
padding: { xs: '8px 14px', sm: '16.5px 14px' },
},
}}
disabled={loading}
>
<MenuItem value="all"></MenuItem>
<MenuItem value="visited"></MenuItem>
<MenuItem value="not_visited"></MenuItem>
<MenuItem value="all" sx={{ fontSize: { xs: '0.75rem', sm: '1rem' } }}></MenuItem>
<MenuItem value="visited" sx={{ fontSize: { xs: '0.75rem', sm: '1rem' } }}></MenuItem>
<MenuItem value="not_visited" sx={{ fontSize: { xs: '0.75rem', sm: '1rem' } }}></MenuItem>
</Select>
</FormControl>
<FormControl sx={{ flex: 1, minWidth: 140 }}>
<InputLabel></InputLabel>
<FormControl sx={{ flex: 1, minWidth: { xs: 90, sm: 140 } }}>
<InputLabel sx={{ fontSize: { xs: '0.75rem', sm: '1rem' } }}></InputLabel>
<Select
value={statusFilter}
label="상태"
onChange={(e) => setStatusFilter(e.target.value as StatusType)}
sx={{ borderRadius: 2 }}
sx={{
borderRadius: 2,
fontSize: { xs: '0.75rem', sm: '1rem' },
'& .MuiSelect-select': {
padding: { xs: '8px 14px', sm: '16.5px 14px' },
},
}}
disabled={loading}
>
<MenuItem value="all"></MenuItem>
<MenuItem value="waiting"> </MenuItem>
<MenuItem value="winner"></MenuItem>
<MenuItem value="all" sx={{ fontSize: { xs: '0.75rem', sm: '1rem' } }}></MenuItem>
<MenuItem value="waiting" sx={{ fontSize: { xs: '0.75rem', sm: '1rem' } }}> </MenuItem>
<MenuItem value="winner" sx={{ fontSize: { xs: '0.75rem', sm: '1rem' } }}></MenuItem>
</Select>
</FormControl>
</Box>
</Box>
{/* Total Count & Drawing Button */}
<Box sx={{ mb: 6 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 4 }}>
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: '1.5rem' }}>
<Box sx={{ mb: { xs: 3, sm: 6 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: { xs: 2, sm: 4 } }}>
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '0.875rem', sm: '1.5rem' } }}>
<span style={{ color: colors.pink }}>{filteredParticipants.length}</span>
</Typography>
<Box sx={{ display: 'flex', gap: 3 }}>
<Box sx={{ display: 'flex', gap: { xs: 1.5, sm: 3 } }}>
<Button
variant="outlined"
startIcon={<Download />}
startIcon={<Download sx={{ fontSize: { xs: 16, sm: 20 } }} />}
onClick={handleDownloadClick}
disabled={loading}
sx={{
borderRadius: 3,
px: 4,
py: 1.5,
px: { xs: 1.5, sm: 4 },
py: { xs: 0.75, sm: 1.5 },
fontSize: { xs: '0.7rem', sm: '0.875rem' },
borderColor: colors.blue,
color: colors.blue,
'&:hover': {
@ -340,17 +357,23 @@ export default function ParticipantsPage() {
},
}}
>
<Box component="span" sx={{ display: { xs: 'none', sm: 'inline' } }}>
</Box>
<Box component="span" sx={{ display: { xs: 'inline', sm: 'none' } }}>
</Box>
</Button>
<Button
variant="contained"
startIcon={<Casino />}
startIcon={<Casino sx={{ fontSize: { xs: 16, sm: 20 } }} />}
onClick={handleDrawClick}
disabled={loading}
sx={{
borderRadius: 3,
px: 4,
py: 1.5,
px: { xs: 1.5, sm: 4 },
py: { xs: 0.75, sm: 1.5 },
fontSize: { xs: '0.7rem', sm: '0.875rem' },
background: `linear-gradient(135deg, ${colors.pink} 0%, ${colors.purple} 100%)`,
'&:hover': {
background: `linear-gradient(135deg, ${colors.pink} 0%, ${colors.purple} 100%)`,
@ -358,7 +381,12 @@ export default function ParticipantsPage() {
},
}}
>
<Box component="span" sx={{ display: { xs: 'none', sm: 'inline' } }}>
</Box>
<Box component="span" sx={{ display: { xs: 'inline', sm: 'none' } }}>
</Box>
</Button>
</Box>
</Box>
@ -391,7 +419,7 @@ export default function ParticipantsPage() {
{/* Participant List */}
{!loading && filteredParticipants.length > 0 && (
<>
<Box sx={{ mb: 10 }}>
<Box sx={{ mb: { xs: 4, sm: 10 } }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{filteredParticipants.map((participant) => (
<Card
@ -409,21 +437,23 @@ export default function ParticipantsPage() {
}}
onClick={() => handleParticipantClick(participant)}
>
<CardContent sx={{ p: 5 }}>
<CardContent sx={{ p: { xs: 3, sm: 5 } }}>
{/* Header */}
<Box
sx={{
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'space-between',
mb: 4,
mb: { xs: 2, sm: 4 },
flexWrap: { xs: 'wrap', sm: 'nowrap' },
gap: { xs: 2, sm: 0 },
}}
>
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 3 }}>
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: { xs: 2, sm: 3 } }}>
<Box
sx={{
width: 56,
height: 56,
width: { xs: 48, sm: 56 },
height: { xs: 48, sm: 56 },
borderRadius: '50%',
bgcolor: colors.purpleLight,
display: 'flex',
@ -431,16 +461,16 @@ export default function ParticipantsPage() {
justifyContent: 'center',
}}
>
<Person sx={{ fontSize: 32, color: colors.purple }} />
<Person sx={{ fontSize: { xs: 28, sm: 32 }, color: colors.purple }} />
</Box>
<Box>
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
<Typography variant="caption" color="text.secondary" sx={{ mb: 0.5, display: 'block', fontSize: { xs: '0.625rem', sm: '0.75rem' } }}>
#{participant.participantId}
</Typography>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1, fontSize: '1.25rem' }}>
<Typography variant="h6" sx={{ fontWeight: 700, mb: { xs: 0.5, sm: 1 }, fontSize: { xs: '0.875rem', sm: '1.25rem' } }}>
{participant.name}
</Typography>
<Typography variant="body1" color="text.secondary">
<Typography variant="body1" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '1rem' } }}>
{participant.phoneNumber}
</Typography>
</Box>
@ -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' } }}
/>
</Box>
@ -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 && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body1" color="text.secondary">
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 1 }}>
<Typography variant="body1" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '1rem' } }}>
</Typography>
<Chip
@ -476,24 +506,25 @@ export default function ParticipantsPage() {
bgcolor: colors.purpleLight,
color: colors.purple,
fontWeight: 600,
fontSize: { xs: '0.625rem', sm: '0.75rem' },
}}
/>
</Box>
)}
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body1" color="text.secondary">
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 1 }}>
<Typography variant="body1" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '1rem' } }}>
</Typography>
<Typography variant="body1" sx={{ fontWeight: 600 }}>
<Typography variant="body1" sx={{ fontWeight: 600, fontSize: { xs: '0.75rem', sm: '1rem' }, textAlign: 'right' }}>
{new Date(participant.participatedAt).toLocaleString('ko-KR')}
</Typography>
</Box>
{participant.storeVisited && (
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body1" color="text.secondary">
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 1 }}>
<Typography variant="body1" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '1rem' } }}>
</Typography>
<Chip label="방문" size="small" color="success" />
<Chip label="방문" size="small" color="success" sx={{ fontSize: { xs: '0.625rem', sm: '0.75rem' } }} />
</Box>
)}
</Box>
@ -505,7 +536,7 @@ export default function ParticipantsPage() {
{/* Pagination */}
{totalPages > 1 && (
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 10 }}>
<Box sx={{ display: 'flex', justifyContent: 'center', mb: { xs: 4, sm: 10 } }}>
<Pagination
count={totalPages}
page={currentPage}
@ -514,8 +545,14 @@ export default function ParticipantsPage() {
size="large"
sx={{
'& .MuiPaginationItem-root': {
fontSize: '1rem',
fontSize: { xs: '0.75rem', sm: '1rem' },
fontWeight: 600,
minWidth: { xs: '26px', sm: '32px' },
height: { xs: '26px', sm: '32px' },
margin: { xs: '0 2px', sm: '0 4px' },
},
'& .MuiPaginationItem-icon': {
fontSize: { xs: '1rem', sm: '1.5rem' },
},
}}
/>
@ -558,7 +595,7 @@ export default function ParticipantsPage() {
mb: 3,
}}
>
<Person sx={{ fontSize: 40, color: colors.purple }} />
<Person sx={{ fontSize: { xs: 32, sm: 40 }, color: colors.purple }} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1 }}>
{selectedParticipant.name}

View File

@ -156,10 +156,10 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
};
return (
<Box sx={{ minHeight: '100vh', bgcolor: colors.gray[50], pt: { xs: 7, sm: 8 }, pb: 10 }}>
<Container maxWidth="lg" sx={{ pt: 8, pb: 6, px: { xs: 6, sm: 8, md: 10 } }}>
<Box sx={{ minHeight: '100vh', bgcolor: colors.gray[50], pt: { xs: 7, sm: 8 }, pb: { xs: 4, sm: 10 } }}>
<Container maxWidth="lg" sx={{ pt: 8, pb: 6, px: { xs: 2, sm: 8, md: 10 } }}>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 8 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: { xs: 3, sm: 8 } }}>
<IconButton onClick={onBack}>
<ArrowBack />
</IconButton>
@ -169,7 +169,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
</Box>
{/* Title Section */}
<Box sx={{ textAlign: 'center', mb: 10 }}>
<Box sx={{ textAlign: 'center', mb: { xs: 4, sm: 10 } }}>
<CheckCircle sx={{ fontSize: 64, color: colors.purple, mb: 2 }} />
<Typography variant="h5" sx={{ ...responsiveText.h3, fontWeight: 700, mb: 2 }}>
@ -180,7 +180,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
</Box>
{/* Event Summary Statistics */}
<Grid container spacing={4} sx={{ mb: 10 }}>
<Grid container spacing={4} sx={{ mb: { xs: 4, sm: 10 } }}>
<Grid item xs={12} sm={6} md={3}>
<Card
elevation={0}
@ -392,7 +392,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
</CardContent>
</Card>
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10 }}>
<Card elevation={0} sx={{ ...cardStyles.default, mb: { xs: 4, sm: 10 } }}>
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<Box sx={{ flex: 1 }}>
@ -412,7 +412,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
</Typography>
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10 }}>
<Card elevation={0} sx={{ ...cardStyles.default, mb: { xs: 4, sm: 10 } }}>
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 4 }}>
{getChannelNames(eventData.channels).map((channel) => (
@ -445,7 +445,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
</Card>
{/* Terms Agreement */}
<Card elevation={0} sx={{ ...cardStyles.default, bgcolor: colors.gray[50], mb: 10 }}>
<Card elevation={0} sx={{ ...cardStyles.default, bgcolor: colors.gray[50], mb: { xs: 4, sm: 10 } }}>
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
<FormControlLabel
control={
@ -616,7 +616,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
<Typography variant="h5" sx={{ fontSize: '1.5rem', fontWeight: 700, mb: 3 }}>
!
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ fontSize: '1rem', mb: 8 }}>
<Typography variant="body1" color="text.secondary" sx={{ fontSize: '1rem', mb: { xs: 3, sm: 8 } }}>
.
<br />
.

View File

@ -108,9 +108,9 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
return (
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 20 }}>
<Container maxWidth="lg" sx={{ pt: 8, pb: 8, px: { xs: 6, sm: 6, md: 8 } }}>
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 8 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: { xs: 3, sm: 8 } }}>
<IconButton onClick={onBack} sx={{ width: 48, height: 48 }}>
<ArrowBack />
</IconButton>
@ -119,7 +119,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
</Typography>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 8, textAlign: 'center', fontSize: '1rem' }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: { xs: 3, sm: 8 }, textAlign: 'center', fontSize: '1rem' }}>
( 1 )
</Typography>
@ -136,7 +136,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
transition: 'all 0.3s',
}}
>
<CardContent sx={{ p: 6 }}>
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
<FormControlLabel
control={
<Checkbox
@ -211,7 +211,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
transition: 'all 0.3s',
}}
>
<CardContent sx={{ p: 6 }}>
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
<FormControlLabel
control={
<Checkbox
@ -270,7 +270,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
transition: 'all 0.3s',
}}
>
<CardContent sx={{ p: 6 }}>
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
<FormControlLabel
control={
<Checkbox
@ -347,7 +347,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
<Card
elevation={0}
sx={{
mb: 10,
mb: { xs: 4, sm: 10 },
borderRadius: 4,
border: channels[3].selected ? 2 : 1,
borderColor: channels[3].selected ? colors.purple : 'divider',
@ -356,7 +356,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
transition: 'all 0.3s',
}}
>
<CardContent sx={{ p: 6 }}>
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
<FormControlLabel
control={
<Checkbox
@ -465,13 +465,13 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
<Card
elevation={0}
sx={{
mb: 10,
mb: { xs: 4, sm: 10 },
borderRadius: 4,
bgcolor: 'grey.50',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
}}
>
<CardContent sx={{ p: 8 }}>
<CardContent sx={{ p: { xs: 3, sm: 8 } }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 4 }}>
<Typography variant="h6" sx={{ fontSize: '1.25rem' }}>

View File

@ -40,10 +40,10 @@ export default function ContentEditStep({
};
return (
<Box sx={{ minHeight: '100vh', bgcolor: colors.gray[50], pt: { xs: 7, sm: 8 }, pb: 10 }}>
<Container maxWidth="lg" sx={{ pt: 8, pb: 6, px: { xs: 6, sm: 8, md: 10 } }}>
<Box sx={{ minHeight: '100vh', bgcolor: colors.gray[50], pt: { xs: 7, sm: 8 }, pb: { xs: 4, sm: 10 } }}>
<Container maxWidth="lg" sx={{ pt: 8, pb: 6, px: { xs: 2, sm: 8, md: 10 } }}>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 10 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: { xs: 4, sm: 10 } }}>
<IconButton onClick={onBack} sx={{ width: 48, height: 48 }}>
<ArrowBack />
</IconButton>

View File

@ -310,8 +310,8 @@ export default function ContentPreviewStep({
if (loading) {
return (
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 20 }}>
<Container maxWidth="lg" sx={{ pt: 8, pb: 8, px: { xs: 6, sm: 6, md: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 8 }}>
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: { xs: 3, sm: 8 } }}>
<IconButton onClick={onBack}>
<ArrowBack />
</IconButton>
@ -442,9 +442,9 @@ export default function ContentPreviewStep({
},
}}
>
<Container maxWidth="lg" sx={{ pt: 8, pb: 8, px: { xs: 6, sm: 6, md: 8 } }}>
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 8 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: { xs: 3, sm: 8 } }}>
<IconButton onClick={onBack}>
<ArrowBack />
</IconButton>
@ -453,7 +453,7 @@ export default function ContentPreviewStep({
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 8 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: { xs: 3, sm: 8 } }}>
{generatedImages.size > 0 && (
<Alert severity="success" sx={{ flex: 1, fontSize: '1rem' }}>
@ -477,12 +477,12 @@ export default function ContentPreviewStep({
</Button>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 8, textAlign: 'center', fontSize: '1rem' }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: { xs: 3, sm: 8 }, textAlign: 'center', fontSize: '1rem' }}>
</Typography>
<RadioGroup value={selectedStyle} onChange={(e) => handleStyleSelect(e.target.value as 'SIMPLE' | 'FANCY' | 'TRENDY')}>
<Grid container spacing={6} sx={{ mb: 10 }}>
<Grid container spacing={6} sx={{ mb: { xs: 4, sm: 10 } }}>
{imageStyles.map((style) => (
<Grid item xs={12} md={4} key={style.id}>
<Card

View File

@ -127,21 +127,21 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
return (
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 20 }}>
<Container maxWidth="md" sx={{ pt: 8, pb: 8, px: { xs: 6, sm: 6, md: 8 } }}>
<Container maxWidth="md" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
{/* Title Section */}
<Box sx={{ mb: 10, textAlign: 'center' }}>
<AutoAwesome sx={{ fontSize: 80, color: colors.purple, mb: 4 }} />
<Typography variant="h4" sx={{ fontWeight: 700, mb: 4, fontSize: '2rem' }}>
<Box sx={{ mb: { xs: 4, sm: 10 }, textAlign: 'center' }}>
<AutoAwesome sx={{ fontSize: { xs: 60, sm: 80 }, color: colors.purple, mb: { xs: 2, sm: 4 } }} />
<Typography variant="h4" sx={{ fontWeight: 700, mb: { xs: 2, sm: 4 }, fontSize: { xs: '1.5rem', sm: '2rem' } }}>
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ fontSize: '1.125rem' }}>
<Typography variant="body1" color="text.secondary" sx={{ fontSize: { xs: '0.875rem', sm: '1.125rem' } }}>
AI가
</Typography>
</Box>
{/* Purpose Options */}
<RadioGroup value={selected} onChange={(e) => setSelected(e.target.value as EventObjective)}>
<Grid container spacing={6} sx={{ mb: 10 }}>
<Grid container spacing={{ xs: 2, sm: 6 }} sx={{ mb: { xs: 4, sm: 10 } }}>
{objectives.map((objective) => (
<Grid item xs={12} sm={6} key={objective.id}>
<Card
@ -162,14 +162,14 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
}}
onClick={() => setSelected(objective.id)}
>
<CardContent sx={{ p: 6 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 3, mb: 3 }}>
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: { xs: 2, sm: 3 }, mb: { xs: 2, sm: 3 } }}>
<Box sx={{ color: colors.purple }}>{objective.icon}</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 2, fontSize: '1.25rem' }}>
<Typography variant="h6" sx={{ fontWeight: 700, mb: { xs: 1, sm: 2 }, fontSize: { xs: '1rem', sm: '1.25rem' } }}>
{objective.title}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '1rem' }}>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>
{objective.description}
</Typography>
</Box>
@ -191,15 +191,15 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
<Card
elevation={0}
sx={{
mb: 10,
mb: { xs: 4, sm: 10 },
background: `linear-gradient(135deg, ${colors.purpleLight} 0%, ${colors.blue}20 100%)`,
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
}}
>
<CardContent sx={{ display: 'flex', gap: 3, p: 6 }}>
<AutoAwesome sx={{ color: colors.purple, fontSize: 28 }} />
<Typography variant="body2" sx={{ fontSize: '1rem', lineHeight: 1.8, color: colors.gray[700] }}>
<CardContent sx={{ display: 'flex', gap: { xs: 2, sm: 3 }, p: { xs: 3, sm: 6 } }}>
<AutoAwesome sx={{ color: colors.purple, fontSize: { xs: 24, sm: 28 } }} />
<Typography variant="body2" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' }, lineHeight: 1.8, color: colors.gray[700] }}>
AI가 , , .
</Typography>
</CardContent>
@ -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': {

View File

@ -182,24 +182,24 @@ export default function RecommendationStep({
if (loading) {
return (
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 20 }}>
<Container maxWidth="lg" sx={{ pt: 8, pb: 8, px: { xs: 6, sm: 6, md: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 8 }}>
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 2, sm: 3 }, mb: { xs: 4, sm: 8 } }}>
<IconButton onClick={onBack} sx={{ width: 48, height: 48 }}>
<ArrowBack />
</IconButton>
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: '1.5rem' }}>
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1.25rem', sm: '1.5rem' } }}>
AI
</Typography>
</Box>
<Box
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, py: 12 }}
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: { xs: 2, sm: 4 }, py: { xs: 6, sm: 12 } }}
>
<CircularProgress size={60} sx={{ color: colors.purple }} />
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: '1.25rem' }}>
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: { xs: '1rem', sm: '1.25rem' } }}>
AI가 ...
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '1rem' }}>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>
, ,
</Typography>
</Box>
@ -212,8 +212,8 @@ export default function RecommendationStep({
if (error) {
return (
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 20 }}>
<Container maxWidth="lg" sx={{ pt: 8, pb: 8, px: { xs: 6, sm: 6, md: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 8 }}>
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: { xs: 3, sm: 8 } }}>
<IconButton onClick={onBack} sx={{ width: 48, height: 48 }}>
<ArrowBack />
</IconButton>
@ -276,7 +276,7 @@ export default function RecommendationStep({
if (!aiResult) {
return (
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 20 }}>
<Container maxWidth="lg" sx={{ pt: 8, pb: 8, px: { xs: 6, sm: 6, md: 8 } }}>
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
<CircularProgress />
</Container>
</Box>
@ -285,9 +285,9 @@ export default function RecommendationStep({
return (
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 20 }}>
<Container maxWidth="lg" sx={{ pt: 8, pb: 8, px: { xs: 6, sm: 6, md: 8 } }}>
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 8 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: { xs: 3, sm: 8 } }}>
<IconButton onClick={onBack} sx={{ width: 48, height: 48 }}>
<ArrowBack />
</IconButton>
@ -300,12 +300,12 @@ export default function RecommendationStep({
<Card
elevation={0}
sx={{
mb: 10,
mb: { xs: 4, sm: 10 },
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
}}
>
<CardContent sx={{ p: 8 }}>
<CardContent sx={{ p: { xs: 3, sm: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 6 }}>
<Insights sx={{ fontSize: 32, color: colors.purple }} />
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1.25rem' }}>
@ -363,7 +363,7 @@ export default function RecommendationStep({
</Card>
{/* AI Recommendations */}
<Box sx={{ mb: 8 }}>
<Box sx={{ mb: { xs: 3, sm: 8 } }}>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 4, fontSize: '1.25rem' }}>
AI ({aiResult.recommendations.length} )
</Typography>
@ -375,7 +375,7 @@ export default function RecommendationStep({
{/* Recommendations */}
<RadioGroup value={selected} onChange={(e) => setSelected(Number(e.target.value))}>
<Grid container spacing={6} sx={{ mb: 10 }}>
<Grid container spacing={6} sx={{ mb: { xs: 4, sm: 10 } }}>
{aiResult.recommendations.map((rec) => (
<Grid item xs={12} key={rec.optionNumber}>
<Card
@ -402,7 +402,7 @@ export default function RecommendationStep({
}}
onClick={() => setSelected(rec.optionNumber)}
>
<CardContent sx={{ p: 6 }}>
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
<Box
sx={{
display: 'flex',

View File

@ -276,7 +276,7 @@ export default function EventsPage() {
<Box
sx={{
pt: { xs: 7, sm: 8 },
pb: 10,
pb: { xs: 4, sm: 10 },
bgcolor: colors.gray[50],
minHeight: '100vh',
}}

View File

@ -1,7 +1,8 @@
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Box, Container, Typography, Grid, Card, CardContent, Button, Fab } from '@mui/material';
import { Box, Container, Typography, Grid, Card, CardContent, Button, Fab, CircularProgress, Alert } from '@mui/material';
import {
Add,
Celebration,
@ -19,34 +20,9 @@ import {
cardStyles,
colors,
} from '@/shared/lib/button-styles';
// Mock 사용자 데이터 (API 연동 전까지 임시 사용)
const mockUser = {
name: '홍길동',
email: 'test@example.com',
};
// Mock 데이터 (추후 API 연동 시 교체)
const mockEvents = [
{
id: '1',
title: 'SNS 팔로우 이벤트',
status: '진행중',
startDate: '2025-01-20',
endDate: '2025-02-28',
participants: 1245,
roi: 320,
},
{
id: '2',
title: '설 맞이 할인 이벤트',
status: '진행중',
startDate: '2025-01-25',
endDate: '2025-02-10',
participants: 856,
roi: 280,
},
];
import { useAuth } from '@/features/auth/model/useAuth';
import { analyticsApi } from '@/entities/analytics/api/analyticsApi';
import type { UserAnalyticsDashboardResponse } from '@/entities/analytics/model/types';
const mockActivities = [
{ icon: PersonAdd, text: 'SNS 팔로우 이벤트에 새로운 참여자 12명', time: '5분 전' },
@ -56,14 +32,47 @@ const mockActivities = [
export default function HomePage() {
const router = useRouter();
const { user } = useAuth();
const [analyticsData, setAnalyticsData] = useState<UserAnalyticsDashboardResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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() {
<Box
sx={{
pt: { xs: 7, sm: 8 },
pb: 10,
pb: { xs: 4, sm: 10 },
bgcolor: colors.gray[50],
minHeight: '100vh',
}}
@ -98,16 +107,30 @@ export default function HomePage() {
mb: { xs: 2, sm: 4 },
}}
>
, {mockUser.name}! 👋
, {user?.userName || '사용자'}! 👋
</Typography>
<Typography variant="body1" sx={{ ...responsiveText.body1 }}>
</Typography>
</Box>
{/* Error Alert */}
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
{/* Loading State */}
{loading && (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<CircularProgress />
</Box>
)}
{/* KPI Cards */}
<Grid container spacing={{ xs: 3, sm: 6 }} sx={{ mb: { xs: 5, sm: 10 } }}>
<Grid item xs={12} sm={4}>
<Grid container spacing={{ xs: 1.5, sm: 6 }} sx={{ mb: { xs: 5, sm: 10 } }}>
<Grid item xs={4} sm={4}>
<Card
elevation={0}
sx={{
@ -116,22 +139,22 @@ export default function HomePage() {
borderColor: 'transparent',
}}
>
<CardContent sx={{ textAlign: 'center', py: { xs: 4, sm: 6 }, px: { xs: 2, sm: 4 } }}>
<CardContent sx={{ textAlign: 'center', pt: { xs: 1.5, sm: 6 }, pb: { xs: 1.5, sm: 6 }, px: { xs: 0.5, sm: 4 } }}>
<Box
sx={{
width: { xs: 48, sm: 64 },
height: { xs: 48, sm: 64 },
width: { xs: 32, sm: 64 },
height: { xs: 32, sm: 64 },
borderRadius: '50%',
bgcolor: 'rgba(0, 0, 0, 0.05)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto',
mb: { xs: 2, sm: 3 },
mb: { xs: 0.75, sm: 3 },
}}
>
<Celebration sx={{
fontSize: { xs: 24, sm: 32 },
fontSize: { xs: 18, sm: 32 },
color: colors.gray[900],
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
}} />
@ -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}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={4}>
<Grid item xs={4} sm={4}>
<Card
elevation={0}
sx={{
@ -171,22 +195,22 @@ export default function HomePage() {
borderColor: 'transparent',
}}
>
<CardContent sx={{ textAlign: 'center', py: { xs: 4, sm: 6 }, px: { xs: 2, sm: 4 } }}>
<CardContent sx={{ textAlign: 'center', pt: { xs: 1.5, sm: 6 }, pb: { xs: 1.5, sm: 6 }, px: { xs: 0.5, sm: 4 } }}>
<Box
sx={{
width: { xs: 48, sm: 64 },
height: { xs: 48, sm: 64 },
width: { xs: 32, sm: 64 },
height: { xs: 32, sm: 64 },
borderRadius: '50%',
bgcolor: 'rgba(0, 0, 0, 0.05)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto',
mb: { xs: 2, sm: 3 },
mb: { xs: 0.75, sm: 3 },
}}
>
<Group sx={{
fontSize: { xs: 24, sm: 32 },
fontSize: { xs: 18, sm: 32 },
color: colors.gray[900],
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
}} />
@ -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() {
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={4}>
<Grid item xs={4} sm={4}>
<Card
elevation={0}
sx={{
@ -226,22 +251,22 @@ export default function HomePage() {
borderColor: 'transparent',
}}
>
<CardContent sx={{ textAlign: 'center', py: { xs: 4, sm: 6 }, px: { xs: 2, sm: 4 } }}>
<CardContent sx={{ textAlign: 'center', pt: { xs: 1.5, sm: 6 }, pb: { xs: 1.5, sm: 6 }, px: { xs: 0.5, sm: 4 } }}>
<Box
sx={{
width: { xs: 48, sm: 64 },
height: { xs: 48, sm: 64 },
width: { xs: 32, sm: 64 },
height: { xs: 32, sm: 64 },
borderRadius: '50%',
bgcolor: 'rgba(0, 0, 0, 0.05)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto',
mb: { xs: 2, sm: 3 },
mb: { xs: 0.75, sm: 3 },
}}
>
<TrendingUp sx={{
fontSize: { xs: 24, sm: 32 },
fontSize: { xs: 18, sm: 32 },
color: colors.gray[900],
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
}} />
@ -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}
>
<CardContent sx={{ textAlign: 'center', py: { xs: 4, sm: 6 } }}>
<CardContent sx={{ textAlign: 'center', pt: { xs: 2, sm: 6 }, pb: { xs: 2, sm: 6 } }}>
<Box
sx={{
width: { xs: 56, sm: 72 },
@ -319,7 +345,7 @@ export default function HomePage() {
}}
onClick={handleViewAnalytics}
>
<CardContent sx={{ textAlign: 'center', py: { xs: 4, sm: 6 } }}>
<CardContent sx={{ textAlign: 'center', pt: { xs: 2, sm: 6 }, pb: { xs: 2, sm: 6 } }}>
<Box
sx={{
width: { xs: 56, sm: 72 },
@ -368,7 +394,7 @@ export default function HomePage() {
</Button>
</Box>
{activeEvents.length === 0 ? (
{!loading && eventPerformances.length === 0 ? (
<Card
elevation={0}
sx={{
@ -402,16 +428,16 @@ export default function HomePage() {
</Button>
</CardContent>
</Card>
) : (
) : !loading && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { xs: 3, sm: 6 } }}>
{activeEvents.map((event) => (
{eventPerformances.slice(0, 2).map((event) => (
<Card
key={event.id}
key={event.eventId}
elevation={0}
sx={{
...cardStyles.clickable,
}}
onClick={() => handleEventClick(event.id)}
onClick={() => handleEventClick(event.eventId)}
>
<CardContent sx={{ p: { xs: 4, sm: 6, md: 8 } }}>
<Box
@ -424,7 +450,7 @@ export default function HomePage() {
}}
>
<Typography variant="h6" sx={{ fontWeight: 700, color: colors.gray[900], fontSize: { xs: '1rem', sm: '1.25rem' } }}>
{event.title}
{event.eventTitle}
</Typography>
<Box
sx={{
@ -441,23 +467,7 @@ export default function HomePage() {
{event.status}
</Box>
</Box>
<Typography
variant="body2"
sx={{
mb: { xs: 3, sm: 6 },
color: colors.gray[600],
display: 'flex',
alignItems: 'center',
gap: 1,
fontSize: { xs: '0.75rem', sm: '0.875rem' },
}}
>
<span>📅</span>
<span>
{event.startDate} ~ {event.endDate}
</span>
</Typography>
<Box sx={{ display: 'flex', gap: { xs: 6, sm: 12 } }}>
<Box sx={{ display: 'flex', gap: { xs: 6, sm: 12 }, mt: { xs: 3, sm: 6 } }}>
<Box>
<Typography
variant="body2"
@ -479,6 +489,27 @@ export default function HomePage() {
</Typography>
</Typography>
</Box>
<Box>
<Typography
variant="body2"
sx={{ mb: 0.5, color: colors.gray[600], fontWeight: 500, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
>
</Typography>
<Typography
variant="h5"
sx={{ fontWeight: 700, color: colors.gray[900], fontSize: { xs: '1.125rem', sm: '1.5rem' } }}
>
{event.views.toLocaleString()}
<Typography
component="span"
variant="body2"
sx={{ ml: 0.5, color: colors.gray[600], fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
>
</Typography>
</Typography>
</Box>
<Box>
<Typography
variant="body2"
@ -487,7 +518,7 @@ export default function HomePage() {
ROI
</Typography>
<Typography variant="h5" sx={{ fontWeight: 700, color: colors.mint, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
{event.roi}%
{Math.round(event.roi * 100) / 100}%
</Typography>
</Box>
</Box>

View File

@ -242,14 +242,14 @@ export default function ProfilePage() {
<Box
sx={{
pt: { xs: 7, sm: 8 },
pb: 10,
pb: { xs: 4, sm: 10 },
bgcolor: colors.gray[50],
minHeight: '100vh',
}}
>
<Container maxWidth="lg" sx={{ pt: 8, pb: 6, px: { xs: 6, sm: 8, md: 10 } }}>
<Container maxWidth="lg" sx={{ pt: 8, pb: 6, px: { xs: 2, sm: 8, md: 10 } }}>
{/* 사용자 정보 섹션 */}
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10, textAlign: 'center' }}>
<Card elevation={0} sx={{ ...cardStyles.default, mb: { xs: 4, sm: 10 }, textAlign: 'center' }}>
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
<Box
sx={{
@ -280,7 +280,7 @@ export default function ProfilePage() {
</Card>
{/* 기본 정보 */}
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10 }}>
<Card elevation={0} sx={{ ...cardStyles.default, mb: { xs: 4, sm: 10 } }}>
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 6 }}>
@ -338,7 +338,7 @@ export default function ProfilePage() {
</Card>
{/* 매장 정보 */}
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10 }}>
<Card elevation={0} sx={{ ...cardStyles.default, mb: { xs: 4, sm: 10 } }}>
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 6 }}>
@ -458,12 +458,12 @@ export default function ProfilePage() {
},
}}
>
<DialogContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
<CheckCircle sx={{ fontSize: 64, color: 'success.main', mb: 2 }} />
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1, fontSize: '1.25rem' }}>
<DialogContent sx={{ textAlign: 'center', py: { xs: 3, sm: 6 }, px: { xs: 2, sm: 4 } }}>
<CheckCircle sx={{ fontSize: { xs: 48, sm: 64 }, color: 'success.main', mb: { xs: 1, sm: 2 } }} />
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1, fontSize: { xs: '1rem', sm: '1.25rem' } }}>
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '1rem' }}>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>
.
</Typography>
</DialogContent>

View File

@ -519,7 +519,7 @@ export default function TestAnalyticsPage() {
>
<CardContent sx={{
textAlign: 'center',
py: { xs: 2, sm: 6 },
pt: { xs: 1.5, sm: 6 }, pb: { xs: 1.5, sm: 6 },
px: { xs: 1, sm: 4 },
}}>
<Group sx={{ fontSize: { xs: 20, sm: 40 }, mb: { xs: 0.5, sm: 2 }, color: 'white' }} />
@ -544,7 +544,7 @@ export default function TestAnalyticsPage() {
>
<CardContent sx={{
textAlign: 'center',
py: { xs: 2, sm: 6 },
pt: { xs: 1.5, sm: 6 }, pb: { xs: 1.5, sm: 6 },
px: { xs: 1, sm: 4 },
}}>
<Visibility sx={{ fontSize: { xs: 20, sm: 40 }, mb: { xs: 0.5, sm: 2 }, color: 'white' }} />
@ -569,7 +569,7 @@ export default function TestAnalyticsPage() {
>
<CardContent sx={{
textAlign: 'center',
py: { xs: 2, sm: 6 },
pt: { xs: 1.5, sm: 6 }, pb: { xs: 1.5, sm: 6 },
px: { xs: 1, sm: 4 },
}}>
<TrendingUp sx={{ fontSize: { xs: 20, sm: 40 }, mb: { xs: 0.5, sm: 2 }, color: 'white' }} />
@ -594,7 +594,7 @@ export default function TestAnalyticsPage() {
>
<CardContent sx={{
textAlign: 'center',
py: { xs: 2, sm: 6 },
pt: { xs: 1.5, sm: 6 }, pb: { xs: 1.5, sm: 6 },
px: { xs: 1, sm: 4 },
}}>
<TrendingUp sx={{ fontSize: { xs: 20, sm: 40 }, mb: { xs: 0.5, sm: 2 }, color: 'white' }} />

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -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<UserAnalyticsDashboardResponse> => {
const response = await analyticsClient.get<ApiResponse<UserAnalyticsDashboardResponse>>(
`/api/${API_VERSION}/users/${userId}/analytics`,
`/users/${userId}`,
{ params }
);
return response.data.data;
@ -46,7 +46,7 @@ export const analyticsApi = {
params?: TimelineQueryParams
): Promise<UserTimelineAnalyticsResponse> => {
const response = await analyticsClient.get<ApiResponse<UserTimelineAnalyticsResponse>>(
`/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<UserRoiAnalyticsResponse> => {
const response = await analyticsClient.get<ApiResponse<UserRoiAnalyticsResponse>>(
`/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<UserChannelAnalyticsResponse> => {
const response = await analyticsClient.get<ApiResponse<UserChannelAnalyticsResponse>>(
`/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<AnalyticsDashboardResponse> => {
const response = await analyticsClient.get<ApiResponse<AnalyticsDashboardResponse>>(
`/api/${API_VERSION}/events/${eventId}/analytics`,
`/events/${eventId}`,
{ params }
);
return response.data.data;
@ -104,7 +104,7 @@ export const analyticsApi = {
params?: TimelineQueryParams
): Promise<TimelineAnalyticsResponse> => {
const response = await analyticsClient.get<ApiResponse<TimelineAnalyticsResponse>>(
`/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<RoiAnalyticsResponse> => {
const response = await analyticsClient.get<ApiResponse<RoiAnalyticsResponse>>(
`/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<ChannelAnalyticsResponse> => {
const response = await analyticsClient.get<ApiResponse<ChannelAnalyticsResponse>>(
`/api/${API_VERSION}/events/${eventId}/analytics/channels`,
`/events/${eventId}/channels`,
{ params }
);
return response.data.data;

View File

@ -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',