cherry2250 aaa03274af Analytics 페이지 채널 및 타임라인 API 연동 및 데이터 표시 개선
- Channels API 연동으로 채널별 성과 데이터 상세 표시
- Impressions 데이터 기반 채널 성과 차트 표시 (참여자 0일 때)
- Timeline API 연동 및 시간대별 데이터 집계 로직 구현
- 시간대별 참여 추이 섹션 임시 주석처리
- 참여자 수를 고정값(1,234명)으로 설정
- Analytics Proxy Route에 상세 로깅 추가 (ROI, Timeline, Channels)
- Mock 데이터 디렉토리 추가

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 12:01:08 +09:00

542 lines
19 KiB
TypeScript

import { useState } from 'react';
import {
Box,
Container,
Typography,
Card,
CardContent,
Button,
Checkbox,
FormControlLabel,
Select,
MenuItem,
TextField,
FormControl,
InputLabel,
IconButton,
} from '@mui/material';
import { ArrowBack } from '@mui/icons-material';
// 디자인 시스템 색상
const colors = {
pink: '#F472B6',
purple: '#C084FC',
purpleLight: '#E9D5FF',
blue: '#60A5FA',
mint: '#34D399',
orange: '#FB923C',
yellow: '#FBBF24',
gray: {
900: '#1A1A1A',
700: '#4A4A4A',
500: '#9E9E9E',
300: '#D9D9D9',
100: '#F5F5F5',
},
};
interface Channel {
id: string;
name: string;
selected: boolean;
config?: Record<string, string | number>;
}
interface ChannelStepProps {
onNext: (channels: string[]) => void;
onBack: () => void;
}
export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
const [channels, setChannels] = useState<Channel[]>([
{ id: 'uriTV', name: '우리동네TV', selected: false, config: { radius: '500', time: 'evening' } },
{ id: 'ringoBiz', name: '링고비즈', selected: false, config: { phone: '010-1234-5678' } },
{ id: 'genieTV', name: '지니TV 광고', selected: false, config: { region: 'suwon', time: 'all', budget: '' } },
{ id: 'sns', name: 'SNS', selected: false, config: { instagram: 'true', naver: 'true', kakao: 'false', schedule: 'now' } },
]);
const handleChannelToggle = (channelId: string) => {
setChannels((prev) =>
prev.map((ch) => (ch.id === channelId ? { ...ch, selected: !ch.selected } : ch))
);
};
const handleConfigChange = (channelId: string, key: string, value: string) => {
setChannels((prev) =>
prev.map((ch) =>
ch.id === channelId ? { ...ch, config: { ...ch.config, [key]: value } } : ch
)
);
};
const getChannelConfig = (channelId: string, key: string): string => {
const channel = channels.find((ch) => ch.id === channelId);
return (channel?.config?.[key] as string) || '';
};
const calculateSummary = () => {
let totalCost = 0;
let totalExposure = 0;
channels.forEach((ch) => {
if (!ch.selected) return;
if (ch.id === 'uriTV') {
totalCost += 80000;
totalExposure += 50000;
} else if (ch.id === 'ringoBiz') {
totalExposure += 30000;
} else if (ch.id === 'genieTV') {
const budget = parseInt(getChannelConfig('genieTV', 'budget')) || 0;
totalCost += budget;
totalExposure += Math.floor(budget / 100) * 1000;
}
});
return { totalCost, totalExposure };
};
const handleNext = () => {
const selectedChannels = channels.filter((ch) => ch.selected).map((ch) => ch.id);
if (selectedChannels.length > 0) {
onNext(selectedChannels);
}
};
const { totalCost, totalExposure } = calculateSummary();
const selectedCount = channels.filter((ch) => ch.selected).length;
return (
<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 } }}>
{/* Header */}
<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 }}>
<ArrowBack sx={{ fontSize: 20 }} />
</IconButton>
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}>
</Typography>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: { xs: 3, sm: 8 }, textAlign: 'center', fontSize: { xs: '0.8125rem', sm: '1rem' } }}>
( 1 )
</Typography>
{/* 우리동네TV */}
<Card
elevation={0}
sx={{
mb: 6,
borderRadius: 4,
border: channels[0].selected ? 2 : 1,
borderColor: channels[0].selected ? colors.purple : 'divider',
bgcolor: channels[0].selected ? `${colors.purpleLight}40` : 'background.paper',
boxShadow: channels[0].selected ? '0 4px 12px rgba(0, 0, 0, 0.15)' : '0 2px 8px rgba(0, 0, 0, 0.08)',
transition: 'all 0.3s',
}}
>
<CardContent sx={{ p: { xs: 2, sm: 6 } }}>
<FormControlLabel
control={
<Checkbox
checked={channels[0].selected}
onChange={() => handleChannelToggle('uriTV')}
sx={{
color: colors.purple,
'&.Mui-checked': {
color: colors.purple,
},
}}
/>
}
label={
<Typography variant="body1" sx={{ fontWeight: channels[0].selected ? 700 : 600, fontSize: '1.125rem' }}>
TV
</Typography>
}
sx={{ mb: channels[0].selected ? 2 : 0 }}
/>
{channels[0].selected && (
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel></InputLabel>
<Select
value={getChannelConfig('uriTV', 'radius')}
onChange={(e) => handleConfigChange('uriTV', 'radius', e.target.value)}
label="반경"
>
<MenuItem value="500">500m</MenuItem>
<MenuItem value="1000">1km</MenuItem>
<MenuItem value="2000">2km</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel> </InputLabel>
<Select
value={getChannelConfig('uriTV', 'time')}
onChange={(e) => handleConfigChange('uriTV', 'time', e.target.value)}
label="노출 시간대"
>
<MenuItem value="morning"> (7-12)</MenuItem>
<MenuItem value="afternoon"> (12-17)</MenuItem>
<MenuItem value="evening"> (17-22)</MenuItem>
<MenuItem value="all"></MenuItem>
</Select>
</FormControl>
<Typography variant="body2" color="text.secondary">
: <strong>5</strong>
</Typography>
<Typography variant="body2" color="text.secondary">
: <strong>8</strong>
</Typography>
</Box>
)}
</CardContent>
</Card>
{/* 링고비즈 */}
<Card
elevation={0}
sx={{
mb: 6,
borderRadius: 4,
border: channels[1].selected ? 2 : 1,
borderColor: channels[1].selected ? colors.purple : 'divider',
bgcolor: channels[1].selected ? `${colors.purpleLight}40` : 'background.paper',
boxShadow: channels[1].selected ? '0 4px 12px rgba(0, 0, 0, 0.15)' : '0 2px 8px rgba(0, 0, 0, 0.08)',
transition: 'all 0.3s',
}}
>
<CardContent sx={{ p: { xs: 2, sm: 6 } }}>
<FormControlLabel
control={
<Checkbox
checked={channels[1].selected}
onChange={() => handleChannelToggle('ringoBiz')}
sx={{
color: colors.purple,
'&.Mui-checked': {
color: colors.purple,
},
}}
/>
}
label={
<Typography variant="body1" sx={{ fontWeight: channels[1].selected ? 700 : 600, fontSize: '1.125rem' }}>
</Typography>
}
sx={{ mb: channels[1].selected ? 2 : 0 }}
/>
{channels[1].selected && (
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}>
<TextField
fullWidth
label="매장 전화번호"
value={getChannelConfig('ringoBiz', 'phone')}
InputProps={{ readOnly: true }}
sx={{ mb: 2 }}
/>
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
</Typography>
<Typography variant="body2" color="text.secondary">
: <strong>3</strong>
</Typography>
<Typography variant="body2" color="text.secondary">
: <strong></strong>
</Typography>
</Box>
)}
</CardContent>
</Card>
{/* 지니TV 광고 */}
<Card
elevation={0}
sx={{
mb: 6,
borderRadius: 4,
border: channels[2].selected ? 2 : 1,
borderColor: channels[2].selected ? colors.purple : 'divider',
bgcolor: channels[2].selected ? `${colors.purpleLight}40` : 'background.paper',
boxShadow: channels[2].selected ? '0 4px 12px rgba(0, 0, 0, 0.15)' : '0 2px 8px rgba(0, 0, 0, 0.08)',
transition: 'all 0.3s',
}}
>
<CardContent sx={{ p: { xs: 2, sm: 6 } }}>
<FormControlLabel
control={
<Checkbox
checked={channels[2].selected}
onChange={() => handleChannelToggle('genieTV')}
sx={{
color: colors.purple,
'&.Mui-checked': {
color: colors.purple,
},
}}
/>
}
label={
<Typography variant="body1" sx={{ fontWeight: channels[2].selected ? 700 : 600, fontSize: '1.125rem' }}>
TV
</Typography>
}
sx={{ mb: channels[2].selected ? 2 : 0 }}
/>
{channels[2].selected && (
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel></InputLabel>
<Select
value={getChannelConfig('genieTV', 'region')}
onChange={(e) => handleConfigChange('genieTV', 'region', e.target.value)}
label="지역"
>
<MenuItem value="suwon"></MenuItem>
<MenuItem value="seoul"></MenuItem>
<MenuItem value="busan"></MenuItem>
</Select>
</FormControl>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel> </InputLabel>
<Select
value={getChannelConfig('genieTV', 'time')}
onChange={(e) => handleConfigChange('genieTV', 'time', e.target.value)}
label="노출 시간대"
>
<MenuItem value="all"></MenuItem>
<MenuItem value="prime"> (19-23)</MenuItem>
</Select>
</FormControl>
<TextField
fullWidth
type="number"
label="예산"
placeholder="예산을 입력하세요"
value={getChannelConfig('genieTV', 'budget')}
onChange={(e) => handleConfigChange('genieTV', 'budget', e.target.value)}
InputProps={{ inputProps: { min: 0, step: 10000 } }}
sx={{ mb: 2 }}
/>
<Typography variant="body2" color="text.secondary">
:{' '}
<strong>
{getChannelConfig('genieTV', 'budget')
? `${(Math.floor(parseInt(getChannelConfig('genieTV', 'budget')) / 100) * 1000 / 10000).toFixed(1)}만명`
: '계산중...'}
</strong>
</Typography>
</Box>
)}
</CardContent>
</Card>
{/* SNS */}
<Card
elevation={0}
sx={{
mb: { xs: 4, sm: 10 },
borderRadius: 4,
border: channels[3].selected ? 2 : 1,
borderColor: channels[3].selected ? colors.purple : 'divider',
bgcolor: channels[3].selected ? `${colors.purpleLight}40` : 'background.paper',
boxShadow: channels[3].selected ? '0 4px 12px rgba(0, 0, 0, 0.15)' : '0 2px 8px rgba(0, 0, 0, 0.08)',
transition: 'all 0.3s',
}}
>
<CardContent sx={{ p: { xs: 2, sm: 6 } }}>
<FormControlLabel
control={
<Checkbox
checked={channels[3].selected}
onChange={() => handleChannelToggle('sns')}
sx={{
color: colors.purple,
'&.Mui-checked': {
color: colors.purple,
},
}}
/>
}
label={
<Typography variant="body1" sx={{ fontWeight: channels[3].selected ? 700 : 600, fontSize: '1.125rem' }}>
SNS
</Typography>
}
sx={{ mb: channels[3].selected ? 2 : 0 }}
/>
{channels[3].selected && (
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}>
<Typography variant="body2" sx={{ mb: 1, fontWeight: 600 }}>
</Typography>
<FormControlLabel
control={
<Checkbox
checked={getChannelConfig('sns', 'instagram') === 'true'}
onChange={(e) =>
handleConfigChange('sns', 'instagram', e.target.checked.toString())
}
sx={{
color: colors.purple,
'&.Mui-checked': {
color: colors.purple,
},
}}
/>
}
label="Instagram"
sx={{ display: 'block' }}
/>
<FormControlLabel
control={
<Checkbox
checked={getChannelConfig('sns', 'naver') === 'true'}
onChange={(e) =>
handleConfigChange('sns', 'naver', e.target.checked.toString())
}
sx={{
color: colors.purple,
'&.Mui-checked': {
color: colors.purple,
},
}}
/>
}
label="Naver Blog"
sx={{ display: 'block' }}
/>
<FormControlLabel
control={
<Checkbox
checked={getChannelConfig('sns', 'kakao') === 'true'}
onChange={(e) =>
handleConfigChange('sns', 'kakao', e.target.checked.toString())
}
sx={{
color: colors.purple,
'&.Mui-checked': {
color: colors.purple,
},
}}
/>
}
label="Kakao Channel"
sx={{ display: 'block', mb: 2 }}
/>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel> </InputLabel>
<Select
value={getChannelConfig('sns', 'schedule')}
onChange={(e) => handleConfigChange('sns', 'schedule', e.target.value)}
label="예약 게시"
>
<MenuItem value="now"></MenuItem>
<MenuItem value="schedule"></MenuItem>
</Select>
</FormControl>
<Typography variant="body2" color="text.secondary">
: <strong>-</strong>
</Typography>
<Typography variant="body2" color="text.secondary">
: <strong></strong>
</Typography>
</Box>
)}
</CardContent>
</Card>
{/* Summary */}
<Card
elevation={0}
sx={{
mb: { xs: 4, sm: 10 },
borderRadius: 4,
bgcolor: 'grey.50',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
}}
>
<CardContent sx={{ p: { xs: 2, sm: 8 } }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 4 }}>
<Typography variant="h6" sx={{ fontSize: '1.25rem' }}>
</Typography>
<Typography variant="h6" color="error.main" sx={{ fontWeight: 700, fontSize: '1.25rem' }}>
{totalCost.toLocaleString()}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="h6" sx={{ fontSize: '1.25rem' }}>
</Typography>
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1.25rem', color: colors.purple }}>
{totalExposure > 0 ? `${totalExposure.toLocaleString()}명+` : '0명'}
</Typography>
</Box>
</CardContent>
</Card>
{/* Action Buttons */}
<Box sx={{ display: 'flex', gap: { xs: 2, sm: 4 } }}>
<Button
fullWidth
variant="outlined"
size="large"
onClick={onBack}
sx={{
py: { xs: 1.5, sm: 3 },
borderRadius: 3,
fontSize: { xs: '0.875rem', sm: '1rem' },
fontWeight: 600,
borderWidth: 2,
'&:hover': {
borderWidth: 2,
},
}}
>
</Button>
<Button
fullWidth
variant="contained"
size="large"
disabled={selectedCount === 0}
onClick={handleNext}
sx={{
py: { xs: 1.5, sm: 3 },
borderRadius: 3,
fontSize: { xs: '0.875rem', sm: '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],
},
}}
>
</Button>
</Box>
</Container>
</Box>
);
}