+
로딩 중...
);
}
-export default function App() {
- const location = useLocation();
+function PublicLayout({ children }) {
return (
<>
-
}>
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
+
}>{children}
>
);
}
+
+export default function App() {
+ const location = useLocation();
+ const isAdmin = location.pathname.startsWith('/admin');
+
+ if (isAdmin) {
+ return (
+
}>
+
+ } />
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ } />
+
+
+ );
+ }
+
+ return (
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ );
+}
diff --git a/workspace/zioinfo-web/frontend/src/pages/admin/AdminDashboard.jsx b/workspace/zioinfo-web/frontend/src/pages/admin/AdminDashboard.jsx
new file mode 100644
index 00000000..cd8affdf
--- /dev/null
+++ b/workspace/zioinfo-web/frontend/src/pages/admin/AdminDashboard.jsx
@@ -0,0 +1,93 @@
+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
로딩 중...
;
+ 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 */}
+
+ {STAT_CARDS.map(s => (
+
+
{s.icon}
+
+
{s.value}
+
{s.label}
{s.sub}
+
+
+ ))}
+ {stats.pendingInquiries > 0 && (
+
+
🔔
+
+
{stats.pendingInquiries}
+
미답변 문의
바로가기 →
+
+
+ )}
+
+
+ {/* Recent panels */}
+
+
+
+
📰 최근 뉴스
+ 전체보기
+
+
+ {(stats.recentNews || []).map(n => (
+ -
+
+ {n.title}
+ {n.category}
+
+ ))}
+ {!stats.recentNews?.length && (
+ - 등록된 뉴스가 없습니다.
+ )}
+
+
+
+
+
+
📩 최근 문의
+ 전체보기
+
+
+ {(stats.recentInquiries || []).map(q => (
+ -
+
+ {q.subject}
+ {q.name}
+
+ ))}
+ {!stats.recentInquiries?.length && (
+ - 접수된 문의가 없습니다.
+ )}
+
+
+
+ >
+ );
+}
diff --git a/workspace/zioinfo-web/frontend/src/pages/admin/AdminInquiry.jsx b/workspace/zioinfo-web/frontend/src/pages/admin/AdminInquiry.jsx
new file mode 100644
index 00000000..42a73023
--- /dev/null
+++ b/workspace/zioinfo-web/frontend/src/pages/admin/AdminInquiry.jsx
@@ -0,0 +1,152 @@
+import { useEffect, useState, useCallback } from 'react';
+
+const token = () => localStorage.getItem('admin_token');
+const authFetch = (url, opts = {}) =>
+ fetch(url, { ...opts, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token()}`, ...opts.headers } });
+
+const STATUS_LABEL = { PENDING: '미답변', ANSWERED: '답변완료', CLOSED: '종결' };
+const STATUS_BADGE = { PENDING: 'badge-red', ANSWERED: 'badge-green', CLOSED: 'badge-gray' };
+
+export default function AdminInquiry() {
+ const [page, setPage] = useState(0);
+ const [filter, setFilter] = useState('');
+ const [data, setData] = useState({ content: [], totalPages: 0, totalElements: 0 });
+ const [selected, setSelected] = useState(null);
+ const [toast, setToast] = useState(null);
+
+ const showToast = (msg, type = 'success') => { setToast({ msg, type }); setTimeout(() => setToast(null), 2500); };
+
+ const load = useCallback(() => {
+ const q = filter ? `&status=${filter}` : '';
+ authFetch(`/api/admin/inquiries?page=${page}&size=10${q}`)
+ .then(r => r.json()).then(setData);
+ }, [page, filter]);
+
+ useEffect(() => { load(); }, [load]);
+
+ const handleStatus = async (id, status) => {
+ const res = await authFetch(`/api/admin/inquiries/${id}/status`, {
+ method: 'PATCH', body: JSON.stringify({ status }),
+ });
+ if (res.ok) {
+ load();
+ if (selected?.id === id) setSelected(p => ({ ...p, status }));
+ showToast('상태가 변경되었습니다.');
+ }
+ };
+
+ const openDetail = async (id) => {
+ const res = await authFetch(`/api/admin/inquiries/${id}`);
+ if (res.ok) setSelected(await res.json());
+ };
+
+ return (
+ <>
+ {toast &&
}
+
+
+
+ 전체 {data.totalElements}건
+
+
+
+
+
+
+ | No | 이름 | 제목 | 카테고리 | 상태 | 접수일 | 관리 |
+
+
+ {data.content.map((q, i) => (
+
+ | {data.totalElements - page * 10 - i} |
+ {q.name} |
+ openDetail(q.id)}>
+ {q.subject}
+ |
+ {q.category || '기타'} |
+ {STATUS_LABEL[q.status] || q.status} |
+ {q.createdAt?.slice(0, 10)} |
+
+
+ {q.status === 'PENDING' && (
+
+ )}
+ {q.status !== 'CLOSED' && (
+
+ )}
+
+ |
+
+ ))}
+ {!data.content.length && (
+ |
+ )}
+
+
+
+
+ {data.totalPages > 1 && (
+
+
페이지 {page + 1} / {data.totalPages}
+
+
+ {Array.from({ length: Math.min(data.totalPages, 7) }, (_, i) => (
+
+ ))}
+
+
+
+ )}
+
+
+ {selected && (
+
e.target === e.currentTarget && setSelected(null)}>
+
+
+
문의 상세
+
+
+
+
+ {[['이름', selected.name], ['이메일', selected.email], ['연락처', selected.phone || '-'], ['유형', selected.category || '기타']].map(([l, v]) => (
+
+ ))}
+
+
+
제목
+
{selected.subject}
+
+
+
내용
+
+ {selected.content}
+
+
+
+ 접수일: {selected.createdAt?.slice(0, 16)}
+ {STATUS_LABEL[selected.status]}
+
+
+
+ {selected.status === 'PENDING' && (
+
+ )}
+ {selected.status !== 'CLOSED' && (
+
+ )}
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/workspace/zioinfo-web/frontend/src/pages/admin/AdminLayout.jsx b/workspace/zioinfo-web/frontend/src/pages/admin/AdminLayout.jsx
new file mode 100644
index 00000000..a355799a
--- /dev/null
+++ b/workspace/zioinfo-web/frontend/src/pages/admin/AdminLayout.jsx
@@ -0,0 +1,108 @@
+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/recruit', icon: '👥', label: '채용공고' },
+ { section: '고객 관리' },
+ { path: '/admin/inquiries', icon: '📩', label: '문의 관리', badgeKey: 'pendingInquiries' },
+ { 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/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 (
+
+ );
+}
diff --git a/workspace/zioinfo-web/frontend/src/pages/admin/AdminLogin.jsx b/workspace/zioinfo-web/frontend/src/pages/admin/AdminLogin.jsx
new file mode 100644
index 00000000..aeb99d09
--- /dev/null
+++ b/workspace/zioinfo-web/frontend/src/pages/admin/AdminLogin.jsx
@@ -0,0 +1,66 @@
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import './admin.css';
+
+export default function AdminLogin() {
+ const [form, setForm] = useState({ username: '', password: '' });
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setError(''); setLoading(true);
+ try {
+ const res = await fetch('/api/admin/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(form),
+ });
+ const data = await res.json();
+ if (!res.ok) { setError(data.message || '로그인 실패'); return; }
+ localStorage.setItem('admin_token', data.token);
+ localStorage.setItem('admin_user', JSON.stringify({ username: data.username, displayName: data.displayName }));
+ navigate('/admin/dashboard');
+ } catch {
+ setError('서버 연결 오류가 발생했습니다.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
ADMIN
+
(주)지오정보기술
+
홈페이지 관리자 시스템
+
+ {error &&
⚠ {error}
}
+
+
+ 홈페이지로 돌아가기: 메인 페이지
+
+
+
+ );
+}
diff --git a/workspace/zioinfo-web/frontend/src/pages/admin/AdminNews.jsx b/workspace/zioinfo-web/frontend/src/pages/admin/AdminNews.jsx
new file mode 100644
index 00000000..c924a766
--- /dev/null
+++ b/workspace/zioinfo-web/frontend/src/pages/admin/AdminNews.jsx
@@ -0,0 +1,175 @@
+import { useEffect, useState, useCallback } from 'react';
+
+const token = () => localStorage.getItem('admin_token');
+const authFetch = (url, opts = {}) =>
+ fetch(url, { ...opts, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token()}`, ...opts.headers } });
+
+const EMPTY = { title: '', category: '공지사항', summary: '', content: '', thumbnailUrl: '', visible: true };
+const CATS = ['공지사항', '보도자료', '이벤트'];
+
+export default function AdminNews() {
+ const [page, setPage] = useState(0);
+ const [data, setData] = useState({ content: [], totalPages: 0, totalElements: 0 });
+ const [modal, setModal] = useState(null); // null | 'create' | 'edit'
+ const [form, setForm] = useState(EMPTY);
+ const [editId, setEditId] = useState(null);
+ const [saving, setSaving] = useState(false);
+ const [toast, setToast] = useState(null);
+
+ const showToast = (msg, type = 'success') => {
+ setToast({ msg, type });
+ setTimeout(() => setToast(null), 2500);
+ };
+
+ const load = useCallback(() => {
+ authFetch(`/api/admin/news?page=${page}&size=10`)
+ .then(r => r.json()).then(setData);
+ }, [page]);
+
+ useEffect(() => { load(); }, [load]);
+
+ const openCreate = () => { setForm(EMPTY); setEditId(null); setModal('form'); };
+ const openEdit = (n) => { setForm({ ...n }); setEditId(n.id); setModal('form'); };
+
+ const handleSave = async () => {
+ setSaving(true);
+ const url = editId ? `/api/admin/news/${editId}` : '/api/admin/news';
+ const method = editId ? 'PUT' : 'POST';
+ const res = await authFetch(url, { method, body: JSON.stringify(form) });
+ setSaving(false);
+ if (res.ok) { setModal(null); load(); showToast(editId ? '수정되었습니다.' : '등록되었습니다.'); }
+ else showToast('저장 실패', 'error');
+ };
+
+ const handleDelete = async (id) => {
+ if (!confirm('삭제하시겠습니까?')) return;
+ const res = await authFetch(`/api/admin/news/${id}`, { method: 'DELETE' });
+ if (res.ok) { load(); showToast('삭제되었습니다.'); }
+ };
+
+ const toggleVisible = async (id) => {
+ await authFetch(`/api/admin/news/${id}/visibility`, { method: 'PATCH' });
+ load();
+ };
+
+ const set = (k, v) => setForm(p => ({ ...p, [k]: v }));
+
+ return (
+ <>
+ {toast && (
+
+ )}
+
+
+
+
전체 {data.totalElements}건
+
+
+
+
+
+
+
+
+
+ | No | 제목 | 카테고리 | 공개 | 조회수 | 등록일 | 관리 |
+
+
+
+ {data.content.map((n, i) => (
+
+ | {data.totalElements - page * 10 - i} |
+ {n.title} |
+ {n.category} |
+
+
+ |
+ {n.viewCount} |
+ {n.createdAt?.slice(0, 10)} |
+
+
+
+
+
+ |
+
+ ))}
+ {!data.content.length && (
+ |
+ )}
+
+
+
+
+ {data.totalPages > 1 && (
+
+
페이지 {page + 1} / {data.totalPages}
+
+
+ {Array.from({ length: data.totalPages }, (_, i) => (
+
+ ))}
+
+
+
+ )}
+
+
+ {modal === 'form' && (
+
e.target === e.currentTarget && setModal(null)}>
+
+
+
{editId ? '뉴스 수정' : '뉴스 등록'}
+
+
+
+
+
+ set('title', e.target.value)} placeholder="뉴스 제목을 입력하세요" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ set('summary', e.target.value)} placeholder="목록에 표시될 요약 문구" />
+
+
+
+ set('thumbnailUrl', e.target.value)} placeholder="https://..." />
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/workspace/zioinfo-web/frontend/src/pages/admin/AdminRecruit.jsx b/workspace/zioinfo-web/frontend/src/pages/admin/AdminRecruit.jsx
new file mode 100644
index 00000000..0fe5e0a2
--- /dev/null
+++ b/workspace/zioinfo-web/frontend/src/pages/admin/AdminRecruit.jsx
@@ -0,0 +1,169 @@
+import { useEffect, useState, useCallback } from 'react';
+
+const token = () => localStorage.getItem('admin_token');
+const authFetch = (url, opts = {}) =>
+ fetch(url, { ...opts, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token()}`, ...opts.headers } });
+
+const EMPTY = { title: '', department: '', jobType: '정규직', description: '', requirements: '', preferred: '', deadline: '', headcount: 1, active: true };
+const JOB_TYPES = ['정규직', '계약직', '인턴', '프리랜서'];
+
+export default function AdminRecruit() {
+ const [page, setPage] = useState(0);
+ const [data, setData] = useState({ content: [], totalPages: 0, totalElements: 0 });
+ const [modal, setModal] = useState(false);
+ const [form, setForm] = useState(EMPTY);
+ const [editId, setEditId] = useState(null);
+ const [saving, setSaving] = useState(false);
+ const [toast, setToast] = useState(null);
+
+ const showToast = (msg, type = 'success') => { setToast({ msg, type }); setTimeout(() => setToast(null), 2500); };
+
+ const load = useCallback(() => {
+ authFetch(`/api/admin/recruits?page=${page}&size=10`)
+ .then(r => r.json()).then(setData);
+ }, [page]);
+
+ useEffect(() => { load(); }, [load]);
+
+ const openCreate = () => { setForm(EMPTY); setEditId(null); setModal(true); };
+ const openEdit = (r) => { setForm({ ...r, deadline: r.deadline || '' }); setEditId(r.id); setModal(true); };
+
+ const handleSave = async () => {
+ setSaving(true);
+ const url = editId ? `/api/admin/recruits/${editId}` : '/api/admin/recruits';
+ const res = await authFetch(url, { method: editId ? 'PUT' : 'POST', body: JSON.stringify(form) });
+ setSaving(false);
+ if (res.ok) { setModal(false); load(); showToast(editId ? '수정되었습니다.' : '등록되었습니다.'); }
+ else showToast('저장 실패', 'error');
+ };
+
+ const handleDelete = async (id) => {
+ if (!confirm('삭제하시겠습니까?')) return;
+ const res = await authFetch(`/api/admin/recruits/${id}`, { method: 'DELETE' });
+ if (res.ok) { load(); showToast('삭제되었습니다.'); }
+ };
+
+ const set = (k, v) => setForm(p => ({ ...p, [k]: v }));
+
+ return (
+ <>
+ {toast &&
}
+
+
+
+
전체 {data.totalElements}건
+
+
+
+
+
+
+
+
+ | No | 공고명 | 부서 | 유형 | 모집인원 | 마감일 | 상태 | 관리 |
+
+
+ {data.content.map((r, i) => (
+
+ | {data.totalElements - page * 10 - i} |
+ {r.title} |
+ {r.department || '-'} |
+ {r.jobType} |
+ {r.headcount}명 |
+ {r.deadline || '상시'} |
+ {r.active ? '진행중' : '마감'} |
+
+
+
+
+
+ |
+
+ ))}
+ {!data.content.length && (
+ |
+ )}
+
+
+
+
+ {data.totalPages > 1 && (
+
+
페이지 {page + 1} / {data.totalPages}
+
+
+ {Array.from({ length: data.totalPages }, (_, i) => (
+
+ ))}
+
+
+
+ )}
+
+
+ {modal && (
+
e.target === e.currentTarget && setModal(false)}>
+
+
+
{editId ? '채용공고 수정' : '채용공고 등록'}
+
+
+
+
+
+ set('title', e.target.value)} placeholder="예: 백엔드 개발자 (Java/Spring)" />
+
+
+
+
+ set('department', e.target.value)} placeholder="개발팀, 영업팀 등" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/workspace/zioinfo-web/frontend/src/pages/admin/AdminSettings.jsx b/workspace/zioinfo-web/frontend/src/pages/admin/AdminSettings.jsx
new file mode 100644
index 00000000..9cd26843
--- /dev/null
+++ b/workspace/zioinfo-web/frontend/src/pages/admin/AdminSettings.jsx
@@ -0,0 +1,91 @@
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+const token = () => localStorage.getItem('admin_token');
+
+export default function AdminSettings() {
+ const navigate = useNavigate();
+ const user = JSON.parse(localStorage.getItem('admin_user') || '{}');
+ const [form, setForm] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' });
+ const [msg, setMsg] = useState(null);
+ const [saving, setSaving] = useState(false);
+
+ const handleChange = async () => {
+ if (form.newPassword !== form.confirmPassword) {
+ setMsg({ text: '새 비밀번호가 일치하지 않습니다.', type: 'error' }); return;
+ }
+ if (form.newPassword.length < 8) {
+ setMsg({ text: '비밀번호는 8자 이상이어야 합니다.', type: 'error' }); return;
+ }
+ setSaving(true);
+ const res = await fetch('/api/admin/password', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token()}` },
+ body: JSON.stringify({ currentPassword: form.currentPassword, newPassword: form.newPassword }),
+ });
+ const data = await res.json();
+ setSaving(false);
+ if (res.ok) {
+ setMsg({ text: '비밀번호가 변경되었습니다. 다시 로그인해주세요.', type: 'success' });
+ setForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
+ setTimeout(() => {
+ localStorage.removeItem('admin_token');
+ navigate('/admin/login');
+ }, 2000);
+ } else {
+ setMsg({ text: data.message || '변경 실패', type: 'error' });
+ }
+ };
+
+ return (
+
+ {/* 계정 정보 */}
+
+
👤 계정 정보
+
+ {[['아이디', user.username], ['표시 이름', user.displayName || '-']].map(([l, v]) => (
+
+ {l}
+ {v}
+
+ ))}
+
+
+
+ {/* 비밀번호 변경 */}
+
+
🔒 비밀번호 변경
+ {msg && (
+
+ {msg.text}
+
+ )}
+
+
+ setForm(p => ({ ...p, currentPassword: e.target.value }))} />
+
+
+
+ setForm(p => ({ ...p, newPassword: e.target.value }))} />
+
+
+
+ setForm(p => ({ ...p, confirmPassword: e.target.value }))} />
+
+
+
+
+ );
+}
diff --git a/workspace/zioinfo-web/frontend/src/pages/admin/admin.css b/workspace/zioinfo-web/frontend/src/pages/admin/admin.css
new file mode 100644
index 00000000..f73fda5e
--- /dev/null
+++ b/workspace/zioinfo-web/frontend/src/pages/admin/admin.css
@@ -0,0 +1,188 @@
+/* ===== Admin System Styles ===== */
+:root {
+ --admin-sidebar-w: 220px;
+ --admin-bg: #f0f2f5;
+ --admin-sidebar-bg: #1a1d2e;
+ --admin-sidebar-hover: #2a2d3e;
+ --admin-accent: #4f6ef7;
+ --admin-accent-hover: #3a5be0;
+ --admin-text: #1e293b;
+ --admin-muted: #64748b;
+ --admin-border: #e2e8f0;
+ --admin-card: #ffffff;
+ --admin-danger: #ef4444;
+ --admin-success: #22c55e;
+ --admin-warning: #f59e0b;
+}
+
+/* Layout */
+.admin-wrap { display: flex; min-height: 100vh; background: var(--admin-bg); font-family: 'Pretendard', -apple-system, sans-serif; }
+
+/* Sidebar */
+.admin-sidebar {
+ width: var(--admin-sidebar-w);
+ background: var(--admin-sidebar-bg);
+ display: flex; flex-direction: column;
+ position: fixed; top: 0; left: 0; height: 100vh;
+ z-index: 100; transition: transform .25s;
+}
+.admin-sidebar-logo {
+ padding: 20px 20px 16px;
+ border-bottom: 1px solid rgba(255,255,255,.08);
+}
+.admin-sidebar-logo h2 { color: #fff; font-size: 15px; font-weight: 700; margin: 0; }
+.admin-sidebar-logo span { color: #7c85a8; font-size: 11px; }
+
+.admin-nav { flex: 1; overflow-y: auto; padding: 12px 0; }
+.admin-nav-section { padding: 12px 16px 4px; color: #7c85a8; font-size: 10px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; }
+.admin-nav a {
+ display: flex; align-items: center; gap: 10px;
+ padding: 9px 20px; color: #b0b7cc; text-decoration: none;
+ font-size: 13.5px; border-radius: 0; transition: all .15s;
+}
+.admin-nav a:hover { background: var(--admin-sidebar-hover); color: #fff; }
+.admin-nav a.active { background: var(--admin-accent); color: #fff; }
+.admin-nav a .nav-icon { width: 16px; text-align: center; flex-shrink: 0; }
+.admin-nav-badge { background: var(--admin-danger); color: #fff; font-size: 10px; padding: 1px 6px; border-radius: 10px; margin-left: auto; }
+
+.admin-sidebar-footer { padding: 16px 20px; border-top: 1px solid rgba(255,255,255,.08); }
+.admin-sidebar-footer button { width: 100%; background: transparent; border: 1px solid rgba(255,255,255,.15); color: #b0b7cc; padding: 8px 12px; border-radius: 6px; font-size: 13px; cursor: pointer; transition: all .15s; }
+.admin-sidebar-footer button:hover { background: rgba(255,255,255,.08); color: #fff; }
+
+/* Main */
+.admin-main { margin-left: var(--admin-sidebar-w); flex: 1; display: flex; flex-direction: column; min-height: 100vh; }
+.admin-topbar { background: var(--admin-card); border-bottom: 1px solid var(--admin-border); padding: 0 28px; height: 56px; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 50; }
+.admin-topbar h1 { font-size: 16px; font-weight: 600; color: var(--admin-text); margin: 0; }
+.admin-topbar-right { display: flex; align-items: center; gap: 12px; }
+.admin-user-badge { background: #e8ecff; color: var(--admin-accent); padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600; }
+
+.admin-content { padding: 28px; flex: 1; }
+
+/* Cards */
+.admin-card { background: var(--admin-card); border-radius: 10px; border: 1px solid var(--admin-border); padding: 20px; }
+.admin-card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
+.admin-card-header h3 { font-size: 14px; font-weight: 600; color: var(--admin-text); margin: 0; }
+
+/* Stat Cards */
+.admin-stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
+.stat-card { background: var(--admin-card); border-radius: 10px; padding: 20px; border: 1px solid var(--admin-border); display: flex; align-items: center; gap: 16px; }
+.stat-icon { width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
+.stat-icon.blue { background: #eff2ff; }
+.stat-icon.green { background: #f0fdf4; }
+.stat-icon.orange { background: #fff7ed; }
+.stat-icon.red { background: #fff1f2; }
+.stat-info h4 { font-size: 22px; font-weight: 700; color: var(--admin-text); margin: 0 0 2px; }
+.stat-info p { font-size: 12px; color: var(--admin-muted); margin: 0; }
+
+/* Table */
+.admin-table-wrap { overflow-x: auto; }
+.admin-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
+.admin-table th { background: #f8fafc; color: var(--admin-muted); font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; padding: 10px 14px; border-bottom: 1px solid var(--admin-border); text-align: left; white-space: nowrap; }
+.admin-table td { padding: 12px 14px; border-bottom: 1px solid #f1f5f9; color: var(--admin-text); vertical-align: middle; }
+.admin-table tr:last-child td { border-bottom: none; }
+.admin-table tr:hover td { background: #f8fafc; }
+.admin-table .truncate { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+
+/* Badges */
+.badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 20px; font-size: 11px; font-weight: 600; }
+.badge-green { background: #dcfce7; color: #16a34a; }
+.badge-red { background: #fee2e2; color: #dc2626; }
+.badge-blue { background: #dbeafe; color: #2563eb; }
+.badge-orange { background: #ffedd5; color: #ea580c; }
+.badge-gray { background: #f1f5f9; color: #64748b; }
+
+/* Buttons */
+.btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 7px; font-size: 13px; font-weight: 500; cursor: pointer; border: none; transition: all .15s; text-decoration: none; }
+.btn-primary { background: var(--admin-accent); color: #fff; }
+.btn-primary:hover { background: var(--admin-accent-hover); }
+.btn-outline { background: transparent; color: var(--admin-text); border: 1px solid var(--admin-border); }
+.btn-outline:hover { background: #f8fafc; }
+.btn-danger { background: transparent; color: var(--admin-danger); border: 1px solid #fecaca; }
+.btn-danger:hover { background: #fff1f2; }
+.btn-sm { padding: 5px 10px; font-size: 12px; }
+.btn-icon { padding: 6px; border-radius: 6px; }
+
+/* Action buttons row */
+.action-btns { display: flex; gap: 6px; align-items: center; }
+
+/* Toolbar */
+.admin-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; }
+.admin-toolbar-right { margin-left: auto; display: flex; gap: 8px; }
+.admin-search { position: relative; }
+.admin-search input { padding: 8px 12px 8px 34px; border: 1px solid var(--admin-border); border-radius: 7px; font-size: 13px; outline: none; width: 200px; background: #fff; color: var(--admin-text); }
+.admin-search input:focus { border-color: var(--admin-accent); }
+.admin-search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--admin-muted); font-size: 14px; }
+.admin-select { padding: 8px 12px; border: 1px solid var(--admin-border); border-radius: 7px; font-size: 13px; outline: none; background: #fff; color: var(--admin-text); cursor: pointer; }
+.admin-select:focus { border-color: var(--admin-accent); }
+
+/* Modal */
+.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.45); z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 16px; }
+.modal { background: var(--admin-card); border-radius: 12px; width: 100%; max-width: 640px; max-height: 90vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,.2); }
+.modal-header { padding: 20px 24px 16px; border-bottom: 1px solid var(--admin-border); display: flex; align-items: center; justify-content: space-between; }
+.modal-header h3 { font-size: 15px; font-weight: 600; margin: 0; }
+.modal-header button { background: none; border: none; cursor: pointer; color: var(--admin-muted); font-size: 18px; line-height: 1; padding: 4px; }
+.modal-body { padding: 24px; }
+.modal-footer { padding: 16px 24px; border-top: 1px solid var(--admin-border); display: flex; justify-content: flex-end; gap: 10px; }
+
+/* Form */
+.form-group { margin-bottom: 16px; }
+.form-group label { display: block; font-size: 12px; font-weight: 600; color: var(--admin-muted); text-transform: uppercase; letter-spacing: .04em; margin-bottom: 6px; }
+.form-control { width: 100%; padding: 9px 12px; border: 1px solid var(--admin-border); border-radius: 7px; font-size: 13.5px; color: var(--admin-text); outline: none; box-sizing: border-box; background: #fff; font-family: inherit; }
+.form-control:focus { border-color: var(--admin-accent); box-shadow: 0 0 0 3px rgba(79,110,247,.12); }
+textarea.form-control { resize: vertical; min-height: 100px; }
+.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
+.form-check { display: flex; align-items: center; gap: 8px; font-size: 13.5px; cursor: pointer; }
+.form-check input { width: 16px; height: 16px; cursor: pointer; accent-color: var(--admin-accent); }
+
+/* Pagination */
+.admin-pagination { display: flex; align-items: center; justify-content: space-between; margin-top: 16px; }
+.admin-pagination-info { font-size: 12px; color: var(--admin-muted); }
+.pagination-btns { display: flex; gap: 4px; }
+.pagination-btns button { padding: 5px 10px; border: 1px solid var(--admin-border); background: #fff; color: var(--admin-text); border-radius: 5px; font-size: 12px; cursor: pointer; transition: all .15s; }
+.pagination-btns button:hover:not(:disabled) { background: var(--admin-accent); color: #fff; border-color: var(--admin-accent); }
+.pagination-btns button.active { background: var(--admin-accent); color: #fff; border-color: var(--admin-accent); }
+.pagination-btns button:disabled { opacity: .4; cursor: not-allowed; }
+
+/* Login */
+.admin-login-page { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #1a1d2e 0%, #2a2d4e 100%); padding: 16px; }
+.admin-login-box { background: #fff; border-radius: 14px; padding: 40px 36px; width: 100%; max-width: 380px; box-shadow: 0 20px 60px rgba(0,0,0,.3); }
+.admin-login-box .login-logo { text-align: center; margin-bottom: 28px; }
+.admin-login-box .login-logo h1 { font-size: 22px; font-weight: 800; color: var(--admin-text); margin: 8px 0 4px; }
+.admin-login-box .login-logo p { font-size: 12px; color: var(--admin-muted); margin: 0; }
+.admin-login-box .login-badge { display: inline-block; background: #eff2ff; color: var(--admin-accent); padding: 2px 10px; border-radius: 20px; font-size: 11px; font-weight: 700; margin-bottom: 6px; }
+.login-input-group { margin-bottom: 14px; }
+.login-input-group label { display: block; font-size: 12px; font-weight: 600; color: var(--admin-muted); margin-bottom: 6px; }
+.login-input-group input { width: 100%; padding: 11px 14px; border: 1.5px solid var(--admin-border); border-radius: 8px; font-size: 14px; outline: none; box-sizing: border-box; transition: border-color .15s; }
+.login-input-group input:focus { border-color: var(--admin-accent); }
+.login-btn { width: 100%; padding: 12px; background: var(--admin-accent); color: #fff; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; margin-top: 6px; transition: background .15s; }
+.login-btn:hover { background: var(--admin-accent-hover); }
+.login-error { background: #fff1f2; color: var(--admin-danger); font-size: 12.5px; padding: 9px 12px; border-radius: 7px; margin-bottom: 14px; border: 1px solid #fecaca; }
+
+/* Dashboard recent list */
+.recent-list { list-style: none; padding: 0; margin: 0; }
+.recent-list li { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #f1f5f9; font-size: 13px; }
+.recent-list li:last-child { border-bottom: none; }
+.recent-list .rl-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--admin-accent); flex-shrink: 0; }
+.recent-list .rl-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--admin-text); }
+.recent-list .rl-meta { color: var(--admin-muted); font-size: 11px; white-space: nowrap; }
+
+/* Empty state */
+.empty-state { text-align: center; padding: 48px 0; color: var(--admin-muted); }
+.empty-state .empty-icon { font-size: 40px; margin-bottom: 12px; }
+.empty-state p { font-size: 13.5px; margin: 0; }
+
+/* Toast */
+.admin-toast { position: fixed; bottom: 24px; right: 24px; z-index: 9999; display: flex; flex-direction: column; gap: 8px; }
+.toast-item { background: #1e293b; color: #fff; padding: 12px 20px; border-radius: 8px; font-size: 13px; animation: slideUp .2s ease; box-shadow: 0 4px 16px rgba(0,0,0,.2); }
+.toast-item.success { border-left: 3px solid var(--admin-success); }
+.toast-item.error { border-left: 3px solid var(--admin-danger); }
+@keyframes slideUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
+
+/* Responsive */
+@media (max-width: 768px) {
+ .admin-sidebar { transform: translateX(-100%); }
+ .admin-sidebar.open { transform: translateX(0); }
+ .admin-main { margin-left: 0; }
+ .form-row { grid-template-columns: 1fr; }
+ .admin-stats { grid-template-columns: 1fr 1fr; }
+}