Hyowon Yang abceae6e2a Analytics 테스트 페이지 디버그 로그 추가
- 이벤트 기간 계산 함수에 상세 디버그 로그 추가
- 차트 데이터 생성 함수에 필터링 과정 로그 추가
- Timeline dataPoints 구조 확인을 위한 콘솔 출력 추가
- ROI 필드 매핑 검증을 위한 로그 추가
2025-10-29 19:33:19 +09:00

992 lines
40 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,
CircularProgress,
IconButton,
Tooltip,
} from '@mui/material';
import {
PieChart as PieChartIcon,
ShowChart as ShowChartIcon,
Payments,
People,
Refresh as RefreshIcon,
} from '@mui/icons-material';
import {
Chart as ChartJS,
ArcElement,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip as ChartTooltip,
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';
import { useAuthContext } from '@/features/auth';
import { analyticsApi } from '@/entities/analytics';
import type {
UserAnalyticsDashboardResponse,
UserTimelineAnalyticsResponse,
UserRoiAnalyticsResponse,
} from '@/entities/analytics';
// Chart.js 등록
ChartJS.register(
ArcElement,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
ChartTooltip,
Legend
);
export default function AnalyticsPage() {
const { user } = useAuthContext();
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [dashboardData, setDashboardData] = useState<UserAnalyticsDashboardResponse | null>(null);
const [timelineData, setTimelineData] = useState<UserTimelineAnalyticsResponse | null>(null);
const [roiData, setRoiData] = useState<UserRoiAnalyticsResponse | null>(null);
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
const [updateText, setUpdateText] = useState('방금 전');
// Analytics 데이터 로드 함수
const fetchAnalytics = async (forceRefresh = false) => {
try {
if (forceRefresh) {
setRefreshing(true);
} else {
setLoading(true);
}
// 로그인하지 않은 경우 테스트용 userId 사용 (로컬 테스트용)
const userId = user?.userId ? String(user.userId) : 'store_001';
console.log('📊 Analytics 데이터 로드 시작:', { userId, isLoggedIn: !!user, refresh: forceRefresh });
// 병렬로 모든 Analytics API 호출
const [dashboard, timeline, roi] = await Promise.all([
analyticsApi.getUserAnalytics(userId, { refresh: forceRefresh }),
analyticsApi.getUserTimelineAnalytics(userId, { interval: 'hourly', refresh: forceRefresh }),
analyticsApi.getUserRoiAnalytics(userId, { includeProjection: true, refresh: forceRefresh }),
]);
console.log('✅ Dashboard 데이터:', dashboard);
console.log('✅ Timeline 데이터:', timeline);
console.log('✅ ROI 데이터:', roi);
setDashboardData(dashboard);
setTimelineData(timeline);
setRoiData(roi);
setLastUpdate(new Date());
} catch (error: any) {
console.error('❌ Analytics 데이터 로드 실패:', error);
// 에러 발생 시에도 로딩 상태 해제
} finally {
setLoading(false);
setRefreshing(false);
}
};
// 새로고침 핸들러
const handleRefresh = () => {
fetchAnalytics(true);
};
// 초기 데이터 로드
useEffect(() => {
fetchAnalytics(false);
}, [user?.userId]);
// 업데이트 시간 표시 갱신
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초마다 갱신
return () => {
clearInterval(updateInterval);
};
}, [lastUpdate]);
// 로딩 중 표시
if (loading) {
return (
<>
<Header title="성과 분석" showBack={true} showMenu={false} showProfile={true} />
<Box
sx={{
pt: { xs: 7, sm: 8 },
pb: 10,
bgcolor: colors.gray[50],
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<CircularProgress />
</Box>
</>
);
}
// 데이터 없음 표시
if (!dashboardData || !timelineData || !roiData) {
return (
<>
<Header title="성과 분석" showBack={true} showMenu={false} showProfile={true} />
<Box
sx={{
pt: { xs: 7, sm: 8 },
pb: 10,
bgcolor: colors.gray[50],
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Typography variant="body1" color="text.secondary">
Analytics .
</Typography>
</Box>
</>
);
}
// API 데이터에서 필요한 값 추출
console.log('📊 === 데이터 추출 디버깅 시작 ===');
console.log('📊 원본 dashboardData.overallSummary:', dashboardData.overallSummary);
console.log('📊 원본 roiData.overallInvestment:', roiData.overallInvestment);
console.log('📊 원본 roiData.overallRevenue:', roiData.overallRevenue);
console.log('📊 원본 roiData.overallRoi:', roiData.overallRoi);
const summary = {
participants: dashboardData.overallSummary.participants,
participantsDelta: dashboardData.overallSummary.participantsDelta,
totalCost: roiData.overallInvestment.total,
expectedRevenue: roiData.overallRevenue.total,
roi: roiData.overallRoi.roiPercentage,
targetRoi: dashboardData.overallSummary.targetRoi,
};
console.log('📊 최종 summary 객체:', summary);
console.log('📊 === 데이터 추출 디버깅 종료 ===');
// 채널별 성과 데이터 변환
console.log('🔍 원본 channelPerformance 데이터:', dashboardData.channelPerformance);
const channelColors = ['#F472B6', '#60A5FA', '#FB923C', '#A78BFA', '#34D399'];
const channelPerformance = dashboardData.channelPerformance.map((channel, index) => {
const totalParticipants = dashboardData.overallSummary.participants;
const percentage = totalParticipants > 0
? Math.round((channel.participants / totalParticipants) * 100)
: 0;
// 채널명 정리 - 안전한 방식으로 처리
let cleanChannelName = channel.channel;
// 백엔드에서 UTF-8로 전달되는 경우 그대로 사용
// URL 인코딩된 경우에만 디코딩 시도
if (cleanChannelName && cleanChannelName.includes('%')) {
try {
cleanChannelName = decodeURIComponent(cleanChannelName);
} catch (e) {
// 디코딩 실패 시 원본 사용
console.warn('⚠️ 채널명 디코딩 실패, 원본 사용:', channel.channel);
}
}
const result = {
channel: cleanChannelName || '알 수 없는 채널',
participants: channel.participants,
percentage,
color: channelColors[index % channelColors.length],
};
console.log('🔍 변환된 채널 데이터:', result);
return result;
});
console.log('🔍 최종 channelPerformance:', channelPerformance);
// 채널 데이터 유효성 확인
const hasChannelData = channelPerformance.length > 0 &&
channelPerformance.some(ch => ch.participants > 0);
// 시간대별 데이터 집계 (0시~23시, 날짜별 평균)
console.log('🔍 원본 timelineData.dataPoints:', timelineData.dataPoints);
// 0시~23시까지 24개 시간대 초기화 (합계와 카운트 추적)
const hourlyData = Array.from({ length: 24 }, (_, hour) => ({
hour,
totalParticipants: 0,
count: 0,
participants: 0, // 최종 평균값
}));
// 각 데이터 포인트를 시간대별로 집계
timelineData.dataPoints.forEach((point) => {
const date = new Date(point.timestamp);
const hour = date.getHours();
if (hour >= 0 && hour < 24) {
hourlyData[hour].totalParticipants += point.participants;
hourlyData[hour].count += 1;
}
});
// 시간대별 평균 계산
hourlyData.forEach((data) => {
data.participants = data.count > 0
? Math.round(data.totalParticipants / data.count)
: 0;
});
console.log('🔍 시간대별 집계 데이터 (평균):', hourlyData);
// 피크 시간 찾기 (hourlyData에서 최대 참여자 수를 가진 시간대)
const peakHour = hourlyData.reduce((max, current) =>
current.participants > max.participants ? current : max
, hourlyData[0]);
console.log('🔍 피크 시간 데이터:', peakHour);
// 시간대별 성과 데이터 (피크 시간 정보)
const timePerformance = {
peakTime: `${peakHour.hour}`,
peakParticipants: peakHour.participants,
avgPerHour: Math.round(
hourlyData.reduce((sum, data) => sum + data.participants, 0) / 24
),
};
// ROI 상세 데이터
console.log('💰 === ROI 상세 데이터 생성 시작 ===');
console.log('💰 overallInvestment 전체:', roiData.overallInvestment);
console.log('💰 breakdown 데이터:', roiData.overallInvestment.breakdown);
const roiDetail = {
totalCost: roiData.overallInvestment.total,
prizeCost: roiData.overallInvestment.prizeCost, // ✅ 백엔드 prizeCost 필드 사용
channelCost: roiData.overallInvestment.distribution,
otherCost: roiData.overallInvestment.contentCreation + roiData.overallInvestment.operation, // ✅ 그 외 비용
expectedRevenue: roiData.overallRevenue.total,
salesIncrease: roiData.overallRevenue.total, // ✅ 변경: total 사용
newCustomerLTV: roiData.overallRevenue.newCustomerRevenue, // ✅ 변경: newCustomerRevenue 사용
};
console.log('💰 최종 roiDetail 객체:', roiDetail);
console.log('💰 === ROI 상세 데이터 생성 종료 ===');
// 참여자 프로필 데이터 (임시로 Mock 데이터 사용 - API에 없음)
const participantProfile = {
age: [
{ label: '20대', percentage: 35 },
{ label: '30대', percentage: 40 },
{ label: '40대', percentage: 25 },
],
gender: [
{ label: '여성', percentage: 60 },
{ label: '남성', percentage: 40 },
],
};
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: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 6 }, px: { xs: 3, sm: 6, md: 10 } }}>
{/* Title with Real-time Indicator */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mb: { xs: 4, sm: 10 },
}}
>
<Typography variant="h5" sx={{ ...responsiveText.h3 }}>
📊 ()
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1, sm: 2 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 0.5, sm: 1 } }}>
<Box
sx={{
width: { xs: 6, sm: 8 },
height: { xs: 6, sm: 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>
<Tooltip title="데이터 새로고침">
<IconButton
onClick={handleRefresh}
disabled={refreshing}
size="small"
sx={{
color: colors.gray[700],
'&:hover': {
bgcolor: colors.gray[100],
color: colors.mint,
},
'&.Mui-disabled': {
color: colors.gray[300],
},
}}
>
<RefreshIcon
sx={{
fontSize: { xs: 20, sm: 24 },
animation: refreshing ? 'spin 1s linear infinite' : 'none',
'@keyframes spin': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' },
},
}}
/>
</IconButton>
</Tooltip>
</Box>
</Box>
{/* Summary KPI Cards */}
<Grid container spacing={{ xs: 2, sm: 6 }} sx={{ mb: { xs: 4, sm: 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: { xs: 3, sm: 6 }, px: { xs: 2, sm: 4 } }}>
<Typography
variant="body2"
sx={{
mb: { xs: 1, sm: 2 },
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: 500,
fontSize: { xs: '0.75rem', sm: '1rem' },
}}
>
</Typography>
<Typography variant="h3" sx={{ fontWeight: 700, color: 'white', fontSize: { xs: '1.5rem', sm: '2.5rem' } }}>
{summary.participants}
</Typography>
<Typography
variant="caption"
sx={{ color: 'rgba(255, 255, 255, 0.8)', fontWeight: 600, fontSize: { xs: '0.6875rem', sm: '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: { xs: 3, sm: 6 }, px: { xs: 2, sm: 4 } }}>
<Typography
variant="body2"
sx={{
mb: { xs: 1, sm: 2 },
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: 500,
fontSize: { xs: '0.75rem', sm: '1rem' },
}}
>
</Typography>
<Typography variant="h3" sx={{ fontWeight: 700, color: 'white', fontSize: { xs: '1.5rem', sm: '2.5rem' } }}>
{Math.floor(summary.totalCost / 10000)}
</Typography>
<Typography
variant="caption"
sx={{ color: 'rgba(255, 255, 255, 0.8)', fontWeight: 600, fontSize: { xs: '0.6875rem', sm: '0.875rem' } }}
>
+
</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: { xs: 3, sm: 6 }, px: { xs: 2, sm: 4 } }}>
<Typography
variant="body2"
sx={{
mb: { xs: 1, sm: 2 },
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: 500,
fontSize: { xs: '0.75rem', sm: '1rem' },
}}
>
</Typography>
<Typography variant="h3" sx={{ fontWeight: 700, color: 'white', fontSize: { xs: '1.5rem', sm: '2.5rem' } }}>
{Math.floor(summary.expectedRevenue / 10000)}
</Typography>
<Typography
variant="caption"
sx={{ color: 'rgba(255, 255, 255, 0.8)', fontWeight: 600, fontSize: { xs: '0.6875rem', sm: '0.875rem' } }}
>
+LTV
</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: { xs: 3, sm: 6 }, px: { xs: 2, sm: 4 } }}>
<Typography
variant="body2"
sx={{
mb: { xs: 1, sm: 2 },
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: 500,
fontSize: { xs: '0.75rem', sm: '1rem' },
}}
>
ROI
</Typography>
<Typography variant="h3" sx={{ fontWeight: 700, color: 'white', fontSize: { xs: '1.5rem', sm: '2.5rem' } }}>
{summary.roi}%
</Typography>
<Typography
variant="caption"
sx={{ color: 'rgba(255, 255, 255, 0.8)', fontWeight: 600, fontSize: { xs: '0.6875rem', sm: '0.875rem' } }}
>
{summary.targetRoi}%
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
{/* Charts Grid */}
<Grid container spacing={{ xs: 3, sm: 6 }} sx={{ mb: { xs: 4, sm: 10 } }}>
{/* Channel Performance */}
<Grid item xs={12} md={6}>
<Card elevation={0} sx={{ ...cardStyles.default, height: '100%' }}>
<CardContent sx={{ p: { xs: 3, sm: 6, md: 8 }, height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 2, sm: 3 }, mb: { xs: 3, sm: 6 } }}>
<Box
sx={{
width: { xs: 32, sm: 40 },
height: { xs: 32, sm: 40 },
borderRadius: '12px',
background: `linear-gradient(135deg, ${colors.pink} 0%, ${colors.pinkLight} 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<PieChartIcon sx={{ fontSize: { xs: 20, sm: 24 }, color: 'white' }} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 700, color: colors.gray[900], fontSize: { xs: '1rem', sm: '1.25rem' } }}>
</Typography>
</Box>
{/* Pie Chart */}
<Box
sx={{
width: '100%',
maxWidth: { xs: 200, sm: 300 },
mx: 'auto',
mb: { xs: 2, sm: 3 },
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{hasChannelData ? (
<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 = total > 0 ? ((value / total) * 100).toFixed(1) : '0';
return `${label}: ${value}명 (${percentage}%)`;
},
},
},
},
}}
/>
) : (
<Box sx={{ textAlign: 'center', py: { xs: 4, sm: 6 } }}>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>
.
</Typography>
<Typography variant="caption" color="text.disabled" sx={{ mt: 1, display: 'block', fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
.
</Typography>
</Box>
)}
</Box>
{/* Legend */}
{hasChannelData && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { xs: 0.75, sm: 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: { xs: 10, sm: 12 },
height: { xs: 10, sm: 12 },
borderRadius: '50%',
bgcolor: item.color,
}}
/>
<Typography variant="body2" sx={{ color: colors.gray[700], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
{item.channel}
</Typography>
</Box>
<Typography variant="body2" sx={{ fontWeight: 600, color: colors.gray[900], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
{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: 3, sm: 6, md: 8 }, height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 2, sm: 3 }, mb: { xs: 3, sm: 6 } }}>
<Box
sx={{
width: { xs: 32, sm: 40 },
height: { xs: 32, sm: 40 },
borderRadius: '12px',
background: `linear-gradient(135deg, ${colors.blue} 0%, ${colors.blueLight} 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<ShowChartIcon sx={{ fontSize: { xs: 20, sm: 24 }, color: 'white' }} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 700, color: colors.gray[900], fontSize: { xs: '1rem', sm: '1.25rem' } }}>
</Typography>
</Box>
{/* Line Chart */}
<Box
sx={{
width: '100%',
mb: { xs: 2, sm: 3 },
flex: 1,
display: 'flex',
alignItems: 'center',
minHeight: { xs: 150, sm: 200 },
}}
>
<Line
data={{
labels: hourlyData.map((item) => `${item.hour}`),
datasets: [
{
label: '참여자 수',
data: hourlyData.map((item) => item.participants),
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], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
: {timePerformance.peakTime} ({timePerformance.peakParticipants})
</Typography>
<Typography variant="body2" sx={{ color: colors.gray[600], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
: {timePerformance.avgPerHour}
</Typography>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
{/* ROI Detail & Participant Profile */}
<Grid container spacing={{ xs: 3, sm: 6 }}>
{/* ROI Detail */}
<Grid item xs={12} md={6}>
<Card elevation={0} sx={{ ...cardStyles.default, height: '100%' }}>
<CardContent sx={{ p: { xs: 3, sm: 6, md: 8 }, height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 2, sm: 3 }, mb: { xs: 3, sm: 6 } }}>
<Box
sx={{
width: { xs: 32, sm: 40 },
height: { xs: 32, sm: 40 },
borderRadius: '12px',
background: `linear-gradient(135deg, ${colors.orange} 0%, ${colors.orangeLight} 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Payments sx={{ fontSize: { xs: 20, sm: 24 }, color: 'white' }} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 700, color: colors.gray[900], fontSize: { xs: '1rem', sm: '1.25rem' } }}>
ROI
</Typography>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { xs: 2, sm: 3 } }}>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: { xs: 1, sm: 1.5 }, color: colors.gray[900], fontSize: { xs: '0.9375rem', sm: '1rem' } }}>
: {Math.floor(roiDetail.totalCost / 10000)}
</Typography>
<Box sx={{ pl: { xs: 1.5, sm: 2 }, display: 'flex', flexDirection: 'column', gap: { xs: 0.75, sm: 1 } }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ color: colors.gray[600], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: colors.gray[900], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
{Math.floor(roiDetail.prizeCost / 10000)}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ color: colors.gray[600], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: colors.gray[900], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
{Math.floor(roiDetail.channelCost / 10000)}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ color: colors.gray[600], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: colors.gray[900], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
{Math.floor(roiDetail.otherCost / 10000)}
</Typography>
</Box>
</Box>
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: { xs: 1, sm: 1.5 }, color: colors.gray[900], fontSize: { xs: '0.9375rem', sm: '1rem' } }}>
: {Math.floor(roiDetail.expectedRevenue / 10000)}
</Typography>
<Box sx={{ pl: { xs: 1.5, sm: 2 }, display: 'flex', flexDirection: 'column', gap: { xs: 0.75, sm: 1 } }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ color: colors.gray[600], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: colors.mint, fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
{Math.floor(roiDetail.salesIncrease / 10000)}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ color: colors.gray[600], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
LTV
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: colors.mint, fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
{Math.floor(roiDetail.newCustomerLTV / 10000)}
</Typography>
</Box>
</Box>
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: { xs: 1, sm: 1.5 }, color: colors.mint, fontSize: { xs: '0.9375rem', sm: '1rem' } }}>
</Typography>
<Box
sx={{
p: { xs: 2, sm: 2.5 },
bgcolor: colors.gray[100],
borderRadius: 2,
textAlign: 'center',
}}
>
<Typography variant="body2" sx={{ mb: 0.5, color: colors.gray[700], fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
( - ) ÷ × 100
</Typography>
<Typography variant="body2" sx={{ mb: { xs: 1, sm: 1.5 }, color: colors.gray[600], fontSize: { xs: '0.6875rem', sm: '0.875rem' } }}>
({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, fontSize: { xs: '1.25rem', sm: '1.5rem' } }}>
= {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: 3, sm: 6, md: 8 }, height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 2, sm: 3 }, mb: { xs: 3, sm: 6 } }}>
<Box
sx={{
width: { xs: 32, sm: 40 },
height: { xs: 32, sm: 40 },
borderRadius: '12px',
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.purpleLight} 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<People sx={{ fontSize: { xs: 20, sm: 24 }, color: 'white' }} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 700, color: colors.gray[900], fontSize: { xs: '1rem', sm: '1.25rem' } }}>
</Typography>
</Box>
{/* Age Distribution */}
<Box sx={{ mb: { xs: 3, sm: 4 } }}>
<Typography variant="body1" sx={{ fontWeight: 600, mb: { xs: 1.5, sm: 2 }, color: colors.gray[900], fontSize: { xs: '0.9375rem', sm: '1rem' } }}>
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { xs: 1.25, sm: 1.5 } }}>
{participantProfile.age.map((item) => (
<Box key={item.label}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1, sm: 1.5 } }}>
<Typography variant="body2" sx={{ minWidth: { xs: 45, sm: 60 }, color: colors.gray[700], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
{item.label}
</Typography>
<Box
sx={{
flex: 1,
height: { xs: 24, sm: 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: { xs: 1, sm: 1.5 },
}}
>
<Typography
variant="caption"
sx={{ color: 'white', fontWeight: 600, fontSize: { xs: 10, sm: 12 } }}
>
{item.percentage}%
</Typography>
</Box>
</Box>
</Box>
</Box>
))}
</Box>
</Box>
{/* Gender Distribution */}
<Box>
<Typography variant="body1" sx={{ fontWeight: 600, mb: { xs: 1.5, sm: 2 }, color: colors.gray[900], fontSize: { xs: '0.9375rem', sm: '1rem' } }}>
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { xs: 1.25, sm: 1.5 } }}>
{participantProfile.gender.map((item) => (
<Box key={item.label}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1, sm: 1.5 } }}>
<Typography variant="body2" sx={{ minWidth: { xs: 45, sm: 60 }, color: colors.gray[700], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
{item.label}
</Typography>
<Box
sx={{
flex: 1,
height: { xs: 24, sm: 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: { xs: 1, sm: 1.5 },
}}
>
<Typography
variant="caption"
sx={{ color: 'white', fontWeight: 600, fontSize: { xs: 10, sm: 12 } }}
>
{item.percentage}%
</Typography>
</Box>
</Box>
</Box>
</Box>
))}
</Box>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</Container>
</Box>
</>
);
}