diff --git a/src/app/(main)/analytics/page.tsx b/src/app/(main)/analytics/page.tsx index 99dc7c2..d988bdb 100644 --- a/src/app/(main)/analytics/page.tsx +++ b/src/app/(main)/analytics/page.tsx @@ -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(null); const [timelineData, setTimelineData] = useState(null); const [roiData, setRoiData] = useState(null); + const [channelData, setChannelData] = useState(null); const [lastUpdate, setLastUpdate] = useState(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 ( <>
@@ -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 && ( - {channelPerformance.map((item) => ( - - - - - {item.channel} + {channelPerformance.map((item) => { + const hasParticipants = channelPerformance.some(ch => ch.participants > 0); + const unit = hasParticipants ? '명' : '노출'; + + return ( + + + + + {item.channel} + + + + {item.percentage}% ({item.displayValue.toLocaleString()}{unit}) - - {item.percentage}% ({item.participants}명) - - - ))} + ); + })} )} - {/* Time Trend */} - + {/* Time Trend - 임시 주석처리 */} + {/* @@ -677,7 +715,6 @@ export default function AnalyticsPage() { - {/* Line Chart */} + {!hasTimelineData && ( + + + + 시간대별 데이터가 수집되면 차트가 표시됩니다. + + + )} `${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() { /> - {/* Stats */} - 피크 시간: {timePerformance.peakTime} ({timePerformance.peakParticipants}명) + 피크 시간: {timePerformance.peakTime} (참여 {timePerformance.peakParticipants}명, 조회 {timePerformance.peakViews}) - 평균 시간당: {timePerformance.avgPerHour}명 + 평균 시간당: 참여 {timePerformance.avgPerHour}명 / 조회 {timePerformance.avgViewsPerHour} - + */} {/* ROI Detail & Participant Profile */} diff --git a/src/app/(main)/events/create/steps/ApprovalStep.tsx b/src/app/(main)/events/create/steps/ApprovalStep.tsx index ee59caf..2937336 100644 --- a/src/app/(main)/events/create/steps/ApprovalStep.tsx +++ b/src/app/(main)/events/create/steps/ApprovalStep.tsx @@ -156,12 +156,12 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS }; return ( - - + + {/* Header */} - - - + + + 최종 승인 @@ -357,7 +357,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS - + @@ -375,7 +375,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS - + @@ -393,7 +393,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS - + @@ -413,7 +413,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS - + {getChannelNames(eventData.channels).map((channel) => ( - + {/* Action Buttons */} - + - + 이벤트에 어울리는 스타일을 선택하세요 handleStyleSelect(e.target.value as 'SIMPLE' | 'FANCY' | 'TRENDY')}> - + {imageStyles.map((style) => ( {/* 스타일 이름 */} - - + + {style.name} @@ -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({ {/* 크게보기 버튼 */} - + - {!loading && eventPerformances.length === 0 ? ( - - - - - event_busy - - - - 진행 중인 이벤트가 없습니다 - - - 새로운 이벤트를 만들어 고객과 소통해보세요 - - - - - ) : !loading && ( - - {eventPerformances.slice(0, 2).map((event) => ( - handleEventClick(event.eventId)} - > - - - - {event.eventTitle} - + {(() => { + // 표시할 이벤트를 최대 2개로 제한 + const displayEvents = (eventPerformances.length > 0 + ? eventPerformances + : activeMockEvents + ).slice(0, 2); + + return !loading && displayEvents.length === 0 ? ( + + + + + event_busy + + + + 진행 중인 이벤트가 없습니다 + + + 새로운 이벤트를 만들어 고객과 소통해보세요 + + + + + ) : ( + !loading && ( + + {displayEvents.map((event) => ( + handleEventClick(event.eventId)} + > + - {event.status} - - - - - 참여자 + {event.eventTitle} - - {event.participants.toLocaleString()} + {event.status} + + + + - 명 + 참여자 - - - - - 조회수 - - - {event.views.toLocaleString()} - 회 + {event.participants.toLocaleString()} + + 명 + - + + + + 조회수 + + + {event.views.toLocaleString()} + + 회 + + + + + + ROI + + + {Math.round(event.roi * 100) / 100}% + + - - - ROI - - - {Math.round(event.roi * 100) / 100}% - - - - - - ))} - - )} + + + ))} + + ) + ); + })()} {/* Recent Activity */} @@ -570,11 +708,22 @@ export default function HomePage() { {activity.text} - + {activity.time} diff --git a/src/app/api/analytics/users/[userId]/channels/route.ts b/src/app/api/analytics/users/[userId]/channels/route.ts index 00b1ae5..219100b 100644 --- a/src/app/api/analytics/users/[userId]/channels/route.ts +++ b/src/app/api/analytics/users/[userId]/channels/route.ts @@ -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 }); } diff --git a/src/app/api/analytics/users/[userId]/roi/route.ts b/src/app/api/analytics/users/[userId]/roi/route.ts index bfe71a7..a1f5cf0 100644 --- a/src/app/api/analytics/users/[userId]/roi/route.ts +++ b/src/app/api/analytics/users/[userId]/roi/route.ts @@ -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 }); } diff --git a/src/app/api/analytics/users/[userId]/timeline/route.ts b/src/app/api/analytics/users/[userId]/timeline/route.ts index 63bd695..4e6dac0 100644 --- a/src/app/api/analytics/users/[userId]/timeline/route.ts +++ b/src/app/api/analytics/users/[userId]/timeline/route.ts @@ -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 }); } diff --git a/src/shared/mock/eventsMockData.ts b/src/shared/mock/eventsMockData.ts new file mode 100644 index 0000000..ec74179 --- /dev/null +++ b/src/shared/mock/eventsMockData.ts @@ -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: '이메일 등록', + }, + ], + }, +];