실시간 성과 분석 화면 개발

- /analytics 경로에 실시간 대시보드 페이지 추가
- 실시간 업데이트 로직 구현 (30초마다 시간 갱신, 5분마다 데이터 갱신)
- KPI 요약 카드 4개 구현 (참여자수, 총비용, 예상수익, ROI)
- 채널별 성과 차트 섹션 (파이차트 플레이스홀더 + 범례)
- 시간대별 참여 추이 차트 섹션 (라인차트 플레이스홀더 + 통계)
- ROI 상세 분석 테이블 (비용/수익 분해 + 계산식 시각화)
- 참여자 프로필 분석 (연령별/성별 막대 그래프)
This commit is contained in:
cherry2250 2025-10-24 15:34:44 +09:00
parent 3f8658f9f3
commit 526bf06182

View File

@ -0,0 +1,511 @@
'use client';
import { useState, useEffect } from 'react';
import {
Box,
Container,
Typography,
Card,
CardContent,
Grid,
} from '@mui/material';
import {
PieChart,
ShowChart,
Payments,
People,
} from '@mui/icons-material';
// Mock 데이터
const mockAnalyticsData = {
summary: {
participants: 128,
participantsDelta: 12,
totalCost: 300000,
expectedRevenue: 1350000,
roi: 450,
targetRoi: 300,
},
channelPerformance: [
{ channel: '우리동네TV', participants: 58, percentage: 45, color: '#E31E24' },
{ channel: '링고비즈', participants: 38, percentage: 30, color: '#0066FF' },
{ channel: 'SNS', participants: 32, percentage: 25, color: '#FFB800' },
],
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 (
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 10 }}>
<Container maxWidth="lg" sx={{ pt: 4, pb: 4, px: { xs: 3, sm: 3, md: 4 } }}>
{/* Title with Real-time Indicator */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mb: 4,
}}
>
<Typography variant="h5" sx={{ fontWeight: 700 }}>
📊 ()
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: 'success.main',
animation: 'pulse 2s infinite',
'@keyframes pulse': {
'0%, 100%': { opacity: 1 },
'50%': { opacity: 0.3 },
},
}}
/>
<Typography variant="caption" color="text.secondary">
{updateText}
</Typography>
</Box>
</Box>
{/* Summary KPI Cards */}
<Grid container spacing={3} sx={{ mb: 5 }}>
<Grid item xs={6} md={3}>
<Card elevation={0} sx={{ borderRadius: 3, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }}>
<CardContent sx={{ textAlign: 'center', py: 3, px: 2 }}>
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600 }}>
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, my: 1 }}>
{summary.participants}
</Typography>
<Typography variant="caption" color="success.main" sx={{ fontWeight: 600 }}>
{summary.participantsDelta} ()
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={3}>
<Card elevation={0} sx={{ borderRadius: 3, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }}>
<CardContent sx={{ textAlign: 'center', py: 3, px: 2 }}>
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600 }}>
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, my: 1 }}>
{Math.floor(summary.totalCost / 10000)}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600 }}>
{Math.floor(roiDetail.prizeCost / 10000)} + {' '}
{Math.floor(roiDetail.channelCost / 10000)}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={3}>
<Card elevation={0} sx={{ borderRadius: 3, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }}>
<CardContent sx={{ textAlign: 'center', py: 3, px: 2 }}>
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600 }}>
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, my: 1, color: 'success.main' }}>
{Math.floor(summary.expectedRevenue / 10000)}
</Typography>
<Typography variant="caption" color="success.main" sx={{ fontWeight: 600 }}>
{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={{ borderRadius: 3, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }}>
<CardContent sx={{ textAlign: 'center', py: 3, px: 2 }}>
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600 }}>
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, my: 1, color: 'success.main' }}>
{summary.roi}%
</Typography>
<Typography variant="caption" color="success.main" sx={{ fontWeight: 600 }}>
{summary.targetRoi}%
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
{/* Charts Grid */}
<Grid container spacing={4} sx={{ mb: 5 }}>
{/* Channel Performance */}
<Grid item xs={12} md={6}>
<Card elevation={0} sx={{ borderRadius: 3, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }}>
<CardContent sx={{ p: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<PieChart color="error" />
<Typography variant="h6" sx={{ fontWeight: 700 }}>
</Typography>
</Box>
{/* Chart Placeholder */}
<Box
sx={{
width: '100%',
aspectRatio: '1 / 1',
bgcolor: 'grey.100',
borderRadius: 3,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
p: 4,
mb: 3,
}}
>
<span className="material-icons" style={{ fontSize: 64, color: '#bdbdbd' }}>
donut_large
</span>
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
</Typography>
</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">{item.channel}</Typography>
</Box>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{item.percentage}% ({item.participants})
</Typography>
</Box>
))}
</Box>
</CardContent>
</Card>
</Grid>
{/* Time Trend */}
<Grid item xs={12} md={6}>
<Card elevation={0} sx={{ borderRadius: 3, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }}>
<CardContent sx={{ p: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<ShowChart color="error" />
<Typography variant="h6" sx={{ fontWeight: 700 }}>
</Typography>
</Box>
{/* Chart Placeholder */}
<Box
sx={{
width: '100%',
aspectRatio: '16 / 9',
bgcolor: 'grey.100',
borderRadius: 3,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
p: 4,
mb: 3,
}}
>
<span className="material-icons" style={{ fontSize: 64, color: '#bdbdbd' }}>
trending_up
</span>
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
</Typography>
</Box>
{/* Stats */}
<Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
: {timePerformance.peakTime} ({timePerformance.peakParticipants})
</Typography>
<Typography variant="body2" color="text.secondary">
: {timePerformance.avgPerHour}
</Typography>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
{/* ROI Detail & Participant Profile */}
<Grid container spacing={4} sx={{ mb: 5 }}>
{/* ROI Detail */}
<Grid item xs={12} md={6}>
<Card elevation={0} sx={{ borderRadius: 3, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }}>
<CardContent sx={{ p: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<Payments color="error" />
<Typography variant="h6" sx={{ fontWeight: 700 }}>
</Typography>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 1 }}>
: {Math.floor(roiDetail.totalCost / 10000)}
</Typography>
<Box sx={{ pl: 2, display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" color="text.secondary">
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{Math.floor(roiDetail.prizeCost / 10000)}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" color="text.secondary">
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{Math.floor(roiDetail.channelCost / 10000)}
</Typography>
</Box>
</Box>
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 1 }}>
: {Math.floor(roiDetail.expectedRevenue / 10000)}
</Typography>
<Box sx={{ pl: 2, display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" color="text.secondary">
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'success.main' }}>
{Math.floor(roiDetail.salesIncrease / 10000)}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" color="text.secondary">
LTV
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'success.main' }}>
{Math.floor(roiDetail.newCustomerLTV / 10000)}
</Typography>
</Box>
</Box>
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 1, color: 'success.main' }}>
</Typography>
<Box
sx={{
p: 2,
bgcolor: 'grey.100',
borderRadius: 2,
textAlign: 'center',
}}
>
<Typography variant="body2" sx={{ mb: 0.5 }}>
( - ) ÷ × 100
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
({Math.floor(roiDetail.expectedRevenue / 10000)} -{' '}
{Math.floor(roiDetail.totalCost / 10000)}) ÷{' '}
{Math.floor(roiDetail.totalCost / 10000)} × 100
</Typography>
<Typography variant="h5" sx={{ fontWeight: 700, color: 'success.main' }}>
= {summary.roi}%
</Typography>
</Box>
</Box>
</Box>
</CardContent>
</Card>
</Grid>
{/* Participant Profile */}
<Grid item xs={12} md={6}>
<Card elevation={0} sx={{ borderRadius: 3, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }}>
<CardContent sx={{ p: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<People color="error" />
<Typography variant="h6" sx={{ fontWeight: 700 }}>
</Typography>
</Box>
{/* Age Distribution */}
<Box sx={{ mb: 4 }}>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 2 }}>
</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 }}>
{item.label}
</Typography>
<Box
sx={{
flex: 1,
height: 24,
bgcolor: 'grey.200',
borderRadius: 1,
overflow: 'hidden',
}}
>
<Box
sx={{
width: `${item.percentage}%`,
height: '100%',
bgcolor: 'info.main',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
pr: 1,
}}
>
<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 }}>
</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 }}>
{item.label}
</Typography>
<Box
sx={{
flex: 1,
height: 24,
bgcolor: 'grey.200',
borderRadius: 1,
overflow: 'hidden',
}}
>
<Box
sx={{
width: `${item.percentage}%`,
height: '100%',
bgcolor: 'error.main',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
pr: 1,
}}
>
<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>
);
}