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

612 lines
24 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 } from 'next/navigation';
import { Box, Container, Typography, Grid, Card, CardContent, Button, Fab, CircularProgress, Alert } from '@mui/material';
import {
Add,
Celebration,
Group,
TrendingUp,
Analytics,
PersonAdd,
Edit,
CheckCircle,
} from '@mui/icons-material';
import Header from '@/shared/ui/Header';
import {
getGradientButtonStyle,
responsiveText,
cardStyles,
colors,
} from '@/shared/lib/button-styles';
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분 전' },
{ icon: Edit, text: '설 맞이 할인 이벤트 내용을 수정했습니다', time: '1시간 전' },
{ icon: CheckCircle, text: '고객 만족도 조사가 종료되었습니다', time: '3시간 전' },
];
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);
// 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');
};
const handleViewAnalytics = () => {
router.push('/analytics');
};
const handleEventClick = (eventId: string) => {
router.push(`/events/${eventId}`);
};
return (
<>
<Header title="대시보드" showBack={false} showMenu={false} showProfile={true} />
<Box
sx={{
pt: { xs: 7, sm: 8 },
pb: { xs: 4, sm: 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 } }}>
{/* Welcome Section */}
<Box sx={{ mb: { xs: 5, sm: 10 } }}>
<Typography
variant="h3"
sx={{
...responsiveText.h2,
mb: { xs: 2, sm: 4 },
}}
>
, {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: 1.5, sm: 6 }} sx={{ mb: { xs: 5, sm: 10 } }}>
<Grid item xs={4} sm={4}>
<Card
elevation={0}
sx={{
...cardStyles.default,
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.purpleLight} 100%)`,
borderColor: 'transparent',
}}
>
<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: 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: 0.75, sm: 3 },
}}
>
<Celebration sx={{
fontSize: { xs: 18, sm: 32 },
color: colors.gray[900],
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
}} />
</Box>
<Typography
variant="body2"
sx={{
mb: 0.5,
color: colors.gray[700],
fontWeight: 500,
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
lineHeight: 1.2,
}}
>
</Typography>
<Typography
variant="h3"
sx={{
fontWeight: 700,
color: colors.gray[900],
fontSize: { xs: '1.375rem', sm: '2.25rem' },
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}
>
{activeEventsCount}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={4} sm={4}>
<Card
elevation={0}
sx={{
...cardStyles.default,
background: `linear-gradient(135deg, ${colors.mint} 0%, ${colors.mintLight} 100%)`,
borderColor: 'transparent',
}}
>
<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: 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: 0.75, sm: 3 },
}}
>
<Group sx={{
fontSize: { xs: 18, sm: 32 },
color: colors.gray[900],
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
}} />
</Box>
<Typography
variant="body2"
sx={{
mb: 0.5,
color: colors.gray[700],
fontWeight: 500,
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
lineHeight: 1.2,
}}
>
</Typography>
<Typography
variant="h3"
sx={{
fontWeight: 700,
color: colors.gray[900],
fontSize: { xs: '1.375rem', sm: '2.25rem' },
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}
>
{totalParticipants.toLocaleString()}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={4} sm={4}>
<Card
elevation={0}
sx={{
...cardStyles.default,
background: `linear-gradient(135deg, ${colors.blue} 0%, ${colors.blueLight} 100%)`,
borderColor: 'transparent',
}}
>
<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: 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: 0.75, sm: 3 },
}}
>
<TrendingUp sx={{
fontSize: { xs: 18, sm: 32 },
color: colors.gray[900],
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
}} />
</Box>
<Typography
variant="body2"
sx={{
mb: 0.5,
color: colors.gray[700],
fontWeight: 500,
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
lineHeight: 1.2,
}}
>
ROI
</Typography>
<Typography
variant="h3"
sx={{
fontWeight: 700,
color: colors.gray[900],
fontSize: { xs: '1.375rem', sm: '2.25rem' },
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}
>
{avgROI}%
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
{/* Quick Actions */}
<Box sx={{ mb: { xs: 5, sm: 10 } }}>
<Typography variant="h5" sx={{ ...responsiveText.h3, mb: { xs: 3, sm: 6 } }}>
</Typography>
<Grid container spacing={{ xs: 3, sm: 6 }}>
<Grid item xs={6} sm={6}>
<Card
elevation={0}
sx={{
...cardStyles.clickable,
}}
onClick={handleCreateEvent}
>
<CardContent sx={{ textAlign: 'center', pt: { xs: 2, sm: 6 }, pb: { xs: 2, sm: 6 } }}>
<Box
sx={{
width: { xs: 56, sm: 72 },
height: { xs: 56, sm: 72 },
borderRadius: { xs: '16px', sm: '20px' },
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.blue} 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto',
mb: { xs: 2, sm: 3 },
boxShadow: '0 4px 14px 0 rgba(167, 139, 250, 0.39)',
}}
>
<Add sx={{ fontSize: { xs: 28, sm: 36 }, color: 'white' }} />
</Box>
<Typography variant="body1" sx={{ fontWeight: 600, color: colors.gray[900], fontSize: { xs: '0.875rem', sm: '1.125rem' } }}>
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} sm={6}>
<Card
elevation={0}
sx={{
...cardStyles.clickable,
}}
onClick={handleViewAnalytics}
>
<CardContent sx={{ textAlign: 'center', pt: { xs: 2, sm: 6 }, pb: { xs: 2, sm: 6 } }}>
<Box
sx={{
width: { xs: 56, sm: 72 },
height: { xs: 56, sm: 72 },
borderRadius: { xs: '16px', sm: '20px' },
background: `linear-gradient(135deg, ${colors.blue} 0%, ${colors.mint} 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto',
mb: { xs: 2, sm: 3 },
boxShadow: '0 4px 14px 0 rgba(96, 165, 250, 0.39)',
}}
>
<Analytics sx={{ fontSize: { xs: 28, sm: 36 }, color: 'white' }} />
</Box>
<Typography variant="body1" sx={{ fontWeight: 600, color: colors.gray[900], fontSize: { xs: '0.875rem', sm: '1.125rem' } }}>
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
{/* Active Events */}
<Box sx={{ mb: { xs: 5, sm: 10 } }}>
<Box
sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: { xs: 3, sm: 6 } }}
>
<Typography variant="h5" sx={{ ...responsiveText.h3 }}>
</Typography>
<Button
size="small"
endIcon={<span className="material-icons" style={{ fontSize: '18px' }}>chevron_right</span>}
onClick={() => router.push('/events')}
sx={{
color: colors.purple,
fontWeight: 600,
fontSize: { xs: '0.8125rem', sm: '0.875rem' },
'&:hover': { bgcolor: 'rgba(167, 139, 250, 0.08)' },
}}
>
</Button>
</Box>
{!loading && eventPerformances.length === 0 ? (
<Card
elevation={0}
sx={{
...cardStyles.default,
}}
>
<CardContent sx={{ textAlign: 'center', py: 10 }}>
<Box sx={{ color: colors.gray[300], mb: 3 }}>
<span className="material-icons" style={{ fontSize: 72 }}>
event_busy
</span>
</Box>
<Typography variant="h6" sx={{ mb: 2, color: colors.gray[700] }}>
</Typography>
<Typography variant="body2" sx={{ mb: 4, color: colors.gray[500] }}>
</Typography>
<Button
variant="contained"
startIcon={<Add />}
onClick={handleCreateEvent}
sx={{
py: { xs: 1.5, sm: 1.75 },
px: { xs: 3, sm: 4 },
fontSize: { xs: 15, sm: 16 },
...getGradientButtonStyle('primary'),
}}
>
</Button>
</CardContent>
</Card>
) : !loading && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { xs: 3, sm: 6 } }}>
{eventPerformances.slice(0, 2).map((event) => (
<Card
key={event.eventId}
elevation={0}
sx={{
...cardStyles.clickable,
}}
onClick={() => handleEventClick(event.eventId)}
>
<CardContent sx={{ p: { xs: 4, sm: 6, md: 8 } }}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'start',
mb: { xs: 3, sm: 6 },
gap: 2,
}}
>
<Typography variant="h6" sx={{ fontWeight: 700, color: colors.gray[900], fontSize: { xs: '1rem', sm: '1.25rem' } }}>
{event.eventTitle}
</Typography>
<Box
sx={{
px: { xs: 2, sm: 2.5 },
py: 0.75,
bgcolor: colors.mint,
color: 'white',
borderRadius: 2,
fontSize: { xs: '0.75rem', sm: '0.875rem' },
fontWeight: 600,
flexShrink: 0,
}}
>
{event.status}
</Box>
</Box>
<Box sx={{ display: 'flex', gap: { xs: 6, sm: 12 }, mt: { xs: 3, sm: 6 } }}>
<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.participants.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"
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"
sx={{ mb: 0.5, color: colors.gray[600], fontWeight: 500, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
>
ROI
</Typography>
<Typography variant="h5" sx={{ fontWeight: 700, color: colors.mint, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
{Math.round(event.roi * 100) / 100}%
</Typography>
</Box>
</Box>
</CardContent>
</Card>
))}
</Box>
)}
</Box>
{/* Recent Activity */}
<Box sx={{ mb: { xs: 5, sm: 10 } }}>
<Typography variant="h5" sx={{ ...responsiveText.h3, mb: { xs: 3, sm: 6 } }}>
</Typography>
<Card
elevation={0}
sx={{
...cardStyles.default,
}}
>
<CardContent sx={{ p: { xs: 4, sm: 6, md: 8 } }}>
{mockActivities.map((activity, index) => (
<Box
key={index}
sx={{
display: 'flex',
gap: { xs: 2, sm: 4 },
pt: index > 0 ? { xs: 3, sm: 6 } : 0,
mt: index > 0 ? { xs: 3, sm: 6 } : 0,
borderTop: index > 0 ? 1 : 0,
borderColor: colors.gray[200],
}}
>
<Box
sx={{
width: { xs: 40, sm: 48 },
height: { xs: 40, sm: 48 },
borderRadius: { xs: '12px', sm: '14px' },
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.purpleLight} 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}
>
<activity.icon sx={{ fontSize: { xs: 20, sm: 24 }, color: 'white' }} />
</Box>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography
variant="body1"
sx={{ fontWeight: 600, color: colors.gray[900], mb: 0.5, fontSize: { xs: '0.8125rem', sm: '1rem' } }}
>
{activity.text}
</Typography>
<Typography variant="body2" sx={{ color: colors.gray[500], fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
{activity.time}
</Typography>
</Box>
</Box>
))}
</CardContent>
</Card>
</Box>
</Container>
{/* Floating Action Button */}
<Fab
sx={{
position: 'fixed',
bottom: { xs: 72, sm: 90 },
right: { xs: 16, sm: 32 },
width: { xs: 56, sm: 72 },
height: { xs: 56, sm: 72 },
...getGradientButtonStyle('primary'),
boxShadow:
'0 10px 25px -5px rgba(167, 139, 250, 0.5), 0 8px 10px -6px rgba(167, 139, 250, 0.5)',
'&:hover': {
boxShadow:
'0 20px 35px -5px rgba(167, 139, 250, 0.6), 0 12px 15px -6px rgba(167, 139, 250, 0.6)',
},
}}
onClick={handleCreateEvent}
>
<Add sx={{ color: 'white', fontSize: { xs: 24, sm: 32 } }} />
</Fab>
</Box>
</>
);
}