zioinfo-mail/workspace/zioinfo-web/frontend/src/components/layout/Header.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

199 lines
7.4 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import './Header.css';
const MENU = [
{
id: 'company', label: '회사소개',
children: [
{ label: 'CEO 인사말', path: '/company/greeting' },
{ label: '연혁', path: '/company/history' },
{ label: '조직도', path: '/company/organization' },
{ label: 'CI 소개', path: '/company/ci' },
{ label: '오시는 길', path: '/company/location' },
]
},
{
id: 'solution', label: '솔루션',
children: [
{ label: 'GUARDiA ITSM', path: '/solution/guardia', badge: 'NEW' },
{ label: 'ERP', path: '/solution/erp' },
{ label: 'CRM', path: '/solution/crm' },
{ label: 'BI', path: '/solution/bi' },
]
},
{
id: 'business', label: '사업실적',
children: [
{ label: '구축 레퍼런스', path: '/business/reference' },
{ label: '파트너', path: '/business/partner' },
]
},
{
id: 'support', label: '고객지원',
children: [
{ label: '공지사항', path: '/support/notice' },
{ label: 'FAQ', path: '/support/faq' },
{ label: '카탈로그', path: '/support/catalog' },
{ label: '문의하기', path: '/support/contact' },
]
},
{
id: 'recruit', label: '채용',
children: [
{ label: '채용공고', path: '/recruit/jobs' },
{ label: '복리후생', path: '/recruit/welfare' },
{ label: '지원하기', path: '/recruit/apply' },
]
},
{
id: 'news', label: '뉴스',
children: [
{ label: '뉴스룸', path: '/news/newsroom' },
{ label: '기술 블로그', path: '/news/blog' },
]
},
];
export default function Header() {
const [scrolled, setScrolled] = useState(false);
const [activeMenu, setActiveMenu] = useState(null);
const [mobileOpen, setMobileOpen] = useState(false);
const [member, setMember] = useState(null);
const location = useLocation();
const navigate = useNavigate();
// 로그인 상태 동기화
useEffect(() => {
const sync = () => {
const u = localStorage.getItem('member_user');
setMember(u ? JSON.parse(u) : null);
};
sync();
window.addEventListener('storage', sync);
return () => window.removeEventListener('storage', sync);
}, [location]);
const logout = () => {
localStorage.removeItem('member_token');
localStorage.removeItem('member_user');
setMember(null);
navigate('/');
};
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 60);
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
useEffect(() => {
setMobileOpen(false);
setActiveMenu(null);
}, [location]);
const isActive = (menu) =>
menu.children?.some(c => location.pathname.startsWith(c.path));
return (
<>
{/* 접근성 스킵 링크 */}
<a href="#main-content" className="skip-link">본문 바로가기</a>
<header className={`header ${scrolled ? 'scrolled' : ''} ${mobileOpen ? 'mobile-open' : ''}`}
role="banner">
<div className="header-inner container">
{/* 로고 */}
<Link to="/" className="logo" aria-label="(주)지오정보기술 홈으로">
<img src="/logo.png" alt="(주)지오정보기술 로고" height="40"
onError={e => { e.target.style.display='none'; e.target.nextSibling.style.display='flex'; }} />
<span className="logo-text" style={{display:'none'}}>
<strong>Zio</strong>Info
</span>
</Link>
{/* 데스크톱 메뉴 */}
<nav className="nav-desktop" role="navigation" aria-label="주요 메뉴">
{MENU.map(menu => (
<div key={menu.id}
className={`nav-item ${isActive(menu) ? 'active' : ''}`}
onMouseEnter={() => setActiveMenu(menu.id)}
onMouseLeave={() => setActiveMenu(null)}>
<button className="nav-trigger" aria-haspopup="true"
aria-expanded={activeMenu === menu.id}>
{menu.label}
</button>
{activeMenu === menu.id && (
<div className="dropdown" role="menu">
{menu.children.map(child => (
<Link key={child.path} to={child.path}
className={`dropdown-item ${location.pathname === child.path ? 'current' : ''}`}
role="menuitem">
{child.label}
{child.badge && <span className="badge badge-new">{child.badge}</span>}
</Link>
))}
</div>
)}
</div>
))}
</nav>
{/* 우측 버튼 영역 */}
<div style={{ display:'flex', alignItems:'center', gap:8 }}>
<Link to="/support/contact" className="btn btn-outline btn-sm">문의하기</Link>
{member ? (
<div style={{ display:'flex', alignItems:'center', gap:8 }}>
<span style={{ fontSize:13, color:'var(--gray-600)', maxWidth:100,
overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
{member.name}
</span>
<button onClick={logout}
style={{ padding:'6px 14px', background:'none', border:'1px solid #e2e8f0',
borderRadius:8, fontSize:12, color:'#64748b', cursor:'pointer' }}>
로그아웃
</button>
</div>
) : (
<Link to="/login" className="btn btn-primary btn-sm">로그인</Link>
)}
</div>
{/* 햄버거 (모바일) */}
<button className="hamburger" aria-label="모바일 메뉴"
aria-expanded={mobileOpen}
onClick={() => setMobileOpen(v => !v)}>
<span/><span/><span/>
</button>
</div>
{/* 모바일 메뉴 */}
{mobileOpen && (
<nav className="nav-mobile" role="navigation" aria-label="모바일 메뉴">
{MENU.map(menu => (
<details key={menu.id} className="mobile-group">
<summary className="mobile-group-header">{menu.label}</summary>
<div className="mobile-children">
{menu.children.map(child => (
<Link key={child.path} to={child.path} className="mobile-child">
{child.label}
{child.badge && <span className="badge badge-new">{child.badge}</span>}
</Link>
))}
</div>
</details>
))}
<div style={{ display:'flex', gap:8, margin:'16px' }}>
<Link to="/support/contact" className="btn btn-outline" style={{ flex:1 }}>문의하기</Link>
{member
? <button onClick={logout} className="btn btn-primary" style={{ flex:1 }}>로그아웃</button>
: <Link to="/login" className="btn btn-primary" style={{ flex:1 }}>로그인 / 가입</Link>
}
</div>
</nav>
)}
</header>
</>
);
}