- Spring Security + JWT 인증 (8시간 토큰) - AdminUser / Recruit 엔터티 추가 - AdminController: 로그인, 대시보드, 뉴스/문의/채용 CRUD - React 어드민 SPA: /admin/* 라우트 (Header/Footer 없음) - 로그인, 대시보드, 뉴스 관리, 문의 관리, 채용공고 관리, 설정 - Jenkinsfile: 서버 환경 맞춤 CI/CD 파이프라인 - .gitignore 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
94 lines
3.6 KiB
JavaScript
94 lines
3.6 KiB
JavaScript
import { useEffect, useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
|
|
const API = (path) => fetch(path, {
|
|
headers: { Authorization: `Bearer ${localStorage.getItem('admin_token')}` },
|
|
}).then(r => r.json());
|
|
|
|
export default function AdminDashboard() {
|
|
const [stats, setStats] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
API('/api/admin/dashboard')
|
|
.then(setStats)
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
if (loading) return <p style={{ color: '#64748b', fontSize: 14 }}>로딩 중...</p>;
|
|
if (!stats) return null;
|
|
|
|
const STAT_CARDS = [
|
|
{ icon: '📰', label: '전체 뉴스', value: stats.totalNews, sub: `공개 ${stats.visibleNews}건`, color: 'blue' },
|
|
{ icon: '📩', label: '전체 문의', value: stats.totalInquiries, sub: `미답변 ${stats.pendingInquiries}건`, color: stats.pendingInquiries > 0 ? 'red' : 'green' },
|
|
{ icon: '👥', label: '채용공고', value: stats.totalRecruits, sub: `진행중 ${stats.activeRecruits}건`, color: 'green' },
|
|
];
|
|
|
|
return (
|
|
<>
|
|
{/* Stats */}
|
|
<div className="admin-stats">
|
|
{STAT_CARDS.map(s => (
|
|
<div className="stat-card" key={s.label}>
|
|
<div className={`stat-icon ${s.color}`}>{s.icon}</div>
|
|
<div className="stat-info">
|
|
<h4>{s.value}</h4>
|
|
<p>{s.label}<br /><span style={{ fontSize: 11 }}>{s.sub}</span></p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{stats.pendingInquiries > 0 && (
|
|
<div className="stat-card" style={{ borderLeft: '3px solid #ef4444' }}>
|
|
<div className="stat-icon red">🔔</div>
|
|
<div className="stat-info">
|
|
<h4 style={{ color: '#ef4444' }}>{stats.pendingInquiries}</h4>
|
|
<p>미답변 문의<br /><Link to="/admin/inquiries" style={{ fontSize: 11, color: '#ef4444' }}>바로가기 →</Link></p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Recent panels */}
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
|
<div className="admin-card">
|
|
<div className="admin-card-header">
|
|
<h3>📰 최근 뉴스</h3>
|
|
<Link to="/admin/news" className="btn btn-outline btn-sm">전체보기</Link>
|
|
</div>
|
|
<ul className="recent-list">
|
|
{(stats.recentNews || []).map(n => (
|
|
<li key={n.id}>
|
|
<span className="rl-dot" />
|
|
<span className="rl-title">{n.title}</span>
|
|
<span className="rl-meta">{n.category}</span>
|
|
</li>
|
|
))}
|
|
{!stats.recentNews?.length && (
|
|
<li style={{ color: '#94a3b8', fontSize: 13 }}>등록된 뉴스가 없습니다.</li>
|
|
)}
|
|
</ul>
|
|
</div>
|
|
|
|
<div className="admin-card">
|
|
<div className="admin-card-header">
|
|
<h3>📩 최근 문의</h3>
|
|
<Link to="/admin/inquiries" className="btn btn-outline btn-sm">전체보기</Link>
|
|
</div>
|
|
<ul className="recent-list">
|
|
{(stats.recentInquiries || []).map(q => (
|
|
<li key={q.id}>
|
|
<span className="rl-dot" style={{ background: q.status === 'PENDING' ? '#ef4444' : '#22c55e' }} />
|
|
<span className="rl-title">{q.subject}</span>
|
|
<span className="rl-meta">{q.name}</span>
|
|
</li>
|
|
))}
|
|
{!stats.recentInquiries?.length && (
|
|
<li style={{ color: '#94a3b8', fontSize: 13 }}>접수된 문의가 없습니다.</li>
|
|
)}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|