merrycoral f414e1e1dd 타입 에러 수정 및 빌드 오류 해결
- EventObjective 타입 명시적으로 지정
- recommendation 중첩 구조에 맞게 속성 접근 수정
- 빌드 성공 확인

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 15:13:01 +09:00

641 lines
23 KiB
TypeScript

import { useState } from 'react';
import {
Box,
Container,
Typography,
Card,
CardContent,
Button,
Checkbox,
FormControlLabel,
Chip,
Grid,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Link,
} from '@mui/material';
import { ArrowBack, CheckCircle, Edit, RocketLaunch, Save, People, AttachMoney, TrendingUp } from '@mui/icons-material';
import { EventData } from '../page';
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 ApprovalStepProps {
eventData: EventData;
onApprove: () => void;
onBack: () => void;
}
export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalStepProps) {
const [agreeTerms, setAgreeTerms] = useState(false);
const [termsDialogOpen, setTermsDialogOpen] = useState(false);
const [successDialogOpen, setSuccessDialogOpen] = useState(false);
const [isDeploying, setIsDeploying] = useState(false);
const handleApprove = async () => {
if (!agreeTerms) return;
setIsDeploying(true);
try {
// 1. 이벤트 생성 API 호출
console.log('📞 Creating event with objective:', eventData.objective);
// objective 매핑 (Frontend → Backend)
const objectiveMap: Record<string, EventObjective> = {
'new_customer': 'CUSTOMER_ACQUISITION',
'revisit': 'Customer Retention',
'sales': 'Sales Promotion',
'awareness': 'awareness',
};
const backendObjective: EventObjective = (objectiveMap[eventData.objective || 'new_customer'] || 'CUSTOMER_ACQUISITION') as EventObjective;
const createResponse = await eventApi.createEvent({
objective: backendObjective,
});
console.log('✅ Event created:', createResponse);
if (createResponse.success && createResponse.data) {
const eventId = createResponse.data.eventId;
console.log('🎯 Event ID:', eventId);
// 2. 이벤트 상세 정보 업데이트
console.log('📞 Updating event details:', eventId);
// 이벤트명 가져오기 (contentEdit.title 또는 recommendation.title)
const eventName = eventData.contentEdit?.title || eventData.recommendation?.recommendation?.title || '이벤트';
// 날짜 설정 (오늘부터 30일간)
const today = new Date();
const endDate = new Date(today);
endDate.setDate(endDate.getDate() + 30);
const startDateStr = today.toISOString().split('T')[0]; // YYYY-MM-DD
const endDateStr = endDate.toISOString().split('T')[0];
await eventApi.updateEvent(eventId, {
eventName: eventName,
description: eventData.contentEdit?.guide || eventData.recommendation?.recommendation?.description || '',
startDate: startDateStr,
endDate: endDateStr,
});
console.log('✅ Event details updated');
// 3. 배포 채널 선택
if (eventData.channels && eventData.channels.length > 0) {
console.log('📞 Selecting channels:', eventData.channels);
// 채널명 매핑 (Frontend → Backend)
const channelMap: Record<string, string> = {
'uriTV': 'WEBSITE',
'ringoBiz': 'EMAIL',
'genieTV': 'KAKAO',
'sns': 'INSTAGRAM',
};
const backendChannels = eventData.channels.map(ch => channelMap[ch] || ch.toUpperCase());
await eventApi.selectChannels(eventId, {
channels: backendChannels,
});
console.log('✅ Channels selected');
}
// 4. TODO: 이미지 선택
// 현재 frontend에서 selectedImageId를 추적하지 않음
// 향후 contentPreview 단계에서 선택된 이미지 ID를 eventData에 저장 필요
console.log('⚠️ Image selection skipped - imageId not tracked in frontend');
// 5. 이벤트 배포 API 호출
console.log('📞 Publishing event:', eventId);
const publishResponse = await eventApi.publishEvent(eventId);
console.log('✅ Event published:', publishResponse);
// 성공 다이얼로그 표시
setIsDeploying(false);
setSuccessDialogOpen(true);
} else {
throw new Error('Event creation failed: No event ID returned');
}
} catch (error) {
console.error('❌ Event deployment failed:', error);
setIsDeploying(false);
alert('이벤트 배포에 실패했습니다. 다시 시도해 주세요.');
}
};
const handleSaveDraft = () => {
// TODO: 임시저장 API 연동
alert('임시저장되었습니다');
};
const getChannelNames = (channels?: string[]) => {
const channelMap: Record<string, string> = {
uriTV: '우리동네TV',
ringoBiz: '링고비즈',
genieTV: '지니TV',
sns: 'SNS',
};
return channels?.map((ch) => channelMap[ch] || ch) || [];
};
return (
<Box sx={{ minHeight: '100vh', bgcolor: colors.gray[50], pt: { xs: 7, sm: 8 }, pb: 10 }}>
<Container maxWidth="lg" sx={{ pt: 8, pb: 6, px: { xs: 6, sm: 8, md: 10 } }}>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 8 }}>
<IconButton onClick={onBack}>
<ArrowBack />
</IconButton>
<Typography variant="h5" sx={{ ...responsiveText.h3, fontWeight: 700 }}>
</Typography>
</Box>
{/* Title Section */}
<Box sx={{ textAlign: 'center', mb: 10 }}>
<CheckCircle sx={{ fontSize: 64, color: colors.purple, mb: 2 }} />
<Typography variant="h5" sx={{ ...responsiveText.h3, fontWeight: 700, mb: 2 }}>
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ ...responsiveText.body1 }}>
</Typography>
</Box>
{/* Event Summary Statistics */}
<Grid container spacing={4} sx={{ mb: 10 }}>
<Grid item xs={12} sm={6} md={3}>
<Card
elevation={0}
sx={{
...cardStyles.default,
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.purpleLight} 100%)`,
borderColor: 'transparent',
}}
>
<CardContent sx={{ textAlign: 'center', py: 4, px: 3 }}>
<CheckCircle sx={{
fontSize: 32,
color: colors.gray[900],
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',
mb: 1,
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
}}>
</Typography>
<Typography
variant="h6"
sx={{
fontWeight: 700,
color: colors.gray[900],
fontSize: '1rem',
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}
>
{eventData.recommendation?.recommendation.title || '이벤트 제목'}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Card
elevation={0}
sx={{
...cardStyles.default,
background: `linear-gradient(135deg, ${colors.blue} 0%, ${colors.blueLight} 100%)`,
borderColor: 'transparent',
}}
>
<CardContent sx={{ textAlign: 'center', py: 4, px: 3 }}>
<People sx={{
fontSize: 32,
color: colors.gray[900],
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',
mb: 1,
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
}}>
</Typography>
<Typography
variant="h4"
sx={{
fontWeight: 700,
color: colors.gray[900],
fontSize: '1.75rem',
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}
>
{eventData.recommendation?.recommendation.expectedMetrics.newCustomers.max || 0}
<Typography component="span" sx={{
fontSize: '1rem',
ml: 0.5,
color: colors.gray[900],
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}>
</Typography>
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Card
elevation={0}
sx={{
...cardStyles.default,
background: `linear-gradient(135deg, ${colors.orange} 0%, ${colors.orangeLight} 100%)`,
borderColor: 'transparent',
}}
>
<CardContent sx={{ textAlign: 'center', py: 4, px: 3 }}>
<AttachMoney sx={{
fontSize: 32,
color: colors.gray[900],
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',
mb: 1,
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
}}>
</Typography>
<Typography
variant="h4"
sx={{
fontWeight: 700,
color: colors.gray[900],
fontSize: '1.75rem',
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}
>
{((eventData.recommendation?.recommendation.estimatedCost.max || 0) / 10000).toFixed(0)}
<Typography component="span" sx={{
fontSize: '1rem',
ml: 0.5,
color: colors.gray[900],
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}>
</Typography>
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Card
elevation={0}
sx={{
...cardStyles.default,
background: `linear-gradient(135deg, ${colors.mint} 0%, ${colors.mintLight} 100%)`,
borderColor: 'transparent',
}}
>
<CardContent sx={{ textAlign: 'center', py: 4, px: 3 }}>
<TrendingUp sx={{
fontSize: 32,
color: colors.gray[900],
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',
mb: 1,
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
}}>
ROI
</Typography>
<Typography
variant="h4"
sx={{
fontWeight: 700,
color: colors.gray[900],
fontSize: '1.75rem',
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}
>
{eventData.recommendation?.recommendation.expectedMetrics.roi.max || 0}%
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
{/* Event Details */}
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 6 }}>
</Typography>
<Card elevation={0} sx={{ ...cardStyles.default, mb: 4 }}>
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<Box sx={{ flex: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ ...responsiveText.body2, fontSize: '0.875rem' }}>
</Typography>
<Typography variant="body1" sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1 }}>
{eventData.recommendation?.recommendation.title}
</Typography>
</Box>
<IconButton size="small">
<Edit fontSize="small" />
</IconButton>
</Box>
</CardContent>
</Card>
<Card elevation={0} sx={{ ...cardStyles.default, mb: 4 }}>
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<Box sx={{ flex: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ ...responsiveText.body2, fontSize: '0.875rem' }}>
</Typography>
<Typography variant="body1" sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1 }}>
{eventData.recommendation?.recommendation.mechanics.details || ''}
</Typography>
</Box>
<IconButton size="small">
<Edit fontSize="small" />
</IconButton>
</Box>
</CardContent>
</Card>
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10 }}>
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<Box sx={{ flex: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ ...responsiveText.body2, fontSize: '0.875rem' }}>
</Typography>
<Typography variant="body1" sx={{ ...responsiveText.body1, fontWeight: 600, mt: 1 }}>
{eventData.recommendation?.recommendation.mechanics.details || ''}
</Typography>
</Box>
</Box>
</CardContent>
</Card>
{/* Distribution Channels */}
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 6 }}>
</Typography>
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10 }}>
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 4 }}>
{getChannelNames(eventData.channels).map((channel) => (
<Chip
key={channel}
label={channel}
sx={{
bgcolor: colors.purple,
color: 'white',
fontWeight: 600,
fontSize: '0.875rem',
px: 2,
py: 2.5,
}}
/>
))}
</Box>
<Button
size="small"
startIcon={<Edit />}
sx={{
...responsiveText.body2,
fontWeight: 600,
color: colors.purple,
}}
>
</Button>
</CardContent>
</Card>
{/* Terms Agreement */}
<Card elevation={0} sx={{ ...cardStyles.default, bgcolor: colors.gray[50], mb: 10 }}>
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
<FormControlLabel
control={
<Checkbox
checked={agreeTerms}
onChange={(e) => setAgreeTerms(e.target.checked)}
sx={{
color: colors.purple,
'&.Mui-checked': {
color: colors.purple,
},
}}
/>
}
label={
<Typography variant="body2" sx={{ ...responsiveText.body1 }}>
{' '}
<Typography component="span" sx={{ color: colors.orange, fontWeight: 600 }}>
()
</Typography>
</Typography>
}
/>
<Link
component="button"
variant="body2"
onClick={() => setTermsDialogOpen(true)}
sx={{ ...responsiveText.body2, ml: 4, mt: 2, fontWeight: 600, color: colors.purple }}
>
</Link>
</CardContent>
</Card>
{/* Action Buttons */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<Button
fullWidth
variant="contained"
size="large"
disabled={!agreeTerms || isDeploying}
onClick={handleApprove}
startIcon={isDeploying ? null : <RocketLaunch />}
sx={{
py: 3,
borderRadius: 3,
fontSize: '1rem',
fontWeight: 700,
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
'&:hover': {
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
opacity: 0.9,
},
'&:disabled': {
background: colors.gray[300],
},
}}
>
{isDeploying ? '배포 중...' : '배포하기'}
</Button>
<Button
fullWidth
variant="outlined"
size="large"
onClick={handleSaveDraft}
startIcon={<Save />}
sx={{
py: 3,
borderRadius: 3,
fontSize: '1rem',
fontWeight: 600,
borderWidth: 2,
borderColor: colors.gray[300],
color: colors.gray[700],
'&:hover': {
borderWidth: 2,
borderColor: colors.gray[400],
bgcolor: colors.gray[50],
},
}}
>
</Button>
</Box>
</Container>
{/* Terms Dialog */}
<Dialog
open={termsDialogOpen}
onClose={() => setTermsDialogOpen(false)}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
borderRadius: 4,
},
}}
>
<DialogTitle sx={{ ...responsiveText.h3, fontWeight: 700, p: 6, pb: 4 }}>
</DialogTitle>
<DialogContent sx={{ px: 6, pb: 6 }}>
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 3 }}>
1 ()
</Typography>
<Typography variant="body2" sx={{ ...responsiveText.body1, mb: 6, color: colors.gray[700] }}>
KT AI
.
</Typography>
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 3 }}>
2 ( )
</Typography>
<Typography variant="body2" sx={{ ...responsiveText.body1, mb: 2, color: colors.gray[700] }}>
항목: 이름, ,
</Typography>
<Typography variant="body2" sx={{ ...responsiveText.body1, mb: 2, color: colors.gray[700] }}>
목적: 이벤트
</Typography>
<Typography variant="body2" sx={{ ...responsiveText.body1, mb: 6, color: colors.gray[700] }}>
기간: 이벤트 6
</Typography>
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 3 }}>
3 ( )
</Typography>
<Typography variant="body2" sx={{ ...responsiveText.body1, color: colors.gray[700] }}>
7 .
</Typography>
</DialogContent>
<DialogActions sx={{ p: 6, pt: 4 }}>
<Button
fullWidth
onClick={() => setTermsDialogOpen(false)}
variant="contained"
sx={{
py: 3,
borderRadius: 3,
fontSize: '1rem',
fontWeight: 700,
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
'&:hover': {
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
opacity: 0.9,
},
}}
>
</Button>
</DialogActions>
</Dialog>
{/* Success Dialog */}
<Dialog
open={successDialogOpen}
onClose={() => {
setSuccessDialogOpen(false);
onApprove();
}}
PaperProps={{
sx: {
borderRadius: 4,
},
}}
>
<DialogContent sx={{ textAlign: 'center', py: 10, px: 8 }}>
<CheckCircle sx={{ fontSize: 80, color: colors.purple, mb: 4 }} />
<Typography variant="h5" sx={{ fontSize: '1.5rem', fontWeight: 700, mb: 3 }}>
!
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ fontSize: '1rem', mb: 8 }}>
.
<br />
.
</Typography>
<Button
fullWidth
variant="contained"
size="large"
onClick={() => {
setSuccessDialogOpen(false);
onApprove();
}}
sx={{
py: 3,
borderRadius: 3,
fontSize: '1rem',
fontWeight: 700,
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
'&:hover': {
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
opacity: 0.9,
},
}}
>
</Button>
</DialogContent>
</Dialog>
</Box>
);
}