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