Compare commits

..

2 Commits

Author SHA1 Message Date
cherry2250
948eb06e71 배포하기 버튼 API 호출 주석처리 및 성공 시뮬레이션 추가
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 14:33:56 +09:00
cherry2250
94e5f2d89a mobile2 2025-10-31 14:29:44 +09:00
13 changed files with 1044 additions and 411 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,11 +3,13 @@
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';
@ -18,6 +20,7 @@ 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;
@ -74,6 +77,7 @@ 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;
@ -97,13 +101,27 @@ export default function EventCreatePage() {
objective={({ history }) => ( objective={({ history }) => (
<ObjectiveStep <ObjectiveStep
onNext={({ objective, eventId }) => { onNext={({ objective, eventId }) => {
history.push('recommendation', { objective, eventId }); history.push('loading', { 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 });
}} }}
@ -122,13 +140,18 @@ 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: context.recommendation?.recommendation.promotionChannels || [], trends: allTrends,
prize: '', prize: '',
}; };
localStorage.setItem('eventCreationData', JSON.stringify(eventData)); localStorage.setItem('eventCreationData', JSON.stringify(eventData));

View File

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { import {
Box, Box,
Container, Container,
@ -17,12 +17,27 @@ import {
DialogActions, DialogActions,
Link, Link,
} from '@mui/material'; } from '@mui/material';
import { ArrowBack, CheckCircle, Edit, RocketLaunch, Save, People, AttachMoney, TrendingUp } from '@mui/icons-material'; import {
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;
@ -35,26 +50,37 @@ 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 DISTRIBUTION_API_BASE_URL = process.env.NEXT_PUBLIC_DISTRIBUTION_HOST || 'http://kt-event-marketing-api.20.214.196.128.nip.io'; const [localStorageData, setLocalStorageData] = useState<EventCreationData | null>(null);
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[eventData.objective || 'new_customer'] || 'CUSTOMER_ACQUISITION') as EventObjective; const backendObjective: EventObjective = (objectiveMap[
eventData.objective || 'new_customer'
] || 'CUSTOMER_ACQUISITION') as EventObjective;
const createResponse = await eventApi.createEvent({ const createResponse = await eventApi.createEvent({
objective: backendObjective, objective: backendObjective,
@ -70,7 +96,10 @@ 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 = eventData.contentEdit?.title || eventData.recommendation?.recommendation?.title || '이벤트'; const eventName =
eventData.contentEdit?.title ||
eventData.recommendation?.recommendation?.title ||
'이벤트';
// 날짜 설정 (오늘부터 30일간) // 날짜 설정 (오늘부터 30일간)
const today = new Date(); const today = new Date();
@ -82,7 +111,10 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
await eventApi.updateEvent(eventId, { await eventApi.updateEvent(eventId, {
eventName: eventName, eventName: eventName,
description: eventData.contentEdit?.guide || eventData.recommendation?.recommendation?.description || '', description:
eventData.contentEdit?.guide ||
eventData.recommendation?.recommendation?.description ||
'',
startDate: startDateStr, startDate: startDateStr,
endDate: endDateStr, endDate: endDateStr,
}); });
@ -96,12 +128,15 @@ 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: eventData.contentEdit?.guide || eventData.recommendation?.recommendation?.description || '', description:
eventData.contentEdit?.guide ||
eventData.recommendation?.recommendation?.description ||
'',
imageUrl: '', // TODO: 이미지 URL 연동 필요 imageUrl: '', // TODO: 이미지 URL 연동 필요
channels: apiChannels, channels: apiChannels,
channelSettings: {}, channelSettings: {},
@ -109,13 +144,16 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
console.log('🚀 Distributing event:', distributionRequest); console.log('🚀 Distributing event:', distributionRequest);
const response = await fetch(`${DISTRIBUTION_API_BASE_URL}/api/v1/distribution/distribute`, { const response = await fetch(
method: 'POST', `${DISTRIBUTION_API_BASE_URL}/api/v1/distribution/distribute`,
headers: { {
'Content-Type': 'application/json', method: 'POST',
}, headers: {
body: JSON.stringify(distributionRequest), 'Content-Type': 'application/json',
}); },
body: JSON.stringify(distributionRequest),
}
);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
@ -127,7 +165,6 @@ 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');
} }
@ -136,8 +173,14 @@ 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 연동
@ -156,10 +199,27 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
}; };
return ( return (
<Box sx={{ minHeight: '100vh', bgcolor: colors.gray[50], pt: { xs: 5, sm: 8 }, pb: { xs: 3, sm: 10 } }}> <Box
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 3, sm: 6 }, px: { xs: 1.5, sm: 8, md: 10 } }}> sx={{
minHeight: '100vh',
bgcolor: colors.gray[50],
pt: { xs: 5, sm: 8 },
pb: { xs: 3, sm: 10 },
}}
>
<Container
maxWidth="lg"
sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 3, sm: 6 }, px: { xs: 2, sm: 4, md: 8 } }}
>
{/* Header */} {/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, 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: 40, height: 40 }}> <IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
<ArrowBack sx={{ fontSize: 20 }} /> <ArrowBack sx={{ fontSize: 20 }} />
</IconButton> </IconButton>
@ -180,7 +240,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
</Box> </Box>
{/* Event Summary Statistics */} {/* Event Summary Statistics */}
<Grid container spacing={4} sx={{ mb: { xs: 4, sm: 10 } }}> <Grid container spacing={{ xs: 2, sm: 3, md: 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}
@ -190,19 +250,24 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
borderColor: 'transparent', borderColor: 'transparent',
}} }}
> >
<CardContent sx={{ textAlign: 'center', py: 4, px: 3 }}> <CardContent sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}>
<CheckCircle sx={{ <CheckCircle
fontSize: 32, sx={{
color: colors.gray[900], fontSize: 32,
mb: 1, color: colors.gray[900],
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))', mb: 1,
}} /> filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
<Typography variant="body2" sx={{ }}
color: colors.gray[700], />
fontSize: '0.875rem', <Typography
mb: 1, variant="body2"
textShadow: '0px 1px 2px rgba(0,0,0,0.1)', sx={{
}}> color: colors.gray[700],
fontSize: '0.875rem',
mb: 1,
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
}}
>
</Typography> </Typography>
<Typography <Typography
@ -210,11 +275,14 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
sx={{ sx={{
fontWeight: 700, fontWeight: 700,
color: colors.gray[900], color: colors.gray[900],
fontSize: '1rem', fontSize: { xs: '0.875rem', sm: '1rem' },
textShadow: '0px 2px 4px rgba(0,0,0,0.15)', textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
wordBreak: 'keep-all',
}} }}
> >
{eventData.recommendation?.recommendation.title || '이벤트 제목'} {localStorageData?.eventTitle ||
eventData.recommendation?.recommendation.title ||
'이벤트 제목'}
</Typography> </Typography>
</CardContent> </CardContent>
</Card> </Card>
@ -228,19 +296,24 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
borderColor: 'transparent', borderColor: 'transparent',
}} }}
> >
<CardContent sx={{ textAlign: 'center', py: 4, px: 3 }}> <CardContent sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}>
<People sx={{ <People
fontSize: 32, sx={{
color: colors.gray[900], fontSize: { xs: 28, sm: 32 },
mb: 1, color: colors.gray[900],
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))', mb: 1,
}} /> filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
<Typography variant="body2" sx={{ }}
color: colors.gray[700], />
fontSize: '0.875rem', <Typography
mb: 1, variant="body2"
textShadow: '0px 1px 2px rgba(0,0,0,0.1)', sx={{
}}> 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
@ -248,17 +321,20 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
sx={{ sx={{
fontWeight: 700, fontWeight: 700,
color: colors.gray[900], color: colors.gray[900],
fontSize: '1.75rem', fontSize: { xs: '1.25rem', sm: '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 component="span" sx={{ <Typography
fontSize: '1rem', component="span"
ml: 0.5, sx={{
color: colors.gray[900], fontSize: { xs: '0.875rem', sm: '1rem' },
textShadow: '0px 2px 4px rgba(0,0,0,0.15)', ml: 0.5,
}}> color: colors.gray[900],
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}
>
</Typography> </Typography>
</Typography> </Typography>
@ -274,19 +350,24 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
borderColor: 'transparent', borderColor: 'transparent',
}} }}
> >
<CardContent sx={{ textAlign: 'center', py: 4, px: 3 }}> <CardContent sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}>
<AttachMoney sx={{ <AttachMoney
fontSize: 32, sx={{
color: colors.gray[900], fontSize: { xs: 28, sm: 32 },
mb: 1, color: colors.gray[900],
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))', mb: 1,
}} /> filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
<Typography variant="body2" sx={{ }}
color: colors.gray[700], />
fontSize: '0.875rem', <Typography
mb: 1, variant="body2"
textShadow: '0px 1px 2px rgba(0,0,0,0.1)', sx={{
}}> 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
@ -294,17 +375,22 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
sx={{ sx={{
fontWeight: 700, fontWeight: 700,
color: colors.gray[900], color: colors.gray[900],
fontSize: '1.75rem', fontSize: { xs: '1.25rem', sm: '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)} {(
<Typography component="span" sx={{ (eventData.recommendation?.recommendation.estimatedCost.max || 0) / 10000
fontSize: '1rem', ).toFixed(0)}
ml: 0.5, <Typography
color: colors.gray[900], component="span"
textShadow: '0px 2px 4px rgba(0,0,0,0.15)', sx={{
}}> 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>
@ -320,19 +406,24 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
borderColor: 'transparent', borderColor: 'transparent',
}} }}
> >
<CardContent sx={{ textAlign: 'center', py: 4, px: 3 }}> <CardContent sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}>
<TrendingUp sx={{ <TrendingUp
fontSize: 32, sx={{
color: colors.gray[900], fontSize: { xs: 28, sm: 32 },
mb: 1, color: colors.gray[900],
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))', mb: 1,
}} /> filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
<Typography variant="body2" sx={{ }}
color: colors.gray[700], />
fontSize: '0.875rem', <Typography
mb: 1, variant="body2"
textShadow: '0px 1px 2px rgba(0,0,0,0.1)', sx={{
}}> 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
@ -340,7 +431,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
sx={{ sx={{
fontWeight: 700, fontWeight: 700,
color: colors.gray[900], color: colors.gray[900],
fontSize: '1.75rem', fontSize: { xs: '1.25rem', sm: '1.75rem' },
textShadow: '0px 2px 4px rgba(0,0,0,0.15)', textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}} }}
> >
@ -357,53 +448,62 @@ 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: 2, sm: 8 } }}> <CardContent sx={{ p: { xs: 3, sm: 6 } }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}> <Typography
<Box sx={{ flex: 1 }}> variant="caption"
<Typography variant="caption" color="text.secondary" sx={{ ...responsiveText.body2, fontSize: '0.875rem' }}> color="text.secondary"
sx={{ ...responsiveText.body2, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
</Typography> >
<Typography variant="body1" sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1 }}>
{eventData.recommendation?.recommendation.title} </Typography>
</Typography> <Typography
</Box> variant="body1"
<IconButton size="small"> sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1, wordBreak: 'keep-all' }}
<Edit fontSize="small" /> >
</IconButton> {localStorageData?.eventTitle ||
</Box> eventData.recommendation?.recommendation.title ||
'이벤트 제목'}
</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: 2, sm: 8 } }}> <CardContent sx={{ p: { xs: 3, sm: 6 } }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}> <Typography
<Box sx={{ flex: 1 }}> variant="caption"
<Typography variant="caption" color="text.secondary" sx={{ ...responsiveText.body2, fontSize: '0.875rem' }}> color="text.secondary"
sx={{ ...responsiveText.body2, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
</Typography> >
<Typography variant="body1" sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1 }}>
{eventData.recommendation?.recommendation.mechanics.details || ''} </Typography>
</Typography> <Typography
</Box> variant="body1"
<IconButton size="small"> sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1, wordBreak: 'keep-all' }}
<Edit fontSize="small" /> >
</IconButton> {localStorageData?.prize ||
</Box> eventData.recommendation?.recommendation.mechanics.details ||
''}
</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: 2, sm: 8 } }}> <CardContent sx={{ p: { xs: 3, sm: 6 } }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}> <Typography
<Box sx={{ flex: 1 }}> variant="caption"
<Typography variant="caption" color="text.secondary" sx={{ ...responsiveText.body2, fontSize: '0.875rem' }}> color="text.secondary"
sx={{ ...responsiveText.body2, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
</Typography> >
<Typography variant="body1" sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1 }}>
{eventData.recommendation?.recommendation.mechanics.details || ''} </Typography>
</Typography> <Typography
</Box> variant="body1"
</Box> sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1, wordBreak: 'keep-all' }}
>
{localStorageData?.eventDescription ||
eventData.recommendation?.recommendation.mechanics.details ||
''}
</Typography>
</CardContent> </CardContent>
</Card> </Card>
@ -413,8 +513,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: 2, sm: 8 } }}> <CardContent sx={{ p: { xs: 3, sm: 6 } }}>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 4 }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: { xs: 1.5, sm: 2 } }}>
{getChannelNames(eventData.channels).map((channel) => ( {getChannelNames(eventData.channels).map((channel) => (
<Chip <Chip
key={channel} key={channel}
@ -423,30 +523,22 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
bgcolor: colors.purple, bgcolor: colors.purple,
color: 'white', color: 'white',
fontWeight: 600, fontWeight: 600,
fontSize: '0.875rem', fontSize: { xs: '0.75rem', sm: '0.875rem' },
px: 2, px: { xs: 1.5, sm: 2 },
py: 2.5, py: { xs: 2, sm: 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 elevation={0} sx={{ ...cardStyles.default, bgcolor: colors.gray[50], mb: { xs: 4, sm: 10 } }}> <Card
<CardContent sx={{ p: { xs: 2, sm: 8 } }}> elevation={0}
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
@ -461,11 +553,8 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS
/> />
} }
label={ label={
<Typography variant="body2" sx={{ ...responsiveText.body1 }}> <Typography variant="body2" sx={{ ...responsiveText.body1, fontWeight: 400 }}>
{' '} {' '}
<Typography component="span" sx={{ color: colors.orange, fontWeight: 600 }}>
()
</Typography>
</Typography> </Typography>
} }
/> />
@ -551,21 +640,33 @@ 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 variant="body2" sx={{ ...responsiveText.body1, mb: 6, color: colors.gray[700] }}> <Typography
KT AI variant="body2"
. 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 variant="body2" sx={{ ...responsiveText.body1, mb: 2, color: colors.gray[700] }}> <Typography
variant="body2"
sx={{ ...responsiveText.body1, mb: 2, color: colors.gray[700] }}
>
항목: 이름, , 항목: 이름, ,
</Typography> </Typography>
<Typography variant="body2" sx={{ ...responsiveText.body1, mb: 2, color: colors.gray[700] }}> <Typography
variant="body2"
sx={{ ...responsiveText.body1, mb: 2, color: colors.gray[700] }}
>
목적: 이벤트 목적: 이벤트
</Typography> </Typography>
<Typography variant="body2" sx={{ ...responsiveText.body1, mb: 6, color: colors.gray[700] }}> <Typography
variant="body2"
sx={{ ...responsiveText.body1, mb: 6, color: colors.gray[700] }}
>
기간: 이벤트 6 기간: 이벤트 6
</Typography> </Typography>
@ -616,7 +717,11 @@ 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 variant="body1" color="text.secondary" sx={{ fontSize: '1rem', mb: { xs: 3, sm: 8 } }}> <Typography
variant="body1"
color="text.secondary"
sx={{ fontSize: '1rem', mb: { xs: 3, sm: 8 } }}
>
. .
<br /> <br />
. .

View File

@ -14,6 +14,8 @@ 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';
@ -108,18 +110,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: 3, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 1.5, sm: 6, md: 8 } }}> <Container maxWidth="lg" sx={{ pt: { xs: 2, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
{/* Header */} {/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, mb: { xs: 3, sm: 8 } }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1, sm: 3 }, mb: { xs: 3, sm: 8 } }}>
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}> <IconButton onClick={onBack} sx={{ width: { xs: 36, sm: 40 }, height: { xs: 36, sm: 40 }, p: { xs: 0.5, sm: 1 } }}>
<ArrowBack sx={{ fontSize: 20 }} /> <ArrowBack sx={{ fontSize: { xs: 18, sm: 20 } }} />
</IconButton> </IconButton>
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}> <Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1rem', sm: '1.5rem' } }}>
</Typography> </Typography>
</Box> </Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: { xs: 3, sm: 8 }, textAlign: 'center', fontSize: { xs: '0.8125rem', sm: '1rem' } }}> <Typography variant="body2" color="text.secondary" sx={{ mb: { xs: 2.5, sm: 8 }, textAlign: 'center', fontSize: { xs: '0.75rem', sm: '1rem' } }}>
( 1 ) ( 1 )
</Typography> </Typography>
@ -127,8 +129,8 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
<Card <Card
elevation={0} elevation={0}
sx={{ sx={{
mb: 6, mb: { xs: 2.5, sm: 6 },
borderRadius: 4, borderRadius: { xs: 3, sm: 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',
@ -136,13 +138,14 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
transition: 'all 0.3s', transition: 'all 0.3s',
}} }}
> >
<CardContent sx={{ p: { xs: 2, sm: 6 } }}> <CardContent sx={{ p: { xs: 2.5, 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,
@ -151,46 +154,48 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
/> />
} }
label={ label={
<Typography variant="body1" sx={{ fontWeight: channels[0].selected ? 700 : 600, fontSize: '1.125rem' }}> <Typography variant="body1" sx={{ fontWeight: channels[0].selected ? 700 : 600, fontSize: { xs: '0.9375rem', sm: '1.125rem' } }}>
TV TV
</Typography> </Typography>
} }
sx={{ mb: channels[0].selected ? 2 : 0 }} sx={{ mb: channels[0].selected ? { xs: 1.5, sm: 2 } : 0 }}
/> />
{channels[0].selected && ( {channels[0].selected && (
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}> <Box sx={{ pl: { xs: 2.5, sm: 4 }, pt: { xs: 1.5, sm: 2 }, borderTop: 1, borderColor: 'divider' }}>
<FormControl fullWidth sx={{ mb: 2 }}> <FormControl fullWidth sx={{ mb: { xs: 1.5, sm: 2 } }}>
<InputLabel></InputLabel> <InputLabel sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></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">500m</MenuItem> <MenuItem value="500" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>500m</MenuItem>
<MenuItem value="1000">1km</MenuItem> <MenuItem value="1000" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>1km</MenuItem>
<MenuItem value="2000">2km</MenuItem> <MenuItem value="2000" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>2km</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth sx={{ mb: 2 }}> <FormControl fullWidth sx={{ mb: { xs: 1.5, sm: 2 } }}>
<InputLabel> </InputLabel> <InputLabel sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}> </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"> (7-12)</MenuItem> <MenuItem value="morning" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}> (7-12)</MenuItem>
<MenuItem value="afternoon"> (12-17)</MenuItem> <MenuItem value="afternoon" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}> (12-17)</MenuItem>
<MenuItem value="evening"> (17-22)</MenuItem> <MenuItem value="evening" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}> (17-22)</MenuItem>
<MenuItem value="all"></MenuItem> <MenuItem value="all" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></MenuItem>
</Select> </Select>
</FormControl> </FormControl>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' }, mb: { xs: 0.5, sm: 0 } }}>
: <strong>5</strong> : <strong>5</strong>
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
: <strong>8</strong> : <strong>8</strong>
</Typography> </Typography>
</Box> </Box>
@ -202,8 +207,8 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
<Card <Card
elevation={0} elevation={0}
sx={{ sx={{
mb: 6, mb: { xs: 2.5, sm: 6 },
borderRadius: 4, borderRadius: { xs: 3, sm: 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',
@ -211,13 +216,14 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
transition: 'all 0.3s', transition: 'all 0.3s',
}} }}
> >
<CardContent sx={{ p: { xs: 2, sm: 6 } }}> <CardContent sx={{ p: { xs: 2.5, 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,
@ -226,30 +232,32 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
/> />
} }
label={ label={
<Typography variant="body1" sx={{ fontWeight: channels[1].selected ? 700 : 600, fontSize: '1.125rem' }}> <Typography variant="body1" sx={{ fontWeight: channels[1].selected ? 700 : 600, fontSize: { xs: '0.9375rem', sm: '1.125rem' } }}>
</Typography> </Typography>
} }
sx={{ mb: channels[1].selected ? 2 : 0 }} sx={{ mb: channels[1].selected ? { xs: 1.5, sm: 2 } : 0 }}
/> />
{channels[1].selected && ( {channels[1].selected && (
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}> <Box sx={{ pl: { xs: 2.5, sm: 4 }, pt: { xs: 1.5, sm: 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: 2 }} sx={{ mb: { xs: 1.5, sm: 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: 0.5 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: { xs: 0.5, sm: 0.5 }, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' }, mb: { xs: 0.5, sm: 0 } }}>
: <strong>3</strong> : <strong>3</strong>
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
: <strong></strong> : <strong></strong>
</Typography> </Typography>
</Box> </Box>
@ -261,8 +269,8 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
<Card <Card
elevation={0} elevation={0}
sx={{ sx={{
mb: 6, mb: { xs: 2.5, sm: 6 },
borderRadius: 4, borderRadius: { xs: 3, sm: 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',
@ -270,13 +278,14 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
transition: 'all 0.3s', transition: 'all 0.3s',
}} }}
> >
<CardContent sx={{ p: { xs: 2, sm: 6 } }}> <CardContent sx={{ p: { xs: 2.5, 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,
@ -285,37 +294,39 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
/> />
} }
label={ label={
<Typography variant="body1" sx={{ fontWeight: channels[2].selected ? 700 : 600, fontSize: '1.125rem' }}> <Typography variant="body1" sx={{ fontWeight: channels[2].selected ? 700 : 600, fontSize: { xs: '0.9375rem', sm: '1.125rem' } }}>
TV TV
</Typography> </Typography>
} }
sx={{ mb: channels[2].selected ? 2 : 0 }} sx={{ mb: channels[2].selected ? { xs: 1.5, sm: 2 } : 0 }}
/> />
{channels[2].selected && ( {channels[2].selected && (
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}> <Box sx={{ pl: { xs: 2.5, sm: 4 }, pt: { xs: 1.5, sm: 2 }, borderTop: 1, borderColor: 'divider' }}>
<FormControl fullWidth sx={{ mb: 2 }}> <FormControl fullWidth sx={{ mb: { xs: 1.5, sm: 2 } }}>
<InputLabel></InputLabel> <InputLabel sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></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"></MenuItem> <MenuItem value="suwon" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></MenuItem>
<MenuItem value="seoul"></MenuItem> <MenuItem value="seoul" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></MenuItem>
<MenuItem value="busan"></MenuItem> <MenuItem value="busan" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></MenuItem>
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth sx={{ mb: 2 }}> <FormControl fullWidth sx={{ mb: { xs: 1.5, sm: 2 } }}>
<InputLabel> </InputLabel> <InputLabel sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}> </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"></MenuItem> <MenuItem value="all" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></MenuItem>
<MenuItem value="prime"> (19-23)</MenuItem> <MenuItem value="prime" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}> (19-23)</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
@ -327,10 +338,12 @@ 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: 2 }} sx={{ mb: { xs: 1.5, sm: 2 } }}
InputLabelProps={{ sx: { fontSize: { xs: '0.875rem', sm: '1rem' } } }}
inputProps={{ sx: { fontSize: { xs: '0.875rem', sm: '1rem' } } }}
/> />
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
:{' '} :{' '}
<strong> <strong>
{getChannelConfig('genieTV', 'budget') {getChannelConfig('genieTV', 'budget')
@ -347,8 +360,8 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
<Card <Card
elevation={0} elevation={0}
sx={{ sx={{
mb: { xs: 4, sm: 10 }, mb: { xs: 3, sm: 10 },
borderRadius: 4, borderRadius: { xs: 3, sm: 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',
@ -356,13 +369,14 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
transition: 'all 0.3s', transition: 'all 0.3s',
}} }}
> >
<CardContent sx={{ p: { xs: 2, sm: 6 } }}> <CardContent sx={{ p: { xs: 2.5, 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,
@ -371,16 +385,16 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
/> />
} }
label={ label={
<Typography variant="body1" sx={{ fontWeight: channels[3].selected ? 700 : 600, fontSize: '1.125rem' }}> <Typography variant="body1" sx={{ fontWeight: channels[3].selected ? 700 : 600, fontSize: { xs: '0.9375rem', sm: '1.125rem' } }}>
SNS SNS
</Typography> </Typography>
} }
sx={{ mb: channels[3].selected ? 2 : 0 }} sx={{ mb: channels[3].selected ? { xs: 1.5, sm: 2 } : 0 }}
/> />
{channels[3].selected && ( {channels[3].selected && (
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}> <Box sx={{ pl: { xs: 2.5, sm: 4 }, pt: { xs: 1.5, sm: 2 }, borderTop: 1, borderColor: 'divider' }}>
<Typography variant="body2" sx={{ mb: 1, fontWeight: 600 }}> <Typography variant="body2" sx={{ mb: { xs: 1, sm: 1 }, fontWeight: 600, fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
</Typography> </Typography>
<FormControlLabel <FormControlLabel
@ -391,6 +405,7 @@ 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,
@ -398,7 +413,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
}} }}
/> />
} }
label="Instagram" label={<Typography sx={{ fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>Instagram</Typography>}
sx={{ display: 'block' }} sx={{ display: 'block' }}
/> />
<FormControlLabel <FormControlLabel
@ -409,6 +424,7 @@ 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,
@ -416,7 +432,7 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
}} }}
/> />
} }
label="Naver Blog" label={<Typography sx={{ fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>Naver Blog</Typography>}
sx={{ display: 'block' }} sx={{ display: 'block' }}
/> />
<FormControlLabel <FormControlLabel
@ -427,6 +443,7 @@ 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,
@ -434,26 +451,27 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
}} }}
/> />
} }
label="Kakao Channel" label={<Typography sx={{ fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>Kakao Channel</Typography>}
sx={{ display: 'block', mb: 2 }} sx={{ display: 'block', mb: { xs: 1.5, sm: 2 } }}
/> />
<FormControl fullWidth sx={{ mb: 2 }}> <FormControl fullWidth sx={{ mb: { xs: 1.5, sm: 2 } }}>
<InputLabel> </InputLabel> <InputLabel sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}> </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"></MenuItem> <MenuItem value="now" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></MenuItem>
<MenuItem value="schedule"></MenuItem> <MenuItem value="schedule" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></MenuItem>
</Select> </Select>
</FormControl> </FormControl>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' }, mb: { xs: 0.5, sm: 0 } }}>
: <strong>-</strong> : <strong>-</strong>
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
: <strong></strong> : <strong></strong>
</Typography> </Typography>
</Box> </Box>
@ -465,26 +483,26 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
<Card <Card
elevation={0} elevation={0}
sx={{ sx={{
mb: { xs: 4, sm: 10 }, mb: { xs: 3, sm: 10 },
borderRadius: 4, borderRadius: { xs: 3, sm: 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, sm: 8 } }}> <CardContent sx={{ p: { xs: 2.5, sm: 8 } }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 4 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: { xs: 2.5, sm: 4 } }}>
<Typography variant="h6" sx={{ fontSize: '1.25rem' }}> <Typography variant="h6" sx={{ fontSize: { xs: '0.9375rem', sm: '1.25rem' } }}>
</Typography> </Typography>
<Typography variant="h6" color="error.main" sx={{ fontWeight: 700, fontSize: '1.25rem' }}> <Typography variant="h6" color="error.main" sx={{ fontWeight: 700, fontSize: { xs: '0.9375rem', sm: '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: '1.25rem' }}> <Typography variant="h6" sx={{ fontSize: { xs: '0.9375rem', sm: '1.25rem' } }}>
</Typography> </Typography>
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1.25rem', color: colors.purple }}> <Typography variant="h6" sx={{ fontWeight: 700, fontSize: { xs: '0.9375rem', sm: '1.25rem' }, color: colors.purple }}>
{totalExposure > 0 ? `${totalExposure.toLocaleString()}명+` : '0명'} {totalExposure > 0 ? `${totalExposure.toLocaleString()}명+` : '0명'}
</Typography> </Typography>
</Box> </Box>
@ -492,16 +510,16 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
</Card> </Card>
{/* Action Buttons */} {/* Action Buttons */}
<Box sx={{ display: 'flex', gap: { xs: 2, sm: 4 } }}> <Box sx={{ display: 'flex', gap: { xs: 1.5, sm: 4 } }}>
<Button <Button
fullWidth fullWidth
variant="outlined" variant="outlined"
size="large" size="large"
onClick={onBack} onClick={onBack}
sx={{ sx={{
py: { xs: 1.5, sm: 3 }, py: { xs: 1.25, sm: 3 },
borderRadius: 3, borderRadius: { xs: 2.5, sm: 3 },
fontSize: { xs: '0.875rem', sm: '1rem' }, fontSize: { xs: '0.8125rem', sm: '1rem' },
fontWeight: 600, fontWeight: 600,
borderWidth: 2, borderWidth: 2,
'&:hover': { '&:hover': {
@ -518,9 +536,9 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
disabled={selectedCount === 0} disabled={selectedCount === 0}
onClick={handleNext} onClick={handleNext}
sx={{ sx={{
py: { xs: 1.5, sm: 3 }, py: { xs: 1.25, sm: 3 },
borderRadius: 3, borderRadius: { xs: 2.5, sm: 3 },
fontSize: { xs: '0.875rem', sm: '1rem' }, fontSize: { xs: '0.8125rem', 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,10 +40,27 @@ export default function ContentEditStep({
}; };
return ( return (
<Box sx={{ minHeight: '100vh', bgcolor: colors.gray[50], pt: { xs: 5, sm: 8 }, pb: { xs: 3, sm: 10 } }}> <Box
<Container maxWidth="lg" sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 3, sm: 6 }, px: { xs: 1.5, sm: 8, md: 10 } }}> sx={{
minHeight: '100vh',
bgcolor: colors.gray[50],
pt: { xs: 5, sm: 8 },
pb: { xs: 3, sm: 10 },
}}
>
<Container
maxWidth="lg"
sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 3, sm: 6 }, px: { xs: 1.5, sm: 8, md: 10 } }}
>
{/* Header */} {/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, mb: { xs: 3, sm: 10 } }}> <Box
sx={{
display: 'flex',
alignItems: 'center',
gap: { xs: 1.5, sm: 3 },
mb: { xs: 3, sm: 10 },
}}
>
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}> <IconButton onClick={onBack} sx={{ width: 40, height: 40 }}>
<ArrowBack sx={{ fontSize: 20 }} /> <ArrowBack sx={{ fontSize: 20 }} />
</IconButton> </IconButton>
@ -54,7 +71,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>
@ -89,17 +106,25 @@ 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 variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: { xs: 3, sm: 6 } }}> <Typography
variant="h6"
</Typography> sx={{ ...responsiveText.h4, fontWeight: 700, mb: { xs: 3, sm: 6 } }}
></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 sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 2 }, mb: { xs: 3, sm: 6 } }}> <Box
sx={{
display: 'flex',
alignItems: 'center',
gap: { xs: 1.5, sm: 2 },
mb: { xs: 3, sm: 6 },
}}
>
<Edit sx={{ color: colors.purple, fontSize: { xs: 20, sm: 28 } }} /> <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 }}>
@ -148,7 +173,7 @@ export default function ContentEditStep({
</Grid> </Grid>
{/* Action Buttons */} {/* Action Buttons */}
<Box sx={{ display: 'flex', gap: { xs: 2, sm: 4 }, mt: { xs: 4, sm: 10 } }}> <Box sx={{ display: 'flex', gap: { xs: 2, sm: 4 }, mt: { xs: 8, sm: 20 } }}>
<Button <Button
fullWidth fullWidth
variant="outlined" variant="outlined"

View File

@ -252,9 +252,34 @@ 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(10); setLoadingProgress(5);
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회)
@ -263,23 +288,12 @@ 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% 잠깐 보여주기
@ -288,6 +302,8 @@ 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

@ -0,0 +1,231 @@
'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,6 +13,8 @@ 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 = {
@ -89,39 +91,80 @@ 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);
const handleNext = () => { // Objective 한글 매핑
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); // 이전 쿠키 삭제 (깨끗한 상태에서 시작)
console.log('💾 localStorage에 eventId 저장 완료:', eventId); deleteCookie('eventId');
console.log('📦 저장된 데이터 확인:', { deleteCookie('jobId');
eventId: eventId,
objective: selected,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('❌ localStorage 저장 실패:', error);
}
// objective와 eventId를 함께 전달 // 새로운 eventId 생성
onNext({ objective: selected, 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 {
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) {
console.error('❌ AI 추천 생성 요청 실패:', error);
alert('AI 추천 생성 요청에 실패했습니다. 다시 시도해 주세요.');
} finally {
setIsLoading(false);
}
} }
}; };
@ -211,7 +254,7 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
fullWidth fullWidth
variant="contained" variant="contained"
size="large" size="large"
disabled={!selected} disabled={!selected || isLoading}
onClick={handleNext} onClick={handleNext}
sx={{ sx={{
py: { xs: 1.5, sm: 3 }, py: { xs: 1.5, sm: 3 },
@ -228,7 +271,7 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
}, },
}} }}
> >
{isLoading ? 'AI 추천 요청 중...' : '다음'}
</Button> </Button>
</Box> </Box>
</Container> </Container>

View File

@ -42,6 +42,7 @@ 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;
} }
@ -60,6 +61,7 @@ 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) {
@ -67,7 +69,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>(null); const [aiResult, setAiResult] = useState<AiRecommendationResult | null>(initialAiResult || 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 }>
@ -78,7 +80,15 @@ export default function RecommendationStep({
// 컴포넌트 마운트 시 AI 추천 결과 조회 // 컴포넌트 마운트 시 AI 추천 결과 조회
useEffect(() => { useEffect(() => {
// props에서만 eventId를 받음 // LoadingStep에서 이미 데이터를 전달받은 경우 API 호출 생략
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) {
@ -95,28 +105,29 @@ export default function RecommendationStep({
console.error('❌ eventId가 없습니다. ObjectiveStep으로 돌아가세요.'); console.error('❌ eventId가 없습니다. ObjectiveStep으로 돌아가세요.');
setError('이벤트 ID가 없습니다. 이전 단계로 돌아가서 다시 시도하세요.'); setError('이벤트 ID가 없습니다. 이전 단계로 돌아가서 다시 시도하세요.');
} }
}, [initialEventId]); }, [initialEventId, initialAiResult]);
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);
// POST /events/{eventId}/ai-recommendations 엔드포인트로 AI 추천 요청 // GET /events/{eventId}/ai-recommendations 엔드포인트로 AI 추천 결과 조회
const recommendations = await eventApi.requestAiRecommendations(evtId); // ObjectiveStep에서 이미 POST 요청으로 생성했으므로 GET으로 조회
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);
@ -132,14 +143,25 @@ export default function RecommendationStep({
try { try {
setLoading(true); setLoading(true);
// AI 추천 선택 API 호출 // localStorage에 선택한 추천 정보 저장
await eventApi.selectRecommendation(eventId, { const selectedRecommendationData = {
recommendationId: `${eventId}-opt${selected}`, eventId,
customizations: { selectedOptionNumber: selected,
eventName: edited?.title || selectedRec.title, recommendation: {
...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({
@ -182,24 +204,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: 3, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 1.5, sm: 6, md: 8 } }}> <Container maxWidth="lg" sx={{ pt: { xs: 2, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, mb: { xs: 3, sm: 8 } }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1, sm: 3 }, mb: { xs: 3, sm: 8 } }}>
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}> <IconButton onClick={onBack} sx={{ width: { xs: 36, sm: 40 }, height: { xs: 36, sm: 40 }, p: { xs: 0.5, sm: 1 } }}>
<ArrowBack sx={{ fontSize: 20 }} /> <ArrowBack sx={{ fontSize: { xs: 18, sm: 20 } }} />
</IconButton> </IconButton>
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}> <Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1rem', sm: '1.5rem' } }}>
AI AI
</Typography> </Typography>
</Box> </Box>
<Box <Box
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: { xs: 1.5, sm: 4 }, py: { xs: 4, sm: 12 } }} sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: { xs: 2, sm: 4 }, py: { xs: 6, sm: 12 } }}
> >
<CircularProgress size={48} sx={{ color: colors.purple }} /> <CircularProgress sx={{ width: { xs: 40, sm: 48 }, height: { xs: 40, sm: 48 }, color: colors.purple }} />
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: { xs: '0.9375rem', sm: '1.25rem' } }}> <Typography variant="h6" sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1.25rem' }, textAlign: 'center', px: 2 }}>
AI가 ... AI가 ...
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.8125rem', sm: '1rem' } }}> <Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '1rem' }, textAlign: 'center', px: 2 }}>
, , , ,
</Typography> </Typography>
</Box> </Box>
@ -212,30 +234,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: 3, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 1.5, sm: 6, md: 8 } }}> <Container maxWidth="lg" sx={{ pt: { xs: 2, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, mb: { xs: 3, sm: 8 } }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1, sm: 3 }, mb: { xs: 3, sm: 8 } }}>
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}> <IconButton onClick={onBack} sx={{ width: { xs: 36, sm: 40 }, height: { xs: 36, sm: 40 }, p: { xs: 0.5, sm: 1 } }}>
<ArrowBack sx={{ fontSize: 20 }} /> <ArrowBack sx={{ fontSize: { xs: 18, sm: 20 } }} />
</IconButton> </IconButton>
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}> <Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1rem', sm: '1.5rem' } }}>
AI AI
</Typography> </Typography>
</Box> </Box>
<Alert severity="error" sx={{ mb: { xs: 3, sm: 4 } }}> <Alert severity="error" sx={{ mb: { xs: 2.5, sm: 4 }, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
{error} {error}
</Alert> </Alert>
<Box sx={{ display: 'flex', gap: { xs: 2, sm: 4 } }}> <Box sx={{ display: 'flex', gap: { xs: 1.5, sm: 4 } }}>
<Button <Button
fullWidth fullWidth
variant="outlined" variant="outlined"
size="large" size="large"
onClick={onBack} onClick={onBack}
sx={{ sx={{
py: { xs: 1.5, sm: 3 }, py: { xs: 1.25, sm: 3 },
borderRadius: 3, borderRadius: { xs: 2.5, sm: 3 },
fontSize: { xs: '0.875rem', sm: '1rem' }, fontSize: { xs: '0.8125rem', sm: '1rem' },
fontWeight: 600, fontWeight: 600,
}} }}
> >
@ -257,9 +279,9 @@ export default function RecommendationStep({
} }
}} }}
sx={{ sx={{
py: { xs: 1.5, sm: 3 }, py: { xs: 1.25, sm: 3 },
borderRadius: 3, borderRadius: { xs: 2.5, sm: 3 },
fontSize: { xs: '0.875rem', sm: '1rem' }, fontSize: { xs: '0.8125rem', 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%)`,
}} }}
@ -285,13 +307,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: 3, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 1.5, sm: 6, md: 8 } }}> <Container maxWidth="lg" sx={{ pt: { xs: 2, sm: 8 }, pb: { xs: 3, sm: 8 }, px: { xs: 2, sm: 6, md: 8 } }}>
{/* Header */} {/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 3 }, mb: { xs: 3, sm: 8 } }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1, sm: 3 }, mb: { xs: 3, sm: 8 } }}>
<IconButton onClick={onBack} sx={{ width: 40, height: 40 }}> <IconButton onClick={onBack} sx={{ width: { xs: 36, sm: 40 }, height: { xs: 36, sm: 40 }, p: { xs: 0.5, sm: 1 } }}>
<ArrowBack sx={{ fontSize: 20 }} /> <ArrowBack sx={{ fontSize: { xs: 18, sm: 20 } }} />
</IconButton> </IconButton>
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}> <Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1rem', sm: '1.5rem' } }}>
AI AI
</Typography> </Typography>
</Box> </Box>
@ -301,20 +323,20 @@ export default function RecommendationStep({
elevation={0} elevation={0}
sx={{ sx={{
mb: { xs: 3, sm: 10 }, mb: { xs: 3, sm: 10 },
borderRadius: 4, borderRadius: { xs: 3, sm: 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, sm: 8 } }}> <CardContent sx={{ p: { xs: 2.5, sm: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, sm: 2 }, mb: { xs: 3, sm: 6 } }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1, sm: 2 }, mb: { xs: 2.5, sm: 6 } }}>
<Insights sx={{ fontSize: { xs: 24, sm: 32 }, color: colors.purple }} /> <Insights sx={{ fontSize: { xs: 20, sm: 32 }, color: colors.purple }} />
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: { xs: '1rem', sm: '1.25rem' } }}> <Typography variant="h6" sx={{ fontWeight: 700, fontSize: { xs: '0.9375rem', sm: '1.25rem' } }}>
AI AI
</Typography> </Typography>
</Box> </Box>
<Grid container spacing={{ xs: 3, sm: 6 }}> <Grid container spacing={{ xs: 2.5, sm: 6 }}>
<Grid item xs={12} md={4}> <Grid item xs={12} md={4}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: { xs: 1.5, sm: 2 }, fontSize: { xs: '0.875rem', sm: '1rem' } }}> <Typography variant="subtitle2" sx={{ fontWeight: 600, mb: { xs: 1, sm: 2 }, fontSize: { xs: '0.8125rem', sm: '1rem' } }}>
📍 📍
</Typography> </Typography>
{aiResult.trendAnalysis.industryTrends.slice(0, 3).map((trend, idx) => ( {aiResult.trendAnalysis.industryTrends.slice(0, 3).map((trend, idx) => (
@ -322,14 +344,14 @@ export default function RecommendationStep({
key={idx} key={idx}
variant="body2" variant="body2"
color="text.secondary" color="text.secondary"
sx={{ fontSize: { xs: '0.8125rem', sm: '0.95rem' }, mb: 1 }} sx={{ fontSize: { xs: '0.75rem', sm: '0.95rem' }, mb: { xs: 0.75, sm: 1 }, lineHeight: 1.5 }}
> >
{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.5, sm: 2 }, fontSize: { xs: '0.875rem', sm: '1rem' } }}> <Typography variant="subtitle2" sx={{ fontWeight: 600, mb: { xs: 1, sm: 2 }, fontSize: { xs: '0.8125rem', sm: '1rem' } }}>
🗺 🗺
</Typography> </Typography>
{aiResult.trendAnalysis.regionalTrends.slice(0, 3).map((trend, idx) => ( {aiResult.trendAnalysis.regionalTrends.slice(0, 3).map((trend, idx) => (
@ -337,14 +359,14 @@ export default function RecommendationStep({
key={idx} key={idx}
variant="body2" variant="body2"
color="text.secondary" color="text.secondary"
sx={{ fontSize: { xs: '0.8125rem', sm: '0.95rem' }, mb: 1 }} sx={{ fontSize: { xs: '0.75rem', sm: '0.95rem' }, mb: { xs: 0.75, sm: 1 }, lineHeight: 1.5 }}
> >
{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.5, sm: 2 }, fontSize: { xs: '0.875rem', sm: '1rem' } }}> <Typography variant="subtitle2" sx={{ fontWeight: 600, mb: { xs: 1, sm: 2 }, fontSize: { xs: '0.8125rem', sm: '1rem' } }}>
</Typography> </Typography>
{aiResult.trendAnalysis.seasonalTrends.slice(0, 3).map((trend, idx) => ( {aiResult.trendAnalysis.seasonalTrends.slice(0, 3).map((trend, idx) => (
@ -352,7 +374,7 @@ export default function RecommendationStep({
key={idx} key={idx}
variant="body2" variant="body2"
color="text.secondary" color="text.secondary"
sx={{ fontSize: { xs: '0.8125rem', sm: '0.95rem' }, mb: 1 }} sx={{ fontSize: { xs: '0.75rem', sm: '0.95rem' }, mb: { xs: 0.75, sm: 1 }, lineHeight: 1.5 }}
> >
{trend.description} {trend.description}
</Typography> </Typography>
@ -363,13 +385,12 @@ export default function RecommendationStep({
</Card> </Card>
{/* AI Recommendations */} {/* AI Recommendations */}
<Box sx={{ mb: { xs: 2, sm: 8 } }}> <Box sx={{ mb: { xs: 2.5, sm: 8 } }}>
<Typography variant="h6" sx={{ fontWeight: 700, mb: { xs: 2, sm: 4 }, fontSize: { xs: '1rem', sm: '1.25rem' } }}> <Typography variant="h6" sx={{ fontWeight: 700, mb: { xs: 1.5, sm: 4 }, fontSize: { xs: '0.9375rem', sm: '1.25rem' } }}>
AI ({aiResult.recommendations.length} ) AI ({aiResult.recommendations.length} )
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: { xs: 3, sm: 6 }, fontSize: { xs: '0.8125rem', sm: '1rem' } }}> <Typography variant="body2" color="text.secondary" sx={{ mb: { xs: 2.5, sm: 6 }, fontSize: { xs: '0.75rem', sm: '1rem' }, lineHeight: 1.6 }}>
. . .
.
</Typography> </Typography>
</Box> </Box>
@ -402,13 +423,13 @@ export default function RecommendationStep({
}} }}
onClick={() => setSelected(rec.optionNumber)} onClick={() => setSelected(rec.optionNumber)}
> >
<CardContent sx={{ p: { xs: 2, sm: 6 } }}> <CardContent sx={{ p: { xs: 3, sm: 8 } }}>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'flex-start', alignItems: 'flex-start',
mb: { xs: 2, sm: 4 }, mb: { xs: 2.5, sm: 4 },
}} }}
> >
<Box sx={{ display: 'flex', gap: { xs: 1, sm: 2 }, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', gap: { xs: 1, sm: 2 }, flexWrap: 'wrap' }}>
@ -416,18 +437,18 @@ export default function RecommendationStep({
label={`옵션 ${rec.optionNumber}`} label={`옵션 ${rec.optionNumber}`}
color="primary" color="primary"
size="small" size="small"
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' }, py: { xs: 1.5, sm: 2 } }} sx={{ fontSize: { xs: '0.6875rem', sm: '0.875rem' }, py: { xs: 1.5, sm: 2 }, height: { xs: 24, sm: 32 } }}
/> />
<Chip <Chip
label={rec.concept} label={rec.concept}
variant="outlined" variant="outlined"
size="small" size="small"
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' }, py: { xs: 1.5, sm: 2 } }} sx={{ fontSize: { xs: '0.6875rem', sm: '0.875rem' }, py: { xs: 1.5, sm: 2 }, height: { xs: 24, sm: 32 } }}
/> />
</Box> </Box>
<FormControlLabel <FormControlLabel
value={rec.optionNumber} value={rec.optionNumber}
control={<Radio />} control={<Radio sx={{ p: { xs: 0.5, sm: 1 } }} />}
label="" label=""
sx={{ m: 0 }} sx={{ m: 0 }}
/> />
@ -439,10 +460,15 @@ 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, sm: 4 } }} sx={{ mb: { xs: 2.5, sm: 4 } }}
InputProps={{ InputProps={{
endAdornment: <Edit fontSize="small" color="action" />, endAdornment: <Edit fontSize="small" color="action" />,
sx: { fontSize: { xs: '0.9375rem', sm: '1.1rem' }, fontWeight: 600, py: { xs: 1.5, sm: 2 } }, sx: {
fontSize: { xs: '0.875rem', sm: '1.1rem' },
fontWeight: 600,
py: { xs: 1, sm: 2 },
px: { xs: 1.5, sm: 2 }
},
}} }}
/> />
@ -454,24 +480,28 @@ 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: 2, sm: 4 } }} sx={{ mb: { xs: 3, sm: 4 } }}
InputProps={{ InputProps={{
sx: { fontSize: { xs: '0.875rem', sm: '1rem' } }, sx: {
fontSize: { xs: '0.8125rem', sm: '1rem' },
py: { xs: 1, sm: 1.5 },
px: { xs: 1.5, sm: 2 }
},
}} }}
/> />
<Grid container spacing={{ xs: 2, sm: 4 }} sx={{ mt: { xs: 1, sm: 2 } }}> <Grid container spacing={{ xs: 2.5, sm: 4 }}>
<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.75rem', sm: '0.875rem' } }} sx={{ fontSize: { xs: '0.6875rem', sm: '0.875rem' }, display: 'block', mb: { xs: 0.5, sm: 1 } }}
> >
</Typography> </Typography>
<Typography <Typography
variant="body2" variant="body2"
sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1rem' }, mt: { xs: 0.5, sm: 1 } }} sx={{ fontWeight: 600, fontSize: { xs: '0.8125rem', sm: '1rem' } }}
> >
{rec.targetAudience} {rec.targetAudience}
</Typography> </Typography>
@ -480,13 +510,13 @@ export default function RecommendationStep({
<Typography <Typography
variant="caption" variant="caption"
color="text.secondary" color="text.secondary"
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }} sx={{ fontSize: { xs: '0.6875rem', sm: '0.875rem' }, display: 'block', mb: { xs: 0.5, sm: 1 } }}
> >
</Typography> </Typography>
<Typography <Typography
variant="body2" variant="body2"
sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1rem' }, mt: { xs: 0.5, sm: 1 } }} sx={{ fontWeight: 600, fontSize: { xs: '0.8125rem', sm: '1rem' } }}
> >
{(rec.estimatedCost.min / 10000).toFixed(0)}~ {(rec.estimatedCost.min / 10000).toFixed(0)}~
{(rec.estimatedCost.max / 10000).toFixed(0)} {(rec.estimatedCost.max / 10000).toFixed(0)}
@ -496,13 +526,13 @@ export default function RecommendationStep({
<Typography <Typography
variant="caption" variant="caption"
color="text.secondary" color="text.secondary"
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }} sx={{ fontSize: { xs: '0.6875rem', sm: '0.875rem' }, display: 'block', mb: { xs: 0.5, sm: 1 } }}
> >
</Typography> </Typography>
<Typography <Typography
variant="body2" variant="body2"
sx={{ fontWeight: 600, fontSize: { xs: '0.875rem', sm: '1rem' }, mt: { xs: 0.5, sm: 1 } }} sx={{ fontWeight: 600, fontSize: { xs: '0.8125rem', sm: '1rem' } }}
> >
{rec.expectedMetrics.newCustomers.min}~ {rec.expectedMetrics.newCustomers.min}~
{rec.expectedMetrics.newCustomers.max} {rec.expectedMetrics.newCustomers.max}
@ -512,29 +542,17 @@ export default function RecommendationStep({
<Typography <Typography
variant="caption" variant="caption"
color="text.secondary" color="text.secondary"
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }} sx={{ fontSize: { xs: '0.6875rem', sm: '0.875rem' }, display: 'block', mb: { xs: 0.5, sm: 1 } }}
> >
ROI ROI
</Typography> </Typography>
<Typography <Typography
variant="body2" variant="body2"
sx={{ fontWeight: 600, color: 'error.main', fontSize: { xs: '0.875rem', sm: '1rem' }, mt: { xs: 0.5, sm: 1 } }} sx={{ fontWeight: 600, color: 'error.main', fontSize: { xs: '0.8125rem', sm: '1rem' } }}
> >
{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>
@ -544,16 +562,16 @@ export default function RecommendationStep({
</RadioGroup> </RadioGroup>
{/* Action Buttons */} {/* Action Buttons */}
<Box sx={{ display: 'flex', gap: { xs: 2, sm: 4 } }}> <Box sx={{ display: 'flex', gap: { xs: 1.5, sm: 4 } }}>
<Button <Button
fullWidth fullWidth
variant="outlined" variant="outlined"
size="large" size="large"
onClick={onBack} onClick={onBack}
sx={{ sx={{
py: { xs: 1.5, sm: 3 }, py: { xs: 1.25, sm: 3 },
borderRadius: 3, borderRadius: { xs: 2.5, sm: 3 },
fontSize: { xs: '0.875rem', sm: '1rem' }, fontSize: { xs: '0.8125rem', sm: '1rem' },
fontWeight: 600, fontWeight: 600,
borderWidth: 2, borderWidth: 2,
'&:hover': { '&:hover': {
@ -570,9 +588,9 @@ export default function RecommendationStep({
disabled={selected === null || loading} disabled={selected === null || loading}
onClick={handleNext} onClick={handleNext}
sx={{ sx={{
py: { xs: 1.5, sm: 3 }, py: { xs: 1.25, sm: 3 },
borderRadius: 3, borderRadius: { xs: 2.5, sm: 3 },
fontSize: { xs: '0.875rem', sm: '1rem' }, fontSize: { xs: '0.8125rem', 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': {
@ -584,7 +602,7 @@ export default function RecommendationStep({
}, },
}} }}
> >
{loading ? <CircularProgress size={24} sx={{ color: 'white' }} /> : '다음'} {loading ? <CircularProgress size={20} sx={{ color: 'white' }} /> : '다음'}
</Button> </Button>
</Box> </Box>
</Container> </Container>

View File

@ -0,0 +1,50 @@
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

@ -0,0 +1,47 @@
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

@ -0,0 +1,49 @@
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,6 +92,9 @@ export interface AiRecommendationRequest {
category: string; category: string;
description?: string; description?: string;
}; };
region?: string;
targetAudience?: string;
budget?: number;
} }
export interface JobAcceptedResponse { export interface JobAcceptedResponse {
@ -306,9 +309,13 @@ export const eventApi = {
}, },
// Step 2: AI 추천 요청 (POST) // Step 2: AI 추천 요청 (POST)
requestAiRecommendations: async (eventId: string): Promise<AiRecommendationResult> => { requestAiRecommendations: async (
const response = await eventApiClient.post<AiRecommendationResult>( eventId: string,
`/events/${eventId}/ai-recommendations` request: AiRecommendationRequest
): 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;
@ -316,11 +323,12 @@ export const eventApi = {
// AI 추천 결과 조회 (GET) // AI 추천 결과 조회 (GET)
getAiRecommendations: async (eventId: string): Promise<AiRecommendationResult> => { getAiRecommendations: async (eventId: string): Promise<AiRecommendationResult> => {
const response = await eventApiClient.get<AiRecommendationResult>( const response = await eventApiClient.get<{ success: boolean; data: AiRecommendationResult; timestamp: string }>(
`/events/${eventId}/ai-recommendations` `/events/${eventId}/ai-recommendations`
); );
console.log('✅ AI 추천 결과 조회:', response.data); console.log('✅ AI 추천 결과 조회 (전체 응답):', response.data);
return response.data; console.log('✅ AI 추천 데이터:', response.data.data);
return response.data.data; // 래퍼에서 실제 데이터 추출
}, },
// AI 추천 선택 // AI 추천 선택