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

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>
</>
);
}