cherry2250 6331ab3fde 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>
2025-10-31 00:34:20 +09:00

1086 lines
41 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation';
import {
Box,
Container,
Typography,
Card,
CardContent,
Chip,
Button,
IconButton,
Grid,
Menu,
MenuItem,
Divider,
LinearProgress,
CircularProgress,
Alert,
Tooltip as MuiTooltip,
} from '@mui/material';
import {
MoreVert,
Group,
Visibility,
TrendingUp,
Share,
CardGiftcard,
AttachMoney,
People,
Edit,
Download,
Person,
Phone,
Email,
ShoppingCart,
Warning,
LocalFireDepartment,
Star,
NewReleases,
Refresh as RefreshIcon,
} from '@mui/icons-material';
import { Line, Bar } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Title,
Tooltip as ChartTooltip,
Legend,
Filler,
} from 'chart.js';
import { analyticsApi } from '@/entities/analytics/api/analyticsApi';
// Chart.js 등록
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Title,
ChartTooltip,
Legend,
Filler
);
// 디자인 시스템 색상
const colors = {
pink: '#F472B6',
purple: '#C084FC',
purpleLight: '#E9D5FF',
blue: '#60A5FA',
mint: '#34D399',
orange: '#FB923C',
yellow: '#FBBF24',
gray: {
900: '#1A1A1A',
700: '#4A4A4A',
500: '#9E9E9E',
300: '#D9D9D9',
100: '#F5F5F5',
},
};
// Mock 데이터
const mockEventData = {
id: '1',
title: 'SNS 팔로우 이벤트',
status: 'active' as const,
startDate: '2025-01-15',
endDate: '2025-02-15',
prize: '커피 쿠폰',
method: 'SNS 팔로우',
cost: 250000,
channels: ['우리동네TV', '링고비즈', 'SNS'],
participants: 128,
views: 456,
roi: 450,
conversion: 28,
targetParticipants: 200,
isAIRecommended: true,
isUrgent: false,
isPopular: true,
isHighROI: true,
isNew: false,
};
const recentParticipants = [
{ name: '김*진', phone: '010-****-1234', time: '5분 전' },
{ name: '이*수', phone: '010-****-5678', time: '12분 전' },
{ name: '박*영', phone: '010-****-9012', time: '25분 전' },
{ name: '최*민', phone: '010-****-3456', time: '1시간 전' },
{ name: '정*희', phone: '010-****-7890', time: '2시간 전' },
];
// 헬퍼 함수
const getMethodIcon = (method: string) => {
switch (method) {
case '전화번호 입력':
return <Phone sx={{ fontSize: 18 }} />;
case 'SNS 팔로우':
return <Share sx={{ fontSize: 18 }} />;
case '구매 인증':
return <ShoppingCart sx={{ fontSize: 18 }} />;
case '이메일 등록':
return <Email sx={{ fontSize: 18 }} />;
default:
return <Share sx={{ fontSize: 18 }} />;
}
};
const calculateProgress = (event: typeof mockEventData) => {
if (event.status !== 'active') return 0;
const total = new Date(event.endDate).getTime() - new Date(event.startDate).getTime();
const elapsed = Date.now() - new Date(event.startDate).getTime();
return Math.min(Math.max((elapsed / total) * 100, 0), 100);
};
export default function EventDetailPage() {
const router = useRouter();
const params = useParams();
const eventId = params.eventId as string;
const [event, setEvent] = useState(mockEventData);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [chartPeriod, setChartPeriod] = useState<'7d' | '30d' | 'all'>('7d');
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [analyticsData, setAnalyticsData] = useState<any>(null);
// Analytics API 호출
const fetchAnalytics = async (forceRefresh = false) => {
try {
if (forceRefresh) {
console.log('🔄 Analytics 데이터 새로고침...');
setRefreshing(true);
} else {
console.log('📊 Analytics 데이터 로딩...');
setLoading(true);
}
setError(null);
// 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',
}),
analyticsApi.getEventRoiAnalytics(eventId, { includeProjection: true }),
analyticsApi.getEventChannelAnalytics(eventId, {}),
]);
console.log('✅ Dashboard 데이터:', dashboard);
console.log('✅ Timeline 데이터:', timeline);
console.log('✅ ROI 데이터:', roi);
console.log('✅ Channels 데이터:', channels);
// Analytics 데이터 저장
const formattedAnalyticsData = {
dashboard,
timeline,
roi,
channels,
};
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);
// 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);
}
};
// 초기 데이터 로드
useEffect(() => {
fetchAnalytics();
}, [eventId]);
// 차트 기간 변경 시 Timeline 데이터 다시 로드
useEffect(() => {
if (analyticsData) {
fetchAnalytics();
}
}, [chartPeriod]);
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
const handleRefresh = () => {
fetchAnalytics(true);
};
// 차트 데이터 생성 함수
const generateParticipationTrendData = () => {
if (!analyticsData?.timeline) {
return {
labels: [],
datasets: [{
label: '일별 참여자',
data: [],
borderColor: colors.blue,
backgroundColor: `${colors.blue}40`,
fill: true,
tension: 0.4,
}],
};
}
const timelineData = analyticsData.timeline;
const dataPoints = timelineData.dataPoints || [];
// 데이터 포인트를 날짜별로 그룹화
const dailyData = new Map<string, number>();
dataPoints.forEach((point: any) => {
const date = new Date(point.timestamp);
const dateKey = `${date.getMonth() + 1}/${date.getDate()}`;
dailyData.set(dateKey, (dailyData.get(dateKey) || 0) + point.participants);
});
const labels = Array.from(dailyData.keys());
const data = Array.from(dailyData.values());
return {
labels,
datasets: [{
label: '일별 참여자',
data,
borderColor: colors.blue,
backgroundColor: `${colors.blue}40`,
fill: true,
tension: 0.4,
}],
};
};
const generateChannelPerformanceData = () => {
if (!analyticsData?.channels?.channels) {
return {
labels: [],
datasets: [{
label: '참여자 수',
data: [],
backgroundColor: [],
borderRadius: 8,
}],
};
}
const channelColors = [colors.pink, colors.blue, colors.orange, colors.purple, colors.mint, colors.yellow];
const channels = analyticsData.channels.channels;
const labels = channels.map((ch: any) => {
let channelName = ch.channelName || ch.channelType || '알 수 없음';
// 채널명 디코딩 처리
if (channelName.includes('%')) {
try {
channelName = decodeURIComponent(channelName);
} catch (e) {
console.warn('⚠️ 채널명 디코딩 실패:', channelName);
}
}
return channelName;
});
const data = channels.map((ch: any) => ch.metrics?.participants || 0);
const backgroundColor = channels.map((_: any, idx: number) => channelColors[idx % channelColors.length]);
return {
labels,
datasets: [{
label: '참여자 수',
data,
backgroundColor,
borderRadius: 8,
}],
};
};
const generateRoiTrendData = () => {
// ROI는 현재 시점의 값만 있으므로 간단한 추이를 표시
if (!analyticsData?.roi) {
return {
labels: ['현재'],
datasets: [{
label: 'ROI (%)',
data: [0],
borderColor: colors.mint,
backgroundColor: `${colors.mint}40`,
fill: true,
tension: 0.4,
}],
};
}
const currentRoi = analyticsData.roi.roi?.roiPercentage || 0;
// 단순 추정: 초기 0에서 현재 ROI까지의 추이
const labels = ['시작', '1주차', '2주차', '3주차', '현재'];
const data = [0, currentRoi * 0.3, currentRoi * 0.5, currentRoi * 0.75, currentRoi];
return {
labels,
datasets: [{
label: 'ROI (%)',
data,
borderColor: colors.mint,
backgroundColor: `${colors.mint}40`,
fill: true,
tension: 0.4,
}],
};
};
const getStatusColor = (status: string) => {
switch (status) {
case 'active':
return 'success';
case 'scheduled':
return 'info';
case 'ended':
return 'default';
default:
return 'default';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'active':
return '진행중';
case 'scheduled':
return '예정';
case 'ended':
return '종료';
default:
return status;
}
};
// 로딩 중
if (loading) {
return (
<Box sx={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<CircularProgress size={60} />
</Box>
);
}
// 에러 발생
if (error) {
return (
<Box sx={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', p: 3 }}>
<Alert severity="error" sx={{ maxWidth: 600 }}>
{error}
</Alert>
</Box>
);
}
return (
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: { xs: 5, sm: 10 } }}>
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 3, sm: 6, md: 10 } }}>
{/* Event Header */}
<Box sx={{ mb: { xs: 4, sm: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: { xs: 2, sm: 4 } }}>
<Typography variant="h4" sx={{ fontWeight: 700, fontSize: { xs: '1.25rem', sm: '2rem' } }}>
{event.title}
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<MuiTooltip title="데이터 새로고침">
<IconButton onClick={handleRefresh} disabled={refreshing}>
<RefreshIcon
sx={{
animation: refreshing ? 'spin 1s linear infinite' : 'none',
'@keyframes spin': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' },
},
}}
/>
</IconButton>
</MuiTooltip>
<IconButton onClick={handleMenuOpen}>
<MoreVert />
</IconButton>
</Box>
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
<MenuItem onClick={handleMenuClose}>
<Edit sx={{ mr: 2 }} />
</MenuItem>
<MenuItem onClick={handleMenuClose}>
<Share sx={{ mr: 2 }} />
</MenuItem>
<MenuItem onClick={handleMenuClose}>
<Download sx={{ mr: 2 }} />
</MenuItem>
<Divider />
<MenuItem onClick={handleMenuClose} sx={{ color: 'error.main' }}>
</MenuItem>
</Menu>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1, sm: 2 }, flexWrap: 'wrap', mb: { xs: 2, sm: 4 } }}>
<Chip
label={getStatusText(event.status)}
color={getStatusColor(event.status) as any}
size="medium"
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' }, height: { xs: 24, sm: 32 } }}
/>
{event.isAIRecommended && (
<Chip label="AI 추천" size="medium" sx={{ bgcolor: colors.purpleLight, color: colors.purple, fontSize: { xs: '0.75rem', sm: '0.875rem' }, height: { xs: 24, sm: 32 } }} />
)}
{event.isUrgent && (
<Chip
icon={<Warning sx={{ fontSize: { xs: '1rem', sm: '1.25rem' } }} />}
label="마감임박"
size="medium"
sx={{ bgcolor: '#FEF3C7', color: '#92400E', fontSize: { xs: '0.75rem', sm: '0.875rem' }, height: { xs: 24, sm: 32 } }}
/>
)}
{event.isPopular && (
<Chip
icon={<LocalFireDepartment sx={{ fontSize: { xs: '1rem', sm: '1.25rem' } }} />}
label="인기"
size="medium"
sx={{ bgcolor: '#FEE2E2', color: '#991B1B', fontSize: { xs: '0.75rem', sm: '0.875rem' }, height: { xs: 24, sm: 32 } }}
/>
)}
{event.isHighROI && (
<Chip
icon={<Star sx={{ fontSize: { xs: '1rem', sm: '1.25rem' } }} />}
label="높은 ROI"
size="medium"
sx={{ bgcolor: '#DCFCE7', color: '#166534', fontSize: { xs: '0.75rem', sm: '0.875rem' }, height: { xs: 24, sm: 32 } }}
/>
)}
{event.isNew && (
<Chip
icon={<NewReleases sx={{ fontSize: { xs: '1rem', sm: '1.25rem' } }} />}
label="신규"
size="medium"
sx={{ bgcolor: '#DBEAFE', color: '#1E40AF', fontSize: { xs: '0.75rem', sm: '0.875rem' }, height: { xs: 24, sm: 32 } }}
/>
)}
</Box>
<Typography variant="body1" color="text.secondary" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' }, mb: { xs: 2, sm: 4 } }}>
📅 {event.startDate} ~ {event.endDate}
</Typography>
{/* 진행률 바 (진행중인 이벤트만) */}
{event.status === 'active' && (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: { xs: 1, sm: 2 } }}>
<Typography variant="body2" sx={{ fontWeight: 600, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
{Math.round(calculateProgress(event))}%
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={calculateProgress(event)}
sx={{
height: { xs: 6, sm: 10 },
borderRadius: 5,
bgcolor: colors.gray[100],
'& .MuiLinearProgress-bar': {
background: `linear-gradient(90deg, ${colors.mint} 0%, ${colors.blue} 100%)`,
borderRadius: 5,
},
}}
/>
</Box>
)}
</Box>
{/* Real-time KPIs */}
<Box sx={{ mb: { xs: 4, sm: 10 } }}>
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, alignItems: { xs: 'flex-start', sm: 'center' }, justifyContent: 'space-between', gap: { xs: 1.5, sm: 0 }, mb: { xs: 2.5, sm: 6 } }}>
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1rem', sm: '1.5rem' } }}>
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 0.75, sm: 1.5 }, color: 'success.main' }}>
<Box
sx={{
width: { xs: 6, sm: 10 },
height: { xs: 6, sm: 10 },
borderRadius: '50%',
bgcolor: 'success.main',
animation: 'pulse 2s infinite',
'@keyframes pulse': {
'0%, 100%': { opacity: 1 },
'50%': { opacity: 0.5 },
},
}}
/>
<Typography variant="body2" sx={{ fontWeight: 600, fontSize: { xs: '0.6875rem', sm: '0.875rem' } }}>
</Typography>
</Box>
</Box>
<Grid container spacing={{ xs: 1.5, sm: 6 }}>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
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%)`,
height: '100%',
}}
>
<CardContent sx={{
textAlign: 'center',
pt: { xs: 1.5, sm: 6 },
pb: { xs: 1.5, sm: 6 },
px: { xs: 1, sm: 4 },
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
height: '100%',
}}>
<Group sx={{ fontSize: { xs: 20, 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.5625rem', sm: '0.75rem' } }}>
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: { xs: '0.9375rem', sm: '1.75rem' }, lineHeight: 1.2 }}>
{event.participants}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.8)', mt: { xs: 0.25, sm: 1 }, display: 'block', fontSize: { xs: '0.5rem', sm: '0.75rem' }, lineHeight: 1.3, minHeight: { xs: '1.3em', sm: 'auto' } }}>
: {event.targetParticipants}<br />
({Math.round((event.participants / event.targetParticipants) * 100)}%)
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
borderRadius: { xs: 2, sm: 4 },
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
background: `linear-gradient(135deg, ${colors.blue} 0%, #93C5FD 100%)`,
height: '100%',
}}
>
<CardContent sx={{
textAlign: 'center',
pt: { xs: 1.5, sm: 6 },
pb: { xs: 1.5, sm: 6 },
px: { xs: 1, sm: 4 },
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
height: '100%',
}}>
<Visibility sx={{ fontSize: { xs: 20, 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.5625rem', sm: '0.75rem' } }}>
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: { xs: '0.9375rem', sm: '1.75rem' }, lineHeight: 1.2 }}>
{event.views}
</Typography>
<Box sx={{ mt: { xs: 0.25, sm: 1 }, minHeight: { xs: '1.3em', sm: 0 } }} />
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
borderRadius: { xs: 2, sm: 4 },
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
background: `linear-gradient(135deg, ${colors.mint} 0%, #6EE7B7 100%)`,
height: '100%',
}}
>
<CardContent sx={{
textAlign: 'center',
pt: { xs: 1.5, sm: 6 },
pb: { xs: 1.5, sm: 6 },
px: { xs: 1, sm: 4 },
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
height: '100%',
}}>
<TrendingUp sx={{ fontSize: { xs: 20, 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.5625rem', sm: '0.75rem' } }}>
ROI
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: { xs: '0.9375rem', sm: '1.75rem' }, lineHeight: 1.2 }}>
{event.roi}%
</Typography>
<Box sx={{ mt: { xs: 0.25, sm: 1 }, minHeight: { xs: '1.3em', sm: 0 } }} />
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
borderRadius: { xs: 2, sm: 4 },
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
background: `linear-gradient(135deg, ${colors.orange} 0%, #FCD34D 100%)`,
height: '100%',
}}
>
<CardContent sx={{
textAlign: 'center',
pt: { xs: 1.5, sm: 6 },
pb: { xs: 1.5, sm: 6 },
px: { xs: 1, sm: 4 },
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
height: '100%',
}}>
<TrendingUp sx={{ fontSize: { xs: 20, 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.5625rem', sm: '0.75rem' } }}>
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: { xs: '0.9375rem', sm: '1.75rem' }, lineHeight: 1.2 }}>
{event.conversion}%
</Typography>
<Box sx={{ mt: { xs: 0.25, sm: 1 }, minHeight: { xs: '1.3em', sm: 0 } }} />
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
{/* Chart Section - 참여 추이 */}
<Box sx={{ mb: { xs: 5, sm: 10 } }}>
<Typography variant="h5" sx={{ fontWeight: 700, mb: { xs: 3, sm: 6 }, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
📈
</Typography>
<Card elevation={0} sx={{ borderRadius: { xs: 3, sm: 4 }, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)' }}>
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
<Box sx={{ display: 'flex', gap: { xs: 1, sm: 2 }, mb: { xs: 3, sm: 6 } }}>
<Button
size="medium"
variant={chartPeriod === '7d' ? 'contained' : 'outlined'}
onClick={() => setChartPeriod('7d')}
sx={{ borderRadius: 2, fontSize: { xs: '0.75rem', sm: '0.875rem' }, py: { xs: 0.5, sm: 1 }, px: { xs: 1.5, sm: 2 } }}
>
7
</Button>
<Button
size="medium"
variant={chartPeriod === '30d' ? 'contained' : 'outlined'}
onClick={() => setChartPeriod('30d')}
sx={{ borderRadius: 2, fontSize: { xs: '0.75rem', sm: '0.875rem' }, py: { xs: 0.5, sm: 1 }, px: { xs: 1.5, sm: 2 } }}
>
30
</Button>
<Button
size="medium"
variant={chartPeriod === 'all' ? 'contained' : 'outlined'}
onClick={() => setChartPeriod('all')}
sx={{ borderRadius: 2, fontSize: { xs: '0.75rem', sm: '0.875rem' }, py: { xs: 0.5, sm: 1 }, px: { xs: 1.5, sm: 2 } }}
>
</Button>
</Box>
<Box sx={{ height: { xs: 200, sm: 320 } }}>
<Line
data={generateParticipationTrendData()}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top' as const,
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
titleFont: { size: 14, weight: 'bold' },
bodyFont: { size: 13 },
},
},
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.05)',
},
},
x: {
grid: {
display: false,
},
},
},
}}
/>
</Box>
</CardContent>
</Card>
</Box>
{/* Chart Section - 채널별 성과 & ROI 추이 */}
<Grid container spacing={{ xs: 2, sm: 6 }} sx={{ mb: { xs: 5, sm: 10 } }}>
<Grid item xs={12} md={6}>
<Typography variant="h5" sx={{ fontWeight: 700, mb: { xs: 3, sm: 6 }, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
📊
</Typography>
<Card elevation={0} sx={{ borderRadius: { xs: 3, sm: 4 }, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)' }}>
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
<Box sx={{ height: { xs: 200, sm: 320 } }}>
<Bar
data={generateChannelPerformanceData()}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
titleFont: { size: 14, weight: 'bold' },
bodyFont: { size: 13 },
},
},
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.05)',
},
},
x: {
grid: {
display: false,
},
},
},
}}
/>
</Box>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="h5" sx={{ fontWeight: 700, mb: { xs: 3, sm: 6 }, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
💰 ROI
</Typography>
<Card elevation={0} sx={{ borderRadius: { xs: 3, sm: 4 }, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)' }}>
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
<Box sx={{ height: { xs: 200, sm: 320 } }}>
<Line
data={generateRoiTrendData()}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top' as const,
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
titleFont: { size: 14, weight: 'bold' },
bodyFont: { size: 13 },
},
},
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.05)',
},
},
x: {
grid: {
display: false,
},
},
},
}}
/>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
{/* Event Details */}
<Box sx={{ mb: { xs: 5, sm: 10 } }}>
<Typography variant="h5" sx={{ fontWeight: 700, mb: { xs: 3, sm: 6 }, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
🎯
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { xs: 2, sm: 4 } }}>
<Card elevation={0} sx={{ borderRadius: { xs: 3, sm: 4 }, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)' }}>
<CardContent sx={{ display: 'flex', alignItems: 'flex-start', gap: { xs: 1.5, sm: 3 }, p: { xs: 3, sm: 4 } }}>
<CardGiftcard sx={{ fontSize: { xs: 20, sm: 28 }, color: colors.pink }} />
<Box sx={{ flex: 1 }}>
<Typography variant="caption" color="text.secondary" display="block" sx={{ mb: { xs: 0.5, sm: 1 }, fontSize: { xs: '0.625rem', sm: '0.75rem' } }}>
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1.125rem' } }}>
{event.prize}
</Typography>
</Box>
</CardContent>
</Card>
<Card elevation={0} sx={{ borderRadius: { xs: 3, sm: 4 }, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)' }}>
<CardContent sx={{ display: 'flex', alignItems: 'flex-start', gap: { xs: 1.5, sm: 3 }, p: { xs: 3, sm: 4 } }}>
{getMethodIcon(event.method)}
<Box sx={{ flex: 1 }}>
<Typography variant="caption" color="text.secondary" display="block" sx={{ mb: { xs: 0.5, sm: 1 }, fontSize: { xs: '0.625rem', sm: '0.75rem' } }}>
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1.125rem' } }}>
{event.method}
</Typography>
</Box>
</CardContent>
</Card>
<Card elevation={0} sx={{ borderRadius: { xs: 3, sm: 4 }, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)' }}>
<CardContent sx={{ display: 'flex', alignItems: 'flex-start', gap: { xs: 1.5, sm: 3 }, p: { xs: 3, sm: 4 } }}>
<AttachMoney sx={{ fontSize: { xs: 20, sm: 28 }, color: colors.mint }} />
<Box sx={{ flex: 1 }}>
<Typography variant="caption" color="text.secondary" display="block" sx={{ mb: { xs: 0.5, sm: 1 }, fontSize: { xs: '0.625rem', sm: '0.75rem' } }}>
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1.125rem' } }}>
{event.cost.toLocaleString()}
</Typography>
</Box>
</CardContent>
</Card>
<Card elevation={0} sx={{ borderRadius: { xs: 3, sm: 4 }, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)' }}>
<CardContent sx={{ display: 'flex', alignItems: 'flex-start', gap: { xs: 1.5, sm: 3 }, p: { xs: 3, sm: 4 } }}>
<Share sx={{ fontSize: { xs: 20, sm: 28 }, color: colors.blue }} />
<Box sx={{ flex: 1 }}>
<Typography variant="caption" color="text.secondary" display="block" sx={{ mb: { xs: 1, sm: 2 }, fontSize: { xs: '0.625rem', sm: '0.75rem' } }}>
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: { xs: 1, sm: 2 } }}>
{event.channels.map((channel) => (
<Chip
key={channel}
label={channel}
size="medium"
sx={{
bgcolor: colors.purpleLight,
color: colors.purple,
fontWeight: 600,
fontSize: { xs: '0.75rem', sm: '0.875rem' },
height: { xs: 24, sm: 32 },
}}
/>
))}
</Box>
</Box>
</CardContent>
</Card>
</Box>
</Box>
{/* Quick Actions */}
<Box sx={{ mb: { xs: 5, sm: 10 } }}>
<Typography variant="h5" sx={{ fontWeight: 700, mb: { xs: 3, sm: 6 }, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
</Typography>
<Grid container spacing={{ xs: 2, sm: 4 }}>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
cursor: 'pointer',
borderRadius: { xs: 3, sm: 4 },
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
transition: 'all 0.2s',
'&:hover': {
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
transform: 'translateY(-4px)',
},
}}
onClick={() => router.push(`/events/${eventId}/participants`)}
>
<CardContent sx={{ textAlign: 'center', py: { xs: 3, sm: 5 } }}>
<People sx={{ fontSize: { xs: 24, sm: 40 }, mb: { xs: 1, sm: 2 }, color: colors.pink }} />
<Typography variant="body1" sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1rem' } }}>
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
cursor: 'pointer',
borderRadius: { xs: 3, sm: 4 },
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
transition: 'all 0.2s',
'&:hover': {
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
transform: 'translateY(-4px)',
},
}}
>
<CardContent sx={{ textAlign: 'center', py: { xs: 3, sm: 5 } }}>
<Edit sx={{ fontSize: { xs: 24, sm: 40 }, mb: { xs: 1, sm: 2 }, color: colors.blue }} />
<Typography variant="body1" sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1rem' } }}>
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
cursor: 'pointer',
borderRadius: { xs: 3, sm: 4 },
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
transition: 'all 0.2s',
'&:hover': {
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
transform: 'translateY(-4px)',
},
}}
>
<CardContent sx={{ textAlign: 'center', py: { xs: 3, sm: 5 } }}>
<Share sx={{ fontSize: { xs: 24, sm: 40 }, mb: { xs: 1, sm: 2 }, color: colors.purple }} />
<Typography variant="body1" sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1rem' } }}>
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
cursor: 'pointer',
borderRadius: { xs: 3, sm: 4 },
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
transition: 'all 0.2s',
'&:hover': {
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
transform: 'translateY(-4px)',
},
}}
>
<CardContent sx={{ textAlign: 'center', py: { xs: 3, sm: 5 } }}>
<Download sx={{ fontSize: { xs: 24, sm: 40 }, mb: { xs: 1, sm: 2 }, color: colors.mint }} />
<Typography variant="body1" sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1rem' } }}>
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
{/* Recent Participants */}
<Box sx={{ mb: { xs: 5, sm: 10 } }}>
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, alignItems: { xs: 'flex-start', sm: 'center' }, justifyContent: 'space-between', gap: { xs: 2, sm: 0 }, mb: { xs: 3, sm: 6 } }}>
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
👥
</Typography>
<Button
size="medium"
endIcon={<Box component="span" className="material-icons" sx={{ fontSize: { xs: 14, sm: 18 } }}>chevron_right</Box>}
onClick={() => router.push(`/events/${eventId}/participants`)}
sx={{ color: colors.pink, fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1rem' } }}
>
</Button>
</Box>
<Card elevation={0} sx={{ borderRadius: { xs: 3, sm: 4 }, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)' }}>
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
{recentParticipants.map((participant, index) => (
<Box key={index}>
{index > 0 && <Divider sx={{ my: { xs: 2, sm: 4 } }} />}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 } }}>
<Box
sx={{
width: { xs: 32, sm: 48 },
height: { xs: 32, sm: 48 },
borderRadius: '50%',
bgcolor: colors.purpleLight,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Person sx={{ color: colors.purple, fontSize: { xs: 16, sm: 24 } }} />
</Box>
<Box>
<Typography variant="body1" sx={{ fontWeight: 600, mb: { xs: 0.25, sm: 0.5 }, fontSize: { xs: '0.875rem', sm: '1rem' } }}>
{participant.name}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
{participant.phone}
</Typography>
</Box>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
{participant.time}
</Typography>
</Box>
</Box>
))}
</CardContent>
</Card>
</Box>
</Container>
</Box>
);
}