fix(ui): 메뉴 클릭 네비게이션 + 드롭다운 딜레이 추가

This commit is contained in:
zio 2026-06-01 01:55:51 +09:00
parent 02f9c55032
commit 72a05db7c3

View File

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