fix(ui): 메뉴 클릭 네비게이션 + 드롭다운 딜레이 추가
This commit is contained in:
parent
02f9c55032
commit
72a05db7c3
@ -1,198 +1,200 @@
|
||||
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="/zioinfo-logo-dark.png" alt="(주)지오정보기술 로고" height="40"
|
||||
onError={e => { e.target.src='/zioinfo-logo.png'; e.target.onerror = () => { 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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 closeTimer = React.useRef(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="/zioinfo-logo-dark.png" alt="(주)지오정보기술 로고" height="40"
|
||||
onError={e => { e.target.src='/zioinfo-logo.png'; e.target.onerror = () => { 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={() => { clearTimeout(closeTimer.current); setActiveMenu(menu.id); }}
|
||||
onMouseLeave={() => { closeTimer.current = setTimeout(() => setActiveMenu(null), 200); }}>
|
||||
<button className="nav-trigger" aria-haspopup="true"
|
||||
aria-expanded={activeMenu === menu.id}
|
||||
onClick={() => navigate(menu.children[0].path)}>
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user