- CompanyHistory JPA entity (tb_company_history) - CompanyHistoryRepository - GET /api/history: DB-based grouped history (year + items[]) - Admin CRUD: GET/POST/PUT/DELETE /api/admin/history - DataInitializer: 35 history items seeded from 2000 to 2026 - Company.jsx: useHistory() hook -> API fetch with fallback - AdminHistory.jsx: year-grouped timeline CRUD UI - AdminLayout: 회사 연혁 menu added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
112 lines
3.7 KiB
JavaScript
112 lines
3.7 KiB
JavaScript
import { useEffect, useState } from 'react';
|
|
import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom';
|
|
import './admin.css';
|
|
|
|
const NAV = [
|
|
{ section: '메인' },
|
|
{ path: '/admin/dashboard', icon: '📊', label: '대시보드' },
|
|
{ section: '콘텐츠 관리' },
|
|
{ path: '/admin/news', icon: '📰', label: '뉴스/공지사항' },
|
|
{ path: '/admin/history', icon: '📅', label: '회사 연혁' },
|
|
{ path: '/admin/recruit', icon: '👥', label: '채용공고' },
|
|
{ section: '고객 관리' },
|
|
{ path: '/admin/inquiries', icon: '📩', label: '문의 관리', badgeKey: 'pendingInquiries' },
|
|
{ path: '/admin/members', icon: '👤', label: '회원 관리' },
|
|
{ section: '시스템' },
|
|
{ path: '/admin/settings', icon: '⚙️', label: '설정' },
|
|
];
|
|
|
|
export default function AdminLayout() {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const [user, setUser] = useState(null);
|
|
const [pageTitle, setPageTitle] = useState('대시보드');
|
|
const [badges, setBadges] = useState({});
|
|
|
|
useEffect(() => {
|
|
const token = localStorage.getItem('admin_token');
|
|
if (!token) { navigate('/admin/login'); return; }
|
|
const userData = JSON.parse(localStorage.getItem('admin_user') || '{}');
|
|
setUser(userData);
|
|
fetchBadges(token);
|
|
}, [navigate]);
|
|
|
|
useEffect(() => {
|
|
const map = {
|
|
'/admin/dashboard': '대시보드',
|
|
'/admin/news': '뉴스/공지사항 관리',
|
|
'/admin/inquiries': '문의 관리',
|
|
'/admin/recruit': '채용공고 관리',
|
|
'/admin/members': '회원 관리',
|
|
'/admin/settings': '설정',
|
|
};
|
|
setPageTitle(map[location.pathname] || '관리자');
|
|
}, [location.pathname]);
|
|
|
|
const fetchBadges = async (token) => {
|
|
try {
|
|
const res = await fetch('/api/admin/dashboard', {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
if (res.ok) {
|
|
const d = await res.json();
|
|
setBadges({ pendingInquiries: d.pendingInquiries || 0 });
|
|
}
|
|
} catch {}
|
|
};
|
|
|
|
const logout = () => {
|
|
localStorage.removeItem('admin_token');
|
|
localStorage.removeItem('admin_user');
|
|
navigate('/admin/login');
|
|
};
|
|
|
|
if (!user) return null;
|
|
|
|
return (
|
|
<div className="admin-wrap">
|
|
<aside className="admin-sidebar">
|
|
<div className="admin-sidebar-logo">
|
|
<h2>ZioInfo Admin</h2>
|
|
<span>(주)지오정보기술 관리자</span>
|
|
</div>
|
|
<nav className="admin-nav">
|
|
{NAV.map((item, i) =>
|
|
item.section ? (
|
|
<div key={i} className="admin-nav-section">{item.section}</div>
|
|
) : (
|
|
<NavLink key={item.path} to={item.path}
|
|
className={({ isActive }) => isActive ? 'active' : ''}>
|
|
<span className="nav-icon">{item.icon}</span>
|
|
{item.label}
|
|
{item.badgeKey && badges[item.badgeKey] > 0 && (
|
|
<span className="admin-nav-badge">{badges[item.badgeKey]}</span>
|
|
)}
|
|
</NavLink>
|
|
)
|
|
)}
|
|
</nav>
|
|
<div className="admin-sidebar-footer">
|
|
<button onClick={logout}>🚪 로그아웃</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<main className="admin-main">
|
|
<div className="admin-topbar">
|
|
<h1>{pageTitle}</h1>
|
|
<div className="admin-topbar-right">
|
|
<span className="admin-user-badge">👤 {user.displayName || user.username}</span>
|
|
<a href="/" target="_blank" rel="noreferrer"
|
|
style={{ fontSize: 12, color: '#64748b', textDecoration: 'none' }}>
|
|
🌐 홈페이지 보기
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div className="admin-content">
|
|
<Outlet />
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|