cherry2250 e8ea659c0b UI 개선: 간격 확대, 차트 색상 변경, 이벤트 목록 기능 추가
- 전체 간격(spacing) 2배 증가 및 컨테이너 패딩 확대
- 기본 폰트 크기 14px→16px 증가
- 채널별 성과 차트 색상을 조화로운 색상으로 변경 (#F472B6, #60A5FA, #FB923C)
- 이벤트 목록 페이지에 통계 요약 카드 추가 (전체/진행중/참여자/평균ROI)
- 진행중인 이벤트에 진행률 바 추가
- 이벤트 상태 배지 추가 (마감임박/인기/높은ROI/신규)
- 상품 및 참여방법에 아이콘 추가
- 이벤트 카드 레이아웃 개선: 참여자/ROI 정보를 오른쪽 하단으로 재배치

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 14:26:39 +09:00

756 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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 {
Box,
Container,
Typography,
Card,
CardContent,
Grid,
} from '@mui/material';
import {
PieChart as PieChartIcon,
ShowChart as ShowChartIcon,
Payments,
People,
} from '@mui/icons-material';
import {
Chart as ChartJS,
ArcElement,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Pie, Line } from 'react-chartjs-2';
import Header from '@/shared/ui/Header';
import {
cardStyles,
colors,
responsiveText,
} from '@/shared/lib/button-styles';
// Chart.js 등록
ChartJS.register(
ArcElement,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
// Mock 데이터
const mockAnalyticsData = {
summary: {
participants: 128,
participantsDelta: 12,
totalCost: 300000,
expectedRevenue: 1350000,
roi: 450,
targetRoi: 300,
},
channelPerformance: [
{ channel: '우리동네TV', participants: 58, percentage: 45, color: '#F472B6' },
{ channel: '링고비즈', participants: 38, percentage: 30, color: '#60A5FA' },
{ channel: 'SNS', participants: 32, percentage: 25, color: '#FB923C' },
],
timePerformance: {
peakTime: '오후 2-4시',
peakParticipants: 35,
avgPerHour: 8,
},
roiDetail: {
totalCost: 300000,
prizeCost: 250000,
channelCost: 50000,
expectedRevenue: 1350000,
salesIncrease: 1000000,
newCustomerLTV: 350000,
},
participantProfile: {
age: [
{ label: '20대', percentage: 35 },
{ label: '30대', percentage: 40 },
{ label: '40대', percentage: 25 },
],
gender: [
{ label: '여성', percentage: 60 },
{ label: '남성', percentage: 40 },
],
},
};
export default function AnalyticsPage() {
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
const [updateText, setUpdateText] = useState('방금 전');
useEffect(() => {
// 업데이트 시간 표시 갱신
const updateInterval = setInterval(() => {
const now = new Date();
const diff = Math.floor((now.getTime() - lastUpdate.getTime()) / 1000);
let text;
if (diff < 60) {
text = '방금 전';
} else if (diff < 3600) {
text = `${Math.floor(diff / 60)}분 전`;
} else {
text = `${Math.floor(diff / 3600)}시간 전`;
}
setUpdateText(text);
}, 30000); // 30초마다 갱신
// 5분마다 데이터 업데이트 시뮬레이션
const dataUpdateInterval = setInterval(() => {
setLastUpdate(new Date());
setUpdateText('방금 전');
}, 300000); // 5분
return () => {
clearInterval(updateInterval);
clearInterval(dataUpdateInterval);
};
}, [lastUpdate]);
const { summary, channelPerformance, timePerformance, roiDetail, participantProfile } =
mockAnalyticsData;
return (
<>
<Header title="성과 분석" showBack={true} showMenu={false} showProfile={true} />
<Box
sx={{
pt: { xs: 7, sm: 8 },
pb: 10,
bgcolor: colors.gray[50],
minHeight: '100vh',
}}
>
<Container maxWidth="lg" sx={{ pt: 8, pb: 6, px: { xs: 6, sm: 8, md: 10 } }}>
{/* Title with Real-time Indicator */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mb: 10,
}}
>
<Typography variant="h5" sx={{ ...responsiveText.h3 }}>
📊 ()
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: colors.mint,
animation: 'pulse 2s infinite',
'@keyframes pulse': {
'0%, 100%': { opacity: 1 },
'50%': { opacity: 0.3 },
},
}}
/>
<Typography variant="caption" sx={{ ...responsiveText.body2 }}>
{updateText}
</Typography>
</Box>
</Box>
{/* Summary KPI Cards */}
<Grid container spacing={6} sx={{ mb: 10 }}>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
...cardStyles.default,
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.purpleLight} 100%)`,
borderColor: 'transparent',
}}
>
<CardContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
<Typography
variant="body2"
sx={{
mb: 2,
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: 500,
fontSize: '1rem',
}}
>
</Typography>
<Typography variant="h3" sx={{ fontWeight: 700, color: 'white', fontSize: '2.5rem' }}>
{summary.participants}
</Typography>
<Typography
variant="caption"
sx={{ color: 'rgba(255, 255, 255, 0.8)', fontWeight: 600, fontSize: '0.875rem' }}
>
{summary.participantsDelta} ()
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
...cardStyles.default,
background: `linear-gradient(135deg, ${colors.orange} 0%, ${colors.orangeLight} 100%)`,
borderColor: 'transparent',
}}
>
<CardContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
<Typography
variant="body2"
sx={{
mb: 2,
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: 500,
fontSize: '1rem',
}}
>
</Typography>
<Typography variant="h3" sx={{ fontWeight: 700, color: 'white', fontSize: '2.5rem' }}>
{Math.floor(summary.totalCost / 10000)}
</Typography>
<Typography
variant="caption"
sx={{ color: 'rgba(255, 255, 255, 0.8)', fontWeight: 600, fontSize: '0.875rem' }}
>
{Math.floor(roiDetail.prizeCost / 10000)} + {' '}
{Math.floor(roiDetail.channelCost / 10000)}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
...cardStyles.default,
background: `linear-gradient(135deg, ${colors.mint} 0%, ${colors.mintLight} 100%)`,
borderColor: 'transparent',
}}
>
<CardContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
<Typography
variant="body2"
sx={{
mb: 2,
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: 500,
fontSize: '1rem',
}}
>
</Typography>
<Typography variant="h3" sx={{ fontWeight: 700, color: 'white', fontSize: '2.5rem' }}>
{Math.floor(summary.expectedRevenue / 10000)}
</Typography>
<Typography
variant="caption"
sx={{ color: 'rgba(255, 255, 255, 0.8)', fontWeight: 600, fontSize: '0.875rem' }}
>
{Math.floor(roiDetail.salesIncrease / 10000)} + LTV{' '}
{Math.floor(roiDetail.newCustomerLTV / 10000)}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
...cardStyles.default,
background: `linear-gradient(135deg, ${colors.blue} 0%, ${colors.blueLight} 100%)`,
borderColor: 'transparent',
}}
>
<CardContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
<Typography
variant="body2"
sx={{
mb: 2,
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: 500,
fontSize: '1rem',
}}
>
</Typography>
<Typography variant="h3" sx={{ fontWeight: 700, color: 'white', fontSize: '2.5rem' }}>
{summary.roi}%
</Typography>
<Typography
variant="caption"
sx={{ color: 'rgba(255, 255, 255, 0.8)', fontWeight: 600, fontSize: '0.875rem' }}
>
{summary.targetRoi}%
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
{/* Charts Grid */}
<Grid container spacing={6} sx={{ mb: 10 }}>
{/* Channel Performance */}
<Grid item xs={12} md={6}>
<Card elevation={0} sx={{ ...cardStyles.default, height: '100%' }}>
<CardContent sx={{ p: { xs: 6, sm: 8 }, height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 6 }}>
<Box
sx={{
width: 40,
height: 40,
borderRadius: '12px',
background: `linear-gradient(135deg, ${colors.pink} 0%, ${colors.pinkLight} 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<PieChartIcon sx={{ fontSize: 24, color: 'white' }} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 700, color: colors.gray[900] }}>
</Typography>
</Box>
{/* Pie Chart */}
<Box
sx={{
width: '100%',
maxWidth: 300,
mx: 'auto',
mb: 3,
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Pie
data={{
labels: channelPerformance.map((item) => item.channel),
datasets: [
{
data: channelPerformance.map((item) => item.participants),
backgroundColor: channelPerformance.map((item) => item.color),
borderColor: '#fff',
borderWidth: 2,
},
],
}}
options={{
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: false,
},
tooltip: {
callbacks: {
label: function (context) {
const label = context.label || '';
const value = context.parsed || 0;
const total = context.dataset.data.reduce((a: number, b: number) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(1);
return `${label}: ${value}명 (${percentage}%)`;
},
},
},
},
}}
/>
</Box>
{/* Legend */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{channelPerformance.map((item) => (
<Box
key={item.channel}
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box
sx={{
width: 12,
height: 12,
borderRadius: '50%',
bgcolor: item.color,
}}
/>
<Typography variant="body2" sx={{ color: colors.gray[700] }}>
{item.channel}
</Typography>
</Box>
<Typography variant="body2" sx={{ fontWeight: 600, color: colors.gray[900] }}>
{item.percentage}% ({item.participants})
</Typography>
</Box>
))}
</Box>
</CardContent>
</Card>
</Grid>
{/* Time Trend */}
<Grid item xs={12} md={6}>
<Card elevation={0} sx={{ ...cardStyles.default, height: '100%' }}>
<CardContent sx={{ p: { xs: 6, sm: 8 }, height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 6 }}>
<Box
sx={{
width: 40,
height: 40,
borderRadius: '12px',
background: `linear-gradient(135deg, ${colors.blue} 0%, ${colors.blueLight} 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<ShowChartIcon sx={{ fontSize: 24, color: 'white' }} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 700, color: colors.gray[900] }}>
</Typography>
</Box>
{/* Line Chart */}
<Box
sx={{
width: '100%',
mb: 3,
flex: 1,
display: 'flex',
alignItems: 'center',
minHeight: 200,
}}
>
<Line
data={{
labels: [
'00시',
'03시',
'06시',
'09시',
'12시',
'15시',
'18시',
'21시',
],
datasets: [
{
label: '참여자 수',
data: [3, 2, 5, 12, 28, 35, 22, 15],
borderColor: colors.blue,
backgroundColor: `${colors.blue}33`,
fill: true,
tension: 0.4,
pointBackgroundColor: colors.blue,
pointBorderColor: '#fff',
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6,
},
],
}}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
tooltip: {
backgroundColor: colors.gray[900],
padding: 12,
displayColors: false,
callbacks: {
label: function (context) {
return `${context.parsed.y}`;
},
},
},
},
scales: {
y: {
beginAtZero: true,
ticks: {
color: colors.gray[600],
},
grid: {
color: colors.gray[200],
},
},
x: {
ticks: {
color: colors.gray[600],
},
grid: {
display: false,
},
},
},
}}
/>
</Box>
{/* Stats */}
<Box>
<Typography variant="body2" sx={{ mb: 0.5, color: colors.gray[600] }}>
: {timePerformance.peakTime} ({timePerformance.peakParticipants})
</Typography>
<Typography variant="body2" sx={{ color: colors.gray[600] }}>
: {timePerformance.avgPerHour}
</Typography>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
{/* ROI Detail & Participant Profile */}
<Grid container spacing={6}>
{/* ROI Detail */}
<Grid item xs={12} md={6}>
<Card elevation={0} sx={{ ...cardStyles.default, height: '100%' }}>
<CardContent sx={{ p: { xs: 6, sm: 8 }, height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 6 }}>
<Box
sx={{
width: 40,
height: 40,
borderRadius: '12px',
background: `linear-gradient(135deg, ${colors.orange} 0%, ${colors.orangeLight} 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Payments sx={{ fontSize: 24, color: 'white' }} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 700, color: colors.gray[900] }}>
</Typography>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 1.5, color: colors.gray[900] }}>
: {Math.floor(roiDetail.totalCost / 10000)}
</Typography>
<Box sx={{ pl: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ color: colors.gray[600] }}>
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: colors.gray[900] }}>
{Math.floor(roiDetail.prizeCost / 10000)}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ color: colors.gray[600] }}>
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: colors.gray[900] }}>
{Math.floor(roiDetail.channelCost / 10000)}
</Typography>
</Box>
</Box>
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 1.5, color: colors.gray[900] }}>
: {Math.floor(roiDetail.expectedRevenue / 10000)}
</Typography>
<Box sx={{ pl: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ color: colors.gray[600] }}>
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: colors.mint }}>
{Math.floor(roiDetail.salesIncrease / 10000)}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ color: colors.gray[600] }}>
LTV
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: colors.mint }}>
{Math.floor(roiDetail.newCustomerLTV / 10000)}
</Typography>
</Box>
</Box>
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 1.5, color: colors.mint }}>
</Typography>
<Box
sx={{
p: 2.5,
bgcolor: colors.gray[100],
borderRadius: 2,
textAlign: 'center',
}}
>
<Typography variant="body2" sx={{ mb: 0.5, color: colors.gray[700] }}>
( - ) ÷ × 100
</Typography>
<Typography variant="body2" sx={{ mb: 1.5, color: colors.gray[600] }}>
({Math.floor(roiDetail.expectedRevenue / 10000)} -{' '}
{Math.floor(roiDetail.totalCost / 10000)}) ÷{' '}
{Math.floor(roiDetail.totalCost / 10000)} × 100
</Typography>
<Typography variant="h5" sx={{ fontWeight: 700, color: colors.mint }}>
= {summary.roi}%
</Typography>
</Box>
</Box>
</Box>
</CardContent>
</Card>
</Grid>
{/* Participant Profile */}
<Grid item xs={12} md={6}>
<Card elevation={0} sx={{ ...cardStyles.default, height: '100%' }}>
<CardContent sx={{ p: { xs: 6, sm: 8 }, height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 6 }}>
<Box
sx={{
width: 40,
height: 40,
borderRadius: '12px',
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.purpleLight} 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<People sx={{ fontSize: 24, color: 'white' }} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 700, color: colors.gray[900] }}>
</Typography>
</Box>
{/* Age Distribution */}
<Box sx={{ mb: 4 }}>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 2, color: colors.gray[900] }}>
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
{participantProfile.age.map((item) => (
<Box key={item.label}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Typography variant="body2" sx={{ minWidth: 60, color: colors.gray[700] }}>
{item.label}
</Typography>
<Box
sx={{
flex: 1,
height: 28,
bgcolor: colors.gray[200],
borderRadius: 1.5,
overflow: 'hidden',
}}
>
<Box
sx={{
width: `${item.percentage}%`,
height: '100%',
background: `linear-gradient(135deg, ${colors.blue} 0%, ${colors.blueLight} 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
pr: 1.5,
}}
>
<Typography
variant="caption"
sx={{ color: 'white', fontWeight: 600, fontSize: 12 }}
>
{item.percentage}%
</Typography>
</Box>
</Box>
</Box>
</Box>
))}
</Box>
</Box>
{/* Gender Distribution */}
<Box>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 2, color: colors.gray[900] }}>
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
{participantProfile.gender.map((item) => (
<Box key={item.label}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Typography variant="body2" sx={{ minWidth: 60, color: colors.gray[700] }}>
{item.label}
</Typography>
<Box
sx={{
flex: 1,
height: 28,
bgcolor: colors.gray[200],
borderRadius: 1.5,
overflow: 'hidden',
}}
>
<Box
sx={{
width: `${item.percentage}%`,
height: '100%',
background: `linear-gradient(135deg, ${colors.pink} 0%, ${colors.pinkLight} 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
pr: 1.5,
}}
>
<Typography
variant="caption"
sx={{ color: 'white', fontWeight: 600, fontSize: 12 }}
>
{item.percentage}%
</Typography>
</Box>
</Box>
</Box>
</Box>
))}
</Box>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</Container>
</Box>
</>
);
}