mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2025-12-06 10:56:23 +00:00
Merge branch 'develop' of https://github.com/ktds-dg0501/kt-event-marketing-fe into develop
This commit is contained in:
commit
06da17ac36
@ -43,6 +43,7 @@ import type {
|
||||
UserAnalyticsDashboardResponse,
|
||||
UserTimelineAnalyticsResponse,
|
||||
UserRoiAnalyticsResponse,
|
||||
UserChannelAnalyticsResponse,
|
||||
} from '@/entities/analytics';
|
||||
|
||||
// Chart.js 등록
|
||||
@ -64,6 +65,7 @@ export default function AnalyticsPage() {
|
||||
const [dashboardData, setDashboardData] = useState<UserAnalyticsDashboardResponse | null>(null);
|
||||
const [timelineData, setTimelineData] = useState<UserTimelineAnalyticsResponse | null>(null);
|
||||
const [roiData, setRoiData] = useState<UserRoiAnalyticsResponse | null>(null);
|
||||
const [channelData, setChannelData] = useState<UserChannelAnalyticsResponse | null>(null);
|
||||
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
|
||||
const [updateText, setUpdateText] = useState('방금 전');
|
||||
|
||||
@ -76,31 +78,35 @@ export default function AnalyticsPage() {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
// 로그인하지 않은 경우 테스트용 userId 사용 (로컬 테스트용)
|
||||
const userId = user?.userId ? String(user.userId) : 'store_001';
|
||||
console.log('📊 Analytics 데이터 로드 시작:', { userId, isLoggedIn: !!user, refresh: forceRefresh });
|
||||
// 항상 store_001로 고정하여 Analytics 조회
|
||||
const userId = 'store_001';
|
||||
|
||||
// 병렬로 모든 Analytics API 호출
|
||||
const [dashboard, timeline, roi] = await Promise.all([
|
||||
const [dashboard, timeline, roi, channels] = await Promise.all([
|
||||
analyticsApi.getUserAnalytics(userId, { refresh: forceRefresh }),
|
||||
analyticsApi.getUserTimelineAnalytics(userId, { interval: 'hourly', refresh: forceRefresh }),
|
||||
analyticsApi.getUserRoiAnalytics(userId, { includeProjection: true, refresh: forceRefresh }),
|
||||
analyticsApi.getUserChannelAnalytics(userId, { sortBy: 'participants', order: 'desc', refresh: forceRefresh }),
|
||||
]);
|
||||
|
||||
console.log('✅ Dashboard 데이터:', dashboard);
|
||||
console.log('✅ Timeline 데이터:', timeline);
|
||||
console.log('✅ ROI 데이터:', roi);
|
||||
console.log('📊 [Analytics] Timeline API response:', {
|
||||
totalEvents: timeline.totalEvents,
|
||||
interval: timeline.interval,
|
||||
dataPointsCount: timeline.dataPoints.length,
|
||||
dataSource: timeline.dataSource,
|
||||
period: timeline.period,
|
||||
});
|
||||
|
||||
setDashboardData(dashboard);
|
||||
setTimelineData(timeline);
|
||||
setRoiData(roi);
|
||||
setChannelData(channels);
|
||||
setLastUpdate(new Date());
|
||||
} catch (error: any) {
|
||||
console.error('❌ Analytics 데이터 로드 실패:', error);
|
||||
|
||||
// 404 또는 400 에러는 아직 Analytics 데이터가 없는 경우
|
||||
if (error.response?.status === 404 || error.response?.status === 400) {
|
||||
console.log('ℹ️ Analytics 데이터가 아직 생성되지 않았습니다.');
|
||||
// 에러 상태를 설정하지 않고 빈 데이터로 표시
|
||||
} else {
|
||||
// 다른 에러는 에러로 처리
|
||||
@ -168,7 +174,7 @@ export default function AnalyticsPage() {
|
||||
}
|
||||
|
||||
// 데이터 없음 표시
|
||||
if (!dashboardData || !timelineData || !roiData) {
|
||||
if (!dashboardData || !timelineData || !roiData || !channelData) {
|
||||
return (
|
||||
<>
|
||||
<Header title="성과 분석" showBack={true} showMenu={false} showProfile={true} />
|
||||
@ -192,14 +198,8 @@ export default function AnalyticsPage() {
|
||||
}
|
||||
|
||||
// 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,
|
||||
participants: 1234, // 고정값으로 설정
|
||||
participantsDelta: dashboardData.overallSummary.participantsDelta,
|
||||
totalCost: roiData.overallInvestment.total,
|
||||
expectedRevenue: roiData.overallRevenue.total,
|
||||
@ -207,21 +207,28 @@ export default function AnalyticsPage() {
|
||||
targetRoi: dashboardData.overallSummary.targetRoi,
|
||||
};
|
||||
|
||||
console.log('📊 최종 summary 객체:', summary);
|
||||
console.log('📊 === 데이터 추출 디버깅 종료 ===');
|
||||
|
||||
// 채널별 성과 데이터 변환
|
||||
console.log('🔍 원본 channelPerformance 데이터:', dashboardData.channelPerformance);
|
||||
|
||||
// 채널별 성과 데이터 변환 (Channels API 상세 데이터 활용)
|
||||
const channelColors = ['#F472B6', '#60A5FA', '#FB923C', '#A78BFA', '#34D399'];
|
||||
const channelPerformance = dashboardData.channelPerformance.map((channel, index) => {
|
||||
const channelPerformance = channelData.channels.map((channel, index) => {
|
||||
// 참여자 수를 기준으로 비율 계산, 참여자가 없으면 노출 수(impressions) 기준으로 계산
|
||||
const totalParticipants = dashboardData.overallSummary.participants;
|
||||
const percentage = totalParticipants > 0
|
||||
? Math.round((channel.participants / totalParticipants) * 100)
|
||||
: 0;
|
||||
const totalImpressions = channelData.channels.reduce((sum, ch) => sum + ch.metrics.impressions, 0);
|
||||
|
||||
let percentage = 0;
|
||||
let displayValue = 0;
|
||||
|
||||
if (totalParticipants > 0) {
|
||||
// 참여자가 있으면 참여자 수 기준
|
||||
percentage = Math.round((channel.metrics.participants / totalParticipants) * 100);
|
||||
displayValue = channel.metrics.participants;
|
||||
} else if (totalImpressions > 0) {
|
||||
// 참여자가 없으면 노출 수 기준
|
||||
percentage = Math.round((channel.metrics.impressions / totalImpressions) * 100);
|
||||
displayValue = channel.metrics.impressions;
|
||||
}
|
||||
|
||||
// 채널명 정리 - 안전한 방식으로 처리
|
||||
let cleanChannelName = channel.channel;
|
||||
let cleanChannelName = channel.channelName;
|
||||
|
||||
// 백엔드에서 UTF-8로 전달되는 경우 그대로 사용
|
||||
// URL 인코딩된 경우에만 디코딩 시도
|
||||
@ -230,36 +237,46 @@ export default function AnalyticsPage() {
|
||||
cleanChannelName = decodeURIComponent(cleanChannelName);
|
||||
} catch (e) {
|
||||
// 디코딩 실패 시 원본 사용
|
||||
console.warn('⚠️ 채널명 디코딩 실패, 원본 사용:', channel.channel);
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
return {
|
||||
channel: cleanChannelName || '알 수 없는 채널',
|
||||
participants: channel.participants,
|
||||
channelType: channel.channelType,
|
||||
participants: channel.metrics.participants,
|
||||
views: channel.metrics.views,
|
||||
clicks: channel.metrics.clicks,
|
||||
impressions: channel.metrics.impressions,
|
||||
conversions: channel.metrics.conversions,
|
||||
engagementRate: channel.performance.engagementRate,
|
||||
conversionRate: channel.performance.conversionRate,
|
||||
clickThroughRate: channel.performance.clickThroughRate,
|
||||
roi: channel.costs.roi,
|
||||
costPerAcquisition: channel.costs.costPerAcquisition,
|
||||
percentage,
|
||||
displayValue, // 차트에 표시할 값 (participants 또는 impressions)
|
||||
color: channelColors[index % channelColors.length],
|
||||
};
|
||||
|
||||
console.log('🔍 변환된 채널 데이터:', result);
|
||||
return result;
|
||||
});
|
||||
|
||||
console.log('🔍 최종 channelPerformance:', channelPerformance);
|
||||
|
||||
// 채널 데이터 유효성 확인
|
||||
// 채널 데이터 유효성 확인 (participants 또는 impressions가 있으면 표시)
|
||||
const hasChannelData = channelPerformance.length > 0 &&
|
||||
channelPerformance.some(ch => ch.participants > 0);
|
||||
channelPerformance.some(ch => ch.participants > 0 || ch.impressions > 0);
|
||||
|
||||
// 시간대별 데이터 집계 (0시~23시, 날짜별 평균)
|
||||
console.log('🔍 원본 timelineData.dataPoints:', timelineData.dataPoints);
|
||||
// 시간대별 데이터 집계 (0시~23시, 날짜별 평균) - API 추가 지표 활용
|
||||
|
||||
// 0시~23시까지 24개 시간대 초기화 (합계와 카운트 추적)
|
||||
const hourlyData = Array.from({ length: 24 }, (_, hour) => ({
|
||||
hour,
|
||||
totalParticipants: 0,
|
||||
totalViews: 0,
|
||||
totalEngagement: 0,
|
||||
totalConversions: 0,
|
||||
count: 0,
|
||||
participants: 0, // 최종 평균값
|
||||
views: 0,
|
||||
engagement: 0,
|
||||
conversions: 0,
|
||||
}));
|
||||
|
||||
// 각 데이터 포인트를 시간대별로 집계
|
||||
@ -268,40 +285,57 @@ export default function AnalyticsPage() {
|
||||
const hour = date.getHours();
|
||||
if (hour >= 0 && hour < 24) {
|
||||
hourlyData[hour].totalParticipants += point.participants;
|
||||
hourlyData[hour].totalViews += point.views;
|
||||
hourlyData[hour].totalEngagement += point.engagement;
|
||||
hourlyData[hour].totalConversions += point.conversions;
|
||||
hourlyData[hour].count += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// 시간대별 평균 계산
|
||||
hourlyData.forEach((data) => {
|
||||
data.participants = data.count > 0
|
||||
? Math.round(data.totalParticipants / data.count)
|
||||
: 0;
|
||||
if (data.count > 0) {
|
||||
data.participants = Math.round(data.totalParticipants / data.count);
|
||||
data.views = Math.round(data.totalViews / data.count);
|
||||
data.engagement = Math.round(data.totalEngagement / data.count);
|
||||
data.conversions = Math.round(data.totalConversions / data.count);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🔍 시간대별 집계 데이터 (평균):', hourlyData);
|
||||
// 시간대별 데이터 유효성 확인
|
||||
const hasTimelineData = hourlyData.some(h => h.participants > 0 || h.views > 0 || h.engagement > 0);
|
||||
|
||||
console.log('📊 [Analytics] Hourly data aggregation:', {
|
||||
totalDataPoints: timelineData.dataPoints.length,
|
||||
hasData: hasTimelineData,
|
||||
sampleHours: hourlyData.slice(8, 12).map(h => ({
|
||||
hour: h.hour,
|
||||
participants: h.participants,
|
||||
views: h.views,
|
||||
engagement: h.engagement,
|
||||
})),
|
||||
});
|
||||
|
||||
// 피크 시간 찾기 (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,
|
||||
peakViews: peakHour.views,
|
||||
peakEngagement: peakHour.engagement,
|
||||
avgPerHour: Math.round(
|
||||
hourlyData.reduce((sum, data) => sum + data.participants, 0) / 24
|
||||
),
|
||||
avgViewsPerHour: Math.round(
|
||||
hourlyData.reduce((sum, data) => sum + data.views, 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 필드 사용
|
||||
@ -312,9 +346,6 @@ export default function AnalyticsPage() {
|
||||
newCustomerLTV: roiData.overallRevenue.newCustomerRevenue, // ✅ 변경: newCustomerRevenue 사용
|
||||
};
|
||||
|
||||
console.log('💰 최종 roiDetail 객체:', roiDetail);
|
||||
console.log('💰 === ROI 상세 데이터 생성 종료 ===');
|
||||
|
||||
// 참여자 프로필 데이터 (임시로 Mock 데이터 사용 - API에 없음)
|
||||
const participantProfile = {
|
||||
age: [
|
||||
@ -582,7 +613,7 @@ export default function AnalyticsPage() {
|
||||
labels: channelPerformance.map((item) => item.channel),
|
||||
datasets: [
|
||||
{
|
||||
data: channelPerformance.map((item) => item.participants),
|
||||
data: channelPerformance.map((item) => item.displayValue),
|
||||
backgroundColor: channelPerformance.map((item) => item.color),
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
@ -603,7 +634,9 @@ export default function AnalyticsPage() {
|
||||
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}%)`;
|
||||
const hasParticipants = channelPerformance.some(ch => ch.participants > 0);
|
||||
const unit = hasParticipants ? '명' : '노출';
|
||||
return `${label}: ${value.toLocaleString()}${unit} (${percentage}%)`;
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -625,37 +658,42 @@ export default function AnalyticsPage() {
|
||||
{/* 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}
|
||||
{channelPerformance.map((item) => {
|
||||
const hasParticipants = channelPerformance.some(ch => ch.participants > 0);
|
||||
const unit = hasParticipants ? '명' : '노출';
|
||||
|
||||
return (
|
||||
<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.displayValue.toLocaleString()}{unit})
|
||||
</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}>
|
||||
{/* 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 } }}>
|
||||
@ -677,7 +715,6 @@ export default function AnalyticsPage() {
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Line Chart */}
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
@ -685,9 +722,28 @@ export default function AnalyticsPage() {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: { xs: 150, sm: 200 },
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{!hasTimelineData && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
textAlign: 'center',
|
||||
color: colors.gray[400],
|
||||
}}
|
||||
>
|
||||
<ShowChartIcon sx={{ fontSize: 48, mb: 2, opacity: 0.3 }} />
|
||||
<Typography variant="body2" sx={{ color: colors.gray[500] }}>
|
||||
시간대별 데이터가 수집되면 차트가 표시됩니다.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Line
|
||||
data={{
|
||||
labels: hourlyData.map((item) => `${item.hour}시`),
|
||||
@ -695,32 +751,76 @@ export default function AnalyticsPage() {
|
||||
{
|
||||
label: '참여자 수',
|
||||
data: hourlyData.map((item) => item.participants),
|
||||
borderColor: colors.blue,
|
||||
backgroundColor: `${colors.blue}33`,
|
||||
borderColor: hasTimelineData ? colors.blue : `${colors.blue}40`,
|
||||
backgroundColor: hasTimelineData ? `${colors.blue}33` : `${colors.blue}10`,
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointBackgroundColor: colors.blue,
|
||||
pointBackgroundColor: hasTimelineData ? colors.blue : `${colors.blue}40`,
|
||||
pointBorderColor: '#fff',
|
||||
pointBorderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
pointRadius: 3,
|
||||
pointHoverRadius: 5,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: '조회 수',
|
||||
data: hourlyData.map((item) => item.views),
|
||||
borderColor: hasTimelineData ? colors.mint : `${colors.mint}40`,
|
||||
backgroundColor: hasTimelineData ? `${colors.mint}20` : `${colors.mint}10`,
|
||||
fill: false,
|
||||
tension: 0.4,
|
||||
pointBackgroundColor: hasTimelineData ? colors.mint : `${colors.mint}40`,
|
||||
pointBorderColor: '#fff',
|
||||
pointBorderWidth: 2,
|
||||
pointRadius: 3,
|
||||
pointHoverRadius: 5,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: '참여 활동',
|
||||
data: hourlyData.map((item) => item.engagement),
|
||||
borderColor: hasTimelineData ? colors.orange : `${colors.orange}40`,
|
||||
backgroundColor: hasTimelineData ? `${colors.orange}20` : `${colors.orange}10`,
|
||||
fill: false,
|
||||
tension: 0.4,
|
||||
pointBackgroundColor: hasTimelineData ? colors.orange : `${colors.orange}40`,
|
||||
pointBorderColor: '#fff',
|
||||
pointBorderWidth: 2,
|
||||
pointRadius: 3,
|
||||
pointHoverRadius: 5,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
display: true,
|
||||
position: 'top',
|
||||
labels: {
|
||||
boxWidth: 12,
|
||||
boxHeight: 12,
|
||||
padding: 10,
|
||||
font: {
|
||||
size: 11,
|
||||
},
|
||||
color: colors.gray[700],
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: colors.gray[900],
|
||||
padding: 12,
|
||||
displayColors: false,
|
||||
displayColors: true,
|
||||
callbacks: {
|
||||
label: function (context) {
|
||||
return `${context.parsed.y}명`;
|
||||
const label = context.dataset.label || '';
|
||||
return `${label}: ${context.parsed.y}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -730,6 +830,9 @@ export default function AnalyticsPage() {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
color: colors.gray[600],
|
||||
font: {
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
color: colors.gray[200],
|
||||
@ -738,6 +841,9 @@ export default function AnalyticsPage() {
|
||||
x: {
|
||||
ticks: {
|
||||
color: colors.gray[600],
|
||||
font: {
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
@ -748,18 +854,17 @@ export default function AnalyticsPage() {
|
||||
/>
|
||||
</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}명)
|
||||
피크 시간: {timePerformance.peakTime} (참여 {timePerformance.peakParticipants}명, 조회 {timePerformance.peakViews})
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: colors.gray[600], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
|
||||
평균 시간당: {timePerformance.avgPerHour}명
|
||||
평균 시간당: 참여 {timePerformance.avgPerHour}명 / 조회 {timePerformance.avgViewsPerHour}
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
|
||||
{/* ROI Detail & Participant Profile */}
|
||||
|
||||
@ -156,12 +156,12 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', bgcolor: colors.gray[50], pt: { xs: 7, sm: 8 }, pb: { xs: 4, sm: 10 } }}>
|
||||
<Container maxWidth="lg" sx={{ pt: 8, pb: 6, px: { xs: 2, sm: 8, md: 10 } }}>
|
||||
<Box sx={{ minHeight: '100vh', bgcolor: colors.gray[50], pt: { xs: 5, sm: 8 }, pb: { xs: 3, sm: 10 } }}>
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 3, sm: 6 }, px: { xs: 1.5, sm: 8, md: 10 } }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: { xs: 3, sm: 8 } }}>
|
||||
<IconButton onClick={onBack}>
|
||||
<ArrowBack />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, mb: { xs: 3, sm: 8 } }}>
|
||||
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
||||
<ArrowBack sx={{ fontSize: 20 }} />
|
||||
</IconButton>
|
||||
<Typography variant="h5" sx={{ ...responsiveText.h3, fontWeight: 700 }}>
|
||||
최종 승인
|
||||
@ -357,7 +357,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
||||
</Typography>
|
||||
|
||||
<Card elevation={0} sx={{ ...cardStyles.default, mb: 4 }}>
|
||||
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ...responsiveText.body2, fontSize: '0.875rem' }}>
|
||||
@ -375,7 +375,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
||||
</Card>
|
||||
|
||||
<Card elevation={0} sx={{ ...cardStyles.default, mb: 4 }}>
|
||||
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ...responsiveText.body2, fontSize: '0.875rem' }}>
|
||||
@ -393,7 +393,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
||||
</Card>
|
||||
|
||||
<Card elevation={0} sx={{ ...cardStyles.default, mb: { xs: 4, sm: 10 } }}>
|
||||
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ...responsiveText.body2, fontSize: '0.875rem' }}>
|
||||
@ -413,7 +413,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
||||
</Typography>
|
||||
|
||||
<Card elevation={0} sx={{ ...cardStyles.default, mb: { xs: 4, sm: 10 } }}>
|
||||
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 4 }}>
|
||||
{getChannelNames(eventData.channels).map((channel) => (
|
||||
<Chip
|
||||
@ -446,7 +446,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
||||
|
||||
{/* Terms Agreement */}
|
||||
<Card elevation={0} sx={{ ...cardStyles.default, bgcolor: colors.gray[50], mb: { xs: 4, sm: 10 } }}>
|
||||
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
@ -481,18 +481,18 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
||||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { xs: 2, sm: 4 } }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
size="large"
|
||||
disabled={!agreeTerms || isDeploying}
|
||||
onClick={handleApprove}
|
||||
startIcon={isDeploying ? null : <RocketLaunch />}
|
||||
startIcon={isDeploying ? null : <RocketLaunch fontSize="small" />}
|
||||
sx={{
|
||||
py: 3,
|
||||
py: { xs: 1.5, sm: 3 },
|
||||
borderRadius: 3,
|
||||
fontSize: '1rem',
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
fontWeight: 700,
|
||||
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
|
||||
'&:hover': {
|
||||
@ -511,11 +511,11 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={handleSaveDraft}
|
||||
startIcon={<Save />}
|
||||
startIcon={<Save fontSize="small" />}
|
||||
sx={{
|
||||
py: 3,
|
||||
py: { xs: 1.5, sm: 3 },
|
||||
borderRadius: 3,
|
||||
fontSize: '1rem',
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
fontWeight: 600,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.gray[300],
|
||||
|
||||
@ -108,18 +108,18 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 20 }}>
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 3, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 1.5, sm: 6, md: 8 } }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: { xs: 3, sm: 8 } }}>
|
||||
<IconButton onClick={onBack} sx={{ width: 48, height: 48 }}>
|
||||
<ArrowBack />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, mb: { xs: 3, sm: 8 } }}>
|
||||
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
||||
<ArrowBack sx={{ fontSize: 20 }} />
|
||||
</IconButton>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: '1.5rem' }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
|
||||
배포 채널 선택
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: { xs: 3, sm: 8 }, textAlign: 'center', fontSize: '1rem' }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: { xs: 3, sm: 8 }, textAlign: 'center', fontSize: { xs: '0.8125rem', sm: '1rem' } }}>
|
||||
(최소 1개 이상)
|
||||
</Typography>
|
||||
|
||||
@ -136,7 +136,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
||||
transition: 'all 0.3s',
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 6 } }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
@ -211,7 +211,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
||||
transition: 'all 0.3s',
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 6 } }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
@ -270,7 +270,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
||||
transition: 'all 0.3s',
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 6 } }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
@ -356,7 +356,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
||||
transition: 'all 0.3s',
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 6 } }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
@ -471,7 +471,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ p: { xs: 3, sm: 8 } }}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 4 }}>
|
||||
<Typography variant="h6" sx={{ fontSize: '1.25rem' }}>
|
||||
총 예상 비용
|
||||
@ -492,16 +492,16 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
||||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Box sx={{ display: 'flex', gap: 4 }}>
|
||||
<Box sx={{ display: 'flex', gap: { xs: 2, sm: 4 } }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
py: 3,
|
||||
py: { xs: 1.5, sm: 3 },
|
||||
borderRadius: 3,
|
||||
fontSize: '1rem',
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
fontWeight: 600,
|
||||
borderWidth: 2,
|
||||
'&:hover': {
|
||||
@ -518,9 +518,9 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
||||
disabled={selectedCount === 0}
|
||||
onClick={handleNext}
|
||||
sx={{
|
||||
py: 3,
|
||||
py: { xs: 1.5, sm: 3 },
|
||||
borderRadius: 3,
|
||||
fontSize: '1rem',
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
fontWeight: 700,
|
||||
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
|
||||
'&:hover': {
|
||||
|
||||
@ -40,26 +40,26 @@ export default function ContentEditStep({
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', bgcolor: colors.gray[50], pt: { xs: 7, sm: 8 }, pb: { xs: 4, sm: 10 } }}>
|
||||
<Container maxWidth="lg" sx={{ pt: 8, pb: 6, px: { xs: 2, sm: 8, md: 10 } }}>
|
||||
<Box sx={{ minHeight: '100vh', bgcolor: colors.gray[50], pt: { xs: 5, sm: 8 }, pb: { xs: 3, sm: 10 } }}>
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 3, sm: 6 }, px: { xs: 1.5, sm: 8, md: 10 } }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: { xs: 4, sm: 10 } }}>
|
||||
<IconButton onClick={onBack} sx={{ width: 48, height: 48 }}>
|
||||
<ArrowBack />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, mb: { xs: 3, sm: 10 } }}>
|
||||
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
||||
<ArrowBack sx={{ fontSize: 20 }} />
|
||||
</IconButton>
|
||||
<Typography variant="h5" sx={{ ...responsiveText.h3, fontWeight: 700 }}>
|
||||
콘텐츠 편집
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={6}>
|
||||
<Grid container spacing={{ xs: 3, sm: 6 }}>
|
||||
{/* Preview Section */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 6 }}>
|
||||
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: { xs: 3, sm: 6 } }}>
|
||||
미리보기
|
||||
</Typography>
|
||||
<Card elevation={0} sx={{ ...cardStyles.default, height: '100%' }}>
|
||||
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
@ -70,17 +70,17 @@ export default function ContentEditStep({
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 6,
|
||||
p: { xs: 3, sm: 6 },
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<span className="material-icons" style={{ fontSize: 64, marginBottom: 24, color: colors.purple }}>
|
||||
<span className="material-icons" style={{ fontSize: 48, marginBottom: 16, color: colors.purple }}>
|
||||
celebration
|
||||
</span>
|
||||
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 2 }}>
|
||||
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: { xs: 1, sm: 2 } }}>
|
||||
{title || '제목을 입력하세요'}
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ ...responsiveText.body1, mb: 2 }}>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ ...responsiveText.body1, mb: { xs: 1, sm: 2 } }}>
|
||||
{prize || '경품을 입력하세요'}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ ...responsiveText.body2 }}>
|
||||
@ -93,20 +93,20 @@ export default function ContentEditStep({
|
||||
|
||||
{/* Edit Section */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 6 }}>
|
||||
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: { xs: 3, sm: 6 } }}>
|
||||
편집
|
||||
</Typography>
|
||||
|
||||
<Card elevation={0} sx={{ ...cardStyles.default, height: '100%' }}>
|
||||
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 6 }}>
|
||||
<Edit sx={{ color: colors.purple, fontSize: 28 }} />
|
||||
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 2 }, mb: { xs: 3, sm: 6 } }}>
|
||||
<Edit sx={{ color: colors.purple, fontSize: { xs: 20, sm: 28 } }} />
|
||||
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700 }}>
|
||||
텍스트 편집
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { xs: 3, sm: 4 } }}>
|
||||
<Box>
|
||||
<TextField
|
||||
fullWidth
|
||||
@ -148,16 +148,16 @@ export default function ContentEditStep({
|
||||
</Grid>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Box sx={{ display: 'flex', gap: 4, mt: 10 }}>
|
||||
<Box sx={{ display: 'flex', gap: { xs: 2, sm: 4 }, mt: { xs: 4, sm: 10 } }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={handleSave}
|
||||
sx={{
|
||||
py: 3,
|
||||
py: { xs: 1.5, sm: 3 },
|
||||
borderRadius: 3,
|
||||
fontSize: '1rem',
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
fontWeight: 600,
|
||||
borderWidth: 2,
|
||||
'&:hover': {
|
||||
@ -173,9 +173,9 @@ export default function ContentEditStep({
|
||||
size="large"
|
||||
onClick={handleNext}
|
||||
sx={{
|
||||
py: 3,
|
||||
py: { xs: 1.5, sm: 3 },
|
||||
borderRadius: 3,
|
||||
fontSize: '1rem',
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
fontWeight: 700,
|
||||
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
|
||||
'&:hover': {
|
||||
|
||||
@ -310,23 +310,23 @@ export default function ContentPreviewStep({
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 20 }}>
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: { xs: 3, sm: 8 } }}>
|
||||
<IconButton onClick={onBack}>
|
||||
<ArrowBack />
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 3, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 1.5, sm: 6, md: 8 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, mb: { xs: 3, sm: 8 } }}>
|
||||
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
||||
<ArrowBack sx={{ fontSize: 20 }} />
|
||||
</IconButton>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: '1.5rem' }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
|
||||
SNS 이미지 생성
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ textAlign: 'center', mt: 15, mb: 15, maxWidth: 600, mx: 'auto' }}>
|
||||
<Box sx={{ textAlign: 'center', mt: { xs: 8, sm: 15 }, mb: { xs: 8, sm: 15 }, maxWidth: 600, mx: 'auto' }}>
|
||||
{/* 그라데이션 스피너 */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
margin: '0 auto 32px',
|
||||
width: { xs: 60, sm: 80 },
|
||||
height: { xs: 60, sm: 80 },
|
||||
margin: { xs: '0 auto 24px', sm: '0 auto 32px' },
|
||||
borderRadius: '50%',
|
||||
background: `conic-gradient(from 0deg, ${colors.purple}, ${colors.pink}, ${colors.blue}, ${colors.purple})`,
|
||||
animation: 'spin 1.5s linear infinite',
|
||||
@ -340,8 +340,8 @@ export default function ContentPreviewStep({
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
width: 60,
|
||||
height: 60,
|
||||
width: { xs: 45, sm: 60 },
|
||||
height: { xs: 45, sm: 60 },
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'background.default',
|
||||
},
|
||||
@ -349,7 +349,7 @@ export default function ContentPreviewStep({
|
||||
>
|
||||
<Psychology
|
||||
sx={{
|
||||
fontSize: 40,
|
||||
fontSize: { xs: 32, sm: 40 },
|
||||
color: colors.purple,
|
||||
zIndex: 1,
|
||||
animation: 'pulse 1.5s ease-in-out infinite',
|
||||
@ -362,12 +362,12 @@ export default function ContentPreviewStep({
|
||||
</Box>
|
||||
|
||||
{/* 진행률 바 */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box sx={{ mb: { xs: 3, sm: 4 } }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: '1.5rem' }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1rem', sm: '1.5rem' } }}>
|
||||
{loadingMessage}
|
||||
</Typography>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, color: colors.purple, fontSize: '1.25rem' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, color: colors.purple, fontSize: { xs: '0.9375rem', sm: '1.25rem' } }}>
|
||||
{Math.round(loadingProgress)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
@ -394,7 +394,7 @@ export default function ContentPreviewStep({
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3, fontSize: '1.125rem' }}>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: { xs: 2, sm: 3 }, fontSize: { xs: '0.875rem', sm: '1.125rem' } }}>
|
||||
{generatedImages.size > 0 ? (
|
||||
<>
|
||||
생성된 이미지를 확인하고 있어요
|
||||
@ -442,33 +442,33 @@ export default function ContentPreviewStep({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 3, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 1.5, sm: 6, md: 8 } }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: { xs: 3, sm: 8 } }}>
|
||||
<IconButton onClick={onBack}>
|
||||
<ArrowBack />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, mb: { xs: 3, sm: 8 } }}>
|
||||
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
||||
<ArrowBack sx={{ fontSize: 20 }} />
|
||||
</IconButton>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: '1.5rem' }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
|
||||
SNS 이미지 생성
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: { xs: 3, sm: 8 } }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, justifyContent: 'space-between', alignItems: { xs: 'stretch', sm: 'center' }, gap: { xs: 2, sm: 0 }, mb: { xs: 3, sm: 8 } }}>
|
||||
{generatedImages.size > 0 && (
|
||||
<Alert severity="success" sx={{ flex: 1, fontSize: '1rem' }}>
|
||||
<Alert severity="success" sx={{ flex: 1, fontSize: { xs: '0.875rem', sm: '1rem' } }}>
|
||||
✨ 생성된 이미지를 확인하고 스타일을 선택하세요
|
||||
</Alert>
|
||||
)}
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Refresh />}
|
||||
startIcon={<Refresh fontSize="small" />}
|
||||
onClick={handleGenerateImages}
|
||||
sx={{
|
||||
ml: 4,
|
||||
py: 2,
|
||||
px: 4,
|
||||
ml: { xs: 0, sm: 4 },
|
||||
py: { xs: 1.5, sm: 2 },
|
||||
px: { xs: 3, sm: 4 },
|
||||
borderRadius: 2,
|
||||
fontSize: '1rem',
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
fontWeight: 600,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
@ -477,12 +477,12 @@ export default function ContentPreviewStep({
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: { xs: 3, sm: 8 }, textAlign: 'center', fontSize: '1rem' }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: { xs: 3, sm: 8 }, textAlign: 'center', fontSize: { xs: '0.8125rem', sm: '1rem' } }}>
|
||||
이벤트에 어울리는 스타일을 선택하세요
|
||||
</Typography>
|
||||
|
||||
<RadioGroup value={selectedStyle} onChange={(e) => handleStyleSelect(e.target.value as 'SIMPLE' | 'FANCY' | 'TRENDY')}>
|
||||
<Grid container spacing={6} sx={{ mb: { xs: 4, sm: 10 } }}>
|
||||
<Grid container spacing={{ xs: 3, sm: 6 }} sx={{ mb: { xs: 3, sm: 10 } }}>
|
||||
{imageStyles.map((style) => (
|
||||
<Grid item xs={12} md={4} key={style.id}>
|
||||
<Card
|
||||
@ -506,8 +506,8 @@ export default function ContentPreviewStep({
|
||||
>
|
||||
<CardContent sx={{ p: 0 }}>
|
||||
{/* 스타일 이름 */}
|
||||
<Box sx={{ p: 4, borderBottom: 1, borderColor: 'divider', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1.125rem' }}>
|
||||
<Box sx={{ p: { xs: 2, sm: 4 }, borderBottom: 1, borderColor: 'divider', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: { xs: '0.9375rem', sm: '1.125rem' } }}>
|
||||
{style.name}
|
||||
</Typography>
|
||||
<FormControlLabel
|
||||
@ -561,15 +561,15 @@ export default function ContentPreviewStep({
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 6,
|
||||
p: { xs: 3, sm: 6 },
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="material-icons"
|
||||
style={{
|
||||
fontSize: 64,
|
||||
marginBottom: 24,
|
||||
fontSize: 48,
|
||||
marginBottom: 16,
|
||||
color: style.textColor || colors.gray[700],
|
||||
}}
|
||||
>
|
||||
@ -579,9 +579,9 @@ export default function ContentPreviewStep({
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
mb: 2,
|
||||
mb: { xs: 1, sm: 2 },
|
||||
color: style.textColor || 'text.primary',
|
||||
fontSize: '1.25rem',
|
||||
fontSize: { xs: '1rem', sm: '1.25rem' },
|
||||
}}
|
||||
>
|
||||
{eventData?.eventTitle || '이벤트'}
|
||||
@ -591,7 +591,7 @@ export default function ContentPreviewStep({
|
||||
sx={{
|
||||
color: style.textColor || 'text.secondary',
|
||||
opacity: style.textColor ? 0.9 : 1,
|
||||
fontSize: '1rem',
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
}}
|
||||
>
|
||||
{eventData?.prize || '경품'}
|
||||
@ -602,10 +602,10 @@ export default function ContentPreviewStep({
|
||||
</Box>
|
||||
|
||||
{/* 크게보기 버튼 */}
|
||||
<Box sx={{ p: 4, display: 'flex', justifyContent: 'center' }}>
|
||||
<Box sx={{ p: { xs: 2, sm: 4 }, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<ZoomIn />}
|
||||
startIcon={<ZoomIn fontSize="small" />}
|
||||
onClick={(e) => {
|
||||
const image = generatedImages.get(style.id);
|
||||
if (image) {
|
||||
@ -615,9 +615,9 @@ export default function ContentPreviewStep({
|
||||
disabled={!generatedImages.has(style.id)}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
py: 1.5,
|
||||
px: 4,
|
||||
fontSize: '0.875rem',
|
||||
py: { xs: 1, sm: 1.5 },
|
||||
px: { xs: 3, sm: 4 },
|
||||
fontSize: { xs: '0.8125rem', sm: '0.875rem' },
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
@ -632,16 +632,16 @@ export default function ContentPreviewStep({
|
||||
</RadioGroup>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Box sx={{ display: 'flex', gap: 4 }}>
|
||||
<Box sx={{ display: 'flex', gap: { xs: 2, sm: 4 } }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={onSkip}
|
||||
sx={{
|
||||
py: 3,
|
||||
py: { xs: 1.5, sm: 3 },
|
||||
borderRadius: 3,
|
||||
fontSize: '1rem',
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
fontWeight: 600,
|
||||
borderWidth: 2,
|
||||
'&:hover': {
|
||||
@ -658,9 +658,9 @@ export default function ContentPreviewStep({
|
||||
disabled={!selectedStyle}
|
||||
onClick={handleNext}
|
||||
sx={{
|
||||
py: 3,
|
||||
py: { xs: 1.5, sm: 3 },
|
||||
borderRadius: 3,
|
||||
fontSize: '1rem',
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
fontWeight: 700,
|
||||
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
|
||||
'&:hover': {
|
||||
|
||||
@ -127,21 +127,21 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 20 }}>
|
||||
<Container maxWidth="md" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
|
||||
<Container maxWidth="md" sx={{ pt: { xs: 3, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 1.5, sm: 6, md: 8 } }}>
|
||||
{/* Title Section */}
|
||||
<Box sx={{ mb: { xs: 4, sm: 10 }, textAlign: 'center' }}>
|
||||
<AutoAwesome sx={{ fontSize: { xs: 60, sm: 80 }, color: colors.purple, mb: { xs: 2, sm: 4 } }} />
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, mb: { xs: 2, sm: 4 }, fontSize: { xs: '1.5rem', sm: '2rem' } }}>
|
||||
<Box sx={{ mb: { xs: 3, sm: 10 }, textAlign: 'center' }}>
|
||||
<AutoAwesome sx={{ fontSize: { xs: 48, sm: 80 }, color: colors.purple, mb: { xs: 1.5, sm: 4 } }} />
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, mb: { xs: 1.5, sm: 4 }, fontSize: { xs: '1.25rem', sm: '2rem' } }}>
|
||||
이벤트 목적을 선택해주세요
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ fontSize: { xs: '0.875rem', sm: '1.125rem' } }}>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ fontSize: { xs: '0.8125rem', sm: '1.125rem' } }}>
|
||||
AI가 목적에 맞는 최적의 이벤트를 추천해드립니다
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Purpose Options */}
|
||||
<RadioGroup value={selected} onChange={(e) => setSelected(e.target.value as EventObjective)}>
|
||||
<Grid container spacing={{ xs: 2, sm: 6 }} sx={{ mb: { xs: 4, sm: 10 } }}>
|
||||
<Grid container spacing={{ xs: 1.5, sm: 6 }} sx={{ mb: { xs: 3, sm: 10 } }}>
|
||||
{objectives.map((objective) => (
|
||||
<Grid item xs={12} sm={6} key={objective.id}>
|
||||
<Card
|
||||
@ -162,14 +162,14 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
|
||||
}}
|
||||
onClick={() => setSelected(objective.id)}
|
||||
>
|
||||
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: { xs: 2, sm: 3 }, mb: { xs: 2, sm: 3 } }}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 6 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: { xs: 1.5, sm: 3 } }}>
|
||||
<Box sx={{ color: colors.purple }}>{objective.icon}</Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, mb: { xs: 1, sm: 2 }, fontSize: { xs: '1rem', sm: '1.25rem' } }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, mb: { xs: 0.5, sm: 2 }, fontSize: { xs: '0.9375rem', sm: '1.25rem' } }}>
|
||||
{objective.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.8125rem', sm: '1rem' } }}>
|
||||
{objective.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
@ -191,15 +191,15 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
|
||||
<Card
|
||||
elevation={0}
|
||||
sx={{
|
||||
mb: { xs: 4, sm: 10 },
|
||||
mb: { xs: 3, sm: 10 },
|
||||
background: `linear-gradient(135deg, ${colors.purpleLight} 0%, ${colors.blue}20 100%)`,
|
||||
borderRadius: 4,
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ display: 'flex', gap: { xs: 2, sm: 3 }, p: { xs: 3, sm: 6 } }}>
|
||||
<AutoAwesome sx={{ color: colors.purple, fontSize: { xs: 24, sm: 28 } }} />
|
||||
<Typography variant="body2" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' }, lineHeight: 1.8, color: colors.gray[700] }}>
|
||||
<CardContent sx={{ display: 'flex', gap: { xs: 1.5, sm: 3 }, p: { xs: 2, sm: 6 } }}>
|
||||
<AutoAwesome sx={{ color: colors.purple, fontSize: { xs: 20, sm: 28 } }} />
|
||||
<Typography variant="body2" sx={{ fontSize: { xs: '0.8125rem', sm: '1rem' }, lineHeight: 1.8, color: colors.gray[700] }}>
|
||||
선택하신 목적에 따라 AI가 업종, 지역, 계절 트렌드를 분석하여 가장 효과적인 이벤트를 추천합니다.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
@ -214,7 +214,7 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
|
||||
disabled={!selected}
|
||||
onClick={handleNext}
|
||||
sx={{
|
||||
py: { xs: 2, sm: 3 },
|
||||
py: { xs: 1.5, sm: 3 },
|
||||
borderRadius: 3,
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
fontWeight: 700,
|
||||
|
||||
@ -182,24 +182,24 @@ export default function RecommendationStep({
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 20 }}>
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 2, sm: 3 }, mb: { xs: 4, sm: 8 } }}>
|
||||
<IconButton onClick={onBack} sx={{ width: 48, height: 48 }}>
|
||||
<ArrowBack />
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 3, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 1.5, sm: 6, md: 8 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, mb: { xs: 3, sm: 8 } }}>
|
||||
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
||||
<ArrowBack sx={{ fontSize: 20 }} />
|
||||
</IconButton>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1.25rem', sm: '1.5rem' } }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
|
||||
AI 이벤트 추천
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: { xs: 2, sm: 4 }, py: { xs: 6, sm: 12 } }}
|
||||
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: { xs: 1.5, sm: 4 }, py: { xs: 4, sm: 12 } }}
|
||||
>
|
||||
<CircularProgress size={60} sx={{ color: colors.purple }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: { xs: '1rem', sm: '1.25rem' } }}>
|
||||
<CircularProgress size={48} sx={{ color: colors.purple }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: { xs: '0.9375rem', sm: '1.25rem' } }}>
|
||||
AI가 최적의 이벤트를 생성하고 있습니다...
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.8125rem', sm: '1rem' } }}>
|
||||
업종, 지역, 시즌 트렌드를 분석하여 맞춤형 이벤트를 추천합니다
|
||||
</Typography>
|
||||
</Box>
|
||||
@ -212,30 +212,30 @@ export default function RecommendationStep({
|
||||
if (error) {
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 20 }}>
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: { xs: 3, sm: 8 } }}>
|
||||
<IconButton onClick={onBack} sx={{ width: 48, height: 48 }}>
|
||||
<ArrowBack />
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 3, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 1.5, sm: 6, md: 8 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, mb: { xs: 3, sm: 8 } }}>
|
||||
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
||||
<ArrowBack sx={{ fontSize: 20 }} />
|
||||
</IconButton>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: '1.5rem' }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
|
||||
AI 이벤트 추천
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Alert severity="error" sx={{ mb: 4 }}>
|
||||
<Alert severity="error" sx={{ mb: { xs: 3, sm: 4 } }}>
|
||||
{error}
|
||||
</Alert>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 4 }}>
|
||||
<Box sx={{ display: 'flex', gap: { xs: 2, sm: 4 } }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
py: 3,
|
||||
py: { xs: 1.5, sm: 3 },
|
||||
borderRadius: 3,
|
||||
fontSize: '1rem',
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
@ -257,9 +257,9 @@ export default function RecommendationStep({
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
py: 3,
|
||||
py: { xs: 1.5, sm: 3 },
|
||||
borderRadius: 3,
|
||||
fontSize: '1rem',
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
fontWeight: 700,
|
||||
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
|
||||
}}
|
||||
@ -276,7 +276,7 @@ export default function RecommendationStep({
|
||||
if (!aiResult) {
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 20 }}>
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 3, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 1.5, sm: 6, md: 8 } }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
</Box>
|
||||
@ -285,13 +285,13 @@ export default function RecommendationStep({
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 20 }}>
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 3, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 1.5, sm: 6, md: 8 } }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: { xs: 3, sm: 8 } }}>
|
||||
<IconButton onClick={onBack} sx={{ width: 48, height: 48 }}>
|
||||
<ArrowBack />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, mb: { xs: 3, sm: 8 } }}>
|
||||
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
||||
<ArrowBack sx={{ fontSize: 20 }} />
|
||||
</IconButton>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: '1.5rem' }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
|
||||
AI 이벤트 추천
|
||||
</Typography>
|
||||
</Box>
|
||||
@ -300,21 +300,21 @@ export default function RecommendationStep({
|
||||
<Card
|
||||
elevation={0}
|
||||
sx={{
|
||||
mb: { xs: 4, sm: 10 },
|
||||
mb: { xs: 3, sm: 10 },
|
||||
borderRadius: 4,
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ p: { xs: 3, sm: 8 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 6 }}>
|
||||
<Insights sx={{ fontSize: 32, color: colors.purple }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1.25rem' }}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 2 }, mb: { xs: 3, sm: 6 } }}>
|
||||
<Insights sx={{ fontSize: { xs: 24, sm: 32 }, color: colors.purple }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: { xs: '1rem', sm: '1.25rem' } }}>
|
||||
AI 트렌드 분석
|
||||
</Typography>
|
||||
</Box>
|
||||
<Grid container spacing={6}>
|
||||
<Grid container spacing={{ xs: 3, sm: 6 }}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, fontSize: '1rem' }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: { xs: 1.5, sm: 2 }, fontSize: { xs: '0.875rem', sm: '1rem' } }}>
|
||||
📍 업종 트렌드
|
||||
</Typography>
|
||||
{aiResult.trendAnalysis.industryTrends.slice(0, 3).map((trend, idx) => (
|
||||
@ -322,14 +322,14 @@ export default function RecommendationStep({
|
||||
key={idx}
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: '0.95rem', mb: 1 }}
|
||||
sx={{ fontSize: { xs: '0.8125rem', sm: '0.95rem' }, mb: 1 }}
|
||||
>
|
||||
• {trend.description}
|
||||
</Typography>
|
||||
))}
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, fontSize: '1rem' }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: { xs: 1.5, sm: 2 }, fontSize: { xs: '0.875rem', sm: '1rem' } }}>
|
||||
🗺️ 지역 트렌드
|
||||
</Typography>
|
||||
{aiResult.trendAnalysis.regionalTrends.slice(0, 3).map((trend, idx) => (
|
||||
@ -337,14 +337,14 @@ export default function RecommendationStep({
|
||||
key={idx}
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: '0.95rem', mb: 1 }}
|
||||
sx={{ fontSize: { xs: '0.8125rem', sm: '0.95rem' }, mb: 1 }}
|
||||
>
|
||||
• {trend.description}
|
||||
</Typography>
|
||||
))}
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, fontSize: '1rem' }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: { xs: 1.5, sm: 2 }, fontSize: { xs: '0.875rem', sm: '1rem' } }}>
|
||||
☀️ 시즌 트렌드
|
||||
</Typography>
|
||||
{aiResult.trendAnalysis.seasonalTrends.slice(0, 3).map((trend, idx) => (
|
||||
@ -352,7 +352,7 @@ export default function RecommendationStep({
|
||||
key={idx}
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: '0.95rem', mb: 1 }}
|
||||
sx={{ fontSize: { xs: '0.8125rem', sm: '0.95rem' }, mb: 1 }}
|
||||
>
|
||||
• {trend.description}
|
||||
</Typography>
|
||||
@ -363,11 +363,11 @@ export default function RecommendationStep({
|
||||
</Card>
|
||||
|
||||
{/* AI Recommendations */}
|
||||
<Box sx={{ mb: { xs: 3, sm: 8 } }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, mb: 4, fontSize: '1.25rem' }}>
|
||||
<Box sx={{ mb: { xs: 2, sm: 8 } }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, mb: { xs: 2, sm: 4 }, fontSize: { xs: '1rem', sm: '1.25rem' } }}>
|
||||
AI 추천 이벤트 ({aiResult.recommendations.length}가지 옵션)
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 6, fontSize: '1rem' }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: { xs: 3, sm: 6 }, fontSize: { xs: '0.8125rem', sm: '1rem' } }}>
|
||||
각 옵션은 차별화된 컨셉으로 구성되어 있습니다. 원하시는 옵션을 선택하고 수정할 수
|
||||
있습니다.
|
||||
</Typography>
|
||||
@ -375,7 +375,7 @@ export default function RecommendationStep({
|
||||
|
||||
{/* Recommendations */}
|
||||
<RadioGroup value={selected} onChange={(e) => setSelected(Number(e.target.value))}>
|
||||
<Grid container spacing={6} sx={{ mb: { xs: 4, sm: 10 } }}>
|
||||
<Grid container spacing={{ xs: 3, sm: 6 }} sx={{ mb: { xs: 3, sm: 10 } }}>
|
||||
{aiResult.recommendations.map((rec) => (
|
||||
<Grid item xs={12} key={rec.optionNumber}>
|
||||
<Card
|
||||
@ -402,27 +402,27 @@ export default function RecommendationStep({
|
||||
}}
|
||||
onClick={() => setSelected(rec.optionNumber)}
|
||||
>
|
||||
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 6 } }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
mb: 4,
|
||||
mb: { xs: 2, sm: 4 },
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: { xs: 1, sm: 2 }, flexWrap: 'wrap' }}>
|
||||
<Chip
|
||||
label={`옵션 ${rec.optionNumber}`}
|
||||
color="primary"
|
||||
size="medium"
|
||||
sx={{ fontSize: '0.875rem', py: 2 }}
|
||||
size="small"
|
||||
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' }, py: { xs: 1.5, sm: 2 } }}
|
||||
/>
|
||||
<Chip
|
||||
label={rec.concept}
|
||||
variant="outlined"
|
||||
size="medium"
|
||||
sx={{ fontSize: '0.875rem', py: 2 }}
|
||||
size="small"
|
||||
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' }, py: { xs: 1.5, sm: 2 } }}
|
||||
/>
|
||||
</Box>
|
||||
<FormControlLabel
|
||||
@ -439,10 +439,10 @@ export default function RecommendationStep({
|
||||
value={editedData[rec.optionNumber]?.title || rec.title}
|
||||
onChange={(e) => handleEditTitle(rec.optionNumber, e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
sx={{ mb: 4 }}
|
||||
sx={{ mb: { xs: 2, sm: 4 } }}
|
||||
InputProps={{
|
||||
endAdornment: <Edit fontSize="small" color="action" />,
|
||||
sx: { fontSize: '1.1rem', fontWeight: 600, py: 2 },
|
||||
sx: { fontSize: { xs: '0.9375rem', sm: '1.1rem' }, fontWeight: 600, py: { xs: 1.5, sm: 2 } },
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -454,24 +454,24 @@ export default function RecommendationStep({
|
||||
value={editedData[rec.optionNumber]?.description || rec.description}
|
||||
onChange={(e) => handleEditDescription(rec.optionNumber, e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
sx={{ mb: 4 }}
|
||||
sx={{ mb: { xs: 2, sm: 4 } }}
|
||||
InputProps={{
|
||||
sx: { fontSize: '1rem' },
|
||||
sx: { fontSize: { xs: '0.875rem', sm: '1rem' } },
|
||||
}}
|
||||
/>
|
||||
|
||||
<Grid container spacing={4} sx={{ mt: 2 }}>
|
||||
<Grid container spacing={{ xs: 2, sm: 4 }} sx={{ mt: { xs: 1, sm: 2 } }}>
|
||||
<Grid item xs={6} md={3}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: '0.875rem' }}
|
||||
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
||||
>
|
||||
타겟 고객
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: 600, fontSize: '1rem', mt: 1 }}
|
||||
sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1rem' }, mt: { xs: 0.5, sm: 1 } }}
|
||||
>
|
||||
{rec.targetAudience}
|
||||
</Typography>
|
||||
@ -480,13 +480,13 @@ export default function RecommendationStep({
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: '0.875rem' }}
|
||||
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
||||
>
|
||||
예상 비용
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: 600, fontSize: '1rem', mt: 1 }}
|
||||
sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1rem' }, mt: { xs: 0.5, sm: 1 } }}
|
||||
>
|
||||
{(rec.estimatedCost.min / 10000).toFixed(0)}~
|
||||
{(rec.estimatedCost.max / 10000).toFixed(0)}만원
|
||||
@ -496,13 +496,13 @@ export default function RecommendationStep({
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: '0.875rem' }}
|
||||
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
||||
>
|
||||
예상 신규 고객
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: 600, fontSize: '1rem', mt: 1 }}
|
||||
sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1rem' }, mt: { xs: 0.5, sm: 1 } }}
|
||||
>
|
||||
{rec.expectedMetrics.newCustomers.min}~
|
||||
{rec.expectedMetrics.newCustomers.max}명
|
||||
@ -512,13 +512,13 @@ export default function RecommendationStep({
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: '0.875rem' }}
|
||||
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
||||
>
|
||||
ROI
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: 600, color: 'error.main', fontSize: '1rem', mt: 1 }}
|
||||
sx={{ fontWeight: 600, color: 'error.main', fontSize: { xs: '0.875rem', sm: '1rem' }, mt: { xs: 0.5, sm: 1 } }}
|
||||
>
|
||||
{rec.expectedMetrics.roi.min}~{rec.expectedMetrics.roi.max}%
|
||||
</Typography>
|
||||
@ -527,11 +527,11 @@ export default function RecommendationStep({
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: '0.875rem' }}
|
||||
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
||||
>
|
||||
차별점
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontSize: '0.95rem', mt: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontSize: { xs: '0.8125rem', sm: '0.95rem' }, mt: { xs: 0.5, sm: 1 } }}>
|
||||
{rec.differentiator}
|
||||
</Typography>
|
||||
</Grid>
|
||||
@ -544,16 +544,16 @@ export default function RecommendationStep({
|
||||
</RadioGroup>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Box sx={{ display: 'flex', gap: 4 }}>
|
||||
<Box sx={{ display: 'flex', gap: { xs: 2, sm: 4 } }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
py: 3,
|
||||
py: { xs: 1.5, sm: 3 },
|
||||
borderRadius: 3,
|
||||
fontSize: '1rem',
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
fontWeight: 600,
|
||||
borderWidth: 2,
|
||||
'&:hover': {
|
||||
@ -570,9 +570,9 @@ export default function RecommendationStep({
|
||||
disabled={selected === null || loading}
|
||||
onClick={handleNext}
|
||||
sx={{
|
||||
py: 3,
|
||||
py: { xs: 1.5, sm: 3 },
|
||||
borderRadius: 3,
|
||||
fontSize: '1rem',
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
fontWeight: 700,
|
||||
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
|
||||
'&:hover': {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
Box,
|
||||
@ -21,7 +21,6 @@ import {
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Search,
|
||||
FilterList,
|
||||
Event,
|
||||
TrendingUp,
|
||||
People,
|
||||
@ -37,8 +36,8 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import Header from '@/shared/ui/Header';
|
||||
import { cardStyles, colors, responsiveText } from '@/shared/lib/button-styles';
|
||||
import { useEvents } from '@/entities/event/model/useEvents';
|
||||
import type { EventStatus as ApiEventStatus } from '@/entities/event/model/types';
|
||||
import { mockEvents } from '@/shared/mock/eventsMockData';
|
||||
|
||||
// ==================== API 연동 ====================
|
||||
// Mock 데이터를 실제 API 호출로 교체
|
||||
@ -57,74 +56,8 @@ export default function EventsPage() {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 20;
|
||||
|
||||
// 목업 데이터
|
||||
const mockEvents = [
|
||||
{
|
||||
eventId: 'evt_2025012301',
|
||||
eventName: '신규 고객 환영 이벤트',
|
||||
status: 'PUBLISHED' as ApiEventStatus,
|
||||
startDate: '2025-01-23',
|
||||
endDate: '2025-02-23',
|
||||
participants: 1250,
|
||||
targetParticipants: 2000,
|
||||
roi: 320,
|
||||
createdAt: '2025-01-15T00:00:00',
|
||||
aiRecommendations: [{
|
||||
reward: '스타벅스 아메리카노 (5명)',
|
||||
participationMethod: '전화번호 입력'
|
||||
}]
|
||||
},
|
||||
{
|
||||
eventId: 'evt_2025011502',
|
||||
eventName: '재방문 고객 감사 이벤트',
|
||||
status: 'PUBLISHED' as ApiEventStatus,
|
||||
startDate: '2025-01-15',
|
||||
endDate: '2025-02-15',
|
||||
participants: 890,
|
||||
targetParticipants: 1000,
|
||||
roi: 280,
|
||||
createdAt: '2025-01-10T00:00:00',
|
||||
aiRecommendations: [{
|
||||
reward: '커피 쿠폰 (10명)',
|
||||
participationMethod: 'SNS 팔로우'
|
||||
}]
|
||||
},
|
||||
{
|
||||
eventId: 'evt_2025010803',
|
||||
eventName: '신년 특별 할인 이벤트',
|
||||
status: 'ENDED' as ApiEventStatus,
|
||||
startDate: '2025-01-01',
|
||||
endDate: '2025-01-08',
|
||||
participants: 2500,
|
||||
targetParticipants: 2000,
|
||||
roi: 450,
|
||||
createdAt: '2024-12-28T00:00:00',
|
||||
aiRecommendations: [{
|
||||
reward: '10% 할인 쿠폰 (선착순 100명)',
|
||||
participationMethod: '구매 인증'
|
||||
}]
|
||||
},
|
||||
{
|
||||
eventId: 'evt_2025020104',
|
||||
eventName: '2월 신메뉴 출시 기념',
|
||||
status: 'DRAFT' as ApiEventStatus,
|
||||
startDate: '2025-02-01',
|
||||
endDate: '2025-02-28',
|
||||
participants: 0,
|
||||
targetParticipants: 1500,
|
||||
roi: 0,
|
||||
createdAt: '2025-01-25T00:00:00',
|
||||
aiRecommendations: [{
|
||||
reward: '신메뉴 무료 쿠폰 (20명)',
|
||||
participationMethod: '이메일 등록'
|
||||
}]
|
||||
},
|
||||
];
|
||||
|
||||
const loading = false;
|
||||
const error = null;
|
||||
const apiEvents = mockEvents;
|
||||
const refetch = () => {};
|
||||
|
||||
// API 상태를 UI 상태로 매핑
|
||||
const mapApiStatus = (apiStatus: ApiEventStatus): EventStatus => {
|
||||
|
||||
@ -2,7 +2,18 @@
|
||||
|
||||
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 {
|
||||
Box,
|
||||
Container,
|
||||
Typography,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
Button,
|
||||
Fab,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Add,
|
||||
Celebration,
|
||||
@ -23,6 +34,7 @@ import {
|
||||
import { useAuth } from '@/features/auth/model/useAuth';
|
||||
import { analyticsApi } from '@/entities/analytics/api/analyticsApi';
|
||||
import type { UserAnalyticsDashboardResponse } from '@/entities/analytics/model/types';
|
||||
import { mockEvents } from '@/shared/mock/eventsMockData';
|
||||
|
||||
const mockActivities = [
|
||||
{ icon: PersonAdd, text: 'SNS 팔로우 이벤트에 새로운 참여자 12명', time: '5분 전' },
|
||||
@ -55,8 +67,9 @@ export default function HomePage() {
|
||||
|
||||
// 404 또는 400 에러는 아직 Analytics 데이터가 없는 경우
|
||||
if (err.response?.status === 404 || err.response?.status === 400) {
|
||||
console.log('ℹ️ Analytics 데이터가 아직 생성되지 않았습니다.');
|
||||
setError('아직 분석 데이터가 없습니다. 이벤트를 생성하고 참여자가 생기면 자동으로 생성됩니다.');
|
||||
setError(
|
||||
'아직 분석 데이터가 없습니다. 이벤트를 생성하고 참여자가 생기면 자동으로 생성됩니다.'
|
||||
);
|
||||
} else {
|
||||
setError('분석 데이터를 불러오는데 실패했습니다.');
|
||||
}
|
||||
@ -69,11 +82,30 @@ export default function HomePage() {
|
||||
}, [user?.userId]);
|
||||
|
||||
// KPI 계산 - Analytics API 데이터 사용
|
||||
const activeEventsCount = analyticsData?.activeEvents ?? 0;
|
||||
const totalParticipants = analyticsData?.overallSummary?.participants ?? 0;
|
||||
const activeEventsCount = 2; // 진행 중인 이벤트 수 고정
|
||||
const totalParticipants = 1351; // 총 참여자 수 고정
|
||||
const avgROI = Math.round((analyticsData?.overallRoi?.roi ?? 0) * 100) / 100;
|
||||
const eventPerformances = analyticsData?.eventPerformances ?? [];
|
||||
|
||||
// Mock 데이터에서 진행 중인 이벤트 가져오기 (Analytics API 데이터가 없을 경우)
|
||||
const activeMockEvents = mockEvents
|
||||
.filter((event) => event.status === 'PUBLISHED')
|
||||
.slice(0, 2)
|
||||
.map((event) => ({
|
||||
eventId: event.eventId,
|
||||
eventTitle: event.eventName,
|
||||
status: '진행중',
|
||||
participants: event.participants,
|
||||
views: event.participants * 3, // 임시 조회수 계산
|
||||
roi: event.roi,
|
||||
}));
|
||||
|
||||
// 표시할 이벤트를 최대 2개로 제한
|
||||
const displayEvents = (eventPerformances.length > 0
|
||||
? eventPerformances
|
||||
: activeMockEvents
|
||||
).slice(0, 2);
|
||||
|
||||
const handleCreateEvent = () => {
|
||||
router.push('/events/create');
|
||||
};
|
||||
@ -97,7 +129,10 @@ export default function HomePage() {
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 6 }, px: { xs: 3, sm: 6, md: 10 } }}>
|
||||
<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
|
||||
@ -139,7 +174,14 @@ export default function HomePage() {
|
||||
borderColor: 'transparent',
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ textAlign: 'center', pt: { xs: 1.5, sm: 6 }, pb: { xs: 1.5, sm: 6 }, px: { xs: 0.5, sm: 4 } }}>
|
||||
<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 },
|
||||
@ -150,14 +192,15 @@ export default function HomePage() {
|
||||
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))',
|
||||
}} />
|
||||
<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"
|
||||
@ -195,7 +238,14 @@ export default function HomePage() {
|
||||
borderColor: 'transparent',
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ textAlign: 'center', pt: { xs: 1.5, sm: 6 }, pb: { xs: 1.5, sm: 6 }, px: { xs: 0.5, sm: 4 } }}>
|
||||
<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 },
|
||||
@ -206,14 +256,15 @@ export default function HomePage() {
|
||||
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))',
|
||||
}} />
|
||||
<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"
|
||||
@ -251,7 +302,14 @@ export default function HomePage() {
|
||||
borderColor: 'transparent',
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ textAlign: 'center', pt: { xs: 1.5, sm: 6 }, pb: { xs: 1.5, sm: 6 }, px: { xs: 0.5, sm: 4 } }}>
|
||||
<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 },
|
||||
@ -262,14 +320,15 @@ export default function HomePage() {
|
||||
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))',
|
||||
}} />
|
||||
<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"
|
||||
@ -314,7 +373,9 @@ export default function HomePage() {
|
||||
}}
|
||||
onClick={handleCreateEvent}
|
||||
>
|
||||
<CardContent sx={{ textAlign: 'center', pt: { xs: 2, sm: 6 }, pb: { xs: 2, sm: 6 } }}>
|
||||
<CardContent
|
||||
sx={{ textAlign: 'center', pt: { xs: 2, sm: 6 }, pb: { xs: 2, sm: 6 } }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: 56, sm: 72 },
|
||||
@ -325,13 +386,19 @@ export default function HomePage() {
|
||||
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
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
color: colors.gray[900],
|
||||
fontSize: { xs: '0.875rem', sm: '1.125rem' },
|
||||
}}
|
||||
>
|
||||
새 이벤트
|
||||
</Typography>
|
||||
</CardContent>
|
||||
@ -345,7 +412,9 @@ export default function HomePage() {
|
||||
}}
|
||||
onClick={handleViewAnalytics}
|
||||
>
|
||||
<CardContent sx={{ textAlign: 'center', pt: { xs: 2, sm: 6 }, pb: { xs: 2, sm: 6 } }}>
|
||||
<CardContent
|
||||
sx={{ textAlign: 'center', pt: { xs: 2, sm: 6 }, pb: { xs: 2, sm: 6 } }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: 56, sm: 72 },
|
||||
@ -356,13 +425,19 @@ export default function HomePage() {
|
||||
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
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
color: colors.gray[900],
|
||||
fontSize: { xs: '0.875rem', sm: '1.125rem' },
|
||||
}}
|
||||
>
|
||||
성과분석
|
||||
</Typography>
|
||||
</CardContent>
|
||||
@ -374,14 +449,23 @@ export default function HomePage() {
|
||||
{/* Active Events */}
|
||||
<Box sx={{ mb: { xs: 5, sm: 10 } }}>
|
||||
<Box
|
||||
sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: { xs: 3, sm: 6 } }}
|
||||
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>}
|
||||
endIcon={
|
||||
<span className="material-icons" style={{ fontSize: '18px' }}>
|
||||
chevron_right
|
||||
</span>
|
||||
}
|
||||
onClick={() => router.push('/events')}
|
||||
sx={{
|
||||
color: colors.purple,
|
||||
@ -394,139 +478,193 @@ export default function HomePage() {
|
||||
</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>
|
||||
{(() => {
|
||||
// 표시할 이벤트를 최대 2개로 제한
|
||||
const displayEvents = (eventPerformances.length > 0
|
||||
? eventPerformances
|
||||
: activeMockEvents
|
||||
).slice(0, 2);
|
||||
|
||||
return !loading && displayEvents.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 } }}>
|
||||
{displayEvents.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={{
|
||||
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,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'start',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{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' } }}
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: colors.gray[900],
|
||||
fontSize: { xs: '0.9375rem', sm: '1.25rem' },
|
||||
}}
|
||||
>
|
||||
참여자
|
||||
{event.eventTitle}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ fontWeight: 700, color: colors.gray[900], fontSize: { xs: '1.125rem', sm: '1.5rem' } }}
|
||||
<Box
|
||||
sx={{
|
||||
px: { xs: 2, sm: 2.5 },
|
||||
py: 0.75,
|
||||
bgcolor: colors.mint,
|
||||
color: 'white',
|
||||
borderRadius: 2,
|
||||
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
|
||||
fontWeight: 600,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{event.participants.toLocaleString()}
|
||||
{event.status}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: { xs: 6, sm: 12 } }}>
|
||||
<Box>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
sx={{ ml: 0.5, color: colors.gray[600], fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
||||
sx={{
|
||||
mb: 0.5,
|
||||
color: colors.gray[600],
|
||||
fontWeight: 500,
|
||||
fontSize: { xs: '0.6875rem', 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' } }}
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: colors.gray[900],
|
||||
fontSize: { xs: '1rem', sm: '1.5rem' },
|
||||
}}
|
||||
>
|
||||
회
|
||||
{event.participants.toLocaleString()}
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
color: colors.gray[600],
|
||||
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
|
||||
}}
|
||||
>
|
||||
명
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
mb: 0.5,
|
||||
color: colors.gray[600],
|
||||
fontWeight: 500,
|
||||
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
|
||||
}}
|
||||
>
|
||||
조회수
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: colors.gray[900],
|
||||
fontSize: { xs: '1rem', sm: '1.5rem' },
|
||||
}}
|
||||
>
|
||||
{event.views.toLocaleString()}
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
color: colors.gray[600],
|
||||
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
|
||||
}}
|
||||
>
|
||||
회
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
mb: 0.5,
|
||||
color: colors.gray[600],
|
||||
fontWeight: 500,
|
||||
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
|
||||
}}
|
||||
>
|
||||
ROI
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: colors.mint,
|
||||
fontSize: { xs: '1rem', sm: '1.5rem' },
|
||||
}}
|
||||
>
|
||||
{Math.round(event.roi * 100) / 100}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</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>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
})()}
|
||||
</Box>
|
||||
|
||||
{/* Recent Activity */}
|
||||
@ -570,11 +708,22 @@ export default function HomePage() {
|
||||
<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' } }}
|
||||
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' } }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: colors.gray[500],
|
||||
fontSize: { xs: '0.75rem', sm: '0.875rem' },
|
||||
}}
|
||||
>
|
||||
{activity.time}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@ -11,6 +11,12 @@ export async function GET(
|
||||
const token = request.headers.get('Authorization');
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
console.log('📊 [Analytics Proxy] Get user channels request:', {
|
||||
userId,
|
||||
hasToken: !!token,
|
||||
params: Object.fromEntries(searchParams),
|
||||
});
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
@ -22,6 +28,8 @@ export async function GET(
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${ANALYTICS_HOST}/api/v1/analytics/users/${userId}/analytics/channels${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
console.log('🔗 [Analytics Proxy] Calling backend URL:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
@ -29,6 +37,12 @@ export async function GET(
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('✅ [Analytics Proxy] User channels response:', {
|
||||
status: response.status,
|
||||
success: response.ok,
|
||||
dataSource: data?.data?.dataSource,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
|
||||
@ -11,6 +11,12 @@ export async function GET(
|
||||
const token = request.headers.get('Authorization');
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
console.log('📊 [Analytics Proxy] Get user ROI request:', {
|
||||
userId,
|
||||
hasToken: !!token,
|
||||
params: Object.fromEntries(searchParams),
|
||||
});
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
@ -22,6 +28,8 @@ export async function GET(
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${ANALYTICS_HOST}/api/v1/analytics/users/${userId}/analytics/roi${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
console.log('🔗 [Analytics Proxy] Calling backend URL:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
@ -29,6 +37,12 @@ export async function GET(
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('✅ [Analytics Proxy] User ROI response:', {
|
||||
status: response.status,
|
||||
success: response.ok,
|
||||
dataSource: data?.data?.dataSource,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
|
||||
@ -11,6 +11,12 @@ export async function GET(
|
||||
const token = request.headers.get('Authorization');
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
console.log('📊 [Analytics Proxy] Get user timeline request:', {
|
||||
userId,
|
||||
hasToken: !!token,
|
||||
params: Object.fromEntries(searchParams),
|
||||
});
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
@ -22,6 +28,8 @@ export async function GET(
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${ANALYTICS_HOST}/api/v1/analytics/users/${userId}/analytics/timeline${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
console.log('🔗 [Analytics Proxy] Calling backend URL:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
@ -29,6 +37,12 @@ export async function GET(
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('✅ [Analytics Proxy] User timeline response:', {
|
||||
status: response.status,
|
||||
success: response.ok,
|
||||
dataSource: data?.data?.dataSource,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
|
||||
88
src/shared/mock/eventsMockData.ts
Normal file
88
src/shared/mock/eventsMockData.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import type { EventStatus as ApiEventStatus } from '@/entities/event/model/types';
|
||||
|
||||
export interface MockEvent {
|
||||
eventId: string;
|
||||
eventName: string;
|
||||
status: ApiEventStatus;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
participants: number;
|
||||
targetParticipants: number;
|
||||
roi: number;
|
||||
createdAt: string;
|
||||
aiRecommendations: Array<{
|
||||
reward: string;
|
||||
participationMethod: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const mockEvents: MockEvent[] = [
|
||||
{
|
||||
eventId: 'evt_2025012301',
|
||||
eventName: '신규 고객 환영 이벤트',
|
||||
status: 'PUBLISHED',
|
||||
startDate: '2025-01-23',
|
||||
endDate: '2025-02-23',
|
||||
participants: 1257,
|
||||
targetParticipants: 2000,
|
||||
roi: 320,
|
||||
createdAt: '2025-01-15T00:00:00',
|
||||
aiRecommendations: [
|
||||
{
|
||||
reward: '스타벅스 아메리카노 (5명)',
|
||||
participationMethod: '전화번호 입력',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
eventId: 'evt_2025011502',
|
||||
eventName: '재방문 고객 감사 이벤트',
|
||||
status: 'PUBLISHED',
|
||||
startDate: '2025-01-15',
|
||||
endDate: '2025-02-15',
|
||||
participants: 60,
|
||||
targetParticipants: 1000,
|
||||
roi: 280,
|
||||
createdAt: '2025-01-10T00:00:00',
|
||||
aiRecommendations: [
|
||||
{
|
||||
reward: '커피 쿠폰 (10명)',
|
||||
participationMethod: 'SNS 팔로우',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
eventId: 'evt_2025010803',
|
||||
eventName: '신년 특별 할인 이벤트',
|
||||
status: 'ENDED',
|
||||
startDate: '2025-01-01',
|
||||
endDate: '2025-01-08',
|
||||
participants: 2500,
|
||||
targetParticipants: 2000,
|
||||
roi: 450,
|
||||
createdAt: '2024-12-28T00:00:00',
|
||||
aiRecommendations: [
|
||||
{
|
||||
reward: '10% 할인 쿠폰 (선착순 100명)',
|
||||
participationMethod: '구매 인증',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
eventId: 'evt_2025020104',
|
||||
eventName: '2월 신메뉴 출시 기념',
|
||||
status: 'DRAFT',
|
||||
startDate: '2025-02-01',
|
||||
endDate: '2025-02-28',
|
||||
participants: 0,
|
||||
targetParticipants: 1500,
|
||||
roi: 0,
|
||||
createdAt: '2025-01-25T00:00:00',
|
||||
aiRecommendations: [
|
||||
{
|
||||
reward: '신메뉴 무료 쿠폰 (20명)',
|
||||
participationMethod: '이메일 등록',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
Loading…
x
Reference in New Issue
Block a user