diff --git a/package-lock.json b/package-lock.json
index ad38b77..9644766 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,11 +18,11 @@
"@use-funnel/browser": "^0.0.12",
"@use-funnel/next": "^0.0.12",
"axios": "^1.7.7",
- "chart.js": "^4.4.6",
+ "chart.js": "^4.5.1",
"dayjs": "^1.11.13",
"next": "^14.2.15",
"react": "^18.3.1",
- "react-chartjs-2": "^5.2.0",
+ "react-chartjs-2": "^5.3.0",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"zod": "^3.23.8",
@@ -30,6 +30,7 @@
},
"devDependencies": {
"@playwright/test": "^1.48.0",
+ "@types/chart.js": "^2.9.41",
"@types/node": "^22.7.5",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
@@ -1215,6 +1216,16 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/chart.js": {
+ "version": "2.9.41",
+ "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.41.tgz",
+ "integrity": "sha512-3dvkDvueckY83UyUXtJMalYoH6faOLkWQoaTlJgB4Djde3oORmNP0Jw85HtzTuXyliUHcdp704s0mZFQKio/KQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "moment": "^2.10.2"
+ }
+ },
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -4684,6 +4695,16 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/moment": {
+ "version": "2.30.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
diff --git a/package.json b/package.json
index eca31d7..b4463e4 100644
--- a/package.json
+++ b/package.json
@@ -20,11 +20,11 @@
"@use-funnel/browser": "^0.0.12",
"@use-funnel/next": "^0.0.12",
"axios": "^1.7.7",
- "chart.js": "^4.4.6",
+ "chart.js": "^4.5.1",
"dayjs": "^1.11.13",
"next": "^14.2.15",
"react": "^18.3.1",
- "react-chartjs-2": "^5.2.0",
+ "react-chartjs-2": "^5.3.0",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"zod": "^3.23.8",
@@ -32,6 +32,7 @@
},
"devDependencies": {
"@playwright/test": "^1.48.0",
+ "@types/chart.js": "^2.9.41",
"@types/node": "^22.7.5",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
diff --git a/src/app/(main)/analytics/page.tsx b/src/app/(main)/analytics/page.tsx
index c2fc0ff..a94244f 100644
--- a/src/app/(main)/analytics/page.tsx
+++ b/src/app/(main)/analytics/page.tsx
@@ -10,12 +10,41 @@ import {
Grid,
} from '@mui/material';
import {
- PieChart,
- ShowChart,
+ PieChart as PieChartIcon,
+ ShowChart as ShowChartIcon,
Payments,
People,
} from '@mui/icons-material';
+import {
+ Chart as ChartJS,
+ ArcElement,
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ Title,
+ Tooltip,
+ Legend,
+} from 'chart.js';
+import { Pie, Line } from 'react-chartjs-2';
import Header from '@/shared/ui/Header';
+import {
+ cardStyles,
+ colors,
+ responsiveText,
+} from '@/shared/lib/button-styles';
+
+// Chart.js 등록
+ChartJS.register(
+ ArcElement,
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ Title,
+ Tooltip,
+ Legend
+);
// Mock 데이터
const mockAnalyticsData = {
@@ -28,9 +57,9 @@ const mockAnalyticsData = {
targetRoi: 300,
},
channelPerformance: [
- { channel: '우리동네TV', participants: 58, percentage: 45, color: '#E31E24' },
- { channel: '링고비즈', participants: 38, percentage: 30, color: '#0066FF' },
- { channel: 'SNS', participants: 32, percentage: 25, color: '#FFB800' },
+ { channel: '우리동네TV', participants: 58, percentage: 45, color: '#F472B6' },
+ { channel: '링고비즈', participants: 38, percentage: 30, color: '#60A5FA' },
+ { channel: 'SNS', participants: 32, percentage: 25, color: '#FB923C' },
],
timePerformance: {
peakTime: '오후 2-4시',
@@ -98,18 +127,25 @@ export default function AnalyticsPage() {
return (
<>
-
-
+
+
{/* Title with Real-time Indicator */}
-
+
📊 요약 (실시간)
@@ -118,7 +154,7 @@ export default function AnalyticsPage() {
width: 8,
height: 8,
borderRadius: '50%',
- bgcolor: 'success.main',
+ bgcolor: colors.mint,
animation: 'pulse 2s infinite',
'@keyframes pulse': {
'0%, 100%': { opacity: 1 },
@@ -126,39 +162,75 @@ export default function AnalyticsPage() {
},
}}
/>
-
+
{updateText}
{/* Summary KPI Cards */}
-
+
-
-
-
+
+
+
참여자 수
-
- {summary.participants}명
+
+ {summary.participants}
-
+
↑ {summary.participantsDelta}명 (오늘)
-
-
-
+
+
+
총 비용
-
- {Math.floor(summary.totalCost / 10000)}만원
+
+ {Math.floor(summary.totalCost / 10000)}만
-
+
경품 {Math.floor(roiDetail.prizeCost / 10000)}만 + 채널{' '}
{Math.floor(roiDetail.channelCost / 10000)}만
@@ -166,31 +238,67 @@ export default function AnalyticsPage() {
-
-
-
+
+
+
예상 수익
-
- {Math.floor(summary.expectedRevenue / 10000)}만원
+
+ {Math.floor(summary.expectedRevenue / 10000)}만
-
- 매출증가 {Math.floor(roiDetail.salesIncrease / 10000)}만 + LTV{' '}
+
+ 매출 {Math.floor(roiDetail.salesIncrease / 10000)}만 + LTV{' '}
{Math.floor(roiDetail.newCustomerLTV / 10000)}만
-
-
-
+
+
+
투자대비수익률
-
+
{summary.roi}%
-
+
목표 {summary.targetRoi}% 달성
@@ -199,39 +307,76 @@ export default function AnalyticsPage() {
{/* Charts Grid */}
-
+
{/* Channel Performance */}
-
-
-
-
-
+
+
+
+
+
+
+
채널별 성과
- {/* Chart Placeholder */}
+ {/* Pie Chart */}
-
- donut_large
-
-
- 파이 차트
-
+ item.channel),
+ datasets: [
+ {
+ data: channelPerformance.map((item) => item.participants),
+ backgroundColor: channelPerformance.map((item) => item.color),
+ borderColor: '#fff',
+ borderWidth: 2,
+ },
+ ],
+ }}
+ options={{
+ responsive: true,
+ maintainAspectRatio: true,
+ plugins: {
+ legend: {
+ display: false,
+ },
+ tooltip: {
+ callbacks: {
+ label: function (context) {
+ const label = context.label || '';
+ const value = context.parsed || 0;
+ const total = context.dataset.data.reduce((a: number, b: number) => a + b, 0);
+ const percentage = ((value / total) * 100).toFixed(1);
+ return `${label}: ${value}명 (${percentage}%)`;
+ },
+ },
+ },
+ },
+ }}
+ />
{/* Legend */}
@@ -250,9 +395,11 @@ export default function AnalyticsPage() {
bgcolor: item.color,
}}
/>
- {item.channel}
+
+ {item.channel}
+
-
+
{item.percentage}% ({item.participants}명)
@@ -264,44 +411,113 @@ export default function AnalyticsPage() {
{/* Time Trend */}
-
-
-
-
-
+
+
+
+
+
+
+
시간대별 참여 추이
- {/* Chart Placeholder */}
+ {/* Line Chart */}
-
- trending_up
-
-
- 라인 차트
-
+
{/* Stats */}
-
+
피크 시간: {timePerformance.peakTime} ({timePerformance.peakParticipants}명)
-
+
평균 시간당: {timePerformance.avgPerHour}명
@@ -311,37 +527,49 @@ export default function AnalyticsPage() {
{/* ROI Detail & Participant Profile */}
-
+
{/* ROI Detail */}
-
-
-
-
-
+
+
+
+
+
+
+
투자대비수익률 상세
-
+
-
+
총 비용: {Math.floor(roiDetail.totalCost / 10000)}만원
-
+
-
+
• 경품 비용
-
+
{Math.floor(roiDetail.prizeCost / 10000)}만원
-
+
• 채널 비용
-
+
{Math.floor(roiDetail.channelCost / 10000)}만원
@@ -349,23 +577,23 @@ export default function AnalyticsPage() {
-
+
예상 수익: {Math.floor(roiDetail.expectedRevenue / 10000)}만원
-
+
-
+
• 매출 증가
-
+
{Math.floor(roiDetail.salesIncrease / 10000)}만원
-
+
• 신규 고객 LTV
-
+
{Math.floor(roiDetail.newCustomerLTV / 10000)}만원
@@ -373,26 +601,26 @@ export default function AnalyticsPage() {
-
+
투자대비수익률
-
+
(수익 - 비용) ÷ 비용 × 100
-
+
({Math.floor(roiDetail.expectedRevenue / 10000)}만 -{' '}
{Math.floor(roiDetail.totalCost / 10000)}만) ÷{' '}
{Math.floor(roiDetail.totalCost / 10000)}만 × 100
-
+
= {summary.roi}%
@@ -404,33 +632,45 @@ export default function AnalyticsPage() {
{/* Participant Profile */}
-
-
-
-
-
+
+
+
+
+
+
+
참여자 프로필
{/* Age Distribution */}
-
+
연령별
{participantProfile.age.map((item) => (
-
+
{item.label}
@@ -438,11 +678,11 @@ export default function AnalyticsPage() {
sx={{
width: `${item.percentage}%`,
height: '100%',
- bgcolor: 'info.main',
+ background: `linear-gradient(135deg, ${colors.blue} 0%, ${colors.blueLight} 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
- pr: 1,
+ pr: 1.5,
}}
>
-
+
성별
{participantProfile.gender.map((item) => (
-
+
{item.label}
@@ -484,11 +724,11 @@ export default function AnalyticsPage() {
sx={{
width: `${item.percentage}%`,
height: '100%',
- bgcolor: 'error.main',
+ background: `linear-gradient(135deg, ${colors.pink} 0%, ${colors.pinkLight} 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
- pr: 1,
+ pr: 1.5,
}}
>
{
+ const getStatusStyle = (status: string) => {
switch (status) {
case 'active':
- return 'success';
+ return {
+ bgcolor: colors.mint,
+ color: 'white',
+ };
case 'scheduled':
- return 'info';
+ return {
+ bgcolor: colors.blue,
+ color: 'white',
+ };
case 'ended':
- return 'default';
+ return {
+ bgcolor: colors.gray[300],
+ color: colors.gray[700],
+ };
default:
- return 'default';
+ return {
+ bgcolor: colors.gray[200],
+ color: colors.gray[600],
+ };
}
};
@@ -140,13 +193,149 @@ export default function EventsPage() {
}
};
+ const getMethodIcon = (method: string) => {
+ switch (method) {
+ case '전화번호 입력':
+ return ;
+ case 'SNS 팔로우':
+ return ;
+ case '구매 인증':
+ return ;
+ case '이메일 등록':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const calculateProgress = (event: typeof mockEvents[0]) => {
+ if (event.status !== 'active') return 0;
+ const total = new Date(event.endDate).getTime() - new Date(event.startDate).getTime();
+ const elapsed = Date.now() - new Date(event.startDate).getTime();
+ return Math.min(Math.max((elapsed / total) * 100, 0), 100);
+ };
+
+ // 통계 계산
+ const stats = {
+ total: mockEvents.length,
+ active: mockEvents.filter((e) => e.status === 'active').length,
+ totalParticipants: mockEvents.reduce((sum, e) => sum + e.participants, 0),
+ avgROI: Math.round(
+ mockEvents.filter((e) => e.roi > 0).reduce((sum, e) => sum + e.roi, 0) /
+ mockEvents.filter((e) => e.roi > 0).length
+ ),
+ };
+
return (
<>
-
-
+
+
+ {/* Summary Statistics */}
+
+
+
+
+
+
+ {stats.total}
+
+
+ 전체 이벤트
+
+
+
+
+
+
+
+
+
+ {stats.active}
+
+
+ 진행중
+
+
+
+
+
+
+
+
+
+ {stats.totalParticipants}
+
+
+ 총 참여자
+
+
+
+
+
+
+
+
+
+ {stats.avgROI}%
+
+
+ 평균 ROI
+
+
+
+
+
+
{/* Search Section */}
-
+
-
+
),
}}
sx={{
'& .MuiOutlinedInput-root': {
- borderRadius: 2,
+ borderRadius: 3,
+ bgcolor: 'white',
+ '& fieldset': {
+ borderColor: colors.gray[200],
+ },
+ '&:hover fieldset': {
+ borderColor: colors.gray[300],
+ },
+ '&.Mui-focused fieldset': {
+ borderColor: colors.purple,
+ },
},
}}
/>
{/* Filters */}
-
-
-
+
+
+
상태
{/* Sorting */}
-
+
-
- 정렬:
+
+ 정렬
{/* Event List */}
-
+
{pageEvents.length === 0 ? (
-
-
- event_busy
-
-
- 검색 결과가 없습니다
-
-
+
+
+
+
+ event_busy
+
+
+
+ 검색 결과가 없습니다
+
+
+ 다른 검색 조건으로 다시 시도해보세요
+
+
+
) : (
-
+
{pageEvents.map((event) => (
handleEventClick(event.id)}
>
-
- {/* Header */}
-
-
- {event.title}
-
-
+ {/* Header with Badges */}
+
+
+
+ {event.title}
+
+
+ {getStatusText(event.status)}
+ {event.status === 'active'
? ` | D-${event.daysLeft}`
: event.status === 'scheduled'
? ` | D+${event.daysLeft}`
- : ''
- }`}
- color={getStatusColor(event.status) as any}
- size="small"
- sx={{ fontWeight: 600 }}
- />
+ : ''}
+
+
+
+ {/* Status Badges */}
+
+ {event.isUrgent && (
+ }
+ label="마감임박"
+ size="small"
+ sx={{
+ bgcolor: '#FEF3C7',
+ color: '#92400E',
+ fontWeight: 600,
+ fontSize: '0.75rem',
+ '& .MuiChip-icon': { color: '#92400E' },
+ }}
+ />
+ )}
+ {event.isPopular && (
+ }
+ label="인기"
+ size="small"
+ sx={{
+ bgcolor: '#FEE2E2',
+ color: '#991B1B',
+ fontWeight: 600,
+ fontSize: '0.75rem',
+ '& .MuiChip-icon': { color: '#991B1B' },
+ }}
+ />
+ )}
+ {event.isHighROI && (
+ }
+ label="높은 ROI"
+ size="small"
+ sx={{
+ bgcolor: '#DCFCE7',
+ color: '#166534',
+ fontWeight: 600,
+ fontSize: '0.75rem',
+ '& .MuiChip-icon': { color: '#166534' },
+ }}
+ />
+ )}
+ {event.isNew && (
+ }
+ label="신규"
+ size="small"
+ sx={{
+ bgcolor: '#DBEAFE',
+ color: '#1E40AF',
+ fontWeight: 600,
+ fontSize: '0.75rem',
+ '& .MuiChip-icon': { color: '#1E40AF' },
+ }}
+ />
+ )}
+
- {/* Stats */}
-
-
-
-
- 참여
+ {/* Progress Bar for Active Events */}
+ {event.status === 'active' && (
+
+
+
+ 이벤트 진행률
-
- {event.participants}명
+
+ {Math.round(calculateProgress(event))}%
-
-
+
+
+ )}
+
+ {/* Event Info and Stats Container */}
+
+ {/* Left: Event Info */}
+
+
+
+
+
+ {event.prize}
+
+
+
+ {getMethodIcon(event.method)}
+
+ {event.method}
+
+
+
+
+ {/* Date */}
+
+ 📅
+
+ {event.startDate} ~ {event.endDate}
+
+
+
+
+ {/* Right: Stats */}
+
-
- 투자대비수익률
+
+ 참여자
-
+
+ {event.participants.toLocaleString()}
+
+ 명
+
+
+ {event.targetParticipants > 0 && (
+
+ 목표: {event.targetParticipants}명 ({Math.round((event.participants / event.targetParticipants) * 100)}%)
+
+ )}
+
+
+
+ ROI
+
+ = 400 ? colors.mint : event.roi >= 200 ? colors.orange : colors.gray[500] }}>
{event.roi}%
-
-
-
- {/* Date */}
-
- {event.startDate} ~ {event.endDate}
-
+
+
))}
@@ -314,13 +697,28 @@ export default function EventsPage() {
{/* Pagination */}
{totalPages > 1 && (
-
+
setCurrentPage(page)}
- color="primary"
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],
+ },
+ },
+ }}
/>
)}
diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx
index 61de739..6dbac59 100644
--- a/src/app/(main)/page.tsx
+++ b/src/app/(main)/page.tsx
@@ -88,14 +88,14 @@ export default function HomePage() {
minHeight: '100vh',
}}
>
-
+
{/* Welcome Section */}
-
+
안녕하세요, {mockUser.name}님! 👋
@@ -106,7 +106,7 @@ export default function HomePage() {
{/* KPI Cards */}
-
+
-
+
@@ -145,7 +145,7 @@ export default function HomePage() {
{activeEvents.length}
@@ -161,7 +161,7 @@ export default function HomePage() {
borderColor: 'transparent',
}}
>
-
+
@@ -190,7 +190,7 @@ export default function HomePage() {
{totalParticipants.toLocaleString()}
@@ -206,7 +206,7 @@ export default function HomePage() {
borderColor: 'transparent',
}}
>
-
+
@@ -235,7 +235,7 @@ export default function HomePage() {
{avgROI}%
@@ -245,11 +245,11 @@ export default function HomePage() {
{/* Quick Actions */}
-
-
+
+
빠른 시작
-
+
-
+
-
+
새 이벤트
@@ -289,7 +289,7 @@ export default function HomePage() {
}}
onClick={handleViewAnalytics}
>
-
+
-
+
성과분석
@@ -316,9 +316,9 @@ export default function HomePage() {
{/* Active Events */}
-
+
진행 중인 이벤트
@@ -372,7 +372,7 @@ export default function HomePage() {
) : (
-
+
{activeEvents.map((event) => (
handleEventClick(event.id)}
>
-
+
@@ -411,11 +411,11 @@ export default function HomePage() {
📅
@@ -423,7 +423,7 @@ export default function HomePage() {
{event.startDate} ~ {event.endDate}
-
+
{/* Recent Activity */}
-
-
+
+
최근 활동
-
+
{mockActivities.map((activity, index) => (
0 ? 3.5 : 0,
- mt: index > 0 ? 3.5 : 0,
+ gap: 4,
+ pt: index > 0 ? 6 : 0,
+ mt: index > 0 ? 6 : 0,
borderTop: index > 0 ? 1 : 0,
borderColor: colors.gray[200],
}}
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 3054163..eb797cc 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -24,13 +24,13 @@
--color-error: #D32F2F;
--color-info: #0288D1;
- /* Spacing (4px Grid) */
- --spacing-xs: 4px;
- --spacing-s: 8px;
- --spacing-m: 16px;
- --spacing-l: 24px;
- --spacing-xl: 32px;
- --spacing-2xl: 48px;
+ /* Spacing (4px Grid) - Doubled values */
+ --spacing-xs: 8px;
+ --spacing-s: 16px;
+ --spacing-m: 32px;
+ --spacing-l: 48px;
+ --spacing-xl: 64px;
+ --spacing-2xl: 96px;
/* Border Radius */
--border-radius-sm: 4px;
@@ -73,8 +73,8 @@ body {
padding: 0;
font-family: "Pretendard", -apple-system, BlinkMacSystemFont, "Segoe UI",
"Roboto", "Helvetica Neue", system-ui, sans-serif;
- font-size: 14px;
- line-height: 1.5;
+ font-size: 16px;
+ line-height: 1.6;
color: var(--color-gray-900);
background-color: var(--color-gray-50);
min-height: 100%;
@@ -149,18 +149,18 @@ button {
width: 100%;
max-width: 1200px;
margin: 0 auto;
- padding: 0 20px;
+ padding: 0 40px;
}
@media (min-width: 768px) {
.container {
- padding: 0 40px;
+ padding: 0 80px;
}
}
@media (min-width: 1024px) {
.container {
- padding: 0 80px;
+ padding: 0 120px;
}
}