zioinfo-mail/workspace/zioinfo-web/frontend/src/pages/admin/AdminLayout.jsx
DESKTOP-TKLFCPR\ython 524235e8fe feat(homepage): 회원가입·로그인·SNS 로그인 + 관리자 회원관리
## 홈페이지 (프론트엔드)
- 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>
2026-05-31 11:49:21 +09:00

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