mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2025-12-06 12:16:24 +00:00
Compare commits
No commits in common. "948eb06e7133ab43736e72fb2cd2341e768c59b1" and "06da17ac3600b9ee5fd647251ecf89391631cb48" have entirely different histories.
948eb06e71
...
06da17ac36
@ -687,8 +687,8 @@ export default function EventDetailPage() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Chart Section - 참여 추이 - 임시 주석처리 */}
|
{/* Chart Section - 참여 추이 */}
|
||||||
{/* <Box sx={{ mb: { xs: 5, sm: 10 } }}>
|
<Box sx={{ mb: { xs: 5, sm: 10 } }}>
|
||||||
<Typography variant="h5" sx={{ fontWeight: 700, mb: { xs: 3, sm: 6 }, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
|
<Typography variant="h5" sx={{ fontWeight: 700, mb: { xs: 3, sm: 6 }, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
|
||||||
📈 참여 추이
|
📈 참여 추이
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -757,7 +757,7 @@ export default function EventDetailPage() {
|
|||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Box> */}
|
</Box>
|
||||||
|
|
||||||
{/* Chart Section - 채널별 성과 & ROI 추이 */}
|
{/* Chart Section - 채널별 성과 & ROI 추이 */}
|
||||||
<Grid container spacing={{ xs: 2, sm: 6 }} sx={{ mb: { xs: 5, sm: 10 } }}>
|
<Grid container spacing={{ xs: 2, sm: 6 }} sx={{ mb: { xs: 5, sm: 10 } }}>
|
||||||
|
|||||||
@ -3,13 +3,11 @@
|
|||||||
import { useFunnel } from '@use-funnel/browser';
|
import { useFunnel } from '@use-funnel/browser';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import ObjectiveStep from './steps/ObjectiveStep';
|
import ObjectiveStep from './steps/ObjectiveStep';
|
||||||
import LoadingStep from './steps/LoadingStep';
|
|
||||||
import RecommendationStep from './steps/RecommendationStep';
|
import RecommendationStep from './steps/RecommendationStep';
|
||||||
import ContentPreviewStep from './steps/ContentPreviewStep';
|
import ContentPreviewStep from './steps/ContentPreviewStep';
|
||||||
import ContentEditStep from './steps/ContentEditStep';
|
import ContentEditStep from './steps/ContentEditStep';
|
||||||
import ChannelStep from './steps/ChannelStep';
|
import ChannelStep from './steps/ChannelStep';
|
||||||
import ApprovalStep from './steps/ApprovalStep';
|
import ApprovalStep from './steps/ApprovalStep';
|
||||||
import type { AiRecommendationResult } from '@/shared/api/eventApi';
|
|
||||||
|
|
||||||
// 이벤트 생성 데이터 타입
|
// 이벤트 생성 데이터 타입
|
||||||
export type EventObjective = 'new_customer' | 'revisit' | 'sales' | 'awareness';
|
export type EventObjective = 'new_customer' | 'revisit' | 'sales' | 'awareness';
|
||||||
@ -20,7 +18,6 @@ export interface EventData {
|
|||||||
eventDraftId?: number;
|
eventDraftId?: number;
|
||||||
eventId?: string;
|
eventId?: string;
|
||||||
objective?: EventObjective;
|
objective?: EventObjective;
|
||||||
aiResult?: AiRecommendationResult;
|
|
||||||
recommendation?: {
|
recommendation?: {
|
||||||
recommendation: {
|
recommendation: {
|
||||||
optionNumber: number;
|
optionNumber: number;
|
||||||
@ -77,7 +74,6 @@ export default function EventCreatePage() {
|
|||||||
|
|
||||||
const funnel = useFunnel<{
|
const funnel = useFunnel<{
|
||||||
objective: EventData;
|
objective: EventData;
|
||||||
loading: EventData;
|
|
||||||
recommendation: EventData;
|
recommendation: EventData;
|
||||||
contentPreview: EventData;
|
contentPreview: EventData;
|
||||||
contentEdit: EventData;
|
contentEdit: EventData;
|
||||||
@ -101,27 +97,13 @@ export default function EventCreatePage() {
|
|||||||
objective={({ history }) => (
|
objective={({ history }) => (
|
||||||
<ObjectiveStep
|
<ObjectiveStep
|
||||||
onNext={({ objective, eventId }) => {
|
onNext={({ objective, eventId }) => {
|
||||||
history.push('loading', { objective, eventId });
|
history.push('recommendation', { objective, eventId });
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
loading={({ context, history }) => (
|
|
||||||
<LoadingStep
|
|
||||||
eventId={context.eventId!}
|
|
||||||
onComplete={(aiResult) => {
|
|
||||||
history.push('recommendation', { ...context, aiResult });
|
|
||||||
}}
|
|
||||||
onError={(error) => {
|
|
||||||
console.error('❌ AI 추천 생성 실패:', error);
|
|
||||||
alert(error);
|
|
||||||
history.go(-1); // ObjectiveStep으로 돌아가기
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
recommendation={({ context, history }) => (
|
recommendation={({ context, history }) => (
|
||||||
<RecommendationStep
|
<RecommendationStep
|
||||||
eventId={context.eventId}
|
eventId={context.eventId}
|
||||||
aiResult={context.aiResult}
|
|
||||||
onNext={(recommendation) => {
|
onNext={(recommendation) => {
|
||||||
history.push('channel', { ...context, recommendation });
|
history.push('channel', { ...context, recommendation });
|
||||||
}}
|
}}
|
||||||
@ -140,18 +122,13 @@ export default function EventCreatePage() {
|
|||||||
|
|
||||||
if (needsContent) {
|
if (needsContent) {
|
||||||
// localStorage에 이벤트 정보 저장
|
// localStorage에 이벤트 정보 저장
|
||||||
const baseTrends = context.recommendation?.recommendation.promotionChannels || [];
|
|
||||||
const requiredTrends = ['Samgyupsal', '삼겹살', 'Korean Pork BBQ'];
|
|
||||||
// 중복 제거하면서 필수 trends 추가
|
|
||||||
const allTrends = [...new Set([...requiredTrends, ...baseTrends])];
|
|
||||||
|
|
||||||
const eventData = {
|
const eventData = {
|
||||||
eventDraftId: context.recommendation?.eventId || String(Date.now()), // eventId 사용
|
eventDraftId: context.recommendation?.eventId || String(Date.now()), // eventId 사용
|
||||||
eventTitle: context.recommendation?.recommendation.title || '',
|
eventTitle: context.recommendation?.recommendation.title || '',
|
||||||
eventDescription: context.recommendation?.recommendation.description || '',
|
eventDescription: context.recommendation?.recommendation.description || '',
|
||||||
industry: '',
|
industry: '',
|
||||||
location: '',
|
location: '',
|
||||||
trends: allTrends,
|
trends: context.recommendation?.recommendation.promotionChannels || [],
|
||||||
prize: '',
|
prize: '',
|
||||||
};
|
};
|
||||||
localStorage.setItem('eventCreationData', JSON.stringify(eventData));
|
localStorage.setItem('eventCreationData', JSON.stringify(eventData));
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Container,
|
Container,
|
||||||
@ -17,27 +17,12 @@ import {
|
|||||||
DialogActions,
|
DialogActions,
|
||||||
Link,
|
Link,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import { ArrowBack, CheckCircle, Edit, RocketLaunch, Save, People, AttachMoney, TrendingUp } from '@mui/icons-material';
|
||||||
ArrowBack,
|
|
||||||
CheckCircle,
|
|
||||||
RocketLaunch,
|
|
||||||
Save,
|
|
||||||
People,
|
|
||||||
AttachMoney,
|
|
||||||
TrendingUp,
|
|
||||||
} from '@mui/icons-material';
|
|
||||||
import { EventData } from '../page';
|
import { EventData } from '../page';
|
||||||
import { cardStyles, colors, responsiveText } from '@/shared/lib/button-styles';
|
import { cardStyles, colors, responsiveText } from '@/shared/lib/button-styles';
|
||||||
|
import { eventApi } from '@/entities/event/api/eventApi';
|
||||||
|
import type { EventObjective } from '@/entities/event/model/types';
|
||||||
|
|
||||||
interface EventCreationData {
|
|
||||||
eventDraftId: string;
|
|
||||||
eventTitle: string;
|
|
||||||
eventDescription: string;
|
|
||||||
industry: string;
|
|
||||||
location: string;
|
|
||||||
trends: string[];
|
|
||||||
prize: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApprovalStepProps {
|
interface ApprovalStepProps {
|
||||||
eventData: EventData;
|
eventData: EventData;
|
||||||
@ -50,37 +35,26 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
const [termsDialogOpen, setTermsDialogOpen] = useState(false);
|
const [termsDialogOpen, setTermsDialogOpen] = useState(false);
|
||||||
const [successDialogOpen, setSuccessDialogOpen] = useState(false);
|
const [successDialogOpen, setSuccessDialogOpen] = useState(false);
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
const [localStorageData, setLocalStorageData] = useState<EventCreationData | null>(null);
|
const DISTRIBUTION_API_BASE_URL = process.env.NEXT_PUBLIC_DISTRIBUTION_HOST || 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const storedData = localStorage.getItem('eventCreationData');
|
|
||||||
if (storedData) {
|
|
||||||
setLocalStorageData(JSON.parse(storedData));
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleApprove = async () => {
|
const handleApprove = async () => {
|
||||||
if (!agreeTerms) return;
|
if (!agreeTerms) return;
|
||||||
|
|
||||||
setIsDeploying(true);
|
setIsDeploying(true);
|
||||||
|
|
||||||
// API 호출 임시 주석처리 - 배포 성공 시뮬레이션
|
|
||||||
/*
|
|
||||||
try {
|
try {
|
||||||
// 1. 이벤트 생성 API 호출
|
// 1. 이벤트 생성 API 호출
|
||||||
console.log('📞 Creating event with objective:', eventData.objective);
|
console.log('📞 Creating event with objective:', eventData.objective);
|
||||||
|
|
||||||
// objective 매핑 (Frontend → Backend)
|
// objective 매핑 (Frontend → Backend)
|
||||||
const objectiveMap: Record<string, EventObjective> = {
|
const objectiveMap: Record<string, EventObjective> = {
|
||||||
new_customer: 'CUSTOMER_ACQUISITION',
|
'new_customer': 'CUSTOMER_ACQUISITION',
|
||||||
revisit: 'Customer Retention',
|
'revisit': 'Customer Retention',
|
||||||
sales: 'Sales Promotion',
|
'sales': 'Sales Promotion',
|
||||||
awareness: 'awareness',
|
'awareness': 'awareness',
|
||||||
};
|
};
|
||||||
|
|
||||||
const backendObjective: EventObjective = (objectiveMap[
|
const backendObjective: EventObjective = (objectiveMap[eventData.objective || 'new_customer'] || 'CUSTOMER_ACQUISITION') as EventObjective;
|
||||||
eventData.objective || 'new_customer'
|
|
||||||
] || 'CUSTOMER_ACQUISITION') as EventObjective;
|
|
||||||
|
|
||||||
const createResponse = await eventApi.createEvent({
|
const createResponse = await eventApi.createEvent({
|
||||||
objective: backendObjective,
|
objective: backendObjective,
|
||||||
@ -96,10 +70,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
console.log('📞 Updating event details:', eventId);
|
console.log('📞 Updating event details:', eventId);
|
||||||
|
|
||||||
// 이벤트명 가져오기 (contentEdit.title 또는 recommendation.title)
|
// 이벤트명 가져오기 (contentEdit.title 또는 recommendation.title)
|
||||||
const eventName =
|
const eventName = eventData.contentEdit?.title || eventData.recommendation?.recommendation?.title || '이벤트';
|
||||||
eventData.contentEdit?.title ||
|
|
||||||
eventData.recommendation?.recommendation?.title ||
|
|
||||||
'이벤트';
|
|
||||||
|
|
||||||
// 날짜 설정 (오늘부터 30일간)
|
// 날짜 설정 (오늘부터 30일간)
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
@ -111,10 +82,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
|
|
||||||
await eventApi.updateEvent(eventId, {
|
await eventApi.updateEvent(eventId, {
|
||||||
eventName: eventName,
|
eventName: eventName,
|
||||||
description:
|
description: eventData.contentEdit?.guide || eventData.recommendation?.recommendation?.description || '',
|
||||||
eventData.contentEdit?.guide ||
|
|
||||||
eventData.recommendation?.recommendation?.description ||
|
|
||||||
'',
|
|
||||||
startDate: startDateStr,
|
startDate: startDateStr,
|
||||||
endDate: endDateStr,
|
endDate: endDateStr,
|
||||||
});
|
});
|
||||||
@ -128,15 +96,12 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
sns: ['INSTAGRAM', 'NAVER', 'KAKAO'],
|
sns: ['INSTAGRAM', 'NAVER', 'KAKAO'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiChannels = eventData.channels?.flatMap((ch) => channelMap[ch] || []) || [];
|
const apiChannels = eventData.channels?.flatMap(ch => channelMap[ch] || []) || [];
|
||||||
|
|
||||||
const distributionRequest = {
|
const distributionRequest = {
|
||||||
eventId: eventId,
|
eventId: eventId,
|
||||||
title: eventName,
|
title: eventName,
|
||||||
description:
|
description: eventData.contentEdit?.guide || eventData.recommendation?.recommendation?.description || '',
|
||||||
eventData.contentEdit?.guide ||
|
|
||||||
eventData.recommendation?.recommendation?.description ||
|
|
||||||
'',
|
|
||||||
imageUrl: '', // TODO: 이미지 URL 연동 필요
|
imageUrl: '', // TODO: 이미지 URL 연동 필요
|
||||||
channels: apiChannels,
|
channels: apiChannels,
|
||||||
channelSettings: {},
|
channelSettings: {},
|
||||||
@ -144,16 +109,13 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
|
|
||||||
console.log('🚀 Distributing event:', distributionRequest);
|
console.log('🚀 Distributing event:', distributionRequest);
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(`${DISTRIBUTION_API_BASE_URL}/api/v1/distribution/distribute`, {
|
||||||
`${DISTRIBUTION_API_BASE_URL}/api/v1/distribution/distribute`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(distributionRequest),
|
body: JSON.stringify(distributionRequest),
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
@ -165,6 +127,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
|
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
setSuccessDialogOpen(true);
|
setSuccessDialogOpen(true);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Event creation failed: No event ID returned');
|
throw new Error('Event creation failed: No event ID returned');
|
||||||
}
|
}
|
||||||
@ -173,15 +136,9 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
alert('이벤트 배포에 실패했습니다. 다시 시도해 주세요.');
|
alert('이벤트 배포에 실패했습니다. 다시 시도해 주세요.');
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
// 배포 시뮬레이션 (2초 후 성공)
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsDeploying(false);
|
|
||||||
setSuccessDialogOpen(true);
|
|
||||||
}, 2000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleSaveDraft = () => {
|
const handleSaveDraft = () => {
|
||||||
// TODO: 임시저장 API 연동
|
// TODO: 임시저장 API 연동
|
||||||
alert('임시저장되었습니다');
|
alert('임시저장되었습니다');
|
||||||
@ -199,27 +156,10 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box sx={{ minHeight: '100vh', bgcolor: colors.gray[50], pt: { xs: 5, sm: 8 }, pb: { xs: 3, sm: 10 } }}>
|
||||||
sx={{
|
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 3, sm: 6 }, px: { xs: 1.5, sm: 8, md: 10 } }}>
|
||||||
minHeight: '100vh',
|
|
||||||
bgcolor: colors.gray[50],
|
|
||||||
pt: { xs: 5, sm: 8 },
|
|
||||||
pb: { xs: 3, sm: 10 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Container
|
|
||||||
maxWidth="lg"
|
|
||||||
sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 3, sm: 6 }, px: { xs: 2, sm: 4, md: 8 } }}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Box
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, mb: { xs: 3, sm: 8 } }}>
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: { xs: 1.5, sm: 3 },
|
|
||||||
mb: { xs: 3, sm: 8 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
||||||
<ArrowBack sx={{ fontSize: 20 }} />
|
<ArrowBack sx={{ fontSize: 20 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -240,7 +180,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Event Summary Statistics */}
|
{/* Event Summary Statistics */}
|
||||||
<Grid container spacing={{ xs: 2, sm: 3, md: 4 }} sx={{ mb: { xs: 4, sm: 10 } }}>
|
<Grid container spacing={4} sx={{ mb: { xs: 4, sm: 10 } }}>
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
<Grid item xs={12} sm={6} md={3}>
|
||||||
<Card
|
<Card
|
||||||
elevation={0}
|
elevation={0}
|
||||||
@ -250,24 +190,19 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
borderColor: 'transparent',
|
borderColor: 'transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardContent sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}>
|
<CardContent sx={{ textAlign: 'center', py: 4, px: 3 }}>
|
||||||
<CheckCircle
|
<CheckCircle sx={{
|
||||||
sx={{
|
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
color: colors.gray[900],
|
color: colors.gray[900],
|
||||||
mb: 1,
|
mb: 1,
|
||||||
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
|
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
|
||||||
}}
|
}} />
|
||||||
/>
|
<Typography variant="body2" sx={{
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
color: colors.gray[700],
|
color: colors.gray[700],
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
mb: 1,
|
mb: 1,
|
||||||
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
|
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
이벤트 제목
|
이벤트 제목
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
@ -275,14 +210,11 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
sx={{
|
sx={{
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: colors.gray[900],
|
color: colors.gray[900],
|
||||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
fontSize: '1rem',
|
||||||
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
||||||
wordBreak: 'keep-all',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{localStorageData?.eventTitle ||
|
{eventData.recommendation?.recommendation.title || '이벤트 제목'}
|
||||||
eventData.recommendation?.recommendation.title ||
|
|
||||||
'이벤트 제목'}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -296,24 +228,19 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
borderColor: 'transparent',
|
borderColor: 'transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardContent sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}>
|
<CardContent sx={{ textAlign: 'center', py: 4, px: 3 }}>
|
||||||
<People
|
<People sx={{
|
||||||
sx={{
|
fontSize: 32,
|
||||||
fontSize: { xs: 28, sm: 32 },
|
|
||||||
color: colors.gray[900],
|
color: colors.gray[900],
|
||||||
mb: 1,
|
mb: 1,
|
||||||
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
|
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
|
||||||
}}
|
}} />
|
||||||
/>
|
<Typography variant="body2" sx={{
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
color: colors.gray[700],
|
color: colors.gray[700],
|
||||||
fontSize: { xs: '0.75rem', sm: '0.875rem' },
|
fontSize: '0.875rem',
|
||||||
mb: 1,
|
mb: 1,
|
||||||
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
|
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
목표 참여자
|
목표 참여자
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
@ -321,20 +248,17 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
sx={{
|
sx={{
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: colors.gray[900],
|
color: colors.gray[900],
|
||||||
fontSize: { xs: '1.25rem', sm: '1.75rem' },
|
fontSize: '1.75rem',
|
||||||
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{eventData.recommendation?.recommendation.expectedMetrics.newCustomers.max || 0}
|
{eventData.recommendation?.recommendation.expectedMetrics.newCustomers.max || 0}
|
||||||
<Typography
|
<Typography component="span" sx={{
|
||||||
component="span"
|
fontSize: '1rem',
|
||||||
sx={{
|
|
||||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
|
||||||
ml: 0.5,
|
ml: 0.5,
|
||||||
color: colors.gray[900],
|
color: colors.gray[900],
|
||||||
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
명
|
명
|
||||||
</Typography>
|
</Typography>
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -350,24 +274,19 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
borderColor: 'transparent',
|
borderColor: 'transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardContent sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}>
|
<CardContent sx={{ textAlign: 'center', py: 4, px: 3 }}>
|
||||||
<AttachMoney
|
<AttachMoney sx={{
|
||||||
sx={{
|
fontSize: 32,
|
||||||
fontSize: { xs: 28, sm: 32 },
|
|
||||||
color: colors.gray[900],
|
color: colors.gray[900],
|
||||||
mb: 1,
|
mb: 1,
|
||||||
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
|
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
|
||||||
}}
|
}} />
|
||||||
/>
|
<Typography variant="body2" sx={{
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
color: colors.gray[700],
|
color: colors.gray[700],
|
||||||
fontSize: { xs: '0.75rem', sm: '0.875rem' },
|
fontSize: '0.875rem',
|
||||||
mb: 1,
|
mb: 1,
|
||||||
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
|
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
예상 비용
|
예상 비용
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
@ -375,22 +294,17 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
sx={{
|
sx={{
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: colors.gray[900],
|
color: colors.gray[900],
|
||||||
fontSize: { xs: '1.25rem', sm: '1.75rem' },
|
fontSize: '1.75rem',
|
||||||
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(
|
{((eventData.recommendation?.recommendation.estimatedCost.max || 0) / 10000).toFixed(0)}
|
||||||
(eventData.recommendation?.recommendation.estimatedCost.max || 0) / 10000
|
<Typography component="span" sx={{
|
||||||
).toFixed(0)}
|
fontSize: '1rem',
|
||||||
<Typography
|
|
||||||
component="span"
|
|
||||||
sx={{
|
|
||||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
|
||||||
ml: 0.5,
|
ml: 0.5,
|
||||||
color: colors.gray[900],
|
color: colors.gray[900],
|
||||||
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
만원
|
만원
|
||||||
</Typography>
|
</Typography>
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -406,24 +320,19 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
borderColor: 'transparent',
|
borderColor: 'transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardContent sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}>
|
<CardContent sx={{ textAlign: 'center', py: 4, px: 3 }}>
|
||||||
<TrendingUp
|
<TrendingUp sx={{
|
||||||
sx={{
|
fontSize: 32,
|
||||||
fontSize: { xs: 28, sm: 32 },
|
|
||||||
color: colors.gray[900],
|
color: colors.gray[900],
|
||||||
mb: 1,
|
mb: 1,
|
||||||
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
|
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
|
||||||
}}
|
}} />
|
||||||
/>
|
<Typography variant="body2" sx={{
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
color: colors.gray[700],
|
color: colors.gray[700],
|
||||||
fontSize: { xs: '0.75rem', sm: '0.875rem' },
|
fontSize: '0.875rem',
|
||||||
mb: 1,
|
mb: 1,
|
||||||
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
|
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
예상 ROI
|
예상 ROI
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
@ -431,7 +340,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
sx={{
|
sx={{
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: colors.gray[900],
|
color: colors.gray[900],
|
||||||
fontSize: { xs: '1.25rem', sm: '1.75rem' },
|
fontSize: '1.75rem',
|
||||||
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -448,62 +357,53 @@ 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: 3, sm: 6 } }}>
|
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||||
<Typography
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||||
variant="caption"
|
<Box sx={{ flex: 1 }}>
|
||||||
color="text.secondary"
|
<Typography variant="caption" color="text.secondary" sx={{ ...responsiveText.body2, fontSize: '0.875rem' }}>
|
||||||
sx={{ ...responsiveText.body2, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
|
||||||
>
|
|
||||||
이벤트 제목
|
이벤트 제목
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography variant="body1" sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1 }}>
|
||||||
variant="body1"
|
{eventData.recommendation?.recommendation.title}
|
||||||
sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1, wordBreak: 'keep-all' }}
|
|
||||||
>
|
|
||||||
{localStorageData?.eventTitle ||
|
|
||||||
eventData.recommendation?.recommendation.title ||
|
|
||||||
'이벤트 제목'}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<IconButton size="small">
|
||||||
|
<Edit fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card elevation={0} sx={{ ...cardStyles.default, mb: 4 }}>
|
<Card elevation={0} sx={{ ...cardStyles.default, mb: 4 }}>
|
||||||
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
|
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||||
<Typography
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||||
variant="caption"
|
<Box sx={{ flex: 1 }}>
|
||||||
color="text.secondary"
|
<Typography variant="caption" color="text.secondary" sx={{ ...responsiveText.body2, fontSize: '0.875rem' }}>
|
||||||
sx={{ ...responsiveText.body2, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
|
||||||
>
|
|
||||||
경품
|
경품
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography variant="body1" sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1 }}>
|
||||||
variant="body1"
|
{eventData.recommendation?.recommendation.mechanics.details || ''}
|
||||||
sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1, wordBreak: 'keep-all' }}
|
|
||||||
>
|
|
||||||
{localStorageData?.prize ||
|
|
||||||
eventData.recommendation?.recommendation.mechanics.details ||
|
|
||||||
''}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<IconButton size="small">
|
||||||
|
<Edit fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</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: 3, sm: 6 } }}>
|
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||||
<Typography
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||||
variant="caption"
|
<Box sx={{ flex: 1 }}>
|
||||||
color="text.secondary"
|
<Typography variant="caption" color="text.secondary" sx={{ ...responsiveText.body2, fontSize: '0.875rem' }}>
|
||||||
sx={{ ...responsiveText.body2, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
|
||||||
>
|
|
||||||
참여 방법
|
참여 방법
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography variant="body1" sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1 }}>
|
||||||
variant="body1"
|
{eventData.recommendation?.recommendation.mechanics.details || ''}
|
||||||
sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1, wordBreak: 'keep-all' }}
|
|
||||||
>
|
|
||||||
{localStorageData?.eventDescription ||
|
|
||||||
eventData.recommendation?.recommendation.mechanics.details ||
|
|
||||||
''}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -513,8 +413,8 @@ 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: 3, sm: 6 } }}>
|
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: { xs: 1.5, sm: 2 } }}>
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 4 }}>
|
||||||
{getChannelNames(eventData.channels).map((channel) => (
|
{getChannelNames(eventData.channels).map((channel) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={channel}
|
key={channel}
|
||||||
@ -523,22 +423,30 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
bgcolor: colors.purple,
|
bgcolor: colors.purple,
|
||||||
color: 'white',
|
color: 'white',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSize: { xs: '0.75rem', sm: '0.875rem' },
|
fontSize: '0.875rem',
|
||||||
px: { xs: 1.5, sm: 2 },
|
px: 2,
|
||||||
py: { xs: 2, sm: 2.5 },
|
py: 2.5,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
startIcon={<Edit />}
|
||||||
|
sx={{
|
||||||
|
...responsiveText.body2,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: colors.purple,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
채널 수정하기
|
||||||
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Terms Agreement */}
|
{/* Terms Agreement */}
|
||||||
<Card
|
<Card elevation={0} sx={{ ...cardStyles.default, bgcolor: colors.gray[50], mb: { xs: 4, sm: 10 } }}>
|
||||||
elevation={0}
|
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||||
sx={{ ...cardStyles.default, bgcolor: colors.gray[50], mb: { xs: 4, sm: 10 } }}
|
|
||||||
>
|
|
||||||
<CardContent sx={{ p: { xs: 3, sm: 6 } }}>
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@ -553,8 +461,11 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
<Typography variant="body2" sx={{ ...responsiveText.body1, fontWeight: 400 }}>
|
<Typography variant="body2" sx={{ ...responsiveText.body1 }}>
|
||||||
이벤트 약관 및 개인정보 처리방침에 동의합니다{' '}
|
이벤트 약관 및 개인정보 처리방침에 동의합니다{' '}
|
||||||
|
<Typography component="span" sx={{ color: colors.orange, fontWeight: 600 }}>
|
||||||
|
(필수)
|
||||||
|
</Typography>
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -640,33 +551,21 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 3 }}>
|
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 3 }}>
|
||||||
제1조 (목적)
|
제1조 (목적)
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography variant="body2" sx={{ ...responsiveText.body1, mb: 6, color: colors.gray[700] }}>
|
||||||
variant="body2"
|
본 약관은 KT AI 이벤트 마케팅 서비스를 통해 진행되는 이벤트의 참여 및 개인정보 처리에 관한
|
||||||
sx={{ ...responsiveText.body1, mb: 6, color: colors.gray[700] }}
|
사항을 규정합니다.
|
||||||
>
|
|
||||||
본 약관은 KT AI 이벤트 마케팅 서비스를 통해 진행되는 이벤트의 참여 및 개인정보 처리에
|
|
||||||
관한 사항을 규정합니다.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 3 }}>
|
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 3 }}>
|
||||||
제2조 (개인정보 수집 및 이용)
|
제2조 (개인정보 수집 및 이용)
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography variant="body2" sx={{ ...responsiveText.body1, mb: 2, color: colors.gray[700] }}>
|
||||||
variant="body2"
|
|
||||||
sx={{ ...responsiveText.body1, mb: 2, color: colors.gray[700] }}
|
|
||||||
>
|
|
||||||
수집 항목: 이름, 전화번호, 이메일
|
수집 항목: 이름, 전화번호, 이메일
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography variant="body2" sx={{ ...responsiveText.body1, mb: 2, color: colors.gray[700] }}>
|
||||||
variant="body2"
|
|
||||||
sx={{ ...responsiveText.body1, mb: 2, color: colors.gray[700] }}
|
|
||||||
>
|
|
||||||
이용 목적: 이벤트 참여 확인 및 경품 제공
|
이용 목적: 이벤트 참여 확인 및 경품 제공
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography variant="body2" sx={{ ...responsiveText.body1, mb: 6, color: colors.gray[700] }}>
|
||||||
variant="body2"
|
|
||||||
sx={{ ...responsiveText.body1, mb: 6, color: colors.gray[700] }}
|
|
||||||
>
|
|
||||||
보유 기간: 이벤트 종료 후 6개월
|
보유 기간: 이벤트 종료 후 6개월
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
@ -717,11 +616,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
|
|||||||
<Typography variant="h5" sx={{ fontSize: '1.5rem', fontWeight: 700, mb: 3 }}>
|
<Typography variant="h5" sx={{ fontSize: '1.5rem', fontWeight: 700, mb: 3 }}>
|
||||||
배포 완료!
|
배포 완료!
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography variant="body1" color="text.secondary" sx={{ fontSize: '1rem', mb: { xs: 3, sm: 8 } }}>
|
||||||
variant="body1"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ fontSize: '1rem', mb: { xs: 3, sm: 8 } }}
|
|
||||||
>
|
|
||||||
이벤트가 성공적으로 배포되었습니다.
|
이벤트가 성공적으로 배포되었습니다.
|
||||||
<br />
|
<br />
|
||||||
실시간으로 참여자를 확인할 수 있습니다.
|
실시간으로 참여자를 확인할 수 있습니다.
|
||||||
|
|||||||
@ -14,8 +14,6 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
IconButton,
|
IconButton,
|
||||||
Radio,
|
|
||||||
RadioGroup,
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { ArrowBack } from '@mui/icons-material';
|
import { ArrowBack } from '@mui/icons-material';
|
||||||
|
|
||||||
@ -110,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: 2, sm: 8 }, pb: { xs: 3, 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: { xs: 1, sm: 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: { xs: 36, sm: 40 }, height: { xs: 36, sm: 40 }, p: { xs: 0.5, sm: 1 } }}>
|
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
||||||
<ArrowBack sx={{ fontSize: { xs: 18, sm: 20 } }} />
|
<ArrowBack sx={{ fontSize: 20 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1rem', sm: '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: 2.5, sm: 8 }, textAlign: 'center', fontSize: { xs: '0.75rem', sm: '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>
|
||||||
|
|
||||||
@ -129,8 +127,8 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
<Card
|
<Card
|
||||||
elevation={0}
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
mb: { xs: 2.5, sm: 6 },
|
mb: 6,
|
||||||
borderRadius: { xs: 3, sm: 4 },
|
borderRadius: 4,
|
||||||
border: channels[0].selected ? 2 : 1,
|
border: channels[0].selected ? 2 : 1,
|
||||||
borderColor: channels[0].selected ? colors.purple : 'divider',
|
borderColor: channels[0].selected ? colors.purple : 'divider',
|
||||||
bgcolor: channels[0].selected ? `${colors.purpleLight}40` : 'background.paper',
|
bgcolor: channels[0].selected ? `${colors.purpleLight}40` : 'background.paper',
|
||||||
@ -138,14 +136,13 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
transition: 'all 0.3s',
|
transition: 'all 0.3s',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardContent sx={{ p: { xs: 2.5, sm: 6 } }}>
|
<CardContent sx={{ p: { xs: 2, sm: 6 } }}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={channels[0].selected}
|
checked={channels[0].selected}
|
||||||
onChange={() => handleChannelToggle('uriTV')}
|
onChange={() => handleChannelToggle('uriTV')}
|
||||||
sx={{
|
sx={{
|
||||||
p: { xs: 0.5, sm: 1 },
|
|
||||||
color: colors.purple,
|
color: colors.purple,
|
||||||
'&.Mui-checked': {
|
'&.Mui-checked': {
|
||||||
color: colors.purple,
|
color: colors.purple,
|
||||||
@ -154,48 +151,46 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
<Typography variant="body1" sx={{ fontWeight: channels[0].selected ? 700 : 600, fontSize: { xs: '0.9375rem', sm: '1.125rem' } }}>
|
<Typography variant="body1" sx={{ fontWeight: channels[0].selected ? 700 : 600, fontSize: '1.125rem' }}>
|
||||||
우리동네TV
|
우리동네TV
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
sx={{ mb: channels[0].selected ? { xs: 1.5, sm: 2 } : 0 }}
|
sx={{ mb: channels[0].selected ? 2 : 0 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{channels[0].selected && (
|
{channels[0].selected && (
|
||||||
<Box sx={{ pl: { xs: 2.5, sm: 4 }, pt: { xs: 1.5, sm: 2 }, borderTop: 1, borderColor: 'divider' }}>
|
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}>
|
||||||
<FormControl fullWidth sx={{ mb: { xs: 1.5, sm: 2 } }}>
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||||
<InputLabel sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>반경</InputLabel>
|
<InputLabel>반경</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={getChannelConfig('uriTV', 'radius')}
|
value={getChannelConfig('uriTV', 'radius')}
|
||||||
onChange={(e) => handleConfigChange('uriTV', 'radius', e.target.value)}
|
onChange={(e) => handleConfigChange('uriTV', 'radius', e.target.value)}
|
||||||
label="반경"
|
label="반경"
|
||||||
sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}
|
|
||||||
>
|
>
|
||||||
<MenuItem value="500" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>500m</MenuItem>
|
<MenuItem value="500">500m</MenuItem>
|
||||||
<MenuItem value="1000" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>1km</MenuItem>
|
<MenuItem value="1000">1km</MenuItem>
|
||||||
<MenuItem value="2000" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>2km</MenuItem>
|
<MenuItem value="2000">2km</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormControl fullWidth sx={{ mb: { xs: 1.5, sm: 2 } }}>
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||||
<InputLabel sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>노출 시간대</InputLabel>
|
<InputLabel>노출 시간대</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={getChannelConfig('uriTV', 'time')}
|
value={getChannelConfig('uriTV', 'time')}
|
||||||
onChange={(e) => handleConfigChange('uriTV', 'time', e.target.value)}
|
onChange={(e) => handleConfigChange('uriTV', 'time', e.target.value)}
|
||||||
label="노출 시간대"
|
label="노출 시간대"
|
||||||
sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}
|
|
||||||
>
|
>
|
||||||
<MenuItem value="morning" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>아침 (7-12시)</MenuItem>
|
<MenuItem value="morning">아침 (7-12시)</MenuItem>
|
||||||
<MenuItem value="afternoon" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>점심 (12-17시)</MenuItem>
|
<MenuItem value="afternoon">점심 (12-17시)</MenuItem>
|
||||||
<MenuItem value="evening" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>저녁 (17-22시)</MenuItem>
|
<MenuItem value="evening">저녁 (17-22시)</MenuItem>
|
||||||
<MenuItem value="all" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>전체</MenuItem>
|
<MenuItem value="all">전체</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' }, mb: { xs: 0.5, sm: 0 } }}>
|
<Typography variant="body2" color="text.secondary">
|
||||||
예상 노출: <strong>5만명</strong>
|
예상 노출: <strong>5만명</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
|
<Typography variant="body2" color="text.secondary">
|
||||||
비용: <strong>8만원</strong>
|
비용: <strong>8만원</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@ -207,8 +202,8 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
<Card
|
<Card
|
||||||
elevation={0}
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
mb: { xs: 2.5, sm: 6 },
|
mb: 6,
|
||||||
borderRadius: { xs: 3, sm: 4 },
|
borderRadius: 4,
|
||||||
border: channels[1].selected ? 2 : 1,
|
border: channels[1].selected ? 2 : 1,
|
||||||
borderColor: channels[1].selected ? colors.purple : 'divider',
|
borderColor: channels[1].selected ? colors.purple : 'divider',
|
||||||
bgcolor: channels[1].selected ? `${colors.purpleLight}40` : 'background.paper',
|
bgcolor: channels[1].selected ? `${colors.purpleLight}40` : 'background.paper',
|
||||||
@ -216,14 +211,13 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
transition: 'all 0.3s',
|
transition: 'all 0.3s',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardContent sx={{ p: { xs: 2.5, sm: 6 } }}>
|
<CardContent sx={{ p: { xs: 2, sm: 6 } }}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={channels[1].selected}
|
checked={channels[1].selected}
|
||||||
onChange={() => handleChannelToggle('ringoBiz')}
|
onChange={() => handleChannelToggle('ringoBiz')}
|
||||||
sx={{
|
sx={{
|
||||||
p: { xs: 0.5, sm: 1 },
|
|
||||||
color: colors.purple,
|
color: colors.purple,
|
||||||
'&.Mui-checked': {
|
'&.Mui-checked': {
|
||||||
color: colors.purple,
|
color: colors.purple,
|
||||||
@ -232,32 +226,30 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
<Typography variant="body1" sx={{ fontWeight: channels[1].selected ? 700 : 600, fontSize: { xs: '0.9375rem', sm: '1.125rem' } }}>
|
<Typography variant="body1" sx={{ fontWeight: channels[1].selected ? 700 : 600, fontSize: '1.125rem' }}>
|
||||||
링고비즈
|
링고비즈
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
sx={{ mb: channels[1].selected ? { xs: 1.5, sm: 2 } : 0 }}
|
sx={{ mb: channels[1].selected ? 2 : 0 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{channels[1].selected && (
|
{channels[1].selected && (
|
||||||
<Box sx={{ pl: { xs: 2.5, sm: 4 }, pt: { xs: 1.5, sm: 2 }, borderTop: 1, borderColor: 'divider' }}>
|
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="매장 전화번호"
|
label="매장 전화번호"
|
||||||
value={getChannelConfig('ringoBiz', 'phone')}
|
value={getChannelConfig('ringoBiz', 'phone')}
|
||||||
InputProps={{ readOnly: true }}
|
InputProps={{ readOnly: true }}
|
||||||
sx={{ mb: { xs: 1.5, sm: 2 } }}
|
sx={{ mb: 2 }}
|
||||||
InputLabelProps={{ sx: { fontSize: { xs: '0.875rem', sm: '1rem' } } }}
|
|
||||||
inputProps={{ sx: { fontSize: { xs: '0.875rem', sm: '1rem' } } }}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: { xs: 0.5, sm: 0.5 }, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
|
||||||
연결음 자동 업데이트
|
연결음 자동 업데이트
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' }, mb: { xs: 0.5, sm: 0 } }}>
|
<Typography variant="body2" color="text.secondary">
|
||||||
예상 노출: <strong>3만명</strong>
|
예상 노출: <strong>3만명</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
|
<Typography variant="body2" color="text.secondary">
|
||||||
비용: <strong>무료</strong>
|
비용: <strong>무료</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@ -269,8 +261,8 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
<Card
|
<Card
|
||||||
elevation={0}
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
mb: { xs: 2.5, sm: 6 },
|
mb: 6,
|
||||||
borderRadius: { xs: 3, sm: 4 },
|
borderRadius: 4,
|
||||||
border: channels[2].selected ? 2 : 1,
|
border: channels[2].selected ? 2 : 1,
|
||||||
borderColor: channels[2].selected ? colors.purple : 'divider',
|
borderColor: channels[2].selected ? colors.purple : 'divider',
|
||||||
bgcolor: channels[2].selected ? `${colors.purpleLight}40` : 'background.paper',
|
bgcolor: channels[2].selected ? `${colors.purpleLight}40` : 'background.paper',
|
||||||
@ -278,14 +270,13 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
transition: 'all 0.3s',
|
transition: 'all 0.3s',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardContent sx={{ p: { xs: 2.5, sm: 6 } }}>
|
<CardContent sx={{ p: { xs: 2, sm: 6 } }}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={channels[2].selected}
|
checked={channels[2].selected}
|
||||||
onChange={() => handleChannelToggle('genieTV')}
|
onChange={() => handleChannelToggle('genieTV')}
|
||||||
sx={{
|
sx={{
|
||||||
p: { xs: 0.5, sm: 1 },
|
|
||||||
color: colors.purple,
|
color: colors.purple,
|
||||||
'&.Mui-checked': {
|
'&.Mui-checked': {
|
||||||
color: colors.purple,
|
color: colors.purple,
|
||||||
@ -294,39 +285,37 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
<Typography variant="body1" sx={{ fontWeight: channels[2].selected ? 700 : 600, fontSize: { xs: '0.9375rem', sm: '1.125rem' } }}>
|
<Typography variant="body1" sx={{ fontWeight: channels[2].selected ? 700 : 600, fontSize: '1.125rem' }}>
|
||||||
지니TV 광고
|
지니TV 광고
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
sx={{ mb: channels[2].selected ? { xs: 1.5, sm: 2 } : 0 }}
|
sx={{ mb: channels[2].selected ? 2 : 0 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{channels[2].selected && (
|
{channels[2].selected && (
|
||||||
<Box sx={{ pl: { xs: 2.5, sm: 4 }, pt: { xs: 1.5, sm: 2 }, borderTop: 1, borderColor: 'divider' }}>
|
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}>
|
||||||
<FormControl fullWidth sx={{ mb: { xs: 1.5, sm: 2 } }}>
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||||
<InputLabel sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>지역</InputLabel>
|
<InputLabel>지역</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={getChannelConfig('genieTV', 'region')}
|
value={getChannelConfig('genieTV', 'region')}
|
||||||
onChange={(e) => handleConfigChange('genieTV', 'region', e.target.value)}
|
onChange={(e) => handleConfigChange('genieTV', 'region', e.target.value)}
|
||||||
label="지역"
|
label="지역"
|
||||||
sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}
|
|
||||||
>
|
>
|
||||||
<MenuItem value="suwon" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>수원</MenuItem>
|
<MenuItem value="suwon">수원</MenuItem>
|
||||||
<MenuItem value="seoul" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>서울</MenuItem>
|
<MenuItem value="seoul">서울</MenuItem>
|
||||||
<MenuItem value="busan" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>부산</MenuItem>
|
<MenuItem value="busan">부산</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormControl fullWidth sx={{ mb: { xs: 1.5, sm: 2 } }}>
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||||
<InputLabel sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>노출 시간대</InputLabel>
|
<InputLabel>노출 시간대</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={getChannelConfig('genieTV', 'time')}
|
value={getChannelConfig('genieTV', 'time')}
|
||||||
onChange={(e) => handleConfigChange('genieTV', 'time', e.target.value)}
|
onChange={(e) => handleConfigChange('genieTV', 'time', e.target.value)}
|
||||||
label="노출 시간대"
|
label="노출 시간대"
|
||||||
sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}
|
|
||||||
>
|
>
|
||||||
<MenuItem value="all" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>전체</MenuItem>
|
<MenuItem value="all">전체</MenuItem>
|
||||||
<MenuItem value="prime" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>프라임 (19-23시)</MenuItem>
|
<MenuItem value="prime">프라임 (19-23시)</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
@ -338,12 +327,10 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
value={getChannelConfig('genieTV', 'budget')}
|
value={getChannelConfig('genieTV', 'budget')}
|
||||||
onChange={(e) => handleConfigChange('genieTV', 'budget', e.target.value)}
|
onChange={(e) => handleConfigChange('genieTV', 'budget', e.target.value)}
|
||||||
InputProps={{ inputProps: { min: 0, step: 10000 } }}
|
InputProps={{ inputProps: { min: 0, step: 10000 } }}
|
||||||
sx={{ mb: { xs: 1.5, sm: 2 } }}
|
sx={{ mb: 2 }}
|
||||||
InputLabelProps={{ sx: { fontSize: { xs: '0.875rem', sm: '1rem' } } }}
|
|
||||||
inputProps={{ sx: { fontSize: { xs: '0.875rem', sm: '1rem' } } }}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
|
<Typography variant="body2" color="text.secondary">
|
||||||
예상 노출:{' '}
|
예상 노출:{' '}
|
||||||
<strong>
|
<strong>
|
||||||
{getChannelConfig('genieTV', 'budget')
|
{getChannelConfig('genieTV', 'budget')
|
||||||
@ -360,8 +347,8 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
<Card
|
<Card
|
||||||
elevation={0}
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
mb: { xs: 3, sm: 10 },
|
mb: { xs: 4, sm: 10 },
|
||||||
borderRadius: { xs: 3, sm: 4 },
|
borderRadius: 4,
|
||||||
border: channels[3].selected ? 2 : 1,
|
border: channels[3].selected ? 2 : 1,
|
||||||
borderColor: channels[3].selected ? colors.purple : 'divider',
|
borderColor: channels[3].selected ? colors.purple : 'divider',
|
||||||
bgcolor: channels[3].selected ? `${colors.purpleLight}40` : 'background.paper',
|
bgcolor: channels[3].selected ? `${colors.purpleLight}40` : 'background.paper',
|
||||||
@ -369,14 +356,13 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
transition: 'all 0.3s',
|
transition: 'all 0.3s',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardContent sx={{ p: { xs: 2.5, sm: 6 } }}>
|
<CardContent sx={{ p: { xs: 2, sm: 6 } }}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={channels[3].selected}
|
checked={channels[3].selected}
|
||||||
onChange={() => handleChannelToggle('sns')}
|
onChange={() => handleChannelToggle('sns')}
|
||||||
sx={{
|
sx={{
|
||||||
p: { xs: 0.5, sm: 1 },
|
|
||||||
color: colors.purple,
|
color: colors.purple,
|
||||||
'&.Mui-checked': {
|
'&.Mui-checked': {
|
||||||
color: colors.purple,
|
color: colors.purple,
|
||||||
@ -385,16 +371,16 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
<Typography variant="body1" sx={{ fontWeight: channels[3].selected ? 700 : 600, fontSize: { xs: '0.9375rem', sm: '1.125rem' } }}>
|
<Typography variant="body1" sx={{ fontWeight: channels[3].selected ? 700 : 600, fontSize: '1.125rem' }}>
|
||||||
SNS
|
SNS
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
sx={{ mb: channels[3].selected ? { xs: 1.5, sm: 2 } : 0 }}
|
sx={{ mb: channels[3].selected ? 2 : 0 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{channels[3].selected && (
|
{channels[3].selected && (
|
||||||
<Box sx={{ pl: { xs: 2.5, sm: 4 }, pt: { xs: 1.5, sm: 2 }, borderTop: 1, borderColor: 'divider' }}>
|
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}>
|
||||||
<Typography variant="body2" sx={{ mb: { xs: 1, sm: 1 }, fontWeight: 600, fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
|
<Typography variant="body2" sx={{ mb: 1, fontWeight: 600 }}>
|
||||||
플랫폼 선택
|
플랫폼 선택
|
||||||
</Typography>
|
</Typography>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
@ -405,7 +391,6 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
handleConfigChange('sns', 'instagram', e.target.checked.toString())
|
handleConfigChange('sns', 'instagram', e.target.checked.toString())
|
||||||
}
|
}
|
||||||
sx={{
|
sx={{
|
||||||
p: { xs: 0.5, sm: 1 },
|
|
||||||
color: colors.purple,
|
color: colors.purple,
|
||||||
'&.Mui-checked': {
|
'&.Mui-checked': {
|
||||||
color: colors.purple,
|
color: colors.purple,
|
||||||
@ -413,7 +398,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={<Typography sx={{ fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>Instagram</Typography>}
|
label="Instagram"
|
||||||
sx={{ display: 'block' }}
|
sx={{ display: 'block' }}
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
@ -424,7 +409,6 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
handleConfigChange('sns', 'naver', e.target.checked.toString())
|
handleConfigChange('sns', 'naver', e.target.checked.toString())
|
||||||
}
|
}
|
||||||
sx={{
|
sx={{
|
||||||
p: { xs: 0.5, sm: 1 },
|
|
||||||
color: colors.purple,
|
color: colors.purple,
|
||||||
'&.Mui-checked': {
|
'&.Mui-checked': {
|
||||||
color: colors.purple,
|
color: colors.purple,
|
||||||
@ -432,7 +416,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={<Typography sx={{ fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>Naver Blog</Typography>}
|
label="Naver Blog"
|
||||||
sx={{ display: 'block' }}
|
sx={{ display: 'block' }}
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
@ -443,7 +427,6 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
handleConfigChange('sns', 'kakao', e.target.checked.toString())
|
handleConfigChange('sns', 'kakao', e.target.checked.toString())
|
||||||
}
|
}
|
||||||
sx={{
|
sx={{
|
||||||
p: { xs: 0.5, sm: 1 },
|
|
||||||
color: colors.purple,
|
color: colors.purple,
|
||||||
'&.Mui-checked': {
|
'&.Mui-checked': {
|
||||||
color: colors.purple,
|
color: colors.purple,
|
||||||
@ -451,27 +434,26 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={<Typography sx={{ fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>Kakao Channel</Typography>}
|
label="Kakao Channel"
|
||||||
sx={{ display: 'block', mb: { xs: 1.5, sm: 2 } }}
|
sx={{ display: 'block', mb: 2 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControl fullWidth sx={{ mb: { xs: 1.5, sm: 2 } }}>
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||||
<InputLabel sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>예약 게시</InputLabel>
|
<InputLabel>예약 게시</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={getChannelConfig('sns', 'schedule')}
|
value={getChannelConfig('sns', 'schedule')}
|
||||||
onChange={(e) => handleConfigChange('sns', 'schedule', e.target.value)}
|
onChange={(e) => handleConfigChange('sns', 'schedule', e.target.value)}
|
||||||
label="예약 게시"
|
label="예약 게시"
|
||||||
sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}
|
|
||||||
>
|
>
|
||||||
<MenuItem value="now" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>즉시</MenuItem>
|
<MenuItem value="now">즉시</MenuItem>
|
||||||
<MenuItem value="schedule" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>예약</MenuItem>
|
<MenuItem value="schedule">예약</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' }, mb: { xs: 0.5, sm: 0 } }}>
|
<Typography variant="body2" color="text.secondary">
|
||||||
예상 노출: <strong>-</strong>
|
예상 노출: <strong>-</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
|
<Typography variant="body2" color="text.secondary">
|
||||||
비용: <strong>무료</strong>
|
비용: <strong>무료</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@ -483,26 +465,26 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
<Card
|
<Card
|
||||||
elevation={0}
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
mb: { xs: 3, sm: 10 },
|
mb: { xs: 4, sm: 10 },
|
||||||
borderRadius: { xs: 3, sm: 4 },
|
borderRadius: 4,
|
||||||
bgcolor: 'grey.50',
|
bgcolor: 'grey.50',
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardContent sx={{ p: { xs: 2.5, sm: 8 } }}>
|
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: { xs: 2.5, sm: 4 } }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 4 }}>
|
||||||
<Typography variant="h6" sx={{ fontSize: { xs: '0.9375rem', sm: '1.25rem' } }}>
|
<Typography variant="h6" sx={{ fontSize: '1.25rem' }}>
|
||||||
총 예상 비용
|
총 예상 비용
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h6" color="error.main" sx={{ fontWeight: 700, fontSize: { xs: '0.9375rem', sm: '1.25rem' } }}>
|
<Typography variant="h6" color="error.main" sx={{ fontWeight: 700, fontSize: '1.25rem' }}>
|
||||||
{totalCost.toLocaleString()}원
|
{totalCost.toLocaleString()}원
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<Typography variant="h6" sx={{ fontSize: { xs: '0.9375rem', sm: '1.25rem' } }}>
|
<Typography variant="h6" sx={{ fontSize: '1.25rem' }}>
|
||||||
총 예상 노출
|
총 예상 노출
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: { xs: '0.9375rem', sm: '1.25rem' }, color: colors.purple }}>
|
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1.25rem', color: colors.purple }}>
|
||||||
{totalExposure > 0 ? `${totalExposure.toLocaleString()}명+` : '0명'}
|
{totalExposure > 0 ? `${totalExposure.toLocaleString()}명+` : '0명'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@ -510,16 +492,16 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<Box sx={{ display: 'flex', gap: { xs: 1.5, sm: 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: { xs: 1.25, sm: 3 },
|
py: { xs: 1.5, sm: 3 },
|
||||||
borderRadius: { xs: 2.5, sm: 3 },
|
borderRadius: 3,
|
||||||
fontSize: { xs: '0.8125rem', sm: '1rem' },
|
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
@ -536,9 +518,9 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
|||||||
disabled={selectedCount === 0}
|
disabled={selectedCount === 0}
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
sx={{
|
sx={{
|
||||||
py: { xs: 1.25, sm: 3 },
|
py: { xs: 1.5, sm: 3 },
|
||||||
borderRadius: { xs: 2.5, sm: 3 },
|
borderRadius: 3,
|
||||||
fontSize: { xs: '0.8125rem', sm: '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,27 +40,10 @@ export default function ContentEditStep({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box sx={{ minHeight: '100vh', bgcolor: colors.gray[50], pt: { xs: 5, sm: 8 }, pb: { xs: 3, sm: 10 } }}>
|
||||||
sx={{
|
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 3, sm: 6 }, px: { xs: 1.5, sm: 8, md: 10 } }}>
|
||||||
minHeight: '100vh',
|
|
||||||
bgcolor: colors.gray[50],
|
|
||||||
pt: { xs: 5, sm: 8 },
|
|
||||||
pb: { xs: 3, sm: 10 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Container
|
|
||||||
maxWidth="lg"
|
|
||||||
sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 3, sm: 6 }, px: { xs: 1.5, sm: 8, md: 10 } }}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Box
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, mb: { xs: 3, sm: 10 } }}>
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: { xs: 1.5, sm: 3 },
|
|
||||||
mb: { xs: 3, sm: 10 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
||||||
<ArrowBack sx={{ fontSize: 20 }} />
|
<ArrowBack sx={{ fontSize: 20 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -71,7 +54,7 @@ export default function ContentEditStep({
|
|||||||
|
|
||||||
<Grid container spacing={{ xs: 3, sm: 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: { xs: 3, sm: 6 } }}>
|
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: { xs: 3, sm: 6 } }}>
|
||||||
미리보기
|
미리보기
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -106,25 +89,17 @@ export default function ContentEditStep({
|
|||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid> */}
|
</Grid>
|
||||||
|
|
||||||
{/* Edit Section */}
|
{/* Edit Section */}
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<Typography
|
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: { xs: 3, sm: 6 } }}>
|
||||||
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: 2, sm: 8 } }}>
|
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||||
<Box
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 2 }, mb: { xs: 3, sm: 6 } }}>
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: { xs: 1.5, sm: 2 },
|
|
||||||
mb: { xs: 3, sm: 6 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Edit sx={{ color: colors.purple, fontSize: { xs: 20, sm: 28 } }} />
|
<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 }}>
|
||||||
텍스트 편집
|
텍스트 편집
|
||||||
@ -173,7 +148,7 @@ export default function ContentEditStep({
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<Box sx={{ display: 'flex', gap: { xs: 2, sm: 4 }, mt: { xs: 8, sm: 20 } }}>
|
<Box sx={{ display: 'flex', gap: { xs: 2, sm: 4 }, mt: { xs: 4, sm: 10 } }}>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
@ -252,34 +252,9 @@ export default function ContentPreviewStep({
|
|||||||
console.log('✅ Image generation request accepted (202)');
|
console.log('✅ Image generation request accepted (202)');
|
||||||
console.log('⏳ AI 이미지 생성 중... 약 60초 소요됩니다.');
|
console.log('⏳ AI 이미지 생성 중... 약 60초 소요됩니다.');
|
||||||
|
|
||||||
setLoadingProgress(5);
|
setLoadingProgress(10);
|
||||||
setLoadingMessage('AI가 이미지를 생성하고 있어요...');
|
setLoadingMessage('AI가 이미지를 생성하고 있어요...');
|
||||||
|
|
||||||
// 시간 기반 진행률 업데이트 (자연스러운 진행)
|
|
||||||
let currentProgress = 5;
|
|
||||||
let progressCompleted = false;
|
|
||||||
|
|
||||||
const progressInterval = setInterval(() => {
|
|
||||||
if (progressCompleted) {
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 진행률을 천천히 증가 (90%까지만)
|
|
||||||
currentProgress += Math.random() * 2; // 0~2% 랜덤 증가
|
|
||||||
const cappedProgress = Math.min(currentProgress, 90);
|
|
||||||
setLoadingProgress(cappedProgress);
|
|
||||||
|
|
||||||
// 진행률에 따른 메시지 업데이트
|
|
||||||
if (cappedProgress < 30) {
|
|
||||||
setLoadingMessage('AI가 이미지를 생성하고 있어요...');
|
|
||||||
} else if (cappedProgress < 60) {
|
|
||||||
setLoadingMessage('스타일을 적용하고 있어요...');
|
|
||||||
} else if (cappedProgress < 90) {
|
|
||||||
setLoadingMessage('거의 완료되었어요...');
|
|
||||||
}
|
|
||||||
}, 500); // 0.5초마다 업데이트
|
|
||||||
|
|
||||||
// 생성 완료까지 대기 (polling)
|
// 생성 완료까지 대기 (polling)
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
const maxAttempts = 30; // 최대 60초 (2초 * 30회)
|
const maxAttempts = 30; // 최대 60초 (2초 * 30회)
|
||||||
@ -288,12 +263,23 @@ export default function ContentPreviewStep({
|
|||||||
attempts++;
|
attempts++;
|
||||||
console.log(`🔄 이미지 확인 시도 ${attempts}/${maxAttempts}...`);
|
console.log(`🔄 이미지 확인 시도 ${attempts}/${maxAttempts}...`);
|
||||||
|
|
||||||
|
// 진행률 업데이트 (10% ~ 90%)
|
||||||
|
const progress = Math.min(10 + (attempts / maxAttempts) * 80, 90);
|
||||||
|
setLoadingProgress(progress);
|
||||||
|
|
||||||
|
// 단계별 메시지 업데이트
|
||||||
|
if (attempts < 10) {
|
||||||
|
setLoadingMessage('AI가 이미지를 생성하고 있어요...');
|
||||||
|
} else if (attempts < 20) {
|
||||||
|
setLoadingMessage('스타일을 적용하고 있어요...');
|
||||||
|
} else {
|
||||||
|
setLoadingMessage('거의 완료되었어요...');
|
||||||
|
}
|
||||||
|
|
||||||
const hasImages = await loadImages(data);
|
const hasImages = await loadImages(data);
|
||||||
|
|
||||||
if (hasImages) {
|
if (hasImages) {
|
||||||
console.log('✅ 이미지 생성 완료!');
|
console.log('✅ 이미지 생성 완료!');
|
||||||
progressCompleted = true;
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
setLoadingProgress(100);
|
setLoadingProgress(100);
|
||||||
setLoadingMessage('이미지 생성 완료!');
|
setLoadingMessage('이미지 생성 완료!');
|
||||||
setTimeout(() => setLoading(false), 500); // 100% 잠깐 보여주기
|
setTimeout(() => setLoading(false), 500); // 100% 잠깐 보여주기
|
||||||
@ -302,8 +288,6 @@ export default function ContentPreviewStep({
|
|||||||
setTimeout(pollImages, 2000);
|
setTimeout(pollImages, 2000);
|
||||||
} else {
|
} else {
|
||||||
console.warn('⚠️ 이미지 생성 시간 초과. "이미지 재생성" 버튼을 클릭하세요.');
|
console.warn('⚠️ 이미지 생성 시간 초과. "이미지 재생성" 버튼을 클릭하세요.');
|
||||||
progressCompleted = true;
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
setError('이미지 생성이 완료되지 않았습니다. 잠시 후 "이미지 재생성" 버튼을 클릭해주세요.');
|
setError('이미지 생성이 완료되지 않았습니다. 잠시 후 "이미지 재생성" 버튼을 클릭해주세요.');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,231 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState, useRef } from 'react';
|
|
||||||
import { Box, Container, Typography, CircularProgress, LinearProgress } from '@mui/material';
|
|
||||||
import { AutoAwesome } from '@mui/icons-material';
|
|
||||||
import { eventApi } from '@/shared/api/eventApi';
|
|
||||||
import type { AiRecommendationResult } from '@/shared/api/eventApi';
|
|
||||||
|
|
||||||
// 디자인 시스템 색상
|
|
||||||
const colors = {
|
|
||||||
pink: '#F472B6',
|
|
||||||
purple: '#C084FC',
|
|
||||||
purpleLight: '#E9D5FF',
|
|
||||||
blue: '#60A5FA',
|
|
||||||
mint: '#34D399',
|
|
||||||
gray: {
|
|
||||||
900: '#1A1A1A',
|
|
||||||
700: '#4A4A4A',
|
|
||||||
500: '#9E9E9E',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
interface LoadingStepProps {
|
|
||||||
eventId: string;
|
|
||||||
onComplete: (data: AiRecommendationResult) => void;
|
|
||||||
onError: (error: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LoadingStep({ eventId, onComplete, onError }: LoadingStepProps) {
|
|
||||||
const [progress, setProgress] = useState(0);
|
|
||||||
const [message, setMessage] = useState('AI가 최적의 이벤트를 분석하고 있습니다...');
|
|
||||||
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
const startTimeRef = useRef<number>(Date.now());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('🔄 LoadingStep 시작, eventId:', eventId);
|
|
||||||
|
|
||||||
// 진행 상태 메시지 업데이트
|
|
||||||
const messageInterval = setInterval(() => {
|
|
||||||
const elapsed = Date.now() - startTimeRef.current;
|
|
||||||
|
|
||||||
if (elapsed < 15000) {
|
|
||||||
setMessage('AI가 업종 및 지역 트렌드를 분석하고 있습니다...');
|
|
||||||
} else if (elapsed < 30000) {
|
|
||||||
setMessage('고객 타겟과 이벤트 컨셉을 생성하고 있습니다...');
|
|
||||||
} else if (elapsed < 45000) {
|
|
||||||
setMessage('예상 효과와 비용을 계산하고 있습니다...');
|
|
||||||
} else {
|
|
||||||
setMessage('최종 추천안을 정리하고 있습니다...');
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
// 진행률 애니메이션 (60초 기준)
|
|
||||||
const progressInterval = setInterval(() => {
|
|
||||||
setProgress((prev) => {
|
|
||||||
const elapsed = Date.now() - startTimeRef.current;
|
|
||||||
const newProgress = Math.min((elapsed / 60000) * 100, 95); // 최대 95%까지만
|
|
||||||
return newProgress;
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
// 폴링으로 AI 추천 결과 조회 (3초마다)
|
|
||||||
const pollRecommendations = async () => {
|
|
||||||
try {
|
|
||||||
console.log('📡 AI 추천 결과 조회 시도...');
|
|
||||||
const result = await eventApi.getAiRecommendations(eventId);
|
|
||||||
|
|
||||||
console.log('✅ AI 추천 결과 조회 성공:', result);
|
|
||||||
console.log('📊 Response 전체 구조:', JSON.stringify(result, null, 2));
|
|
||||||
|
|
||||||
// 데이터가 준비되었는지 확인
|
|
||||||
// recommendations 배열이 있으면 성공으로 간주
|
|
||||||
if (result && result.recommendations && result.recommendations.length > 0) {
|
|
||||||
console.log('🎉 AI 추천 데이터 준비 완료!');
|
|
||||||
setProgress(100);
|
|
||||||
setMessage('추천안 생성 완료!');
|
|
||||||
|
|
||||||
// 정리
|
|
||||||
if (pollingIntervalRef.current) clearInterval(pollingIntervalRef.current);
|
|
||||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
||||||
clearInterval(messageInterval);
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
|
|
||||||
// 완료 콜백 호출
|
|
||||||
setTimeout(() => {
|
|
||||||
onComplete(result);
|
|
||||||
}, 500);
|
|
||||||
} else {
|
|
||||||
console.log('⏳ 데이터는 받았지만 추천안이 아직 없음, 계속 대기...');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('⏳ AI 추천 결과 아직 준비되지 않음 (에러 발생), 계속 대기...');
|
|
||||||
console.log(' 에러 상태:', error.response?.status);
|
|
||||||
console.log(' 에러 메시지:', error.message);
|
|
||||||
// 404나 데이터 없음 에러는 정상 (아직 생성 중)
|
|
||||||
// 계속 폴링
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 초기 조회 (즉시)
|
|
||||||
pollRecommendations();
|
|
||||||
|
|
||||||
// 폴링 시작 (3초마다)
|
|
||||||
pollingIntervalRef.current = setInterval(pollRecommendations, 3000);
|
|
||||||
|
|
||||||
// 타임아웃 설정 (60초)
|
|
||||||
timeoutRef.current = setTimeout(() => {
|
|
||||||
console.error('⏰ AI 추천 생성 타임아웃 (60초 초과)');
|
|
||||||
|
|
||||||
// 정리
|
|
||||||
if (pollingIntervalRef.current) clearInterval(pollingIntervalRef.current);
|
|
||||||
clearInterval(messageInterval);
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
|
|
||||||
onError('AI 추천 생성 시간이 초과되었습니다. 다시 시도해 주세요.');
|
|
||||||
}, 60000);
|
|
||||||
|
|
||||||
// 클린업
|
|
||||||
return () => {
|
|
||||||
if (pollingIntervalRef.current) clearInterval(pollingIntervalRef.current);
|
|
||||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
||||||
clearInterval(messageInterval);
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
};
|
|
||||||
}, [eventId, onComplete, onError]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
minHeight: '100vh',
|
|
||||||
bgcolor: 'background.default',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Container maxWidth="sm">
|
|
||||||
<Box sx={{ textAlign: 'center' }}>
|
|
||||||
{/* AI 아이콘 애니메이션 */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mb: 6,
|
|
||||||
animation: 'pulse 2s ease-in-out infinite',
|
|
||||||
'@keyframes pulse': {
|
|
||||||
'0%, 100%': {
|
|
||||||
opacity: 1,
|
|
||||||
transform: 'scale(1)',
|
|
||||||
},
|
|
||||||
'50%': {
|
|
||||||
opacity: 0.8,
|
|
||||||
transform: 'scale(1.05)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AutoAwesome sx={{ fontSize: 80, color: colors.purple }} />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 로딩 메시지 */}
|
|
||||||
<Typography
|
|
||||||
variant="h4"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 700,
|
|
||||||
mb: 2,
|
|
||||||
fontSize: { xs: '1.5rem', sm: '2rem' },
|
|
||||||
color: colors.gray[900],
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
AI 추천 생성 중
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
sx={{
|
|
||||||
mb: 6,
|
|
||||||
color: colors.gray[700],
|
|
||||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{message}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* 진행률 바 */}
|
|
||||||
<Box sx={{ mb: 2 }}>
|
|
||||||
<LinearProgress
|
|
||||||
variant="determinate"
|
|
||||||
value={progress}
|
|
||||||
sx={{
|
|
||||||
height: 8,
|
|
||||||
borderRadius: 4,
|
|
||||||
bgcolor: colors.purpleLight,
|
|
||||||
'& .MuiLinearProgress-bar': {
|
|
||||||
borderRadius: 4,
|
|
||||||
background: `linear-gradient(90deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Typography variant="caption" sx={{ color: colors.gray[500] }}>
|
|
||||||
{Math.round(progress)}%
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* 안내 메시지 */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mt: 8,
|
|
||||||
p: 3,
|
|
||||||
borderRadius: 3,
|
|
||||||
bgcolor: `${colors.purpleLight}40`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
color: colors.gray[700],
|
|
||||||
fontSize: { xs: '0.8125rem', sm: '0.875rem' },
|
|
||||||
lineHeight: 1.6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
💡 업종, 지역, 계절 트렌드를 분석하여 가장 효과적인 이벤트 3가지를 추천해 드립니다.
|
|
||||||
<br />
|
|
||||||
최대 1분 정도 소요될 수 있습니다.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -13,8 +13,6 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { AutoAwesome, TrendingUp, Replay, Store, Campaign } from '@mui/icons-material';
|
import { AutoAwesome, TrendingUp, Replay, Store, Campaign } from '@mui/icons-material';
|
||||||
import { EventObjective } from '../page';
|
import { EventObjective } from '../page';
|
||||||
import { eventApi } from '@/shared/api/eventApi';
|
|
||||||
import type { AiRecommendationRequest } from '@/shared/api/eventApi';
|
|
||||||
|
|
||||||
// 디자인 시스템 색상
|
// 디자인 시스템 색상
|
||||||
const colors = {
|
const colors = {
|
||||||
@ -91,24 +89,9 @@ const deleteCookie = (name: string) => {
|
|||||||
|
|
||||||
export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
|
export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
|
||||||
const [selected, setSelected] = useState<EventObjective | null>(null);
|
const [selected, setSelected] = useState<EventObjective | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
// Objective 한글 매핑
|
const handleNext = () => {
|
||||||
const getObjectiveKorean = (objective: EventObjective): string => {
|
|
||||||
const map: Record<EventObjective, string> = {
|
|
||||||
'new_customer': '신규 고객 유치',
|
|
||||||
'revisit': '재방문 유도',
|
|
||||||
'sales': '매출 증대',
|
|
||||||
'awareness': '인지도 향상',
|
|
||||||
};
|
|
||||||
return map[objective];
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNext = async () => {
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 이전 쿠키 삭제 (깨끗한 상태에서 시작)
|
// 이전 쿠키 삭제 (깨끗한 상태에서 시작)
|
||||||
deleteCookie('eventId');
|
deleteCookie('eventId');
|
||||||
deleteCookie('jobId');
|
deleteCookie('jobId');
|
||||||
@ -137,34 +120,8 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
|
|||||||
console.error('❌ localStorage 저장 실패:', error);
|
console.error('❌ localStorage 저장 실패:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI 추천 생성 요청 body 준비
|
// objective와 eventId를 함께 전달
|
||||||
const aiRequest: AiRecommendationRequest = {
|
|
||||||
objective: getObjectiveKorean(selected),
|
|
||||||
storeInfo: {
|
|
||||||
storeId: 'store_001',
|
|
||||||
storeName: '홍길동 삼겹살',
|
|
||||||
category: '음식점',
|
|
||||||
description: '길동이가 직접 구워주는 삼겹살 맛집!',
|
|
||||||
},
|
|
||||||
region: '서울특별시 강남구',
|
|
||||||
targetAudience: '20-30대 직장인',
|
|
||||||
budget: 500000,
|
|
||||||
};
|
|
||||||
|
|
||||||
// AI 추천 생성 요청 API 호출 및 200 응답 대기
|
|
||||||
console.log('🤖 AI 추천 생성 요청 시작:', eventId);
|
|
||||||
console.log('📤 Request Body:', aiRequest);
|
|
||||||
await eventApi.requestAiRecommendations(eventId, aiRequest);
|
|
||||||
console.log('✅ AI 추천 생성 요청 성공 (200 응답 수신)');
|
|
||||||
|
|
||||||
// 200 응답 받은 후 다음 단계로 이동
|
|
||||||
onNext({ objective: selected, eventId });
|
onNext({ objective: selected, eventId });
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ AI 추천 생성 요청 실패:', error);
|
|
||||||
alert('AI 추천 생성 요청에 실패했습니다. 다시 시도해 주세요.');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -254,7 +211,7 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
|
|||||||
fullWidth
|
fullWidth
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="large"
|
size="large"
|
||||||
disabled={!selected || isLoading}
|
disabled={!selected}
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
sx={{
|
sx={{
|
||||||
py: { xs: 1.5, sm: 3 },
|
py: { xs: 1.5, sm: 3 },
|
||||||
@ -271,7 +228,7 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isLoading ? 'AI 추천 요청 중...' : '다음'}
|
다음
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@ -42,7 +42,6 @@ const colors = {
|
|||||||
|
|
||||||
interface RecommendationStepProps {
|
interface RecommendationStepProps {
|
||||||
eventId?: string; // 이전 단계에서 생성된 eventId
|
eventId?: string; // 이전 단계에서 생성된 eventId
|
||||||
aiResult?: AiRecommendationResult; // LoadingStep에서 전달받은 AI 추천 결과
|
|
||||||
onNext: (data: { recommendation: EventRecommendation; eventId: string }) => void;
|
onNext: (data: { recommendation: EventRecommendation; eventId: string }) => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
@ -61,7 +60,6 @@ const getCookie = (name: string): string | null => {
|
|||||||
|
|
||||||
export default function RecommendationStep({
|
export default function RecommendationStep({
|
||||||
eventId: initialEventId,
|
eventId: initialEventId,
|
||||||
aiResult: initialAiResult,
|
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
}: RecommendationStepProps) {
|
}: RecommendationStepProps) {
|
||||||
@ -69,7 +67,7 @@ export default function RecommendationStep({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const [aiResult, setAiResult] = useState<AiRecommendationResult | null>(initialAiResult || null);
|
const [aiResult, setAiResult] = useState<AiRecommendationResult | null>(null);
|
||||||
const [selected, setSelected] = useState<number | null>(null);
|
const [selected, setSelected] = useState<number | null>(null);
|
||||||
const [editedData, setEditedData] = useState<
|
const [editedData, setEditedData] = useState<
|
||||||
Record<number, { title: string; description: string }>
|
Record<number, { title: string; description: string }>
|
||||||
@ -80,15 +78,7 @@ export default function RecommendationStep({
|
|||||||
|
|
||||||
// 컴포넌트 마운트 시 AI 추천 결과 조회
|
// 컴포넌트 마운트 시 AI 추천 결과 조회
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// LoadingStep에서 이미 데이터를 전달받은 경우 API 호출 생략
|
// props에서만 eventId를 받음
|
||||||
if (initialAiResult) {
|
|
||||||
console.log('✅ LoadingStep에서 전달받은 AI 추천 결과 사용:', initialAiResult);
|
|
||||||
setAiResult(initialAiResult);
|
|
||||||
setEventId(initialEventId || null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// props에서만 eventId를 받음 (하위 호환성)
|
|
||||||
if (initialEventId) {
|
if (initialEventId) {
|
||||||
// 이미 요청한 eventId면 중복 요청하지 않음
|
// 이미 요청한 eventId면 중복 요청하지 않음
|
||||||
if (requestedEventIdRef.current === initialEventId) {
|
if (requestedEventIdRef.current === initialEventId) {
|
||||||
@ -105,29 +95,28 @@ export default function RecommendationStep({
|
|||||||
console.error('❌ eventId가 없습니다. ObjectiveStep으로 돌아가세요.');
|
console.error('❌ eventId가 없습니다. ObjectiveStep으로 돌아가세요.');
|
||||||
setError('이벤트 ID가 없습니다. 이전 단계로 돌아가서 다시 시도하세요.');
|
setError('이벤트 ID가 없습니다. 이전 단계로 돌아가서 다시 시도하세요.');
|
||||||
}
|
}
|
||||||
}, [initialEventId, initialAiResult]);
|
}, [initialEventId]);
|
||||||
|
|
||||||
const fetchAIRecommendations = async (evtId: string) => {
|
const fetchAIRecommendations = async (evtId: string) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
console.log('📡 AI 추천 결과 조회 시작, eventId:', evtId);
|
console.log('📡 AI 추천 요청 시작, eventId:', evtId);
|
||||||
|
|
||||||
// GET /events/{eventId}/ai-recommendations 엔드포인트로 AI 추천 결과 조회
|
// POST /events/{eventId}/ai-recommendations 엔드포인트로 AI 추천 요청
|
||||||
// ObjectiveStep에서 이미 POST 요청으로 생성했으므로 GET으로 조회
|
const recommendations = await eventApi.requestAiRecommendations(evtId);
|
||||||
const recommendations = await eventApi.getAiRecommendations(evtId);
|
|
||||||
|
|
||||||
console.log('✅ AI 추천 결과 조회 성공:', recommendations);
|
console.log('✅ AI 추천 요청 성공:', recommendations);
|
||||||
setAiResult(recommendations);
|
setAiResult(recommendations);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('❌ AI 추천 결과 조회 실패:', err);
|
console.error('❌ AI 추천 요청 실패:', err);
|
||||||
|
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
err.response?.data?.message ||
|
err.response?.data?.message ||
|
||||||
err.response?.data?.error ||
|
err.response?.data?.error ||
|
||||||
'AI 추천 결과를 조회하는데 실패했습니다';
|
'AI 추천을 생성하는데 실패했습니다';
|
||||||
|
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -143,25 +132,14 @@ export default function RecommendationStep({
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// localStorage에 선택한 추천 정보 저장
|
// AI 추천 선택 API 호출
|
||||||
const selectedRecommendationData = {
|
await eventApi.selectRecommendation(eventId, {
|
||||||
eventId,
|
recommendationId: `${eventId}-opt${selected}`,
|
||||||
selectedOptionNumber: selected,
|
customizations: {
|
||||||
recommendation: {
|
eventName: edited?.title || selectedRec.title,
|
||||||
...selectedRec,
|
|
||||||
title: edited?.title || selectedRec.title,
|
|
||||||
description: edited?.description || selectedRec.description,
|
description: edited?.description || selectedRec.description,
|
||||||
},
|
},
|
||||||
trendAnalysis: aiResult.trendAnalysis,
|
});
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
localStorage.setItem('selectedRecommendation', JSON.stringify(selectedRecommendationData));
|
|
||||||
console.log('💾 localStorage에 선택한 추천 정보 저장 완료:', selectedRecommendationData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ localStorage 저장 실패:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 다음 단계로 이동
|
// 다음 단계로 이동
|
||||||
onNext({
|
onNext({
|
||||||
@ -204,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: 2, sm: 8 }, pb: { xs: 3, 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: 1, sm: 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: { xs: 36, sm: 40 }, height: { xs: 36, sm: 40 }, p: { xs: 0.5, sm: 1 } }}>
|
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
||||||
<ArrowBack sx={{ fontSize: { xs: 18, sm: 20 } }} />
|
<ArrowBack sx={{ fontSize: 20 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1rem', 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 sx={{ width: { xs: 40, sm: 48 }, height: { xs: 40, sm: 48 }, color: colors.purple }} />
|
<CircularProgress size={48} sx={{ color: colors.purple }} />
|
||||||
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1.25rem' }, textAlign: 'center', px: 2 }}>
|
<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.75rem', sm: '1rem' }, textAlign: 'center', px: 2 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.8125rem', sm: '1rem' } }}>
|
||||||
업종, 지역, 시즌 트렌드를 분석하여 맞춤형 이벤트를 추천합니다
|
업종, 지역, 시즌 트렌드를 분석하여 맞춤형 이벤트를 추천합니다
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@ -234,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: 2, sm: 8 }, pb: { xs: 3, 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: 1, sm: 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: { xs: 36, sm: 40 }, height: { xs: 36, sm: 40 }, p: { xs: 0.5, sm: 1 } }}>
|
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
||||||
<ArrowBack sx={{ fontSize: { xs: 18, sm: 20 } }} />
|
<ArrowBack sx={{ fontSize: 20 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1rem', sm: '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: { xs: 2.5, sm: 4 }, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
|
<Alert severity="error" sx={{ mb: { xs: 3, sm: 4 } }}>
|
||||||
{error}
|
{error}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', gap: { xs: 1.5, sm: 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: { xs: 1.25, sm: 3 },
|
py: { xs: 1.5, sm: 3 },
|
||||||
borderRadius: { xs: 2.5, sm: 3 },
|
borderRadius: 3,
|
||||||
fontSize: { xs: '0.8125rem', sm: '1rem' },
|
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -279,9 +257,9 @@ export default function RecommendationStep({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
py: { xs: 1.25, sm: 3 },
|
py: { xs: 1.5, sm: 3 },
|
||||||
borderRadius: { xs: 2.5, sm: 3 },
|
borderRadius: 3,
|
||||||
fontSize: { xs: '0.8125rem', sm: '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%)`,
|
||||||
}}
|
}}
|
||||||
@ -307,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: 2, sm: 8 }, pb: { xs: 3, 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: { xs: 1, sm: 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: { xs: 36, sm: 40 }, height: { xs: 36, sm: 40 }, p: { xs: 0.5, sm: 1 } }}>
|
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
|
||||||
<ArrowBack sx={{ fontSize: { xs: 18, sm: 20 } }} />
|
<ArrowBack sx={{ fontSize: 20 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1rem', sm: '1.5rem' } }}>
|
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
|
||||||
AI 이벤트 추천
|
AI 이벤트 추천
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@ -323,20 +301,20 @@ export default function RecommendationStep({
|
|||||||
elevation={0}
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
mb: { xs: 3, sm: 10 },
|
mb: { xs: 3, sm: 10 },
|
||||||
borderRadius: { xs: 3, sm: 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: 2.5, sm: 8 } }}>
|
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1, sm: 2 }, mb: { xs: 2.5, sm: 6 } }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 2 }, mb: { xs: 3, sm: 6 } }}>
|
||||||
<Insights sx={{ fontSize: { xs: 20, sm: 32 }, color: colors.purple }} />
|
<Insights sx={{ fontSize: { xs: 24, sm: 32 }, color: colors.purple }} />
|
||||||
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: { xs: '0.9375rem', sm: '1.25rem' } }}>
|
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: { xs: '1rem', sm: '1.25rem' } }}>
|
||||||
AI 트렌드 분석
|
AI 트렌드 분석
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Grid container spacing={{ xs: 2.5, sm: 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: { xs: 1, sm: 2 }, fontSize: { xs: '0.8125rem', sm: '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) => (
|
||||||
@ -344,14 +322,14 @@ export default function RecommendationStep({
|
|||||||
key={idx}
|
key={idx}
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ fontSize: { xs: '0.75rem', sm: '0.95rem' }, mb: { xs: 0.75, sm: 1 }, lineHeight: 1.5 }}
|
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: { xs: 1, sm: 2 }, fontSize: { xs: '0.8125rem', sm: '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) => (
|
||||||
@ -359,14 +337,14 @@ export default function RecommendationStep({
|
|||||||
key={idx}
|
key={idx}
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ fontSize: { xs: '0.75rem', sm: '0.95rem' }, mb: { xs: 0.75, sm: 1 }, lineHeight: 1.5 }}
|
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: { xs: 1, sm: 2 }, fontSize: { xs: '0.8125rem', sm: '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) => (
|
||||||
@ -374,7 +352,7 @@ export default function RecommendationStep({
|
|||||||
key={idx}
|
key={idx}
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ fontSize: { xs: '0.75rem', sm: '0.95rem' }, mb: { xs: 0.75, sm: 1 }, lineHeight: 1.5 }}
|
sx={{ fontSize: { xs: '0.8125rem', sm: '0.95rem' }, mb: 1 }}
|
||||||
>
|
>
|
||||||
• {trend.description}
|
• {trend.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -385,12 +363,13 @@ export default function RecommendationStep({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* AI Recommendations */}
|
{/* AI Recommendations */}
|
||||||
<Box sx={{ mb: { xs: 2.5, sm: 8 } }}>
|
<Box sx={{ mb: { xs: 2, sm: 8 } }}>
|
||||||
<Typography variant="h6" sx={{ fontWeight: 700, mb: { xs: 1.5, sm: 4 }, fontSize: { xs: '0.9375rem', sm: '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: { xs: 2.5, sm: 6 }, fontSize: { xs: '0.75rem', sm: '1rem' }, lineHeight: 1.6 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ mb: { xs: 3, sm: 6 }, fontSize: { xs: '0.8125rem', sm: '1rem' } }}>
|
||||||
각 옵션은 차별화된 컨셉으로 구성되어 있습니다. 원하시는 옵션을 선택하고 수정할 수 있습니다.
|
각 옵션은 차별화된 컨셉으로 구성되어 있습니다. 원하시는 옵션을 선택하고 수정할 수
|
||||||
|
있습니다.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@ -423,13 +402,13 @@ export default function RecommendationStep({
|
|||||||
}}
|
}}
|
||||||
onClick={() => setSelected(rec.optionNumber)}
|
onClick={() => setSelected(rec.optionNumber)}
|
||||||
>
|
>
|
||||||
<CardContent sx={{ p: { xs: 3, sm: 8 } }}>
|
<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: { xs: 2.5, sm: 4 },
|
mb: { xs: 2, sm: 4 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', gap: { xs: 1, sm: 2 }, flexWrap: 'wrap' }}>
|
<Box sx={{ display: 'flex', gap: { xs: 1, sm: 2 }, flexWrap: 'wrap' }}>
|
||||||
@ -437,18 +416,18 @@ export default function RecommendationStep({
|
|||||||
label={`옵션 ${rec.optionNumber}`}
|
label={`옵션 ${rec.optionNumber}`}
|
||||||
color="primary"
|
color="primary"
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ fontSize: { xs: '0.6875rem', sm: '0.875rem' }, py: { xs: 1.5, sm: 2 }, height: { xs: 24, sm: 32 } }}
|
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="small"
|
size="small"
|
||||||
sx={{ fontSize: { xs: '0.6875rem', sm: '0.875rem' }, py: { xs: 1.5, sm: 2 }, height: { xs: 24, sm: 32 } }}
|
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' }, py: { xs: 1.5, sm: 2 } }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={rec.optionNumber}
|
value={rec.optionNumber}
|
||||||
control={<Radio sx={{ p: { xs: 0.5, sm: 1 } }} />}
|
control={<Radio />}
|
||||||
label=""
|
label=""
|
||||||
sx={{ m: 0 }}
|
sx={{ m: 0 }}
|
||||||
/>
|
/>
|
||||||
@ -460,15 +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: { xs: 2.5, sm: 4 } }}
|
sx={{ mb: { xs: 2, sm: 4 } }}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <Edit fontSize="small" color="action" />,
|
endAdornment: <Edit fontSize="small" color="action" />,
|
||||||
sx: {
|
sx: { fontSize: { xs: '0.9375rem', sm: '1.1rem' }, fontWeight: 600, py: { xs: 1.5, sm: 2 } },
|
||||||
fontSize: { xs: '0.875rem', sm: '1.1rem' },
|
|
||||||
fontWeight: 600,
|
|
||||||
py: { xs: 1, sm: 2 },
|
|
||||||
px: { xs: 1.5, sm: 2 }
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -480,28 +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: { xs: 3, sm: 4 } }}
|
sx={{ mb: { xs: 2, sm: 4 } }}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
sx: {
|
sx: { fontSize: { xs: '0.875rem', sm: '1rem' } },
|
||||||
fontSize: { xs: '0.8125rem', sm: '1rem' },
|
|
||||||
py: { xs: 1, sm: 1.5 },
|
|
||||||
px: { xs: 1.5, sm: 2 }
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Grid container spacing={{ xs: 2.5, sm: 4 }}>
|
<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: { xs: '0.6875rem', sm: '0.875rem' }, display: 'block', mb: { xs: 0.5, sm: 1 } }}
|
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
||||||
>
|
>
|
||||||
타겟 고객
|
타겟 고객
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{ fontWeight: 600, fontSize: { xs: '0.8125rem', sm: '1rem' } }}
|
sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1rem' }, mt: { xs: 0.5, sm: 1 } }}
|
||||||
>
|
>
|
||||||
{rec.targetAudience}
|
{rec.targetAudience}
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -510,13 +480,13 @@ export default function RecommendationStep({
|
|||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ fontSize: { xs: '0.6875rem', sm: '0.875rem' }, display: 'block', mb: { xs: 0.5, sm: 1 } }}
|
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
||||||
>
|
>
|
||||||
예상 비용
|
예상 비용
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{ fontWeight: 600, fontSize: { xs: '0.8125rem', sm: '1rem' } }}
|
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)}만원
|
||||||
@ -526,13 +496,13 @@ export default function RecommendationStep({
|
|||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ fontSize: { xs: '0.6875rem', sm: '0.875rem' }, display: 'block', mb: { xs: 0.5, sm: 1 } }}
|
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
||||||
>
|
>
|
||||||
예상 신규 고객
|
예상 신규 고객
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{ fontWeight: 600, fontSize: { xs: '0.8125rem', sm: '1rem' } }}
|
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}명
|
||||||
@ -542,17 +512,29 @@ export default function RecommendationStep({
|
|||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ fontSize: { xs: '0.6875rem', sm: '0.875rem' }, display: 'block', mb: { xs: 0.5, sm: 1 } }}
|
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: { xs: '0.8125rem', sm: '1rem' } }}
|
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>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
||||||
|
>
|
||||||
|
차별점
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontSize: { xs: '0.8125rem', sm: '0.95rem' }, mt: { xs: 0.5, sm: 1 } }}>
|
||||||
|
{rec.differentiator}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -562,16 +544,16 @@ export default function RecommendationStep({
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<Box sx={{ display: 'flex', gap: { xs: 1.5, sm: 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: { xs: 1.25, sm: 3 },
|
py: { xs: 1.5, sm: 3 },
|
||||||
borderRadius: { xs: 2.5, sm: 3 },
|
borderRadius: 3,
|
||||||
fontSize: { xs: '0.8125rem', sm: '1rem' },
|
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
@ -588,9 +570,9 @@ export default function RecommendationStep({
|
|||||||
disabled={selected === null || loading}
|
disabled={selected === null || loading}
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
sx={{
|
sx={{
|
||||||
py: { xs: 1.25, sm: 3 },
|
py: { xs: 1.5, sm: 3 },
|
||||||
borderRadius: { xs: 2.5, sm: 3 },
|
borderRadius: 3,
|
||||||
fontSize: { xs: '0.8125rem', sm: '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': {
|
||||||
@ -602,7 +584,7 @@ export default function RecommendationStep({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loading ? <CircularProgress size={20} sx={{ color: 'white' }} /> : '다음'}
|
{loading ? <CircularProgress size={24} sx={{ color: 'white' }} /> : '다음'}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Content API Proxy: 이미지 조회
|
|
||||||
* GET /api/content/events/{eventId}/images
|
|
||||||
*/
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: { eventId: string } }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { eventId } = params;
|
|
||||||
const { searchParams } = new URL(request.url);
|
|
||||||
|
|
||||||
const CONTENT_API_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
|
||||||
const queryString = searchParams.toString();
|
|
||||||
const backendUrl = `${CONTENT_API_HOST}/api/v1/content/events/${eventId}/images${queryString ? `?${queryString}` : ''}`;
|
|
||||||
|
|
||||||
console.log('🎨 Proxying to Content API:', backendUrl);
|
|
||||||
|
|
||||||
const backendResponse = await fetch(backendUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': request.headers.get('Authorization') || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await backendResponse.json();
|
|
||||||
|
|
||||||
console.log('✅ Content API Response:', {
|
|
||||||
status: backendResponse.status,
|
|
||||||
imageCount: Array.isArray(data) ? data.length : 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json(data, { status: backendResponse.status });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Content API Proxy Error:', error);
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
errorCode: 'PROXY_ERROR',
|
|
||||||
message: 'Content API 호출 중 오류가 발생했습니다',
|
|
||||||
details: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Content API Proxy: 이미지 생성
|
|
||||||
* POST /api/content/images/generate
|
|
||||||
*/
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json();
|
|
||||||
|
|
||||||
const CONTENT_API_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
|
||||||
const backendUrl = `${CONTENT_API_HOST}/api/v1/content/images/generate`;
|
|
||||||
|
|
||||||
console.log('🎨 Proxying to Content API:', backendUrl);
|
|
||||||
console.log('📦 Request body:', body);
|
|
||||||
|
|
||||||
const backendResponse = await fetch(backendUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': request.headers.get('Authorization') || '',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await backendResponse.json();
|
|
||||||
|
|
||||||
console.log('✅ Content API Response:', {
|
|
||||||
status: backendResponse.status,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json(data, { status: backendResponse.status });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Content API Proxy Error:', error);
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
errorCode: 'PROXY_ERROR',
|
|
||||||
message: 'Content API 호출 중 오류가 발생했습니다',
|
|
||||||
details: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Content API Proxy: Job 상태 조회
|
|
||||||
* GET /api/content/images/jobs/{jobId}
|
|
||||||
*/
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: { jobId: string } }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { jobId } = params;
|
|
||||||
|
|
||||||
const CONTENT_API_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
|
||||||
const backendUrl = `${CONTENT_API_HOST}/api/v1/content/images/jobs/${jobId}`;
|
|
||||||
|
|
||||||
console.log('🎨 Proxying to Content API:', backendUrl);
|
|
||||||
|
|
||||||
const backendResponse = await fetch(backendUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': request.headers.get('Authorization') || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await backendResponse.json();
|
|
||||||
|
|
||||||
console.log('✅ Content API Response:', {
|
|
||||||
status: backendResponse.status,
|
|
||||||
jobStatus: data.status,
|
|
||||||
progress: data.progress,
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json(data, { status: backendResponse.status });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Content API Proxy Error:', error);
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
errorCode: 'PROXY_ERROR',
|
|
||||||
message: 'Content API 호출 중 오류가 발생했습니다',
|
|
||||||
details: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -92,9 +92,6 @@ export interface AiRecommendationRequest {
|
|||||||
category: string;
|
category: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
region?: string;
|
|
||||||
targetAudience?: string;
|
|
||||||
budget?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JobAcceptedResponse {
|
export interface JobAcceptedResponse {
|
||||||
@ -309,13 +306,9 @@ export const eventApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Step 2: AI 추천 요청 (POST)
|
// Step 2: AI 추천 요청 (POST)
|
||||||
requestAiRecommendations: async (
|
requestAiRecommendations: async (eventId: string): Promise<AiRecommendationResult> => {
|
||||||
eventId: string,
|
const response = await eventApiClient.post<AiRecommendationResult>(
|
||||||
request: AiRecommendationRequest
|
`/events/${eventId}/ai-recommendations`
|
||||||
): Promise<JobAcceptedResponse> => {
|
|
||||||
const response = await eventApiClient.post<JobAcceptedResponse>(
|
|
||||||
`/events/${eventId}/ai-recommendations`,
|
|
||||||
request
|
|
||||||
);
|
);
|
||||||
console.log('✅ AI 추천 요청 성공:', response.data);
|
console.log('✅ AI 추천 요청 성공:', response.data);
|
||||||
return response.data;
|
return response.data;
|
||||||
@ -323,12 +316,11 @@ export const eventApi = {
|
|||||||
|
|
||||||
// AI 추천 결과 조회 (GET)
|
// AI 추천 결과 조회 (GET)
|
||||||
getAiRecommendations: async (eventId: string): Promise<AiRecommendationResult> => {
|
getAiRecommendations: async (eventId: string): Promise<AiRecommendationResult> => {
|
||||||
const response = await eventApiClient.get<{ success: boolean; data: AiRecommendationResult; timestamp: string }>(
|
const response = await eventApiClient.get<AiRecommendationResult>(
|
||||||
`/events/${eventId}/ai-recommendations`
|
`/events/${eventId}/ai-recommendations`
|
||||||
);
|
);
|
||||||
console.log('✅ AI 추천 결과 조회 (전체 응답):', response.data);
|
console.log('✅ AI 추천 결과 조회:', response.data);
|
||||||
console.log('✅ AI 추천 데이터:', response.data.data);
|
return response.data;
|
||||||
return response.data.data; // 래퍼에서 실제 데이터 추출
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// AI 추천 선택
|
// AI 추천 선택
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user