Compare commits

..

No commits in common. "948eb06e7133ab43736e72fb2cd2341e768c59b1" and "06da17ac3600b9ee5fd647251ecf89391631cb48" have entirely different histories.

13 changed files with 410 additions and 1043 deletions

View File

@ -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 } }}>

View File

@ -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));

View File

@ -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',
{ headers: {
method: 'POST', 'Content-Type': 'application/json',
headers: { },
'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={{
/> color: colors.gray[700],
<Typography fontSize: '0.875rem',
variant="body2" mb: 1,
sx={{ textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
color: colors.gray[700], }}>
fontSize: '0.875rem',
mb: 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={{
/> color: colors.gray[700],
<Typography fontSize: '0.875rem',
variant="body2" mb: 1,
sx={{ textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
color: colors.gray[700], }}>
fontSize: { xs: '0.75rem', sm: '0.875rem' },
mb: 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={{ ml: 0.5,
fontSize: { xs: '0.875rem', sm: '1rem' }, color: colors.gray[900],
ml: 0.5, textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
color: colors.gray[900], }}>
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={{
/> color: colors.gray[700],
<Typography fontSize: '0.875rem',
variant="body2" mb: 1,
sx={{ textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
color: colors.gray[700], }}>
fontSize: { xs: '0.75rem', sm: '0.875rem' },
mb: 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 ml: 0.5,
component="span" color: colors.gray[900],
sx={{ textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
fontSize: { xs: '0.875rem', sm: '1rem' }, }}>
ml: 0.5,
color: colors.gray[900],
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={{
/> color: colors.gray[700],
<Typography fontSize: '0.875rem',
variant="body2" mb: 1,
sx={{ textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
color: colors.gray[700], }}>
fontSize: { xs: '0.75rem', sm: '0.875rem' },
mb: 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 variant="body1" sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1 }}>
</Typography> {eventData.recommendation?.recommendation.title}
<Typography </Typography>
variant="body1" </Box>
sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1, wordBreak: 'keep-all' }} <IconButton size="small">
> <Edit fontSize="small" />
{localStorageData?.eventTitle || </IconButton>
eventData.recommendation?.recommendation.title || </Box>
'이벤트 제목'}
</Typography>
</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 variant="body1" sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1 }}>
</Typography> {eventData.recommendation?.recommendation.mechanics.details || ''}
<Typography </Typography>
variant="body1" </Box>
sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1, wordBreak: 'keep-all' }} <IconButton size="small">
> <Edit fontSize="small" />
{localStorageData?.prize || </IconButton>
eventData.recommendation?.recommendation.mechanics.details || </Box>
''}
</Typography>
</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 variant="body1" sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1 }}>
</Typography> {eventData.recommendation?.recommendation.mechanics.details || ''}
<Typography </Typography>
variant="body1" </Box>
sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1, wordBreak: 'keep-all' }} </Box>
>
{localStorageData?.eventDescription ||
eventData.recommendation?.recommendation.mechanics.details ||
''}
</Typography>
</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 />
. .

View File

@ -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': {

View File

@ -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"

View File

@ -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);
} }

View File

@ -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>
);
}

View File

@ -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,80 +89,39 @@ 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); // 이전 쿠키 삭제 (깨끗한 상태에서 시작)
deleteCookie('eventId');
deleteCookie('jobId');
// 새로운 eventId 생성
const eventId = generateEventId();
console.log('🎉 ========================================');
console.log('✅ 새로운 이벤트 ID 생성:', eventId);
console.log('📋 선택된 목적:', selected);
console.log('🎉 ========================================');
// 쿠키에 저장
setCookie('eventId', eventId, 1); // 1일 동안 유지
console.log('🍪 쿠키에 eventId 저장 완료:', eventId);
// localStorage에도 저장
try { try {
// 이전 쿠키 삭제 (깨끗한 상태에서 시작) localStorage.setItem('eventId', eventId);
deleteCookie('eventId'); console.log('💾 localStorage에 eventId 저장 완료:', eventId);
deleteCookie('jobId'); console.log('📦 저장된 데이터 확인:', {
eventId: eventId,
// 새로운 eventId 생성 objective: selected,
const eventId = generateEventId(); timestamp: new Date().toISOString()
console.log('🎉 ========================================'); });
console.log('✅ 새로운 이벤트 ID 생성:', eventId);
console.log('📋 선택된 목적:', selected);
console.log('🎉 ========================================');
// 쿠키에 저장
setCookie('eventId', eventId, 1); // 1일 동안 유지
console.log('🍪 쿠키에 eventId 저장 완료:', eventId);
// localStorage에도 저장
try {
localStorage.setItem('eventId', eventId);
console.log('💾 localStorage에 eventId 저장 완료:', eventId);
console.log('📦 저장된 데이터 확인:', {
eventId: eventId,
objective: selected,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('❌ localStorage 저장 실패:', error);
}
// AI 추천 생성 요청 body 준비
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 });
} catch (error) { } catch (error) {
console.error('❌ AI 추천 생성 요청 실패:', error); console.error('❌ localStorage 저장 실패:', error);
alert('AI 추천 생성 요청에 실패했습니다. 다시 시도해 주세요.');
} finally {
setIsLoading(false);
} }
// objective와 eventId를 함께 전달
onNext({ objective: selected, eventId });
} }
}; };
@ -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>

View File

@ -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>

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -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 추천 선택