- Docker Compose with Postgres, Redis, MinIO, backend, frontend (port 3200/3201) - Prisma schema: User, Project, ProjectFile, Product, Order, FlashToken, Review, AuditLog - Backend: JWT auth, project CRUD + file upload (MinIO + sharp WebP), admin approval flow - Frontend: React + Vite SPA with auth, project/shop browse, seller dashboard, admin panel - Admin: pending approval queue, user management, audit log viewer, stats dashboard - Audit logging middleware for legal compliance - Admin init script: createAdmin.js - Full design document in PLATFORM_DESIGN.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
58 lines
2.8 KiB
JavaScript
58 lines
2.8 KiB
JavaScript
import { useEffect, useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import api from '../../api/client';
|
|
|
|
export default function AdminIndex() {
|
|
const [stats, setStats] = useState(null);
|
|
|
|
useEffect(() => {
|
|
api.get('/admin/stats').then(r => setStats(r.data)).catch(() => {});
|
|
}, []);
|
|
|
|
const cards = stats ? [
|
|
{ label: '총 사용자', value: stats.users.toLocaleString(), icon: '👤' },
|
|
{ label: '전체 프로젝트', value: stats.projects.total.toLocaleString(), icon: '📁' },
|
|
{ label: '승인 대기', value: stats.projects.pending.toLocaleString(), icon: '⏳', warn: stats.projects.pending > 0 },
|
|
{ label: '총 매출', value: `₩${(stats.revenue.total || 0).toLocaleString()}`, icon: '💰' },
|
|
{ label: '총 주문', value: stats.revenue.orders.toLocaleString(), icon: '🛒' },
|
|
] : [];
|
|
|
|
return (
|
|
<div className="container page">
|
|
<h2 style={{ marginBottom: 24 }}>관리자 대시보드</h2>
|
|
|
|
{/* 통계 카드 */}
|
|
{stats && (
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 16, marginBottom: 32 }}>
|
|
{cards.map(c => (
|
|
<div key={c.label} className="card" style={{ borderColor: c.warn ? 'var(--warn)' : undefined }}>
|
|
<div style={{ fontSize: 28, marginBottom: 8 }}>{c.icon}</div>
|
|
<div style={{ fontSize: 22, fontWeight: 700, color: c.warn ? 'var(--warn)' : 'var(--text)' }}>{c.value}</div>
|
|
<div className="text-muted" style={{ fontSize: 13 }}>{c.label}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 빠른 메뉴 */}
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16 }}>
|
|
{[
|
|
{ to: '/admin/projects', icon: '📋', title: '프로젝트 승인', desc: '검토 대기 중인 프로젝트를 승인/반려' },
|
|
{ to: '/admin/users', icon: '👥', title: '사용자 관리', desc: '사용자 목록, 역할 변경, 계정 비활성화' },
|
|
{ to: '/admin/logs', icon: '📜', title: '감사 로그', desc: '모든 주요 행동 기록 조회' },
|
|
].map(m => (
|
|
<Link key={m.to} to={m.to} style={{ textDecoration: 'none' }}>
|
|
<div className="card" style={{ cursor: 'pointer', transition: 'border-color .2s' }}
|
|
onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--accent)'}
|
|
onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border)'}>
|
|
<div style={{ fontSize: 32, marginBottom: 8 }}>{m.icon}</div>
|
|
<div style={{ fontWeight: 600, marginBottom: 4 }}>{m.title}</div>
|
|
<div className="text-muted" style={{ fontSize: 13 }}>{m.desc}</div>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|