mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2025-12-06 12:16:24 +00:00
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
921 lines
36 KiB
TypeScript
921 lines
36 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import {
|
|
Box,
|
|
Container,
|
|
TextField,
|
|
Select,
|
|
MenuItem,
|
|
FormControl,
|
|
InputLabel,
|
|
Card,
|
|
CardContent,
|
|
Typography,
|
|
InputAdornment,
|
|
Pagination,
|
|
Grid,
|
|
LinearProgress,
|
|
Chip,
|
|
} from '@mui/material';
|
|
import {
|
|
Search,
|
|
FilterList,
|
|
Event,
|
|
TrendingUp,
|
|
People,
|
|
CardGiftcard,
|
|
Phone,
|
|
Share,
|
|
ShoppingCart,
|
|
Email,
|
|
LocalFireDepartment,
|
|
NewReleases,
|
|
Warning,
|
|
Star,
|
|
} from '@mui/icons-material';
|
|
import Header from '@/shared/ui/Header';
|
|
import { cardStyles, colors, responsiveText } from '@/shared/lib/button-styles';
|
|
import { useEvents } from '@/entities/event/model/useEvents';
|
|
import type { EventStatus as ApiEventStatus } from '@/entities/event/model/types';
|
|
|
|
// ==================== API 연동 ====================
|
|
// Mock 데이터를 실제 API 호출로 교체
|
|
// 백업 파일: page.tsx.backup
|
|
|
|
type EventStatus = 'all' | 'active' | 'scheduled' | 'ended';
|
|
type Period = '1month' | '3months' | '6months' | '1year' | 'all';
|
|
type SortBy = 'latest' | 'participants' | 'roi';
|
|
|
|
export default function EventsPage() {
|
|
const router = useRouter();
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [statusFilter, setStatusFilter] = useState<EventStatus>('all');
|
|
const [periodFilter, setPeriodFilter] = useState<Period>('1month');
|
|
const [sortBy, setSortBy] = useState<SortBy>('latest');
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const itemsPerPage = 20;
|
|
|
|
// API 데이터 가져오기
|
|
const { events: apiEvents, loading, error, pageInfo, refetch } = useEvents({
|
|
page: currentPage - 1,
|
|
size: itemsPerPage,
|
|
sort: 'createdAt',
|
|
order: 'desc'
|
|
});
|
|
|
|
// API 상태를 UI 상태로 매핑
|
|
const mapApiStatus = (apiStatus: ApiEventStatus): EventStatus => {
|
|
switch (apiStatus) {
|
|
case 'PUBLISHED':
|
|
return 'active';
|
|
case 'DRAFT':
|
|
return 'scheduled';
|
|
case 'ENDED':
|
|
return 'ended';
|
|
default:
|
|
return 'all';
|
|
}
|
|
};
|
|
|
|
// API 이벤트를 UI 형식으로 변환
|
|
const transformedEvents = apiEvents.map(event => ({
|
|
id: event.eventId,
|
|
title: event.eventName || '제목 없음',
|
|
status: mapApiStatus(event.status),
|
|
startDate: event.startDate ? new Date(event.startDate).toLocaleDateString('ko-KR') : '-',
|
|
endDate: event.endDate ? new Date(event.endDate).toLocaleDateString('ko-KR') : '-',
|
|
prize: event.aiRecommendations[0]?.reward || '경품 정보 없음',
|
|
method: event.aiRecommendations[0]?.participationMethod || '참여 방법 없음',
|
|
participants: event.participants || 0,
|
|
targetParticipants: event.targetParticipants || 0,
|
|
roi: event.roi || 0,
|
|
daysLeft: event.endDate
|
|
? Math.ceil((new Date(event.endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
|
|
: 0,
|
|
isUrgent: event.endDate
|
|
? Math.ceil((new Date(event.endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)) <= 3
|
|
: false,
|
|
isPopular: event.participants && event.targetParticipants
|
|
? (event.participants / event.targetParticipants) >= 0.8
|
|
: false,
|
|
isHighROI: event.roi ? event.roi >= 300 : false,
|
|
isNew: event.createdAt
|
|
? (Date.now() - new Date(event.createdAt).getTime()) < (7 * 24 * 60 * 60 * 1000)
|
|
: false,
|
|
}));
|
|
|
|
// 필터링 및 정렬
|
|
const filteredEvents = transformedEvents
|
|
.filter((event) => {
|
|
const matchesSearch = event.title.toLowerCase().includes(searchTerm.toLowerCase());
|
|
const matchesStatus = statusFilter === 'all' || event.status === statusFilter;
|
|
return matchesSearch && matchesStatus;
|
|
})
|
|
.sort((a, b) => {
|
|
if (sortBy === 'latest') {
|
|
return new Date(b.startDate).getTime() - new Date(a.startDate).getTime();
|
|
} else if (sortBy === 'participants') {
|
|
return b.participants - a.participants;
|
|
} else if (sortBy === 'roi') {
|
|
return b.roi - a.roi;
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
// 페이지네이션
|
|
const totalPages = Math.ceil(filteredEvents.length / itemsPerPage);
|
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
|
const endIndex = Math.min(startIndex + itemsPerPage, filteredEvents.length);
|
|
const pageEvents = filteredEvents.slice(startIndex, endIndex);
|
|
|
|
const handleEventClick = (eventId: string) => {
|
|
router.push(`/events/${eventId}`);
|
|
};
|
|
|
|
const getStatusStyle = (status: string) => {
|
|
switch (status) {
|
|
case 'active':
|
|
return {
|
|
bgcolor: colors.mint,
|
|
color: 'white',
|
|
};
|
|
case 'scheduled':
|
|
return {
|
|
bgcolor: colors.blue,
|
|
color: 'white',
|
|
};
|
|
case 'ended':
|
|
return {
|
|
bgcolor: colors.gray[300],
|
|
color: colors.gray[700],
|
|
};
|
|
default:
|
|
return {
|
|
bgcolor: colors.gray[200],
|
|
color: colors.gray[600],
|
|
};
|
|
}
|
|
};
|
|
|
|
const getStatusText = (status: string) => {
|
|
switch (status) {
|
|
case 'active':
|
|
return '진행중';
|
|
case 'scheduled':
|
|
return '예정';
|
|
case 'ended':
|
|
return '종료';
|
|
default:
|
|
return status;
|
|
}
|
|
};
|
|
|
|
const getMethodIcon = (method: string) => {
|
|
switch (method) {
|
|
case '전화번호 입력':
|
|
return <Phone sx={{ fontSize: 18 }} />;
|
|
case 'SNS 팔로우':
|
|
return <Share sx={{ fontSize: 18 }} />;
|
|
case '구매 인증':
|
|
return <ShoppingCart sx={{ fontSize: 18 }} />;
|
|
case '이메일 등록':
|
|
return <Email sx={{ fontSize: 18 }} />;
|
|
default:
|
|
return <Event sx={{ fontSize: 18 }} />;
|
|
}
|
|
};
|
|
|
|
const calculateProgress = (event: typeof transformedEvents[0]) => {
|
|
if (event.status !== 'active') return 0;
|
|
const startTime = new Date(event.startDate).getTime();
|
|
const endTime = new Date(event.endDate).getTime();
|
|
const total = endTime - startTime;
|
|
const elapsed = Date.now() - startTime;
|
|
return Math.min(Math.max((elapsed / total) * 100, 0), 100);
|
|
};
|
|
|
|
// 통계 계산
|
|
const stats = {
|
|
total: transformedEvents.length,
|
|
active: transformedEvents.filter((e) => e.status === 'active').length,
|
|
totalParticipants: transformedEvents.reduce((sum, e) => sum + e.participants, 0),
|
|
avgROI: transformedEvents.filter((e) => e.roi > 0).length > 0
|
|
? Math.round(
|
|
transformedEvents.filter((e) => e.roi > 0).reduce((sum, e) => sum + e.roi, 0) /
|
|
transformedEvents.filter((e) => e.roi > 0).length
|
|
)
|
|
: 0,
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Header title="이벤트 목록" showBack={true} showMenu={false} showProfile={true} />
|
|
<Box
|
|
sx={{
|
|
pt: { xs: 7, sm: 8 },
|
|
pb: 10,
|
|
bgcolor: colors.gray[50],
|
|
minHeight: '100vh',
|
|
}}
|
|
>
|
|
<Container
|
|
maxWidth="lg"
|
|
sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 6 }, px: { xs: 3, sm: 6, md: 10 } }}
|
|
>
|
|
{/* Loading State */}
|
|
{loading && (
|
|
<Box sx={{ mb: 4 }}>
|
|
<LinearProgress sx={{ borderRadius: 1 }} />
|
|
<Typography
|
|
sx={{
|
|
mt: 2,
|
|
textAlign: 'center',
|
|
color: colors.gray[600],
|
|
fontSize: { xs: '0.875rem', sm: '1rem' },
|
|
}}
|
|
>
|
|
이벤트 목록을 불러오는 중...
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Error State */}
|
|
{error && (
|
|
<Card elevation={0} sx={{ ...cardStyles.default, mb: 4, bgcolor: '#FEE2E2' }}>
|
|
<CardContent sx={{ textAlign: 'center', py: 4 }}>
|
|
<Warning sx={{ fontSize: 48, color: '#DC2626', mb: 2 }} />
|
|
<Typography
|
|
variant="h6"
|
|
sx={{ mb: 1, color: '#991B1B', fontSize: { xs: '1rem', sm: '1.25rem' } }}
|
|
>
|
|
이벤트 목록을 불러오는데 실패했습니다
|
|
</Typography>
|
|
<Typography variant="body2" sx={{ color: '#7F1D1D', mb: 2 }}>
|
|
{error.message}
|
|
</Typography>
|
|
<Box
|
|
component="button"
|
|
onClick={() => refetch()}
|
|
sx={{
|
|
px: 3,
|
|
py: 1.5,
|
|
borderRadius: 2,
|
|
border: 'none',
|
|
bgcolor: '#DC2626',
|
|
color: 'white',
|
|
fontSize: '0.875rem',
|
|
fontWeight: 600,
|
|
cursor: 'pointer',
|
|
'&:hover': { bgcolor: '#B91C1C' },
|
|
}}
|
|
>
|
|
다시 시도
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Summary Statistics */}
|
|
<Grid container spacing={{ xs: 2, sm: 4 }} sx={{ mb: { xs: 4, sm: 8 } }}>
|
|
<Grid item xs={6} sm={3}>
|
|
<Card
|
|
elevation={0}
|
|
sx={{
|
|
...cardStyles.default,
|
|
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.purpleLight} 100%)`,
|
|
borderColor: 'transparent',
|
|
}}
|
|
>
|
|
<CardContent
|
|
sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}
|
|
>
|
|
<Event
|
|
sx={{
|
|
fontSize: { xs: 24, sm: 32 },
|
|
color: colors.gray[900],
|
|
mb: 1,
|
|
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
|
|
}}
|
|
/>
|
|
<Typography
|
|
variant="h4"
|
|
sx={{
|
|
fontWeight: 700,
|
|
color: colors.gray[900],
|
|
fontSize: { xs: '1.25rem', sm: '1.75rem' },
|
|
mb: 0.5,
|
|
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
|
}}
|
|
>
|
|
{stats.total}
|
|
</Typography>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{
|
|
color: colors.gray[700],
|
|
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
|
|
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
|
|
}}
|
|
>
|
|
전체 이벤트
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
<Grid item xs={6} sm={3}>
|
|
<Card
|
|
elevation={0}
|
|
sx={{
|
|
...cardStyles.default,
|
|
background: `linear-gradient(135deg, ${colors.mint} 0%, ${colors.mintLight} 100%)`,
|
|
borderColor: 'transparent',
|
|
}}
|
|
>
|
|
<CardContent
|
|
sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}
|
|
>
|
|
<LocalFireDepartment
|
|
sx={{
|
|
fontSize: { xs: 24, sm: 32 },
|
|
color: colors.gray[900],
|
|
mb: 1,
|
|
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
|
|
}}
|
|
/>
|
|
<Typography
|
|
variant="h4"
|
|
sx={{
|
|
fontWeight: 700,
|
|
color: colors.gray[900],
|
|
fontSize: { xs: '1.25rem', sm: '1.75rem' },
|
|
mb: 0.5,
|
|
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
|
}}
|
|
>
|
|
{stats.active}
|
|
</Typography>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{
|
|
color: colors.gray[700],
|
|
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
|
|
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
|
|
}}
|
|
>
|
|
진행중
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
<Grid item xs={6} sm={3}>
|
|
<Card
|
|
elevation={0}
|
|
sx={{
|
|
...cardStyles.default,
|
|
background: `linear-gradient(135deg, ${colors.blue} 0%, ${colors.blueLight} 100%)`,
|
|
borderColor: 'transparent',
|
|
}}
|
|
>
|
|
<CardContent
|
|
sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}
|
|
>
|
|
<People
|
|
sx={{
|
|
fontSize: { xs: 24, sm: 32 },
|
|
color: colors.gray[900],
|
|
mb: 1,
|
|
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
|
|
}}
|
|
/>
|
|
<Typography
|
|
variant="h4"
|
|
sx={{
|
|
fontWeight: 700,
|
|
color: colors.gray[900],
|
|
fontSize: { xs: '1.25rem', sm: '1.75rem' },
|
|
mb: 0.5,
|
|
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
|
}}
|
|
>
|
|
{stats.totalParticipants}
|
|
</Typography>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{
|
|
color: colors.gray[700],
|
|
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
|
|
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
|
|
}}
|
|
>
|
|
총 참여자
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
<Grid item xs={6} sm={3}>
|
|
<Card
|
|
elevation={0}
|
|
sx={{
|
|
...cardStyles.default,
|
|
background: `linear-gradient(135deg, ${colors.orange} 0%, ${colors.orangeLight} 100%)`,
|
|
borderColor: 'transparent',
|
|
}}
|
|
>
|
|
<CardContent
|
|
sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}
|
|
>
|
|
<TrendingUp
|
|
sx={{
|
|
fontSize: { xs: 24, sm: 32 },
|
|
color: colors.gray[900],
|
|
mb: 1,
|
|
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
|
|
}}
|
|
/>
|
|
<Typography
|
|
variant="h4"
|
|
sx={{
|
|
fontWeight: 700,
|
|
color: colors.gray[900],
|
|
fontSize: { xs: '1.25rem', sm: '1.75rem' },
|
|
mb: 0.5,
|
|
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
|
|
}}
|
|
>
|
|
{stats.avgROI}%
|
|
</Typography>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{
|
|
color: colors.gray[700],
|
|
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
|
|
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
|
|
}}
|
|
>
|
|
평균 ROI
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{/* Search Section */}
|
|
<Box sx={{ mb: { xs: 4, sm: 8 } }}>
|
|
<TextField
|
|
fullWidth
|
|
placeholder="이벤트명 검색..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
InputProps={{
|
|
startAdornment: (
|
|
<InputAdornment position="start">
|
|
<Search sx={{ color: colors.gray[400], fontSize: { xs: 20, sm: 24 } }} />
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
sx={{
|
|
'& .MuiOutlinedInput-root': {
|
|
borderRadius: 3,
|
|
bgcolor: 'white',
|
|
fontSize: { xs: '0.875rem', sm: '1rem' },
|
|
'& fieldset': {
|
|
borderColor: colors.gray[200],
|
|
},
|
|
'&:hover fieldset': {
|
|
borderColor: colors.gray[300],
|
|
},
|
|
'&.Mui-focused fieldset': {
|
|
borderColor: colors.purple,
|
|
},
|
|
},
|
|
}}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Filters */}
|
|
<Box sx={{ mb: { xs: 4, sm: 8 } }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 2, sm: 4 }, flexWrap: 'wrap' }}>
|
|
<FormControl sx={{ flex: 1, minWidth: 120 }}>
|
|
<InputLabel sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>상태</InputLabel>
|
|
<Select
|
|
value={statusFilter}
|
|
label="상태"
|
|
onChange={(e) => setStatusFilter(e.target.value as EventStatus)}
|
|
sx={{
|
|
borderRadius: 2,
|
|
bgcolor: 'white',
|
|
fontSize: { xs: '0.875rem', sm: '1rem' },
|
|
'& .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: colors.gray[200],
|
|
},
|
|
'&:hover .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: colors.gray[300],
|
|
},
|
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: colors.purple,
|
|
},
|
|
}}
|
|
>
|
|
<MenuItem value="all" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>전체</MenuItem>
|
|
<MenuItem value="active" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>진행중</MenuItem>
|
|
<MenuItem value="scheduled" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>예정</MenuItem>
|
|
<MenuItem value="ended" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>종료</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
<FormControl sx={{ flex: 1, minWidth: 140 }}>
|
|
<InputLabel sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>기간</InputLabel>
|
|
<Select
|
|
value={periodFilter}
|
|
label="기간"
|
|
onChange={(e) => setPeriodFilter(e.target.value as Period)}
|
|
sx={{
|
|
borderRadius: 2,
|
|
bgcolor: 'white',
|
|
fontSize: { xs: '0.875rem', sm: '1rem' },
|
|
'& .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: colors.gray[200],
|
|
},
|
|
'&:hover .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: colors.gray[300],
|
|
},
|
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: colors.purple,
|
|
},
|
|
}}
|
|
>
|
|
<MenuItem value="1month" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>최근 1개월</MenuItem>
|
|
<MenuItem value="3months" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>최근 3개월</MenuItem>
|
|
<MenuItem value="6months" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>최근 6개월</MenuItem>
|
|
<MenuItem value="1year" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>최근 1년</MenuItem>
|
|
<MenuItem value="all" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}>전체</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Sorting */}
|
|
<Box sx={{ mb: { xs: 4, sm: 8 } }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<Typography variant="body2" sx={{ ...responsiveText.body2, fontWeight: 600 }}>
|
|
정렬
|
|
</Typography>
|
|
<FormControl sx={{ width: { xs: 140, sm: 160 } }}>
|
|
<Select
|
|
value={sortBy}
|
|
onChange={(e) => setSortBy(e.target.value as SortBy)}
|
|
size="small"
|
|
sx={{
|
|
borderRadius: 2,
|
|
bgcolor: 'white',
|
|
fontSize: { xs: '0.8125rem', sm: '0.875rem' },
|
|
'& .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: colors.gray[200],
|
|
},
|
|
'&:hover .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: colors.gray[300],
|
|
},
|
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: colors.purple,
|
|
},
|
|
}}
|
|
>
|
|
<MenuItem value="latest" sx={{ fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>최신순</MenuItem>
|
|
<MenuItem value="participants" sx={{ fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>참여자순</MenuItem>
|
|
<MenuItem value="roi" sx={{ fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>투자대비수익률순</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Event List */}
|
|
<Box sx={{ mb: { xs: 5, sm: 10 } }}>
|
|
{pageEvents.length === 0 ? (
|
|
<Card
|
|
elevation={0}
|
|
sx={{
|
|
...cardStyles.default,
|
|
}}
|
|
>
|
|
<CardContent sx={{ textAlign: 'center', py: { xs: 10, sm: 20 } }}>
|
|
<Box sx={{ color: colors.gray[300], mb: { xs: 2, sm: 3 } }}>
|
|
<Box
|
|
component="span"
|
|
className="material-icons"
|
|
sx={{ fontSize: { xs: 48, sm: 72 } }}
|
|
>
|
|
event_busy
|
|
</Box>
|
|
</Box>
|
|
<Typography variant="h6" sx={{ mb: { xs: 1, sm: 2 }, color: colors.gray[700], fontSize: { xs: '1rem', sm: '1.25rem' } }}>
|
|
검색 결과가 없습니다
|
|
</Typography>
|
|
<Typography variant="body2" sx={{ color: colors.gray[500], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
|
|
다른 검색 조건으로 다시 시도해보세요
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { xs: 3, sm: 6 } }}>
|
|
{pageEvents.map((event) => (
|
|
<Card
|
|
key={event.id}
|
|
elevation={0}
|
|
sx={{
|
|
...cardStyles.clickable,
|
|
}}
|
|
onClick={() => handleEventClick(event.id)}
|
|
>
|
|
<CardContent sx={{ p: { xs: 3, sm: 6, md: 8 } }}>
|
|
{/* Header with Badges */}
|
|
<Box sx={{ mb: { xs: 2, sm: 4 } }}>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'start',
|
|
mb: { xs: 2, sm: 3 },
|
|
flexDirection: { xs: 'column', sm: 'row' },
|
|
gap: { xs: 1.5, sm: 0 },
|
|
}}
|
|
>
|
|
<Typography
|
|
variant="h6"
|
|
sx={{ fontWeight: 700, color: colors.gray[900], flex: 1, fontSize: { xs: '1rem', sm: '1.25rem' } }}
|
|
>
|
|
{event.title}
|
|
</Typography>
|
|
<Box
|
|
sx={{
|
|
px: { xs: 2, sm: 2.5 },
|
|
py: { xs: 0.5, sm: 0.75 },
|
|
borderRadius: 2,
|
|
fontSize: { xs: '0.75rem', sm: '0.875rem' },
|
|
fontWeight: 600,
|
|
...getStatusStyle(event.status),
|
|
}}
|
|
>
|
|
{getStatusText(event.status)}
|
|
{event.status === 'active'
|
|
? ` | D-${event.daysLeft}`
|
|
: event.status === 'scheduled'
|
|
? ` | D+${event.daysLeft}`
|
|
: ''}
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Status Badges */}
|
|
<Box sx={{ display: 'flex', gap: { xs: 1, sm: 1.5 }, flexWrap: 'wrap' }}>
|
|
{event.isUrgent && (
|
|
<Chip
|
|
icon={<Warning sx={{ fontSize: { xs: 14, sm: 16 } }} />}
|
|
label="마감임박"
|
|
size="small"
|
|
sx={{
|
|
bgcolor: '#FEF3C7',
|
|
color: '#92400E',
|
|
fontWeight: 600,
|
|
fontSize: { xs: '0.6875rem', sm: '0.75rem' },
|
|
height: { xs: 24, sm: 28 },
|
|
'& .MuiChip-icon': { color: '#92400E' },
|
|
}}
|
|
/>
|
|
)}
|
|
{event.isPopular && (
|
|
<Chip
|
|
icon={<LocalFireDepartment sx={{ fontSize: { xs: 14, sm: 16 } }} />}
|
|
label="인기"
|
|
size="small"
|
|
sx={{
|
|
bgcolor: '#FEE2E2',
|
|
color: '#991B1B',
|
|
fontWeight: 600,
|
|
fontSize: { xs: '0.6875rem', sm: '0.75rem' },
|
|
height: { xs: 24, sm: 28 },
|
|
'& .MuiChip-icon': { color: '#991B1B' },
|
|
}}
|
|
/>
|
|
)}
|
|
{event.isHighROI && (
|
|
<Chip
|
|
icon={<Star sx={{ fontSize: { xs: 14, sm: 16 } }} />}
|
|
label="높은 ROI"
|
|
size="small"
|
|
sx={{
|
|
bgcolor: '#DCFCE7',
|
|
color: '#166534',
|
|
fontWeight: 600,
|
|
fontSize: { xs: '0.6875rem', sm: '0.75rem' },
|
|
height: { xs: 24, sm: 28 },
|
|
'& .MuiChip-icon': { color: '#166534' },
|
|
}}
|
|
/>
|
|
)}
|
|
{event.isNew && (
|
|
<Chip
|
|
icon={<NewReleases sx={{ fontSize: { xs: 14, sm: 16 } }} />}
|
|
label="신규"
|
|
size="small"
|
|
sx={{
|
|
bgcolor: '#DBEAFE',
|
|
color: '#1E40AF',
|
|
fontWeight: 600,
|
|
fontSize: { xs: '0.6875rem', sm: '0.75rem' },
|
|
height: { xs: 24, sm: 28 },
|
|
'& .MuiChip-icon': { color: '#1E40AF' },
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Progress Bar for Active Events */}
|
|
{event.status === 'active' && (
|
|
<Box sx={{ mb: { xs: 2, sm: 4 } }}>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
mb: 1,
|
|
}}
|
|
>
|
|
<Typography
|
|
variant="caption"
|
|
sx={{ color: colors.gray[600], fontSize: { xs: '0.6875rem', sm: '0.75rem' } }}
|
|
>
|
|
이벤트 진행률
|
|
</Typography>
|
|
<Typography
|
|
variant="caption"
|
|
sx={{ color: colors.gray[700], fontWeight: 600, fontSize: { xs: '0.6875rem', sm: '0.75rem' } }}
|
|
>
|
|
{Math.round(calculateProgress(event))}%
|
|
</Typography>
|
|
</Box>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={calculateProgress(event)}
|
|
sx={{
|
|
height: { xs: 6, sm: 8 },
|
|
borderRadius: 4,
|
|
bgcolor: colors.gray[200],
|
|
'& .MuiLinearProgress-bar': {
|
|
borderRadius: 4,
|
|
background: `linear-gradient(90deg, ${colors.mint} 0%, ${colors.blue} 100%)`,
|
|
},
|
|
}}
|
|
/>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Event Info and Stats Container */}
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-end',
|
|
flexDirection: { xs: 'column', sm: 'row' },
|
|
gap: { xs: 3, sm: 0 },
|
|
}}
|
|
>
|
|
{/* Left: Event Info */}
|
|
<Box sx={{ width: { xs: '100%', sm: 'auto' } }}>
|
|
<Box sx={{ mb: { xs: 2, sm: 4 }, display: 'flex', flexDirection: 'column', gap: { xs: 1.5, sm: 2 } }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<CardGiftcard sx={{ fontSize: { xs: 16, sm: 18 }, color: colors.pink }} />
|
|
<Typography
|
|
variant="body2"
|
|
sx={{ color: colors.gray[700], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}
|
|
>
|
|
{event.prize}
|
|
</Typography>
|
|
</Box>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
{getMethodIcon(event.method)}
|
|
<Typography
|
|
variant="body2"
|
|
sx={{ color: colors.gray[700], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}
|
|
>
|
|
{event.method}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Date */}
|
|
<Typography
|
|
variant="body2"
|
|
sx={{
|
|
color: colors.gray[600],
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: { xs: 1.5, sm: 2 },
|
|
fontSize: { xs: '0.75rem', sm: '0.875rem' },
|
|
}}
|
|
>
|
|
<span>📅</span>
|
|
<span>
|
|
{event.startDate} ~ {event.endDate}
|
|
</span>
|
|
</Typography>
|
|
</Box>
|
|
|
|
{/* Right: Stats */}
|
|
<Box sx={{ display: 'flex', gap: { xs: 4, sm: 8 }, textAlign: 'right', width: { xs: '100%', sm: 'auto' }, justifyContent: { xs: 'flex-start', sm: 'flex-end' } }}>
|
|
<Box>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{ mb: { xs: 0.5, sm: 1 }, color: colors.gray[600], fontWeight: 500, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
|
>
|
|
참여자
|
|
</Typography>
|
|
<Typography
|
|
variant="h5"
|
|
sx={{ fontWeight: 700, color: colors.gray[900], mb: 0.5, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}
|
|
>
|
|
{event.participants.toLocaleString()}
|
|
<Typography
|
|
component="span"
|
|
variant="body2"
|
|
sx={{ ml: 0.5, color: colors.gray[600], fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
|
>
|
|
명
|
|
</Typography>
|
|
</Typography>
|
|
{event.targetParticipants > 0 && (
|
|
<Typography
|
|
variant="caption"
|
|
sx={{ color: colors.gray[500], fontSize: { xs: '0.6875rem', sm: '0.75rem' } }}
|
|
>
|
|
목표: {event.targetParticipants}명 (
|
|
{Math.round((event.participants / event.targetParticipants) * 100)}
|
|
%)
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
<Box>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{ mb: { xs: 0.5, sm: 1 }, color: colors.gray[600], fontWeight: 500, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
|
>
|
|
ROI
|
|
</Typography>
|
|
<Typography
|
|
variant="h5"
|
|
sx={{
|
|
fontWeight: 700,
|
|
fontSize: { xs: '1.125rem', sm: '1.5rem' },
|
|
color:
|
|
event.roi >= 400
|
|
? colors.mint
|
|
: event.roi >= 200
|
|
? colors.orange
|
|
: colors.gray[500],
|
|
}}
|
|
>
|
|
{event.roi}%
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Pagination */}
|
|
{totalPages > 1 && (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
|
<Pagination
|
|
count={totalPages}
|
|
page={currentPage}
|
|
onChange={(_, page) => setCurrentPage(page)}
|
|
size="large"
|
|
sx={{
|
|
'& .MuiPaginationItem-root': {
|
|
color: colors.gray[700],
|
|
fontWeight: 600,
|
|
'&.Mui-selected': {
|
|
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.blue} 100%)`,
|
|
color: 'white',
|
|
'&:hover': {
|
|
background: `linear-gradient(135deg, ${colors.purpleLight} 0%, ${colors.blueLight} 100%)`,
|
|
},
|
|
},
|
|
'&:hover': {
|
|
bgcolor: colors.gray[100],
|
|
},
|
|
},
|
|
}}
|
|
/>
|
|
</Box>
|
|
)}
|
|
</Container>
|
|
</Box>
|
|
</>
|
|
);
|
|
}
|