zioinfo-web/frontend/src/pages/admin/AdminDashboard.jsx
DESKTOP-TKLFCPRython 6e02e7efe0 feat(admin): 홈페이지 관리자 시스템 구현
- 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>
2026-05-30 18:40:24 +09:00

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>
</>
);
}