cherry2250 28f0ebde33 추첨 페이지 디자인 개선 및 통일
- 페이지 헤더 및 요약 카드 추가 (이벤트명, 참여자 수)
- 추첨 설정 카드 간격 2배 확대 및 버튼 크기 증가
- 추첨 이력 섹션 간격 확대 및 카드 디자인 개선
- 당첨자 목록 카드 디자인 개선 (순위 배지 크기 증가, 간격 확대)
- 모든 액션 버튼에 그라데이션 배경 적용
- 모든 다이얼로그 컴포넌트 간격 및 디자인 통일
- 애니메이션 다이얼로그 크기 증가
- 미사용 import 제거 (EmojiEvents)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 14:56:43 +09:00

653 lines
22 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import {
Box,
Container,
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
Card,
CardContent,
Typography,
Button,
InputAdornment,
Pagination,
Chip,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Grid,
} from '@mui/material';
import {
Search,
FilterList,
Casino,
Download,
People,
TrendingUp,
Person,
AccessTime,
} 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',
},
};
// Mock 데이터
const mockParticipants = [
{
id: '0001',
name: '김**',
phone: '010-****-1234',
channel: 'SNS (Instagram)',
channelType: 'sns',
date: '2025-11-02 14:23',
status: 'waiting' as const,
},
{
id: '0002',
name: '이**',
phone: '010-****-5678',
channel: '우리동네TV',
channelType: 'uriTV',
date: '2025-11-02 15:45',
status: 'waiting' as const,
},
{
id: '0003',
name: '박**',
phone: '010-****-9012',
channel: '링고비즈',
channelType: 'ringoBiz',
date: '2025-11-02 16:12',
status: 'waiting' as const,
},
{
id: '0004',
name: '최**',
phone: '010-****-3456',
channel: 'SNS (Naver)',
channelType: 'sns',
date: '2025-11-02 17:30',
status: 'winner' as const,
},
{
id: '0005',
name: '정**',
phone: '010-****-7890',
channel: '우리동네TV',
channelType: 'uriTV',
date: '2025-11-02 18:15',
status: 'loser' as const,
},
];
type ChannelType = 'all' | 'uriTV' | 'ringoBiz' | 'sns';
type StatusType = 'all' | 'waiting' | 'winner' | 'loser';
export default function ParticipantsPage() {
const params = useParams();
const router = useRouter();
const eventId = params.eventId as string;
const [searchTerm, setSearchTerm] = useState('');
const [channelFilter, setChannelFilter] = useState<ChannelType>('all');
const [statusFilter, setStatusFilter] = useState<StatusType>('all');
const [currentPage, setCurrentPage] = useState(1);
const [selectedParticipant, setSelectedParticipant] = useState<typeof mockParticipants[0] | null>(null);
const [detailDialogOpen, setDetailDialogOpen] = useState(false);
const itemsPerPage = 20;
// 필터링
const filteredParticipants = mockParticipants.filter((participant) => {
const matchesSearch =
participant.name.includes(searchTerm) || participant.phone.includes(searchTerm);
const matchesChannel = channelFilter === 'all' || participant.channelType === channelFilter;
const matchesStatus = statusFilter === 'all' || participant.status === statusFilter;
return matchesSearch && matchesChannel && matchesStatus;
});
// 페이지네이션
const totalPages = Math.ceil(filteredParticipants.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, filteredParticipants.length);
const pageParticipants = filteredParticipants.slice(startIndex, endIndex);
const getStatusColor = (status: string) => {
switch (status) {
case 'waiting':
return 'default';
case 'winner':
return 'success';
case 'loser':
return 'error';
default:
return 'default';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'waiting':
return '당첨 대기';
case 'winner':
return '당첨';
case 'loser':
return '미당첨';
default:
return status;
}
};
const handleParticipantClick = (participant: typeof mockParticipants[0]) => {
setSelectedParticipant(participant);
setDetailDialogOpen(true);
};
const handleDrawClick = () => {
router.push(`/events/${eventId}/draw`);
};
const handleDownloadClick = () => {
alert('엑셀 다운로드 기능은 추후 구현됩니다');
};
// 통계 계산
const stats = {
total: mockParticipants.length,
waiting: mockParticipants.filter((p) => p.status === 'waiting').length,
winner: mockParticipants.filter((p) => p.status === 'winner').length,
channelDistribution: {
uriTV: mockParticipants.filter((p) => p.channelType === 'uriTV').length,
ringoBiz: mockParticipants.filter((p) => p.channelType === 'ringoBiz').length,
sns: mockParticipants.filter((p) => p.channelType === 'sns').length,
},
};
return (
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 10 }}>
<Container maxWidth="lg" sx={{ pt: 8, pb: 8, px: { xs: 6, sm: 8, md: 10 } }}>
{/* Page Header */}
<Box sx={{ mb: 8 }}>
<Typography variant="h4" sx={{ fontWeight: 700, fontSize: '2rem', mb: 2 }}>
👥
</Typography>
<Typography variant="body1" color="text.secondary">
</Typography>
</Box>
{/* Statistics Cards */}
<Grid container spacing={6} sx={{ mb: 10 }}>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.purpleLight} 100%)`,
}}
>
<CardContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
<People sx={{ fontSize: 40, mb: 2, color: 'white' }} />
<Typography variant="caption" display="block" sx={{ mb: 2, color: 'rgba(255,255,255,0.9)' }}>
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: '1.75rem' }}>
{stats.total}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
background: `linear-gradient(135deg, ${colors.yellow} 0%, #FCD34D 100%)`,
}}
>
<CardContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
<AccessTime sx={{ fontSize: 40, mb: 2, color: 'white' }} />
<Typography variant="caption" display="block" sx={{ mb: 2, color: 'rgba(255,255,255,0.9)' }}>
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: '1.75rem' }}>
{stats.waiting}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
background: `linear-gradient(135deg, ${colors.mint} 0%, #6EE7B7 100%)`,
}}
>
<CardContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
<TrendingUp sx={{ fontSize: 40, mb: 2, color: 'white' }} />
<Typography variant="caption" display="block" sx={{ mb: 2, color: 'rgba(255,255,255,0.9)' }}>
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: '1.75rem' }}>
{stats.winner}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} md={3}>
<Card
elevation={0}
sx={{
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
background: `linear-gradient(135deg, ${colors.blue} 0%, #93C5FD 100%)`,
}}
>
<CardContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
<Casino sx={{ fontSize: 40, mb: 2, color: 'white' }} />
<Typography variant="caption" display="block" sx={{ mb: 2, color: 'rgba(255,255,255,0.9)' }}>
</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'white', fontSize: '1.75rem' }}>
{stats.total > 0 ? Math.round((stats.winner / stats.total) * 100) : 0}%
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
{/* Search Section */}
<Box sx={{ mb: 6 }}>
<TextField
fullWidth
placeholder="이름 또는 전화번호 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
),
}}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 3,
bgcolor: 'white',
},
}}
/>
</Box>
{/* Filters */}
<Box sx={{ mb: 8 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, flexWrap: 'wrap' }}>
<FilterList sx={{ fontSize: 28, color: colors.pink }} />
<FormControl sx={{ flex: 1, minWidth: 160 }}>
<InputLabel> </InputLabel>
<Select
value={channelFilter}
label="참여 경로"
onChange={(e) => setChannelFilter(e.target.value as ChannelType)}
sx={{ borderRadius: 2 }}
>
<MenuItem value="all"> </MenuItem>
<MenuItem value="uriTV">TV</MenuItem>
<MenuItem value="ringoBiz"></MenuItem>
<MenuItem value="sns">SNS</MenuItem>
</Select>
</FormControl>
<FormControl sx={{ flex: 1, minWidth: 140 }}>
<InputLabel></InputLabel>
<Select
value={statusFilter}
label="상태"
onChange={(e) => setStatusFilter(e.target.value as StatusType)}
sx={{ borderRadius: 2 }}
>
<MenuItem value="all"></MenuItem>
<MenuItem value="waiting"> </MenuItem>
<MenuItem value="winner"></MenuItem>
<MenuItem value="loser"></MenuItem>
</Select>
</FormControl>
</Box>
</Box>
{/* Total Count & Drawing Button */}
<Box sx={{ mb: 6 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 4 }}>
<Typography variant="h5" sx={{ fontWeight: 700, fontSize: '1.5rem' }}>
<span style={{ color: colors.pink }}>{filteredParticipants.length}</span>
</Typography>
<Box sx={{ display: 'flex', gap: 3 }}>
<Button
variant="outlined"
startIcon={<Download />}
onClick={handleDownloadClick}
sx={{
borderRadius: 3,
px: 4,
py: 1.5,
borderColor: colors.blue,
color: colors.blue,
'&:hover': {
borderColor: colors.blue,
bgcolor: `${colors.blue}10`,
},
}}
>
</Button>
<Button
variant="contained"
startIcon={<Casino />}
onClick={handleDrawClick}
sx={{
borderRadius: 3,
px: 4,
py: 1.5,
background: `linear-gradient(135deg, ${colors.pink} 0%, ${colors.purple} 100%)`,
'&:hover': {
background: `linear-gradient(135deg, ${colors.pink} 0%, ${colors.purple} 100%)`,
opacity: 0.9,
},
}}
>
</Button>
</Box>
</Box>
</Box>
{/* Participant List */}
<Box sx={{ mb: 10 }}>
{pageParticipants.length === 0 ? (
<Box sx={{ textAlign: 'center', py: 16 }}>
<Person sx={{ fontSize: 80, color: colors.gray[300], mb: 3 }} />
<Typography variant="h6" color="text.secondary" sx={{ fontWeight: 600 }}>
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
</Typography>
</Box>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{pageParticipants.map((participant) => (
<Card
key={participant.id}
elevation={0}
sx={{
cursor: 'pointer',
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
transition: 'all 0.2s ease',
'&:hover': {
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
transform: 'translateY(-4px)',
},
}}
onClick={() => handleParticipantClick(participant)}
>
<CardContent sx={{ p: 5 }}>
{/* Header */}
<Box
sx={{
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'space-between',
mb: 4,
}}
>
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 3 }}>
<Box
sx={{
width: 56,
height: 56,
borderRadius: '50%',
bgcolor: colors.purpleLight,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Person sx={{ fontSize: 32, color: colors.purple }} />
</Box>
<Box>
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
#{participant.id}
</Typography>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1, fontSize: '1.25rem' }}>
{participant.name}
</Typography>
<Typography variant="body1" color="text.secondary">
{participant.phone}
</Typography>
</Box>
</Box>
<Chip
label={getStatusText(participant.status)}
color={getStatusColor(participant.status) as any}
size="medium"
sx={{ fontWeight: 600, px: 2, py: 2.5 }}
/>
</Box>
{/* Info */}
<Box
sx={{
borderTop: '1px solid',
borderColor: colors.gray[100],
pt: 4,
display: 'flex',
flexDirection: 'column',
gap: 2,
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body1" color="text.secondary">
</Typography>
<Chip
label={participant.channel}
size="small"
sx={{
bgcolor: colors.purpleLight,
color: colors.purple,
fontWeight: 600,
}}
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body1" color="text.secondary">
</Typography>
<Typography variant="body1" sx={{ fontWeight: 600 }}>
{participant.date}
</Typography>
</Box>
</Box>
</CardContent>
</Card>
))}
</Box>
)}
</Box>
{/* Pagination */}
{totalPages > 1 && (
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 10 }}>
<Pagination
count={totalPages}
page={currentPage}
onChange={(_, page) => setCurrentPage(page)}
color="primary"
size="large"
sx={{
'& .MuiPaginationItem-root': {
fontSize: '1rem',
fontWeight: 600,
},
}}
/>
</Box>
)}
{/* Participant Detail Dialog */}
<Dialog
open={detailDialogOpen}
onClose={() => setDetailDialogOpen(false)}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
borderRadius: 4,
},
}}
>
<DialogTitle sx={{ p: 5, pb: 3 }}>
<Typography variant="h5" sx={{ fontWeight: 700 }}>
</Typography>
</DialogTitle>
<DialogContent dividers sx={{ p: 5 }}>
{selectedParticipant && (
<Box>
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Box
sx={{
width: 80,
height: 80,
borderRadius: '50%',
bgcolor: colors.purpleLight,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto',
mb: 3,
}}
>
<Person sx={{ fontSize: 40, color: colors.purple }} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1 }}>
{selectedParticipant.name}
</Typography>
<Typography variant="body2" color="text.secondary">
#{selectedParticipant.id}
</Typography>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<Box
sx={{
p: 3,
bgcolor: colors.gray[100],
borderRadius: 3,
}}
>
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
</Typography>
<Typography variant="body1" sx={{ fontWeight: 600 }}>
{selectedParticipant.phone}
</Typography>
</Box>
<Box
sx={{
p: 3,
bgcolor: colors.gray[100],
borderRadius: 3,
}}
>
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
</Typography>
<Typography variant="body1" sx={{ fontWeight: 600 }}>
{selectedParticipant.channel}
</Typography>
</Box>
<Box
sx={{
p: 3,
bgcolor: colors.gray[100],
borderRadius: 3,
}}
>
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
</Typography>
<Typography variant="body1" sx={{ fontWeight: 600 }}>
{selectedParticipant.date}
</Typography>
</Box>
<Box
sx={{
p: 3,
bgcolor: colors.gray[100],
borderRadius: 3,
}}
>
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
</Typography>
<Chip
label={getStatusText(selectedParticipant.status)}
color={getStatusColor(selectedParticipant.status) as any}
size="medium"
sx={{ fontWeight: 600 }}
/>
</Box>
</Box>
</Box>
)}
</DialogContent>
<DialogActions sx={{ p: 5, pt: 3 }}>
<Button
onClick={() => setDetailDialogOpen(false)}
variant="contained"
fullWidth
sx={{
borderRadius: 3,
py: 1.5,
background: `linear-gradient(135deg, ${colors.pink} 0%, ${colors.purple} 100%)`,
}}
>
</Button>
</DialogActions>
</Dialog>
</Container>
</Box>
);
}