## 홈페이지 (프론트엔드) - MemberLogin.jsx: 회원가입/로그인 통합 페이지 + 카카오·네이버·구글 SNS 버튼 - MemberAuth.css: 인증 페이지 공통 스타일 - hooks/useMemberAuth.jsx: 회원 인증 상태 훅 + MemberOnly 컴포넌트 (회원 전용 잠금) - Header.jsx: 로그인/회원가입 버튼 + 로그인 시 이름/로그아웃 표시 - Contact.jsx: 문의 상담 신청 → 회원 전용 (MemberOnly 적용) - App.jsx: /login, /register 라우트 추가 ## 관리자 (Admin) - AdminMember.jsx: 회원 목록/검색/상태변경/삭제 페이지 - AdminLayout.jsx: '회원 관리' 메뉴 추가 - App.jsx: /admin/members 라우트 추가 ## 백엔드 (Spring Boot) - Member.java: 회원 엔티티 (id/name/email/password/phone/company/role/active) - MemberRepository.java: 이메일 조회·중복확인·키워드 검색 - MemberController.java: 회원가입·이메일 중복확인·로그인·SNS 로그인·내 정보 CRUD - AdminController.java: 회원관리 API (목록/상세/상태변경/삭제) + 대시보드에 회원 수 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
3.7 KiB
JavaScript
111 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/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>
|
|
);
|
|
}
|