You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
468 lines
17 KiB
468 lines
17 KiB
import { useEffect, useRef, useState } from "react"; |
|
import { Link, NavLink } from "react-router-dom"; |
|
|
|
const menuData = [ |
|
{ |
|
key: "company", |
|
label: "Company", |
|
to: "/company", |
|
panelTitle: "PAL Networks", |
|
panelDesc: "회사 소개와 비전, 연혁, 파트너십 정보를 확인할 수 있습니다.", |
|
sections: [ |
|
{ |
|
title: "회사 소개", |
|
items: [ |
|
{ label: "회사소개", to: "/company/about", desc: "기업 철학과 핵심 가치" }, |
|
{ label: "연혁", to: "/company/history", desc: "주요 실적과 성장 과정" }, |
|
], |
|
}, |
|
{ |
|
title: "신뢰 정보", |
|
items: [ |
|
{ label: "고객 및 협력사", to: "/company/partners", desc: "주요 고객과 협력 네트워크" }, |
|
{ label: "찾아오시는 길", to: "/company/location", desc: "위치 및 연락처 안내" }, |
|
], |
|
}, |
|
], |
|
featured: { |
|
eyebrow: "About Us", |
|
title: "신뢰를 기반으로\n항공·플랫폼 기술을 확장합니다.", |
|
// text: "기업 소개 영역은 가볍게 보이지 않도록, 핵심 메시지와 신뢰 요소를 함께 노출하는 구성이 좋습니다.", |
|
cta: { label: "회사소개 보기", to: "/company" }, |
|
}, |
|
}, |
|
{ |
|
key: "uam", |
|
label: "UAM/UATM", |
|
to: "/uam", |
|
panelTitle: "UAM / UATM", |
|
panelDesc: "도심 항공 모빌리티와 통합 항공 교통 관리 기술을 소개합니다.", |
|
sections: [ |
|
{ |
|
title: "기술 소개", |
|
items: [ |
|
{ label: "UAM/UATM 소개", to: "/uam/intro", desc: "도심 항공 모빌리티 핵심 기술" }, |
|
{ label: "도입사례", to: "/uam/case", desc: "주요 도입 및 운영 사례" }, |
|
], |
|
}, |
|
], |
|
featured: { |
|
eyebrow: "Advanced Air Mobility", |
|
title: "안전한 하늘길,\n기술로 완성합니다.", |
|
text: "PAL Networks의 UAM·UATM 기술은 도심 상공의 안전 운항과 통합 관제를 실현합니다.", |
|
cta: { label: "UAM/UATM 보기", to: "/uam" }, |
|
}, |
|
}, |
|
{ |
|
key: "business", |
|
label: "Business", |
|
to: "/business", |
|
panelTitle: "Business Area", |
|
panelDesc: "구축부터 운영까지, PAL Networks의 종합 IT 서비스 역량을 소개합니다.", |
|
sections: [ |
|
{ |
|
title: "구축 · 개발", |
|
items: [ |
|
{ label: "System Integration", to: "/business/si", desc: "맞춤형 정보시스템 구축" }, |
|
{ label: "R&D", to: "/business/rnd", desc: "연구 개발 및 기술 고도화" }, |
|
], |
|
}, |
|
{ |
|
title: "운영 · 지원", |
|
items: [{ label: "운영 · 유지보수", to: "/business/maintenance", desc: "안정적인 시스템 운영과 사후 관리" }], |
|
}, |
|
], |
|
featured: { |
|
eyebrow: "Core Capability", |
|
title: "구축에서 운영까지,\n끝까지 책임지는 파트너.", |
|
text: "단순 납품이 아닌 장기 파트너십으로, 고객 시스템의 안정적 운영을 함께합니다.", |
|
cta: { label: "사업영역 보기", to: "/business" }, |
|
}, |
|
}, |
|
{ |
|
key: "solution", |
|
label: "Solution", |
|
to: "/solution", |
|
panelTitle: "Solution & Service", |
|
panelDesc: "산업별 솔루션과 서비스 포트폴리오를 확인하실 수 있습니다.", |
|
sections: [ |
|
{ |
|
title: "운영 솔루션", |
|
items: [ |
|
{ label: "비행상황관리 시스템", to: "/solution/flight-control", desc: "실시간 비행 상황 통합 관제" }, |
|
{ label: "IBE (Internet Booking Engine)", to: "/solution/ibe", desc: "항공 예약·발권 엔진" }, |
|
], |
|
}, |
|
{ |
|
title: "플랫폼 · 인프라", |
|
items: [ |
|
{ label: "스마트 관광 예약 플랫폼", to: "/solution/smart-tour", desc: "관광 예약 통합 운영 플랫폼" }, |
|
{ label: "KT G-cloud 인천총판", to: "/solution/kt-gcloud", desc: "공공 클라우드 인프라 공급" }, |
|
], |
|
}, |
|
], |
|
featured: { |
|
eyebrow: "Scalable Solutions", |
|
title: "검증된 솔루션으로\n비즈니스 가치를 만듭니다.", |
|
text: "운영 노하우가 축적된 자체 솔루션과 파트너십 기반 인프라를 함께 제공합니다.", |
|
cta: { label: "솔루션 보기", to: "/solution" }, |
|
}, |
|
}, |
|
{ |
|
key: "contact", |
|
label: "Contact Us", |
|
to: "/contact", |
|
panelTitle: "Contact Us", |
|
panelDesc: "프로젝트 문의와 인재 채용 정보를 확인하실 수 있습니다.", |
|
sections: [ |
|
{ |
|
title: "문의 및 채용", |
|
items: [ |
|
{ label: "문의하기", to: "/contact/inquiry", desc: "프로젝트 및 협업 문의" }, |
|
{ label: "채용정보", to: "/contact/recruit", desc: "함께할 동료를 찾습니다" }, |
|
], |
|
}, |
|
], |
|
featured: { |
|
eyebrow: "Get in Touch", |
|
title: "함께 만들어갈\n파트너를 기다립니다.", |
|
text: "프로젝트 협업이든 커리어든, 편하게 연락 주세요.", |
|
cta: { label: "문의 바로가기", to: "/contact/inquiry" }, |
|
}, |
|
}, |
|
]; |
|
export default function PalRenewalHeader() { |
|
const [activeMenu, setActiveMenu] = useState(null); |
|
const [isHeaderHover, setIsHeaderHover] = useState(false); |
|
const [isScrolled, setIsScrolled] = useState(false); |
|
const [isDarkHero, setIsDarkHero] = useState(false); |
|
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); |
|
const [mobileOpenKey, setMobileOpenKey] = useState(null); |
|
|
|
const closeTimer = useRef(null); |
|
const navRefs = useRef({}); |
|
const mobileFirstFocusableRef = useRef(null); |
|
|
|
const activeData = menuData.find((item) => item.key === activeMenu); |
|
const showPanel = Boolean(activeData && !activeData.simple && isHeaderHover); |
|
|
|
useEffect(() => { |
|
const updateHeaderState = () => { |
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || 0; |
|
const darkHeroActive = document.body.classList.contains("is-dark-hero"); |
|
|
|
setIsDarkHero(darkHeroActive); |
|
setIsScrolled(!darkHeroActive && scrollTop > 80); |
|
}; |
|
|
|
updateHeaderState(); |
|
|
|
const observer = new MutationObserver(() => { |
|
updateHeaderState(); |
|
}); |
|
|
|
observer.observe(document.body, { attributes: true, attributeFilter: ["class"] }); |
|
window.addEventListener("scroll", updateHeaderState, { passive: true }); |
|
|
|
return () => { |
|
observer.disconnect(); |
|
window.removeEventListener("scroll", updateHeaderState); |
|
}; |
|
}, []); |
|
|
|
useEffect(() => { |
|
return () => { |
|
if (closeTimer.current) clearTimeout(closeTimer.current); |
|
}; |
|
}, []); |
|
|
|
useEffect(() => { |
|
if (isMobileMenuOpen) { |
|
document.body.style.overflow = "hidden"; |
|
|
|
const timer = setTimeout(() => { |
|
mobileFirstFocusableRef.current?.focus(); |
|
}, 50); |
|
|
|
return () => { |
|
clearTimeout(timer); |
|
document.body.style.overflow = ""; |
|
}; |
|
} |
|
|
|
document.body.style.overflow = ""; |
|
|
|
return () => { |
|
document.body.style.overflow = ""; |
|
}; |
|
}, [isMobileMenuOpen]); |
|
|
|
const clearCloseTimer = () => { |
|
if (closeTimer.current) clearTimeout(closeTimer.current); |
|
}; |
|
|
|
const openMenu = (key) => { |
|
clearCloseTimer(); |
|
setIsHeaderHover(true); |
|
setActiveMenu(key); |
|
}; |
|
|
|
const scheduleClose = () => { |
|
clearCloseTimer(); |
|
closeTimer.current = setTimeout(() => { |
|
setIsHeaderHover(false); |
|
setActiveMenu(null); |
|
}, 120); |
|
}; |
|
|
|
const closeDesktopMenu = () => { |
|
setIsHeaderHover(false); |
|
setActiveMenu(null); |
|
}; |
|
|
|
const closeMobileMenu = () => { |
|
setIsMobileMenuOpen(false); |
|
setMobileOpenKey(null); |
|
}; |
|
|
|
const closeAllMenus = () => { |
|
closeDesktopMenu(); |
|
closeMobileMenu(); |
|
}; |
|
|
|
const toggleMobileMenu = () => { |
|
setIsMobileMenuOpen((prev) => !prev); |
|
}; |
|
|
|
const handleMobileAccordion = (key) => { |
|
setMobileOpenKey((prev) => (prev === key ? null : key)); |
|
}; |
|
|
|
const handleNavKeyDown = (e, index, item) => { |
|
if (e.key === "ArrowRight") { |
|
const nextItem = menuData[index + 1] || menuData[0]; |
|
navRefs.current[nextItem.key]?.focus(); |
|
} |
|
|
|
if (e.key === "ArrowLeft") { |
|
const prevItem = menuData[index - 1] || menuData[menuData.length - 1]; |
|
navRefs.current[prevItem.key]?.focus(); |
|
} |
|
|
|
if (e.key === "ArrowDown" && !item.simple) { |
|
e.preventDefault(); |
|
openMenu(item.key); |
|
} |
|
|
|
if (e.key === "Escape") { |
|
scheduleClose(); |
|
} |
|
}; |
|
|
|
const isActiveHeader = isScrolled || showPanel || isMobileMenuOpen; |
|
const logoSrc = isActiveHeader || !isDarkHero ? "./images/pal_logo.png" : "./images/pal_logo_wh.png"; |
|
|
|
return ( |
|
<> |
|
<header className={`pal-header ${isScrolled ? "is-scrolled" : ""} ${showPanel ? "is-open" : ""} ${isMobileMenuOpen ? "is-mobile-open" : ""}`} onMouseEnter={clearCloseTimer} onMouseLeave={scheduleClose}> |
|
<div className="pal-header-inner"> |
|
<h1 className="pal-header-logo"> |
|
<Link to="/Main" onClick={closeAllMenus}> |
|
<img src={logoSrc} alt="PAL Networks" /> |
|
</Link> |
|
</h1> |
|
|
|
<nav className="pal-gnb" aria-label="Primary Navigation"> |
|
<ul className="pal-gnb-depth1"> |
|
{menuData.map((item, index) => { |
|
const isActive = activeMenu === item.key; |
|
|
|
return ( |
|
<li |
|
className={`pal-gnb-item ${isActive ? "is-active" : ""}`} |
|
key={item.key} |
|
onMouseEnter={() => { |
|
if (!item.simple) { |
|
openMenu(item.key); |
|
} else { |
|
clearCloseTimer(); |
|
closeDesktopMenu(); |
|
} |
|
}} |
|
> |
|
{item.simple ? ( |
|
<NavLink |
|
to={item.to} |
|
className="pal-gnb-link" |
|
ref={(el) => { |
|
navRefs.current[item.key] = el; |
|
}} |
|
onClick={closeAllMenus} |
|
onFocus={() => { |
|
closeDesktopMenu(); |
|
}} |
|
onKeyDown={(e) => handleNavKeyDown(e, index, item)} |
|
> |
|
<span className="pal-gnb-link-text">{item.label}</span> |
|
<span className="pal-gnb-link-line"></span> |
|
</NavLink> |
|
) : ( |
|
<button |
|
type="button" |
|
className="pal-gnb-link" |
|
ref={(el) => { |
|
navRefs.current[item.key] = el; |
|
}} |
|
aria-expanded={isActive} |
|
aria-haspopup="true" |
|
onFocus={() => openMenu(item.key)} |
|
onClick={() => openMenu(item.key)} |
|
onKeyDown={(e) => handleNavKeyDown(e, index, item)} |
|
> |
|
<span className="pal-gnb-link-text">{item.label}</span> |
|
<span className="pal-gnb-link-line"></span> |
|
</button> |
|
)} |
|
</li> |
|
); |
|
})} |
|
</ul> |
|
</nav> |
|
|
|
<div className="pal-header-util"> |
|
<div className="pal-header-lang"> |
|
<button type="button" className="is-active"> |
|
KOR |
|
</button> |
|
<span className="pal-header-lang-divider">|</span> |
|
<button type="button">ENG</button> |
|
</div> |
|
|
|
<button type="button" className={`pal-header-hamburger ${isMobileMenuOpen ? "is-active" : ""}`} aria-label={isMobileMenuOpen ? "모바일 메뉴 닫기" : "모바일 메뉴 열기"} aria-expanded={isMobileMenuOpen} aria-controls="pal-mobile-menu" onClick={toggleMobileMenu}> |
|
<span></span> |
|
<span></span> |
|
<span></span> |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div |
|
className={`pal-mega-panel ${showPanel ? "is-visible" : ""}`} |
|
onMouseEnter={() => { |
|
clearCloseTimer(); |
|
setIsHeaderHover(true); |
|
}} |
|
onMouseLeave={scheduleClose} |
|
> |
|
{activeData && !activeData.simple && ( |
|
<div className="pal-mega-panel-inner"> |
|
<div className="pal-mega-panel-intro"> |
|
<span className="pal-mega-panel-eyebrow">{activeData.panelTitle}</span> |
|
<h2>{activeData.featured.title}</h2> |
|
<p>{activeData.featured.text}</p> |
|
<Link to={activeData.featured.cta.to} className="pal-mega-panel-cta" onClick={closeAllMenus}> |
|
{activeData.featured.cta.label} |
|
</Link> |
|
</div> |
|
|
|
<div className="pal-mega-panel-content"> |
|
<div className="pal-mega-panel-top"> |
|
<strong>{activeData.panelTitle}</strong> |
|
<p>{activeData.panelDesc}</p> |
|
</div> |
|
|
|
<div className="pal-mega-panel-grid"> |
|
{activeData.sections.map((section) => ( |
|
<div className="pal-mega-section" key={section.title}> |
|
<h3>{section.title}</h3> |
|
<ul> |
|
{section.items.map((item) => ( |
|
<li key={item.label}> |
|
<Link to={item.to} className="pal-mega-item" onClick={closeAllMenus}> |
|
<span className="pal-mega-item-title">{item.label}</span> |
|
<span className="pal-mega-item-desc">{item.desc}</span> |
|
</Link> |
|
</li> |
|
))} |
|
</ul> |
|
</div> |
|
))} |
|
</div> |
|
</div> |
|
</div> |
|
)} |
|
</div> |
|
</header> |
|
|
|
<button type="button" className={`pal-header-dim ${showPanel ? "is-visible" : ""}`} aria-label="메뉴 닫기" onClick={closeDesktopMenu}></button> |
|
|
|
<div className={`pal-mobile-dim ${isMobileMenuOpen ? "is-visible" : ""}`} onClick={closeAllMenus}></div> |
|
|
|
<aside id="pal-mobile-menu" className={`pal-mobile-menu ${isMobileMenuOpen ? "is-open" : ""}`} aria-hidden={!isMobileMenuOpen}> |
|
<div className="pal-mobile-menu-head"> |
|
<strong>MENU</strong> |
|
<button type="button" className="pal-mobile-menu-close" aria-label="모바일 메뉴 닫기" onClick={closeAllMenus} ref={mobileFirstFocusableRef}> |
|
<span></span> |
|
<span></span> |
|
</button> |
|
</div> |
|
|
|
<div className="pal-mobile-menu-body"> |
|
<ul className="pal-mobile-nav"> |
|
{menuData.map((menu) => { |
|
const isOpen = mobileOpenKey === menu.key; |
|
|
|
return ( |
|
<li className={`pal-mobile-nav-item ${isOpen ? "is-open" : ""}`} key={menu.key}> |
|
{menu.simple ? ( |
|
<Link to={menu.to} className="pal-mobile-nav-link" onClick={closeAllMenus}> |
|
<span>{menu.label}</span> |
|
</Link> |
|
) : ( |
|
<> |
|
<button type="button" className="pal-mobile-nav-toggle" onClick={() => handleMobileAccordion(menu.key)} aria-expanded={isOpen}> |
|
<span>{menu.label}</span> |
|
<i className="pal-mobile-nav-arrow"></i> |
|
</button> |
|
|
|
<div className="pal-mobile-submenu"> |
|
{menu.sections.map((section) => ( |
|
<div className="pal-mobile-submenu-group" key={section.title}> |
|
<h3>{section.title}</h3> |
|
<ul> |
|
{section.items.map((item) => ( |
|
<li key={item.label}> |
|
<Link to={item.to} className="pal-mobile-submenu-link" onClick={closeAllMenus}> |
|
<strong>{item.label}</strong> |
|
<p>{item.desc}</p> |
|
</Link> |
|
</li> |
|
))} |
|
</ul> |
|
</div> |
|
))} |
|
|
|
<Link to={menu.featured.cta.to} className="pal-mobile-featured-link" onClick={closeAllMenus}> |
|
<span>{menu.featured.eyebrow}</span> |
|
<strong>{menu.featured.cta.label}</strong> |
|
</Link> |
|
</div> |
|
</> |
|
)} |
|
</li> |
|
); |
|
})} |
|
</ul> |
|
|
|
<div className="pal-mobile-contact-box"> |
|
<p>프로젝트 문의 및 협업 상담이 필요하시면 연락해 주세요.</p> |
|
<Link to="/contact" className="pal-mobile-contact-link" onClick={closeAllMenus}> |
|
Contact Us |
|
</Link> |
|
</div> |
|
</div> |
|
</aside> |
|
</> |
|
); |
|
}
|
|
|