fix(ui): 메뉴 클릭 네비게이션 + 드롭다운 딜레이 추가
This commit is contained in:
parent
02f9c55032
commit
72a05db7c3
@ -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>
|
||||||
}
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user