Browse Source

영문 번역

main
김지은 3 weeks ago
parent
commit
6e3140274f
  1. 95
      src/Router.jsx
  2. 88
      src/components/Footer.jsx
  3. 349
      src/components/Header.jsx
  4. 117
      src/components/PrivacyModal.jsx
  5. 86
      src/components/SubHero.jsx
  6. 780
      src/components/main/MainContact.jsx
  7. 87
      src/components/main/MainNews.jsx
  8. 306
      src/components/main/MainSolution.jsx
  9. 67
      src/components/main/MainUam.jsx
  10. 284
      src/components/main/MainUtm.jsx
  11. 12
      src/components/main/MainVisual.jsx
  12. 41
      src/context/LanguageContext.jsx
  13. 23
      src/css/common.css
  14. 1
      src/css/main.css
  15. 3
      src/main.jsx
  16. 305
      src/pages/business/MaintenancePage.jsx
  17. 358
      src/pages/business/RndPage.jsx
  18. 406
      src/pages/business/SiPage.jsx
  19. 298
      src/pages/company/AboutPage.jsx
  20. 205
      src/pages/company/CertPage.jsx
  21. 109
      src/pages/company/HistoryPage.jsx
  22. 290
      src/pages/company/LocationPage.jsx
  23. 146
      src/pages/company/PartnersPage.jsx
  24. 107
      src/pages/contact/InquiryPage.jsx
  25. 87
      src/pages/contact/RecruitPage.jsx
  26. 435
      src/pages/solution/FlightControlPage.jsx
  27. 409
      src/pages/solution/IbePage.jsx
  28. 44
      src/pages/utm/CasePage.jsx
  29. 906
      src/pages/utm/IntroPage.jsx

95
src/Router.jsx

@ -32,6 +32,45 @@ import SolutionKtGcloudPage from "./pages/solution/KtGcloudPage";
import ContactInquiryPage from "./pages/contact/InquiryPage"; import ContactInquiryPage from "./pages/contact/InquiryPage";
import ContactRecruitPage from "./pages/contact/RecruitPage"; import ContactRecruitPage from "./pages/contact/RecruitPage";
// (ko ). renderSubRoutes() "/en" prefix .
const subRouteList = [
{ path: "/company", redirectTo: "/company/about" },
{ path: "/company/about", element: <CompanyAboutPage /> },
{ path: "/company/cert", element: <CompanyCertPage /> },
{ path: "/company/history", element: <CompanyHistoryPage /> },
{ path: "/company/partners", element: <CompanyPartnersPage /> },
{ path: "/company/location", element: <CompanyLocationPage /> },
{ path: "/utm", redirectTo: "/utm/intro" },
{ path: "/utm/intro", element: <UTMIntroPage /> },
{ path: "/utm/case", element: <UTMCasePage /> },
{ path: "/business", redirectTo: "/business/si" },
{ path: "/business/si", element: <BusinessSiPage /> },
{ path: "/business/rnd", element: <BusinessRndPage /> },
{ path: "/business/maintenance", element: <BusinessMaintenancePage /> },
{ path: "/solution", redirectTo: "/solution/flight-control" },
{ path: "/solution/flight-control", element: <SolutionFlightControlPage /> },
{ path: "/solution/ibe", element: <SolutionIbePage /> },
{ path: "/solution/smart-tour", element: <SolutionSmartTourPage /> },
{ path: "/solution/kt-gcloud", element: <SolutionKtGcloudPage /> },
{ path: "/contact", redirectTo: "/contact/inquiry" },
{ path: "/contact/inquiry", element: <ContactInquiryPage /> },
{ path: "/contact/recruit", element: <ContactRecruitPage /> },
];
function renderSubRoutes(prefix = "") {
return subRouteList.map(({ path, element, redirectTo }) => {
const fullPath = prefix + path;
if (redirectTo) {
return <Route key={fullPath} path={fullPath} element={<Navigate to={prefix + redirectTo} replace />} />;
}
return <Route key={fullPath} path={fullPath} element={element} />;
});
}
function Router() { function Router() {
return ( return (
<Routes> <Routes>
@ -41,61 +80,13 @@ function Router() {
{/* 메인 페이지 */} {/* 메인 페이지 */}
<Route element={<MainLayout />}> <Route element={<MainLayout />}>
<Route path="/main" element={<MainPage />} /> <Route path="/main" element={<MainPage />} />
<Route path="/en/main" element={<MainPage />} />
</Route> </Route>
{/* 서브 페이지 */} {/* 서브 페이지 (ko + en) */}
<Route element={<SubLayout />}> <Route element={<SubLayout />}>
{/* Company */} {renderSubRoutes()}
<Route {renderSubRoutes("/en")}
path="/company"
element={<Navigate to="/company/about" replace />}
/>
<Route path="/company/about" element={<CompanyAboutPage />} />
<Route path="/company/cert" element={<CompanyCertPage />} />
<Route path="/company/history" element={<CompanyHistoryPage />} />
<Route path="/company/partners" element={<CompanyPartnersPage />} />
<Route path="/company/location" element={<CompanyLocationPage />} />
{/* UTM/UATM */}
<Route path="/utm" element={<Navigate to="/utm/intro" replace />} />
<Route path="/utm/intro" element={<UTMIntroPage />} />
<Route path="/utm/case" element={<UTMCasePage />} />
{/* Business */}
<Route
path="/business"
element={<Navigate to="/business/si" replace />}
/>
<Route path="/business/si" element={<BusinessSiPage />} />
<Route path="/business/rnd" element={<BusinessRndPage />} />
<Route
path="/business/maintenance"
element={<BusinessMaintenancePage />}
/>
{/* Solution */}
<Route
path="/solution"
element={<Navigate to="/solution/flight-control" replace />}
/>
<Route
path="/solution/flight-control"
element={<SolutionFlightControlPage />}
/>
<Route path="/solution/ibe" element={<SolutionIbePage />} />
<Route
path="/solution/smart-tour"
element={<SolutionSmartTourPage />}
/>
<Route path="/solution/kt-gcloud" element={<SolutionKtGcloudPage />} />
{/* Contact Us */}
<Route
path="/contact"
element={<Navigate to="/contact/inquiry" replace />}
/>
<Route path="/contact/inquiry" element={<ContactInquiryPage />} />
<Route path="/contact/recruit" element={<ContactRecruitPage />} />
</Route> </Route>
{/* 404: 잘못된 경로는 메인으로 */} {/* 404: 잘못된 경로는 메인으로 */}

88
src/components/Footer.jsx

@ -1,10 +1,12 @@
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useLanguage } from "../context/LanguageContext";
const footerNav = [ const footerNavKo = [
{ {
title: "COMPANY", title: "COMPANY",
items: [ items: [
{ label: "회사소개", to: "/company/about" }, { label: "회사소개", to: "/company/about" },
{ label: "인증 및 특허현황", to: "/company/cert" },
{ label: "연혁", to: "/company/history" }, { label: "연혁", to: "/company/history" },
{ label: "고객 및 협력사", to: "/company/partners" }, { label: "고객 및 협력사", to: "/company/partners" },
{ label: "찾아오시는 길", to: "/company/location" }, { label: "찾아오시는 길", to: "/company/location" },
@ -13,8 +15,8 @@ const footerNav = [
{ {
title: "UTM/UATM", title: "UTM/UATM",
items: [ items: [
{ label: "소개", to: "/UTM/intro" }, { label: "소개", to: "/utm/intro" },
{ label: "도입사례", to: "/UTM/case" }, { label: "도입사례", to: "/utm/case" },
], ],
}, },
{ {
@ -43,13 +45,74 @@ const footerNav = [
}, },
]; ];
const footerNavEn = [
{
title: "COMPANY",
items: [
{ label: "About Us", to: "/company/about" },
{ label: "Certifications & Patents", to: "/company/cert" },
{ label: "History", to: "/company/history" },
{ label: "Clients & Partners", to: "/company/partners" },
{ label: "Location", to: "/company/location" },
],
},
{
title: "UTM/UATM",
items: [
{ label: "Overview", to: "/utm/intro" },
{ label: "Case Studies", to: "/utm/case" },
],
},
{
title: "BUSINESS",
items: [
{ label: "System Integration", to: "/business/si" },
{ label: "R&D", to: "/business/rnd" },
{ label: "Maintenance", to: "/business/maintenance" },
],
},
{
title: "SOLUTION",
items: [
{ label: "Flight Control System", to: "/solution/flight-control" },
{ label: "IBE", to: "/solution/ibe" },
// { label: "Smart Tourism Booking", to: "/solution/smart-tour" },
// { label: "KT G-cloud Incheon Distributor", to: "/solution/kt-gcloud" },
],
},
{
title: "CONTACT",
items: [
{ label: "Inquiry", to: "/contact/inquiry" },
{ label: "Careers", to: "/contact/recruit" },
],
},
];
const INFO_TEXT = {
ko: {
ceo: "대표. 최현식",
bizNo: "사업자등록번호. 393-81-00110",
address: "인천광역시 서구 로봇랜드로 155-11 로봇랜드 14층 1401~2호",
},
en: {
ceo: "CEO. Choi Hyun-sik",
bizNo: "Biz. Reg. No. 393-81-00110",
address: "14F, 1401-2, 155-11 Robot Land-ro, Seo-gu, Incheon, Republic of Korea",
},
};
function Footer() { function Footer() {
const { lang, withLang } = useLanguage();
const footerNav = lang === "en" ? footerNavEn : footerNavKo;
const info = INFO_TEXT[lang];
return ( return (
<footer className="site-footer"> <footer className="site-footer">
<div className="footer-inner"> <div className="footer-inner">
<div className="footer-top"> <div className="footer-top">
<div className="footer-brand"> <div className="footer-brand">
<Link to="/main" className="footer-logo"> <Link to={withLang("/main")} className="footer-logo">
<img src="./images/pal_logo_wh.png" alt="PAL Networks" /> <img src="./images/pal_logo_wh.png" alt="PAL Networks" />
</Link> </Link>
</div> </div>
@ -61,7 +124,7 @@ function Footer() {
<ul> <ul>
{group.items.map((item) => ( {group.items.map((item) => (
<li key={item.label}> <li key={item.label}>
<Link to={item.to}>{item.label}</Link> <Link to={withLang(item.to)}>{item.label}</Link>
</li> </li>
))} ))}
</ul> </ul>
@ -75,14 +138,12 @@ function Footer() {
<p> <p>
<span className="info-item strong">() PALNETWORKS</span> <span className="info-item strong">() PALNETWORKS</span>
<span className="sep">|</span> <span className="sep">|</span>
<span className="info-item">대표. 최현식</span> <span className="info-item">{info.ceo}</span>
<span className="sep">|</span> <span className="sep">|</span>
<span className="info-item">사업자등록번호. 393-81-00110</span> <span className="info-item">{info.bizNo}</span>
</p> </p>
<p> <p>
<span className="info-item"> <span className="info-item">{info.address}</span>
인천광역시 서구 로봇랜드로 155-11 로봇랜드 14 1401~2
</span>
</p> </p>
<p> <p>
<span className="info-item"> <span className="info-item">
@ -94,17 +155,14 @@ function Footer() {
</span> </span>
<span className="sep">|</span> <span className="sep">|</span>
<span className="info-item"> <span className="info-item">
<span className="strong">E-mail.</span>{" "} <span className="strong">E-mail.</span> <a href="mailto:help@palnet.co.kr">help@palnet.co.kr</a>
<a href="mailto:help@palnet.co.kr">help@palnet.co.kr</a>
</span> </span>
</p> </p>
</div> </div>
</div> </div>
<div className="footer-bot"> <div className="footer-bot">
<p className="footer-copy"> <p className="footer-copy">Copyright © () PALNETWORKS. All rights reserved.</p>
Copyright © () PALNETWORKS. All rights reserved.
</p>
<ul className="footer-policy"> <ul className="footer-policy">
<li> <li>
<a href="#none">Privacy Policy</a> <a href="#none">Privacy Policy</a>

349
src/components/Header.jsx

@ -1,7 +1,8 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Link, NavLink } from "react-router-dom"; import { Link, NavLink } from "react-router-dom";
import { useLanguage } from "../context/LanguageContext";
const menuData = [ const menuDataKo = [
{ {
key: "company", key: "company",
label: "Company", label: "Company",
@ -87,8 +88,7 @@ const menuData = [
label: "Business", label: "Business",
to: "/business", to: "/business",
panelTitle: "Business Area", panelTitle: "Business Area",
panelDesc: panelDesc: "구축부터 운영까지, PAL Networks의 종합 IT 서비스 역량을 소개합니다.",
"구축부터 운영까지, PAL Networks의 종합 IT 서비스 역량을 소개합니다.",
sections: [ sections: [
{ {
title: "구축 · 개발", title: "구축 · 개발",
@ -191,7 +191,192 @@ const menuData = [
}, },
}, },
]; ];
const menuDataEn = [
{
key: "company",
label: "Company",
to: "/company",
panelTitle: "PAL Networks",
panelDesc: "Learn about our company, vision, history, and partnerships.",
sections: [
{
title: "About Us",
items: [
{
label: "About Us",
to: "/company/about",
desc: "Corporate philosophy and core values",
},
{
label: "Certifications & Patents",
to: "/company/cert",
desc: "Certifications and patents held",
},
{
label: "History",
to: "/company/history",
desc: "Key milestones and growth",
},
],
},
{
title: "Trust & Credibility",
items: [
{
label: "Clients & Partners",
to: "/company/partners",
desc: "Key clients and partnership network",
},
{
label: "Location",
to: "/company/location",
desc: "Location and contact information",
},
],
},
],
featured: {
eyebrow: "About Us",
title: "Expanding aviation and platform\ntechnology built on trust.",
cta: { label: "View Company", to: "/company" },
},
},
{
key: "utm",
label: "UTM/UATM",
to: "/utm",
panelTitle: "UTM / UATM",
panelDesc: "Introducing our urban air mobility and integrated air traffic management technology.",
sections: [
{
title: "Technology",
items: [
{
label: "UTM/UATM Overview",
to: "/utm/intro",
desc: "Core technology for urban air mobility",
},
{
label: "Case Studies",
to: "/utm/case",
desc: "Key deployment and operational cases",
},
],
},
],
featured: {
eyebrow: "Advanced Air Mobility",
title: "Safer skies,\nrealized through technology.",
text: "PAL Networks' UTM/UATM technology enables safe flight operations and integrated air traffic control over cities.",
cta: { label: "View UTM/UATM", to: "/utm" },
},
},
{
key: "business",
label: "Business",
to: "/business",
panelTitle: "Business Area",
panelDesc: "From implementation to operation, our end-to-end IT service capabilities.",
sections: [
{
title: "Development",
items: [
{
label: "System Integration",
to: "/business/si",
desc: "Custom information systems",
},
{
label: "R&D",
to: "/business/rnd",
desc: "Research and technology advancement",
},
],
},
{
title: "Operations & Support",
items: [
{
label: "Maintenance",
to: "/business/maintenance",
desc: "Stable system operation and after-service",
},
],
},
],
featured: {
eyebrow: "Core Capability",
title: "A partner responsible\nfrom build to operation.",
text: "More than delivery, we build long-term partnerships for stable system operations.",
cta: { label: "View Business", to: "/business" },
},
},
{
key: "solution",
label: "Solution",
to: "/solution",
panelTitle: "Solution & Service",
panelDesc: "Explore our industry-specific solutions and service portfolio.",
sections: [
{
title: "Operations Solutions",
items: [
{
label: "Flight Control System",
to: "/solution/flight-control",
desc: "Real-time integrated flight operations control",
},
{
label: "IBE (Internet Booking Engine)",
to: "/solution/ibe",
desc: "Airline reservation and ticketing engine",
},
],
},
],
featured: {
eyebrow: "Scalable Solutions",
title: "Proven solutions,\nreal business value.",
text: "We provide proprietary solutions built on operational know-how, together with partner-based infrastructure.",
cta: { label: "View Solutions", to: "/solution" },
},
},
{
key: "contact",
label: "Contact Us",
to: "/contact",
panelTitle: "Contact Us",
panelDesc: "Get in touch for project inquiries or career opportunities.",
sections: [
{
title: "Contact & Careers",
items: [
{
label: "Inquiry",
to: "/contact/inquiry",
desc: "Project and business inquiries",
},
{
label: "Careers",
to: "/contact/recruit",
desc: "Join our team",
},
],
},
],
featured: {
eyebrow: "Get in Touch",
title: "We're looking for partners\nto build the future with.",
text: "Whether it's a project collaboration or a career opportunity, feel free to reach out.",
cta: { label: "Contact Us", to: "/contact/inquiry" },
},
},
];
export default function PalRenewalHeader() { export default function PalRenewalHeader() {
const { lang, withLang, toggleLang } = useLanguage();
const menuData = lang === "en" ? menuDataEn : menuDataKo;
const [activeMenu, setActiveMenu] = useState(null); const [activeMenu, setActiveMenu] = useState(null);
const [isHeaderHover, setIsHeaderHover] = useState(false); const [isHeaderHover, setIsHeaderHover] = useState(false);
const [isScrolled, setIsScrolled] = useState(false); const [isScrolled, setIsScrolled] = useState(false);
@ -209,8 +394,7 @@ export default function PalRenewalHeader() {
useEffect(() => { useEffect(() => {
const updateHeaderState = () => { const updateHeaderState = () => {
const scrollTop = const scrollTop = window.pageYOffset || document.documentElement.scrollTop || 0;
window.pageYOffset || document.documentElement.scrollTop || 0;
const darkHeroActive = document.body.classList.contains("is-dark-hero"); const darkHeroActive = document.body.classList.contains("is-dark-hero");
setIsDarkHero(darkHeroActive); setIsDarkHero(darkHeroActive);
@ -325,21 +509,14 @@ export default function PalRenewalHeader() {
}; };
const isActiveHeader = isScrolled || showPanel || isMobileMenuOpen; const isActiveHeader = isScrolled || showPanel || isMobileMenuOpen;
const logoSrc = const logoSrc = isActiveHeader || !isDarkHero ? "./images/pal_logo.png" : "./images/pal_logo_wh.png";
isActiveHeader || !isDarkHero
? "./images/pal_logo.png"
: "./images/pal_logo_wh.png";
return ( return (
<> <>
<header <header className={`pal-header ${isScrolled ? "is-scrolled" : ""} ${showPanel ? "is-open" : ""} ${isMobileMenuOpen ? "is-mobile-open" : ""}`} onMouseEnter={clearCloseTimer} onMouseLeave={scheduleClose}>
className={`pal-header ${isScrolled ? "is-scrolled" : ""} ${showPanel ? "is-open" : ""} ${isMobileMenuOpen ? "is-mobile-open" : ""}`}
onMouseEnter={clearCloseTimer}
onMouseLeave={scheduleClose}
>
<div className="pal-header-inner"> <div className="pal-header-inner">
<h1 className="pal-header-logo"> <h1 className="pal-header-logo">
<Link to="/main" onClick={closeAllMenus}> <Link to={withLang("/main")} onClick={closeAllMenus}>
<img src={logoSrc} alt="PAL Networks" /> <img src={logoSrc} alt="PAL Networks" />
</Link> </Link>
</h1> </h1>
@ -364,7 +541,7 @@ export default function PalRenewalHeader() {
> >
{item.simple ? ( {item.simple ? (
<NavLink <NavLink
to={item.to} to={withLang(item.to)}
className="pal-gnb-link" className="pal-gnb-link"
ref={(el) => { ref={(el) => {
navRefs.current[item.key] = el; navRefs.current[item.key] = el;
@ -402,37 +579,16 @@ export default function PalRenewalHeader() {
</nav> </nav>
<div className="pal-header-util"> <div className="pal-header-util">
<div <div className="pal-header-lang" role="group" aria-label="언어 선택">
className="pal-header-lang" <button type="button" className={`pal-header-lang-btn ${lang === "ko" ? "is-active" : ""}`} aria-pressed={lang === "ko"} onClick={() => lang !== "ko" && toggleLang()}>
role="group"
aria-label="언어 선택"
>
<button
type="button"
className="pal-header-lang-btn is-active"
aria-pressed="true"
>
KOR KOR
</button> </button>
<button <button type="button" className={`pal-header-lang-btn ${lang === "en" ? "is-active" : ""}`} aria-pressed={lang === "en"} onClick={() => lang !== "en" && toggleLang()}>
type="button"
className="pal-header-lang-btn"
aria-pressed="false"
>
ENG ENG
</button> </button>
</div> </div>
<button <button type="button" className={`pal-header-hamburger ${isMobileMenuOpen ? "is-active" : ""}`} aria-label={isMobileMenuOpen ? "모바일 메뉴 닫기" : "모바일 메뉴 열기"} aria-expanded={isMobileMenuOpen} aria-controls="pal-mobile-menu" onClick={toggleMobileMenu}>
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> <span></span>
<span></span> <span></span>
@ -451,16 +607,10 @@ export default function PalRenewalHeader() {
{activeData && !activeData.simple && ( {activeData && !activeData.simple && (
<div className="pal-mega-panel-inner"> <div className="pal-mega-panel-inner">
<div className="pal-mega-panel-intro"> <div className="pal-mega-panel-intro">
<span className="pal-mega-panel-eyebrow"> <span className="pal-mega-panel-eyebrow">{activeData.panelTitle}</span>
{activeData.panelTitle}
</span>
<h2>{activeData.featured.title}</h2> <h2>{activeData.featured.title}</h2>
<p>{activeData.featured.text}</p> <p>{activeData.featured.text}</p>
<Link <Link to={withLang(activeData.featured.cta.to)} className="pal-mega-panel-cta" onClick={closeAllMenus}>
to={activeData.featured.cta.to}
className="pal-mega-panel-cta"
onClick={closeAllMenus}
>
{activeData.featured.cta.label} {activeData.featured.cta.label}
</Link> </Link>
</div> </div>
@ -478,17 +628,9 @@ export default function PalRenewalHeader() {
<ul> <ul>
{section.items.map((item) => ( {section.items.map((item) => (
<li key={item.label}> <li key={item.label}>
<Link <Link to={withLang(item.to)} className="pal-mega-item" onClick={closeAllMenus}>
to={item.to} <span className="pal-mega-item-title">{item.label}</span>
className="pal-mega-item" <span className="pal-mega-item-desc">{item.desc}</span>
onClick={closeAllMenus}
>
<span className="pal-mega-item-title">
{item.label}
</span>
<span className="pal-mega-item-desc">
{item.desc}
</span>
</Link> </Link>
</li> </li>
))} ))}
@ -502,32 +644,14 @@ export default function PalRenewalHeader() {
</div> </div>
</header> </header>
<button <button type="button" className={`pal-header-dim ${showPanel ? "is-visible" : ""}`} aria-label="메뉴 닫기" onClick={closeDesktopMenu}></button>
type="button"
className={`pal-header-dim ${showPanel ? "is-visible" : ""}`}
aria-label="메뉴 닫기"
onClick={closeDesktopMenu}
></button>
<div <div className={`pal-mobile-dim ${isMobileMenuOpen ? "is-visible" : ""}`} onClick={closeAllMenus}></div>
className={`pal-mobile-dim ${isMobileMenuOpen ? "is-visible" : ""}`}
onClick={closeAllMenus}
></div>
<aside <aside id="pal-mobile-menu" className={`pal-mobile-menu ${isMobileMenuOpen ? "is-open" : ""}`} aria-hidden={!isMobileMenuOpen}>
id="pal-mobile-menu"
className={`pal-mobile-menu ${isMobileMenuOpen ? "is-open" : ""}`}
aria-hidden={!isMobileMenuOpen}
>
<div className="pal-mobile-menu-head"> <div className="pal-mobile-menu-head">
<strong>MENU</strong> <strong>MENU</strong>
<button <button type="button" className="pal-mobile-menu-close" aria-label="모바일 메뉴 닫기" onClick={closeAllMenus} ref={mobileFirstFocusableRef}>
type="button"
className="pal-mobile-menu-close"
aria-label="모바일 메뉴 닫기"
onClick={closeAllMenus}
ref={mobileFirstFocusableRef}
>
<span></span> <span></span>
<span></span> <span></span>
</button> </button>
@ -539,45 +663,26 @@ export default function PalRenewalHeader() {
const isOpen = mobileOpenKey === menu.key; const isOpen = mobileOpenKey === menu.key;
return ( return (
<li <li className={`pal-mobile-nav-item ${isOpen ? "is-open" : ""}`} key={menu.key}>
className={`pal-mobile-nav-item ${isOpen ? "is-open" : ""}`}
key={menu.key}
>
{menu.simple ? ( {menu.simple ? (
<Link <Link to={withLang(menu.to)} className="pal-mobile-nav-link" onClick={closeAllMenus}>
to={menu.to}
className="pal-mobile-nav-link"
onClick={closeAllMenus}
>
<span>{menu.label}</span> <span>{menu.label}</span>
</Link> </Link>
) : ( ) : (
<> <>
<button <button type="button" className="pal-mobile-nav-toggle" onClick={() => handleMobileAccordion(menu.key)} aria-expanded={isOpen}>
type="button"
className="pal-mobile-nav-toggle"
onClick={() => handleMobileAccordion(menu.key)}
aria-expanded={isOpen}
>
<span>{menu.label}</span> <span>{menu.label}</span>
<i className="pal-mobile-nav-arrow"></i> <i className="pal-mobile-nav-arrow"></i>
</button> </button>
<div className="pal-mobile-submenu"> <div className="pal-mobile-submenu">
{menu.sections.map((section) => ( {menu.sections.map((section) => (
<div <div className="pal-mobile-submenu-group" key={section.title}>
className="pal-mobile-submenu-group"
key={section.title}
>
<h3>{section.title}</h3> <h3>{section.title}</h3>
<ul> <ul>
{section.items.map((item) => ( {section.items.map((item) => (
<li key={item.label}> <li key={item.label}>
<Link <Link to={withLang(item.to)} className="pal-mobile-submenu-link" onClick={closeAllMenus}>
to={item.to}
className="pal-mobile-submenu-link"
onClick={closeAllMenus}
>
<strong>{item.label}</strong> <strong>{item.label}</strong>
<p>{item.desc}</p> <p>{item.desc}</p>
</Link> </Link>
@ -587,11 +692,7 @@ export default function PalRenewalHeader() {
</div> </div>
))} ))}
<Link <Link to={withLang(menu.featured.cta.to)} className="pal-mobile-featured-link" onClick={closeAllMenus}>
to={menu.featured.cta.to}
className="pal-mobile-featured-link"
onClick={closeAllMenus}
>
<span>{menu.featured.eyebrow}</span> <span>{menu.featured.eyebrow}</span>
<strong>{menu.featured.cta.label}</strong> <strong>{menu.featured.cta.label}</strong>
</Link> </Link>
@ -605,34 +706,18 @@ export default function PalRenewalHeader() {
{/* 모바일 언어 토글 */} {/* 모바일 언어 토글 */}
<div className="pal-mobile-lang"> <div className="pal-mobile-lang">
<span className="pal-mobile-lang-label">Language</span> <span className="pal-mobile-lang-label">Language</span>
<div <div className="pal-mobile-lang-toggle" role="group" aria-label="언어 선택">
className="pal-mobile-lang-toggle" <button type="button" className={`pal-mobile-lang-btn ${lang === "ko" ? "is-active" : ""}`} aria-pressed={lang === "ko"} onClick={() => lang !== "ko" && toggleLang()}>
role="group"
aria-label="언어 선택"
>
<button
type="button"
className="pal-mobile-lang-btn is-active"
aria-pressed="true"
>
KOR KOR
</button> </button>
<button <button type="button" className={`pal-mobile-lang-btn ${lang === "en" ? "is-active" : ""}`} aria-pressed={lang === "en"} onClick={() => lang !== "en" && toggleLang()}>
type="button"
className="pal-mobile-lang-btn"
aria-pressed="false"
>
ENG ENG
</button> </button>
</div> </div>
</div> </div>
<div className="pal-mobile-contact-box"> <div className="pal-mobile-contact-box">
<p>프로젝트 문의 협업 상담이 필요하시면 연락해 주세요.</p> <p>{lang === "en" ? "Reach out for project inquiries or collaboration." : "프로젝트 문의 및 협업 상담이 필요하시면 연락해 주세요."}</p>
<Link <Link to={withLang("/contact")} className="pal-mobile-contact-link" onClick={closeAllMenus}>
to="/contact"
className="pal-mobile-contact-link"
onClick={closeAllMenus}
>
Contact Us Contact Us
</Link> </Link>
</div> </div>

117
src/components/PrivacyModal.jsx

@ -1,25 +1,11 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useLanguage } from "../context/LanguageContext";
export default function PrivacyModal({ onClose }) { const TEXT = {
useEffect(() => { ko: {
document.body.style.overflow = "hidden"; title: "개인정보처리방침",
return () => { body: (
document.body.style.overflow = ""; <>
};
}, []);
return (
<div className="main-contact-modal">
<div className="main-contact-modal-dim" onClick={onClose} />
<div className="main-contact-modal-card">
<div className="main-contact-modal-head">
<h3>개인정보처리방침</h3>
<button type="button" onClick={onClose}>
×
</button>
</div>
<div className="main-contact-modal-body">
<p>
'주식회사 팔네트웍스'(이하 '회사') 고객님의 개인정보를 중요시하며, "개인정보보호법" "정보통신망 이용촉진 및 정보보호에 관한 법률" 준수하고 있습니다. 회사는 개인정보취급방침을 통하여 고객님께서 제공하시는 개인정보가 어떠한 용도와 방식으로 이용되고 있으며, 개인정보보호를 위해 어떠한 조치가 취해지고 있는지 알려드립니다. 회사는 개인정보취급방침을 개정하는 경우 웹사이트 공지사항(또는 개별공지) 통하여 공지할 것입니다. '주식회사 팔네트웍스'(이하 '회사') 고객님의 개인정보를 중요시하며, "개인정보보호법" "정보통신망 이용촉진 및 정보보호에 관한 법률" 준수하고 있습니다. 회사는 개인정보취급방침을 통하여 고객님께서 제공하시는 개인정보가 어떠한 용도와 방식으로 이용되고 있으며, 개인정보보호를 위해 어떠한 조치가 취해지고 있는지 알려드립니다. 회사는 개인정보취급방침을 개정하는 경우 웹사이트 공지사항(또는 개별공지) 통하여 공지할 것입니다.
<br /> <br />
1. 수집하는 개인정보 항목 회사는 서비스 신청, 상담, 문의 등을 위해 아래와 같은 개인정보를 수집하고 있습니다. 1. 수집하는 개인정보 항목 회사는 서비스 신청, 상담, 문의 등을 위해 아래와 같은 개인정보를 수집하고 있습니다.
@ -75,7 +61,96 @@ export default function PrivacyModal({ onClose }) {
공고일자: 2022 05 26 공고일자: 2022 05 26
<br /> <br />
시행일자: 2022 05 26 시행일자: 2022 05 26
</p> </>
),
},
en: {
title: "Privacy Policy",
body: (
<>
PAL Networks Co., Ltd. (hereinafter "the Company") values your personal information and complies with the Personal Information Protection Act and the Act on Promotion of Information and Communications Network Utilization and Information Protection. Through this Privacy Policy, the Company informs you how the personal information you provide is used and what measures are taken to protect it. Should the Company revise this Privacy Policy, it will announce the revision through a notice on the website (or individual notice).
<br />
Article 1. Items of Personal Information Collected. The Company collects the following personal information for service applications, consultations, and inquiries.
<br />
<br />
a. Items collected: Name, company name, phone or mobile number, email address, access logs
<br />
b. Method of collection: Website (online inquiries), sales activities for customer management purposes
<br />
<br />
Article 2. Purpose of Collection and Use of Personal Information
<br />
The Company uses the personal information it collects for the following purposes.
<br />
<br />
a. Service inquiries: Identifying and confirming the content of inquiries, processing and sending responses, and maintaining smooth communication with the inquiring customer
<br />
b. Customer management: Providing technical support, sales contact information, and contract information
<br />
c. Marketing and advertising: Delivering promotional information about services, products, seminars, and events; providing services and advertisements tailored to demographic characteristics; sending newsletters
<br />
<br />
Article 3. Retention and Use Period of Personal Information
<br />
The Company destroys personal information without delay once the purpose of collection and use has been achieved. However, the following information is retained for the period specified below for the stated reasons.
<br />
<br />
a. Items retained: Name, company name, phone or mobile number, email address, access logs
<br />
b. Basis for retention: Terms of Service and the Act on Consumer Protection in Electronic Commerce
<br />
c. Retention period: 5 years (however, information related to a contract is retained for the duration of the contract even after 5 years have passed from the date of collection)
<br />
Article 4. Procedures and Methods for Destroying Personal Information
<br />
In principle, the Company destroys personal information without delay once the purpose of collection and use has been achieved.
<br />
<br />
Article 10. Civil Complaint Service Related to Personal Information
<br />
For other reports or consultations regarding personal information infringement, please contact the organizations below.
<br />
<br />
1. Personal Information Dispute Mediation Committee: 1833-6972 (www.kopico.go.kr)
<br />
2. Personal Information Infringement Report Center: 118 (privacy.kisa.or.kr)
<br />
3. Supreme Prosecutors' Office: 1301 (www.spo.go.kr)
<br />
4. National Police Agency: 182 (ecrm.cyber.go.kr)
<br />
<br />
Announcement date: May 26, 2022
<br />
Effective date: May 26, 2022
</>
),
},
};
export default function PrivacyModal({ onClose }) {
const { lang } = useLanguage();
const t = TEXT[lang];
useEffect(() => {
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = "";
};
}, []);
return (
<div className="main-contact-modal">
<div className="main-contact-modal-dim" onClick={onClose} />
<div className="main-contact-modal-card">
<div className="main-contact-modal-head">
<h3>{t.title}</h3>
<button type="button" onClick={onClose}>
×
</button>
</div>
<div className="main-contact-modal-body">
<p>{t.body}</p>
</div> </div>
</div> </div>
</div> </div>

86
src/components/SubHero.jsx

@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Link, useLocation } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useLanguage } from "../context/LanguageContext";
const menuMap = { const menuMap = {
"/company": { label: "Company" }, "/company": { label: "Company" },
@ -105,8 +106,7 @@ function NetworkGlobe() {
const d = Math.sqrt(dx * dx + dy * dy + dz * dz); const d = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (d < 0.55) { if (d < 0.55) {
const depth = (a.sz + b.sz) * 0.5; const depth = (a.sz + b.sz) * 0.5;
const alpha = const alpha = Math.max(0, 0.05 + (depth + 1) * 0.1) * (1 - d / 0.55);
Math.max(0, 0.05 + (depth + 1) * 0.1) * (1 - d / 0.55);
const aHex = Math.round(Math.min(255, alpha * 255)) const aHex = Math.round(Math.min(255, alpha * 255))
.toString(16) .toString(16)
.padStart(2, "0"); .padStart(2, "0");
@ -135,14 +135,7 @@ function NetworkGlobe() {
.toString(16) .toString(16)
.padStart(2, "0"); .padStart(2, "0");
const glow = ctx.createRadialGradient( const glow = ctx.createRadialGradient(n.sx, n.sy, 0, n.sx, n.sy, r * 3.5);
n.sx,
n.sy,
0,
n.sx,
n.sy,
r * 3.5,
);
glow.addColorStop(0, n.color + gHex); glow.addColorStop(0, n.color + gHex);
glow.addColorStop(1, n.color + "00"); glow.addColorStop(1, n.color + "00");
ctx.beginPath(); ctx.beginPath();
@ -180,12 +173,7 @@ function NetworkGlobe() {
function TitleLine({ children, delay }) { function TitleLine({ children, delay }) {
return ( return (
<span className="sh4-title-line"> <span className="sh4-title-line">
<motion.span <motion.span className="sh4-title-line-inner" initial={{ y: "105%" }} animate={{ y: "0%" }} transition={{ duration: 1.1, delay, ease: [0.16, 1, 0.3, 1] }}>
className="sh4-title-line-inner"
initial={{ y: "105%" }}
animate={{ y: "0%" }}
transition={{ duration: 1.1, delay, ease: [0.16, 1, 0.3, 1] }}
>
{children} {children}
</motion.span> </motion.span>
</span> </span>
@ -194,9 +182,14 @@ function TitleLine({ children, delay }) {
export default function SubHero({ title, desc, navItems, rightSlot }) { export default function SubHero({ title, desc, navItems, rightSlot }) {
const { pathname } = useLocation(); const { pathname } = useLocation();
const { lang, withLang } = useLanguage();
const [isPill, setIsPill] = useState(false); const [isPill, setIsPill] = useState(false);
const navRef = useRef(null); const navRef = useRef(null);
// navItems to ko (canonical path) .
// pathname /en prefix .
const logicalPath = lang === "en" ? pathname.replace(/^\/en/, "") || "/main" : pathname;
useEffect(() => { useEffect(() => {
const onScroll = () => { const onScroll = () => {
setIsPill(window.scrollY > 80); setIsPill(window.scrollY > 80);
@ -215,9 +208,7 @@ export default function SubHero({ title, desc, navItems, rightSlot }) {
)) ))
: // JSX: <br/> TitleLine : // JSX: <br/> TitleLine
(() => { (() => {
const children = Array.isArray(title.props?.children) const children = Array.isArray(title.props?.children) ? title.props.children : [title.props?.children ?? title];
? title.props.children
: [title.props?.children ?? title];
const lines = []; const lines = [];
let current = []; let current = [];
children.forEach((child, i) => { children.forEach((child, i) => {
@ -244,39 +235,21 @@ export default function SubHero({ title, desc, navItems, rightSlot }) {
<div className="sh4-inner"> <div className="sh4-inner">
<div className="sh4-left"> <div className="sh4-left">
{/* // 브레드크럼 */} {/* // 브레드크럼 */}
<motion.nav <motion.nav className="sh4-breadcrumb" initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }} aria-label="breadcrumb">
className="sh4-breadcrumb" <Link to={withLang("/main")} className="sh4-breadcrumb-item">
initial={{ opacity: 0, y: 8 }} <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
aria-label="breadcrumb"
>
<Link to="/" className="sh4-breadcrumb-item">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z" /> <path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
<polyline points="9 22 9 12 15 12 15 22" /> <polyline points="9 22 9 12 15 12 15 22" />
</svg> </svg>
</Link> </Link>
<span className="sh4-breadcrumb-sep">/</span> <span className="sh4-breadcrumb-sep">/</span>
<Link <Link to={withLang("/" + logicalPath.split("/")[1])} className="sh4-breadcrumb-item">
to={"/" + pathname.split("/")[1]} {menuMap["/" + logicalPath.split("/")[1]]?.label}
className="sh4-breadcrumb-item"
>
{menuMap["/" + pathname.split("/")[1]]?.label}
</Link> </Link>
{pathname.split("/")[2] && ( {logicalPath.split("/")[2] && (
<> <>
<span className="sh4-breadcrumb-sep">/</span> <span className="sh4-breadcrumb-sep">/</span>
<span className="sh4-breadcrumb-item sh4-breadcrumb-item--active"> <span className="sh4-breadcrumb-item sh4-breadcrumb-item--active">{navItems?.find((n) => n.to === logicalPath)?.label}</span>
{navItems?.find((n) => n.to === pathname)?.label}
</span>
</> </>
)} )}
</motion.nav> </motion.nav>
@ -288,11 +261,7 @@ export default function SubHero({ title, desc, navItems, rightSlot }) {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ transition={{
duration: 0.6, duration: 0.6,
delay: delay: 0.1 + (typeof title === "string" ? title.split("\n").length : 2) * 0.1 + 0.15,
0.1 +
(typeof title === "string" ? title.split("\n").length : 2) *
0.1 +
0.15,
ease: [0.16, 1, 0.3, 1], ease: [0.16, 1, 0.3, 1],
}} }}
> >
@ -302,12 +271,7 @@ export default function SubHero({ title, desc, navItems, rightSlot }) {
</div> </div>
{rightSlot && ( {rightSlot && (
<motion.div <motion.div className="sh4-right" initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 1, delay: 0.3 }}>
className="sh4-right"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1, delay: 0.3 }}
>
{rightSlot} {rightSlot}
</motion.div> </motion.div>
)} )}
@ -315,18 +279,10 @@ export default function SubHero({ title, desc, navItems, rightSlot }) {
</section> </section>
{navItems?.length > 1 && ( {navItems?.length > 1 && (
<nav <nav ref={navRef} className={`sh4-nav-wrap${isPill ? " is-pill" : ""}`} aria-label="Sub Navigation">
ref={navRef}
className={`sh4-nav-wrap${isPill ? " is-pill" : ""}`}
aria-label="Sub Navigation"
>
<div className="sh4-nav"> <div className="sh4-nav">
{navItems.map((item) => ( {navItems.map((item) => (
<Link <Link key={item.to} to={withLang(item.to)} className={`sh4-nav-tab${logicalPath === item.to ? " sh4-nav-tab--active" : ""}`}>
key={item.to}
to={item.to}
className={`sh4-nav-tab${pathname === item.to ? " sh4-nav-tab--active" : ""}`}
>
{item.label} {item.label}
</Link> </Link>
))} ))}

780
src/components/main/MainContact.jsx

@ -1,186 +1,48 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { gsap } from "gsap"; import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger"; import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useLanguage } from "../../context/LanguageContext";
gsap.registerPlugin(ScrollTrigger); gsap.registerPlugin(ScrollTrigger);
function MainContact() { const TEXT = {
const sectionRef = useRef(null); ko: {
const headRef = useRef(null); eyebrow: "CONTACT US",
const formRef = useRef(null); title: (
const [isPrivacyOpen, setIsPrivacyOpen] = useState(false); <>
useEffect(() => {
const ctx = gsap.context(() => {
gsap.set(headRef.current, {
opacity: 0,
x: -80,
y: 20,
});
gsap.set(formRef.current, {
opacity: 0,
x: 90,
y: 20,
scale: 0.96,
});
const tl = gsap.timeline({
scrollTrigger: {
trigger: sectionRef.current,
start: "top 68%",
toggleActions: "play none none reverse",
},
});
tl.to(headRef.current, {
opacity: 1,
x: 0,
y: 0,
duration: 0.9,
ease: "power3.out",
}).to(
formRef.current,
{
opacity: 1,
x: 0,
y: 0,
scale: 1,
duration: 1,
ease: "power3.out",
},
"-=0.55",
);
}, sectionRef);
return () => ctx.revert();
}, []);
return (
<section className="main-contact-section" ref={sectionRef}>
<div className="contact-orb contact-orb--1" />
<div className="contact-orb contact-orb--2" />
<div className="contact-orb contact-orb--3" />
<div className="main-contact-inner">
<div className="main-contact-head" ref={headRef}>
<p className="main-contact-eyebrow">CONTACT US</p>
<h2 className="main-contact-title">
프로젝트 문의를 프로젝트 문의를
<br /> <br />
남겨주세요. 남겨주세요.
</h2> </>
</div> ),
fields: {
<form className="main-contact-form" ref={formRef}> name: "이름",
<div className="main-contact-grid"> namePh: "이름을 입력해 주세요.",
<label> email: "이메일",
<span> emailPh: "이메일을 입력해 주세요.",
이름 <em>*</em> phone: "연락처",
</span> phonePh: "연락처를 입력해 주세요.",
website: "홈페이지",
<input type="text" placeholder="이름을 입력해 주세요." required /> websitePh: "홈페이지 주소를 입력해 주세요.",
</label> subject: "제목",
subjectPh: "문의 제목을 입력해 주세요.",
<label> message: "내용",
<span> messagePh: "문의 내용을 입력해 주세요.",
이메일 <em>*</em> },
</span> agree: "개인정보처리방침에 동의합니다.",
viewPolicy: "개인정보처리방침 보기",
<input submit: "문의하기",
type="email" policyTitle: "개인정보처리방침",
placeholder="이메일을 입력해 주세요." policyBody: (
required <>
/> '주식회사 팔네트웍스'(이하 '회사') 고객님의 개인정보를 중요시하며, "개인정보보호법" "정보통신망 이용촉진 및 정보보호에 관한 법률" 준수하고 있습니다. 회사는 개인정보취급방침을 통하여 고객님께서 제공하시는 개인정보가 어떠한 용도와 방식으로 이용되고 있으며, 개인정보보호를 위해 어떠한 조치가 취해지고 있는지 알려드립니다. 회사는 개인정보취급방침을 개정하는 경우 웹사이트 공지사항(또는 개별공지) 통하여 공지할 것입니다.
</label>
<label>
<span>연락처</span>
<input type="tel" placeholder="연락처를 입력해 주세요." />
</label>
<label>
<span>홈페이지</span>
<input type="url" placeholder="홈페이지 주소를 입력해 주세요." />
</label>
<label className="main-contact-full">
<span>
제목 <em>*</em>
</span>
<input
type="text"
placeholder="문의 제목을 입력해 주세요."
required
/>
</label>
<label className="main-contact-full">
<span>
내용 <em>*</em>
</span>
<textarea placeholder="문의 내용을 입력해 주세요." required />
</label>
</div>
<div className="main-contact-form-bottom">
<label className="main-contact-check">
<input type="checkbox" required />
<span>개인정보처리방침에 동의합니다.</span>
</label>
<button
type="button"
className="main-contact-privacy-open"
onClick={() => setIsPrivacyOpen(true)}
>
개인정보처리방침 보기
</button>
</div>
<button type="submit" className="main-contact-submit">
문의하기
</button>
</form>
</div>
{isPrivacyOpen && (
<div className="main-contact-modal">
<div
className="main-contact-modal-dim"
onClick={() => setIsPrivacyOpen(false)}
/>
<div className="main-contact-modal-card">
<div className="main-contact-modal-head">
<h3>개인정보처리방침</h3>
<button type="button" onClick={() => setIsPrivacyOpen(false)}>
×
</button>
</div>
<div className="main-contact-modal-body">
<p>
'주식회사 팔네트웍스'(이하 '회사') 고객님의 개인정보를
중요시하며, 개인정보보호법 "정보통신망 이용촉진
정보보호에 관한 법률 준수하고 있습니다. 회사는
개인정보취급방침을 통하여 고객님께서 제공하시는 개인정보가
어떠한 용도와 방식으로 이용되고 있으며, 개인정보보호를 위해
어떠한 조치가 취해지고 있는지 알려드립니다. 회사는
개인정보취급방침을 개정하는 경우 웹사이트 공지사항(또는
개별공지) 통하여 공지할 것입니다.
<br /> <br />
1. 수집하는 개인정보 항목 회사는 서비스 신청, 상담, 문의 1. 수집하는 개인정보 항목 회사는 서비스 신청, 상담, 문의 등을 위해 아래와 같은 개인정보를 수집하고 있습니다.
등을 위해 아래와 같은 개인정보를 수집하고 있습니다.
<br /> <br />
<br /> <br />
. 수집항목 : 성명, 회사명, 전화번호 혹은 휴대폰번호, 이메일 . 수집항목 : 성명, 회사명, 전화번호 혹은 휴대폰번호, 이메일 주소, 접속 로그
주소, 접속 로그
<br /> <br />
. 개인정보 수집방법 : 웹사이트(온라인 문의, 고객 관리 목적의 . 개인정보 수집방법 : 웹사이트(온라인 문의, 고객 관리 목적의 영업활동
영업활동
<br /> <br />
<br /> <br />
2. 개인정보의 수집 이용목적 2. 개인정보의 수집 이용목적
@ -188,264 +50,159 @@ function MainContact() {
회사는 수집한 개인정보를 다음의 목적을 위해 활용합니다. 회사는 수집한 개인정보를 다음의 목적을 위해 활용합니다.
<br /> <br />
<br /> <br />
. 서비스 문의 : 정확한 문의 내용 파악 확인, 문의에 대한 . 서비스 문의 : 정확한 문의 내용 파악 확인, 문의에 대한 답변 진행, 답변 발송, 문의 고객과의 원활한 의사소통
답변 진행, 답변 발송, 문의 고객과의 원활한 의사소통
<br /> <br />
. 고객 관리 : 기술 지원 영업담당자 정보, 계약 정보 등의 . 고객 관리 : 기술 지원 영업담당자 정보, 계약 정보 등의 정보 제공
정보 제공
<br /> <br />
. 마케팅 광고에 활용 : 서비스제품세미나 이벤트 . 마케팅 광고에 활용 : 서비스제품세미나 이벤트 광고성 정보 전달, 인구통계 학적 특성에 따른 서비스 제공 광고 게재, 뉴스레터 메일 발송
광고성 정보 전달, 인구통계 학적 특성에 따른 서비스 제공 광고
게재, 뉴스레터 메일 발송
<br /> <br />
<br /> <br />
3. 개인정보의 보유 이용기간 3. 개인정보의 보유 이용기간
<br /> <br />
회사는 개인정보의 수집 이용목적이 달성된 후에는 해당 정보를 회사는 개인정보의 수집 이용목적이 달성된 후에는 해당 정보를 지체 없이 파기합니다. , 다음의 정보에 대해서는 아래의 이유로 명시한 기간 동안 보존합니다.관련법령에 의한 정보보유 사유 전자상거래 등에서의 소비자보호에 관한 법률, 상법 법령의 규정에 의하여 보존할 필요가 있는 경우에는 회사는 관계법령에서 정한 일정한 기간 동안 회원정보를 보관하며, 경우 보관하는 정보를 보관의 목적으로만 이용하며 보존기간은 아래와 같습니다.
지체 없이 파기합니다. , 다음의 정보에 대해서는 아래의 이유로
명시한 기간 동안 보존합니다.관련법령에 의한 정보보유 사유
전자상거래 등에서의 소비자보호에 관한 법률, 상법 법령의
규정에 의하여 보존할 필요가 있는 경우에는 회사는 관계법령에서
정한 일정한 기간 동안 회원정보를 보관하며, 경우 보관하는
정보를 보관의 목적으로만 이용하며 보존기간은 아래와 같습니다.
<br /> <br />
<br /> <br />
보존 항목 : 성명, 회사명, 전화번호 혹은 휴대폰번호, 이메일 보존 항목 : 성명, 회사명, 전화번호 혹은 휴대폰번호, 이메일 주소, 접속 로그
주소, 접속 로그
<br /> <br />
보존 근거 : 이용약관 전자상거래 등에서의 소비자보호에 관한 보존 근거 : 이용약관 전자상거래 등에서의 소비자보호에 관한 법률
법률
<br /> <br />
보존 기간 : 5(, 수집일로 부터 5년이 경과하여도 계약에 보존 기간 : 5(, 수집일로 부터 5년이 경과하여도 계약에 관련된 정보주체의 정보는 계약 기간 동안 보존)
관련된 정보주체의 정보는 계약 기간 동안 보존)
<br /> <br />
4. 개인정보의 파기절차 방법 4. 개인정보의 파기절차 방법
<br /> <br />
회사는 원칙적으로 개인정보 수집 이용목적이 달성된 후에는 해당 회사는 원칙적으로 개인정보 수집 이용목적이 달성된 후에는 해당 정보를 지체 없이 파기합니다. 파기절차 방법은 다음과 같습니다.
정보를 지체 없이 파기합니다. 파기절차 방법은 다음과 같습니다.
<br /> <br />
<br /> <br />
. 파기절차 . 파기절차
<br /> <br />
입력된 정보는 목적이 달성된 별도의 DB로 옮겨져(종이의 경우 입력된 정보는 목적이 달성된 별도의 DB로 옮겨져(종이의 경우 별도의 서류함) 내부 방침 기타 관련 법령에 의한 정보 보호 사유에 따라(보유 이용기간 참조) 일정 기간 저장된 파기합니다. 별도 DB로 옮겨진 개인정보는 법률에 의한 경우가 아니고서는 보유 이외의 다른 목적으로 이용되지 않습니다.
별도의 서류함) 내부 방침 기타 관련 법령에 의한 정보 보호
사유에 따라(보유 이용기간 참조) 일정 기간 저장된
파기합니다. 별도 DB로 옮겨진 개인정보는 법률에 의한 경우가
아니고서는 보유 이외의 다른 목적으로 이용되지 않습니다.
<br /> <br />
<br /> <br />
. 파기방법 . 파기방법
<br /> <br />
전자적 파일형태로 저장된 개인정보는 기록을 재생할 없는 기술적 전자적 파일형태로 저장된 개인정보는 기록을 재생할 없는 기술적 방법을 사용하여 삭제합니다.
방법을 사용하여 삭제합니다.
<br /> <br />
<br /> <br />
5. 개인정보의 이용 5. 개인정보의 이용
<br /> <br />
1. 회사가 수집하는 개인정보는 서비스의 제공에 필요한 최소한으로 1. 회사가 수집하는 개인정보는 서비스의 제공에 필요한 최소한으로 하되, 필요한 경우 자세한 정보를 요구할 있습니다.
하되, 필요한 경우 자세한 정보를 요구할 있습니다.
<br /> <br />
2. 회사는 이용자의 동의 하에 개인정보를 제3자에게 제공할 2. 회사는 이용자의 동의 하에 개인정보를 제3자에게 제공할 있습니다. 이러한 경우에도 개인정보의 제3자 제공은 이용자의 동의 하에서만 이루어지며 개인정보가 제공되는 것을 원하지 않는 경우에는, 특정 서비스를 이용하지 않거나 특정한 형태의 판촉이나 이벤트에 참여하지 않으면 됩니다(, 경우 별도 공지함)
있습니다. 이러한 경우에도 개인정보의 제3자 제공은 이용자의 동의
하에서만 이루어지며 개인정보가 제공되는 것을 원하지 않는
경우에는, 특정 서비스를 이용하지 않거나 특정한 형태의 판촉이나
이벤트에 참여하지 않으면 됩니다(, 경우 별도 공지함)
<br /> <br />
<br /> <br />
6. 수집한 개인정보의 위탁 6. 수집한 개인정보의 위탁
<br /> <br />
회사가 외부업체(이하, 위탁 받는 업체) 상기 특정서비스의 회사가 외부업체(이하, '위탁 받는 업체') 상기 특정서비스의 제공을 위탁하는 경우, 서비스 제공에 필요한 회원의 개인정보를 회원의 동의를 받아 위탁 받는 업체에 제공할 있으며, 경우 서비스 위탁 사실을 명시 합니다. 위탁 받는 업체는 제공 받은 회원의 개인정보의 수집, 취급, 관리에 있어 위탁 받은 목적 외의 용도로 이를 이용하거나 제3자에게 제공하지 않습니다.
제공을 위탁하는 경우, 서비스 제공에 필요한 회원의 개인정보를
회원의 동의를 받아 위탁 받는 업체에 제공할 있으며, 경우
서비스 위탁 사실을 명시 합니다. 위탁 받는 업체는 제공 받은
회원의 개인정보의 수집, 취급, 관리에 있어 위탁 받은 목적 외의
용도로 이를 이용하거나 제3자에게 제공하지 않습니다.
<br /> <br />
회사는 서비스 이행을 위해 아래와 같이 외부업체에 개인정보를 회사는 서비스 이행을 위해 아래와 같이 외부업체에 개인정보를 위탁하여 운영하고 있습니다. 회사의 개인정보 위탁처리 기관 위탁업무 내용은 아래와 같습니다.
위탁하여 운영하고 있습니다. 회사의 개인정보 위탁처리 기관
위탁업무 내용은 아래와 같습니다.
<br /> <br />
<br /> <br />
7. 이용자 법정대리인의 권리와 행사 방법 7. 이용자 법정대리인의 권리와 행사 방법
<br /> <br />
1. 회사는 고객의 개인정보를 보호하고 개인정보와 관련한 불만을 1. 회사는 고객의 개인정보를 보호하고 개인정보와 관련한 불만을 처리하기 위하여 아래와 같이 관련 부서 개인정보관리책임자를 지정하고 있습니다.
처리하기 위하여 아래와 같이 관련 부서 개인정보관리책임자를
지정하고 있습니다.
<br /> <br />
. 이용자는 회사의 개인정보 관리책임자에게 서면, 전화 또는 . 이용자는 회사의 개인정보 관리책임자에게 서면, 전화 또는 이메일로 연락하여 열람수정삭제를 요청할 있습니다.
이메일로 연락하여 열람수정삭제를 요청할 있습니다.
<br /> <br />
. 이용자가 개인정보의 오류에 대한 정정을 요청한 경우에는 . 이용자가 개인정보의 오류에 대한 정정을 요청한 경우에는 정정을 완료하기 전까지 당해 개인정보를 이용 또는 제공하지 않습니다. 또한 잘못된 개인정보를 제3자에게 이미 제공한 경우에는 정정 처리결과를 제3자에게 지체 없이 통지하여 정정이 이루어지도록 하겠습니다.
정정을 완료하기 전까지 당해 개인정보를 이용 또는 제공하지
않습니다. 또한 잘못된 개인정보를 제3자에게 이미 제공한 경우에는
정정 처리결과를 제3자에게 지체 없이 통지하여 정정이 이루어지도록
하겠습니다.
<br /> <br />
. 회사는 이용자의 요청에 의해 해지 또는 삭제된 개인정보는 . 회사는 이용자의 요청에 의해 해지 또는 삭제된 개인정보는 3조에 따라 처리하고 외의 용도로 열람 또는 이용할 없도록 처리하고 있습니다.
3조에 따라 처리하고 외의 용도로 열람 또는 이용할 없도록
처리하고 있습니다.
<br /> <br />
<br /> <br />
2. 이용자의 개인정보를 최신의 상태로 정확하게 입력하여 불의의 2. 이용자의 개인정보를 최신의 상태로 정확하게 입력하여 불의의 사고를 예방해 주시기 바랍니다. 이용자가 입력한 부정확한 정보로 인해 발생하는 사고의 책임은 이용자 자신에게 있으며 타인 정보의 도용 허위정보를 입력할 경우 회원자격이 상실될 있습니다.
사고를 예방해 주시기 바랍니다. 이용자가 입력한 부정확한 정보로
인해 발생하는 사고의 책임은 이용자 자신에게 있으며 타인 정보의
도용 허위정보를 입력할 경우 회원자격이 상실될 있습니다.
<br /> <br />
3. 이용자는 개인정보를 보호 받을 권리와 함께 스스로를 보호하고 3. 이용자는 개인정보를 보호 받을 권리와 함께 스스로를 보호하고 타인의 정보를 침해하지 않을 의무도 가지고 있습니다. 이용자의 개인정보가 유출되지 않도록 조심하시고 게시물을 포함한 타인의 개인정보를 훼손하지 않도록 유의해 주십시오. 만약 같은 책임을 다하지 못하고 타인의 정보 존엄성을 훼손할 시에는 정보통신망 이용촉진 정보보호 등에 관한 법률등에 의해 처벌 받을 있습니다.
타인의 정보를 침해하지 않을 의무도 가지고 있습니다. 이용자의
개인정보가 유출되지 않도록 조심하시고 게시물을 포함한 타인의
개인정보를 훼손하지 않도록 유의해 주십시오. 만약 같은 책임을
다하지 못하고 타인의 정보 존엄성을 훼손할 시에는 정보통신망
이용촉진 정보보호 등에 관한 법률등에 의해 처벌 받을
있습니다.
<br /> <br />
<br /> <br />
8. 개인정보 자동수집 장치의 설치, 운영 거부에 관한 8. 개인정보 자동수집 장치의 설치, 운영 거부에 관한 사항
사항
<br /> <br />
회사는 귀하의 정보를 수시로 저장하고 찾아내는 '쿠키(cookie)' 회사는 귀하의 정보를 수시로 저장하고 찾아내는 '쿠키(cookie)' 등을 운용하지 않습니다.
등을 운용하지 않습니다.
<br /> <br />
<br /> <br />
9. 기타 개인정보 취급에 관한 방침 9. 기타 개인정보 취급에 관한 방침
<br /> <br />
1. 개인정보보호를 위한 기술 관리적 대책 1. 개인정보보호를 위한 기술 관리적 대책
<br /> <br />
회사는 이용자의 개인정보를 취급함에 있어 개인정보가 분실, 도난, 회사는 이용자의 개인정보를 취급함에 있어 개인정보가 분실, 도난, 누출, 변조 또는 훼손되지 않도록 안전성 확보를 위하여 다음과 같은 기술적 대책을 강구하고 있습니다.
누출, 변조 또는 훼손되지 않도록 안전성 확보를 위하여 다음과 같은
기술적 대책을 강구하고 있습니다.
<br /> <br />
<br /> <br />
. 이용자의 개인정보는 비밀번호에 의해 보호되며, 파일 전송 . 이용자의 개인정보는 비밀번호에 의해 보호되며, 파일 전송 데이터를 암호화하여거나 파일 잠금기능(Lock) 사용하여 중요한 데이터는 별도의 보안기능을 통해 보호되고 있습니다.
데이터를 암호화하여거나 파일 잠금기능(Lock) 사용하여 중요한
데이터는 별도의 보안기능을 통해 보호되고 있습니다.
<br /> <br />
. 회사는 백신프로그램을 이용하여 컴퓨터바이러스에 의한 피해를 . 회사는 백신프로그램을 이용하여 컴퓨터바이러스에 의한 피해를 방지 하기 위한 조치를 취하고 있습니다. 백신프로그램은 주기적으로 업데이트되며 갑작스런 바이러스가 출현할 경우 백신이 나오는 즉시 이를 제공함으로써 개인 정보가 침해되는 것을 방지하고 있습니다.
방지 하기 위한 조치를 취하고 있습니다. 백신프로그램은 주기적으로
업데이트되며 갑작스런 바이러스가 출현할 경우 백신이 나오는 즉시
이를 제공함으로써 개인 정보가 침해되는 것을 방지하고 있습니다.
<br /> <br />
. 해킹 외부침입에 대비하여 서버마다 침입차단시스템 . 해킹 외부침입에 대비하여 서버마다 침입차단시스템 취약점 분석 시스템 등을 이용하여 보안에 만전을 기하고 있습니다.
취약점 분석 시스템 등을 이용하여 보안에 만전을 기하고 있습니다.
<br /> <br />
<br /> <br />
회사는 이용자의 개인정보를 취급함에 있어 개인정보가 분실, 도난, 회사는 이용자의 개인정보를 취급함에 있어 개인정보가 분실, 도난, 누출, 변조 또는 훼손되지 않도록 안전성 확보를 위하여 다음과 같은 관리적 대책을 강구하고 있습니다.
누출, 변조 또는 훼손되지 않도록 안전성 확보를 위하여 다음과 같은
관리적 대책을 강구하고 있습니다.
<br /> <br />
. 회사는 이용자의 개인정보에 대한 접근권한을 이용자를 직접 . 회사는 이용자의 개인정보에 대한 접근권한을 이용자를 직접 상대로 하여 마케팅 업무를 수행하는 , 개인정보관리책임자 담당자 개인정보관리업무를 수행하는 , 기타 업무상 개인정보의 취급이 불가피한 자로 제한하고 있습니다.
상대로 하여 마케팅 업무를 수행하는 , 개인정보관리책임자
담당자 개인정보관리업무를 수행하는 , 기타 업무상 개인정보의
취급이 불가피한 자로 제한하고 있습니다.
<br /> <br />
. 개인정보를 취급하는 직원을 대상으로 새로운 보안 기술 습득 . 개인정보를 취급하는 직원을 대상으로 새로운 보안 기술 습득 개인정보 보호 의무 등에 관해 사내 교육을 실시하고 있습니다.
개인정보 보호 의무 등에 관해 사내 교육을 실시하고 있습니다.
<br /> <br />
. 개인정보 관련 취급자의 업무 인수인계는 보안이 유지된 . 개인정보 관련 취급자의 업무 인수인계는 보안이 유지된 상태에서 철저하게 이뤄지고 있으며 입사 퇴사 개인정보 사고에 대한 책임을 명확화하고 있습니다.
상태에서 철저하게 이뤄지고 있으며 입사 퇴사 개인정보
사고에 대한 책임을 명확화하고 있습니다.
<br /> <br />
. 회사는 이용자 개인의 실수나 기본적인 인터넷의 위험성 때문에 . 회사는 이용자 개인의 실수나 기본적인 인터넷의 위험성 때문에 일어나는 일들에 대해 책임을 지지 않습니다.
일어나는 일들에 대해 책임을 지지 않습니다.
<br /> <br />
. 내부 관리자의 실수나 기술관리상의 사고로 인해 . 내부 관리자의 실수나 기술관리상의 사고로 인해 개인정보의 상실, 유출, 변조, 훼손이 유발될 경우 회사는 즉각 이용자께 사실을 알리고 적절한 대책과 보상을 강구할 것입니다.
개인정보의 상실, 유출, 변조, 훼손이 유발될 경우 회사는 즉각
이용자께 사실을 알리고 적절한 대책과 보상을 강구할 것입니다.
<br /> <br />
<br /> <br />
2. 링크사이트 제공 방침 2. 링크사이트 제공 방침
<br /> <br />
회사는 이용자에게 다른 회사의 웹사이트 또는 자료에 대한 링크를 회사는 이용자에게 다른 회사의 웹사이트 또는 자료에 대한 링크를 제공할 있습니다. 경우 회사는 외부사이트 자료에 대한 아무런 통제권이 없으므로 그로부터 제공받는 서비스나 자료의 유용성에 대해 책임질 없으며 보증할 없습니다. 회사가 포함하고 있는 링크를 클릭하여 사이트의 페이지로 옮겨갈 경우 해당 사이트의 개인정보보호정책은 회사와 무관하므로 새로 방문한 사이트의 정책을 검토해 보시기 바랍니다.
제공할 있습니다. 경우 회사는 외부사이트 자료에 대한
아무런 통제권이 없으므로 그로부터 제공받는 서비스나 자료의
유용성에 대해 책임질 없으며 보증할 없습니다. 회사가
포함하고 있는 링크를 클릭하여 사이트의 페이지로 옮겨갈 경우
해당 사이트의 개인정보보호정책은 회사와 무관하므로 새로 방문한
사이트의 정책을 검토해 보시기 바랍니다.
<br /> <br />
<br /> <br />
3. 게시물 운영 방침 3. 게시물 운영 방침
<br /> <br />
회사는 이용자의 게시물을 소중하게 생각하며 변조, 훼손, 삭제되지 회사는 이용자의 게시물을 소중하게 생각하며 변조, 훼손, 삭제되지 않도록 최선을 다하여 보호합니다. 그러나 다음의 경우는 그렇지 아니합니다.
않도록 최선을 다하여 보호합니다. 그러나 다음의 경우는 그렇지
아니합니다.
<br /> <br />
. 스팸(spam) 게시물 ( : 행운의 편지, 8 메일, 특정사이트 . 스팸(spam) 게시물 ( : 행운의 편지, 8 메일, 특정사이트 광고 )
광고 )
<br /> <br />
. 타인을 비방할 목적으로 허위 사실을 유포하여 타인의 명예를 . 타인을 비방할 목적으로 허위 사실을 유포하여 타인의 명예를 훼손하는 게시물
훼손하는 게시물
<br /> <br />
. 동의 없는 타인의 신상공개 게시물 . 동의 없는 타인의 신상공개 게시물
<br /> <br />
. 회사 또는 제3자의 지적재산권 권리를 침해하는 내용의 . 회사 또는 제3자의 지적재산권 권리를 침해하는 내용의 게시물
게시물
<br /> <br />
. 기타 게시판 주제와 다른 내용의 게시물 . 기타 게시판 주제와 다른 내용의 게시물
<br /> <br />
. 회사는 바람직한 게시판 문화를 활성화하기 위하여 동의 없는 . 회사는 바람직한 게시판 문화를 활성화하기 위하여 동의 없는 타인의 신상 공개 특정 부분을 삭제하거나 기호 등으로 수정하여 게시할 있으며, 다른 주제의 게시판으로 이동 가능한 내용일 경우 해당 게시물에 이동 경로를 밝혀 오해가 없도록 하고 있습니다.
타인의 신상 공개 특정 부분을 삭제하거나 기호 등으로 수정하여
게시할 있으며, 다른 주제의 게시판으로 이동 가능한 내용일 경우
해당 게시물에 이동 경로를 밝혀 오해가 없도록 하고 있습니다.
<br /> <br />
. 외의 경우 명시적 또는 개별적인 경고 삭제 조치할 . 외의 경우 명시적 또는 개별적인 경고 삭제 조치할 있습니다.
있습니다.
<br /> <br />
. 근본적으로 게시물에 관련된 제반 권리와 책임은 작성자 . 근본적으로 게시물에 관련된 제반 권리와 책임은 작성자 개인에게 있습니다. 게시물을 통해 자발적으로 공개된 정보는 보호받기 어려우므로 정보 공개 전에 심사 숙고하시기 바랍니다.
개인에게 있습니다. 게시물을 통해 자발적으로 공개된 정보는
보호받기 어려우므로 정보 공개 전에 심사 숙고하시기 바랍니다.
<br /> <br />
<br /> <br />
4. 이메일 무단수집 거부 방침 4. 이메일 무단수집 거부 방침
<br /> <br />
회사는 게시된 이메일 주소가 전자우편 수집 프로그램이나 밖의 회사는 게시된 이메일 주소가 전자우편 수집 프로그램이나 밖의 기술적 장치를 이용하여 무단 수집되는 것을 거부합니다. 이를 위반 정보통신망 이용촉진 정보보호 등에 관한 법률 등에 의해 처벌 받을 있습니다.
기술적 장치를 이용하여 무단 수집되는 것을 거부합니다. 이를 위반
정보통신망 이용촉진 정보보호 등에 관한 법률 등에 의해
처벌 받을 있습니다.
<br /> <br />
<br /> <br />
5. 광고성 정보의 전송 5. 광고성 정보의 전송
<br /> <br />
회사는 이용자의 명시적인 수신거부의사에 반하여 영리목적의 광고성 회사는 이용자의 명시적인 수신거부의사에 반하여 영리목적의 광고성 정보를 전송하지 않습니다. 회사는 이용자가 상품정보 안내, 뉴스레터 전자우편 전송에 대한 동의를 경우, 전자우편의 제목란 본문란에 다음 사항과 같이 이용자가 쉽게 알아 있도록 조치합니다.
정보를 전송하지 않습니다. 회사는 이용자가 상품정보 안내,
뉴스레터 전자우편 전송에 대한 동의를 경우, 전자우편의
제목란 본문란에 다음 사항과 같이 이용자가 쉽게 알아
있도록 조치합니다.
<br /> <br />
. 전자우편의 제목란 . 전자우편의 제목란
<br /> <br />
- (광고)라는 문구를 제목란에 표시하지 않을 있으며 전자우편 - (광고)라는 문구를 제목란에 표시하지 않을 있으며 전자우편 본문란의 주요 내용을 표시합니다.
본문란의 주요 내용을 표시합니다.
<br /> <br />
. 전자우편의 본문란 . 전자우편의 본문란
<br /> <br />
- 이용자가 수신거부의 의사표시를 있는 전송자의 명칭, - 이용자가 수신거부의 의사표시를 있는 전송자의 명칭, 전자우편주소를 명시합니다.
전자우편주소를 명시합니다.
<br /> <br />
- 이용자가 수신 거부의 의사를 쉽게 표시할 있는 방법을 - 이용자가 수신 거부의 의사를 쉽게 표시할 있는 방법을 명시합니다.
명시합니다.
<br /> <br />
<br /> <br />
10. 개인정보에 관한 민원서비스 10. 개인정보에 관한 민원서비스
<br /> <br />
회사는 고객의 개인정보를 보호하고 개인정보와 관련한 불만을 회사는 고객의 개인정보를 보호하고 개인정보와 관련한 불만을 처리하기 위하여 아래와 같이 관련 부서 개인정보관리책임자를 지정하고 있습니다. 회사의 서비스를 이용하시며 발생하는 모든 개인정보보호 관련 민원을 개인정보관리책임자 혹은 담당부서로 신고하실 있습니다. 회사는 이용자들의 신고사항에 대해 신속하게 충분한 답변을 드릴 것입니다.
처리하기 위하여 아래와 같이 관련 부서 개인정보관리책임자를
지정하고 있습니다. 회사의 서비스를 이용하시며 발생하는 모든
개인정보보호 관련 민원을 개인정보관리책임자 혹은 담당부서로
신고하실 있습니다. 회사는 이용자들의 신고사항에 대해 신속하게
충분한 답변을 드릴 것입니다.
<br /> <br />
<br /> <br />
고객서비스담당 부서: 전화번호 : 이메일: 개인정보관리책임자 성명: 고객서비스담당 부서: 전화번호 : 이메일: 개인정보관리책임자 성명: 전화번호: 이메일:
전화번호: 이메일:
<br /> <br />
<br /> <br />
기타 개인정보침해에 대한 신고나 상담이 필요하신 경우에는 아래 기타 개인정보침해에 대한 신고나 상담이 필요하신 경우에는 아래 기관에 문의하시기 바랍니다.
기관에 문의하시기 바랍니다.
<br /> <br />
<br /> <br />
1. 개인정보분쟁조정위원회 : (국번없이) 1833-6972 1. 개인정보분쟁조정위원회 : (국번없이) 1833-6972 (www.kopico.go.kr)
(www.kopico.go.kr)
<br /> <br />
<br /> <br />
2. 개인정보침해신고센터 : (국번없이) 118 (privacy.kisa.or.kr) 2. 개인정보침해신고센터 : (국번없이) 118 (privacy.kisa.or.kr)
@ -459,16 +216,381 @@ function MainContact() {
<br /> <br />
11. 고지의 의무 11. 고지의 의무
<br /> <br />
개인정보취급방침 내용 추가, 삭제 수정이 있을 시에는 개정 개인정보취급방침 내용 추가, 삭제 수정이 있을 시에는 개정 최소 7일전부터 웹사이트를 통해 고지합니다. 개인정보의 수집 활용, 제3자의 제공 등과 같이 이용자 권리의 중요한 변경이 있을 경우에는 최소 30 전에 고지합니다.
최소 7일전부터 웹사이트를 통해 고지합니다. 개인정보의 수집
활용, 제3자의 제공 등과 같이 이용자 권리의 중요한 변경이 있을
경우에는 최소 30 전에 고지합니다.
<br /> <br />
<br /> <br />
공고일자: 2022 05 26 공고일자: 2022 05 26
<br /> <br />
시행일자: 2022 05 26 시행일자: 2022 05 26
</p> </>
),
},
en: {
eyebrow: "CONTACT US",
title: (
<>
Leave Us a Project
<br />
Inquiry.
</>
),
fields: {
name: "Name",
namePh: "Please enter your name.",
email: "Email",
emailPh: "Please enter your email.",
phone: "Phone",
phonePh: "Please enter your phone number.",
website: "Website",
websitePh: "Please enter your website URL.",
subject: "Subject",
subjectPh: "Please enter the subject of your inquiry.",
message: "Message",
messagePh: "Please enter your inquiry.",
},
agree: "I agree to the Privacy Policy.",
viewPolicy: "View Privacy Policy",
submit: "Submit",
policyTitle: "Privacy Policy",
policyBody: (
<>
PAL Networks Co., Ltd. (hereinafter "the Company") values your personal information and complies with the Personal Information Protection Act and the Act on Promotion of Information and Communications Network Utilization and Information Protection. Through this Privacy Policy, the Company informs you how the personal information you provide is used and what measures are taken to protect it. Should the Company revise this Privacy Policy, it will announce the revision through a notice on the website (or individual notice).
<br />
<br />
Article 1. Items of Personal Information Collected
<br />
The Company collects the following personal information for service applications, consultations, and inquiries.
<br />
<br />
a. Items collected: Name, company name, phone or mobile number, email address, access logs
<br />
b. Method of collection: Website (online inquiries), sales activities for customer management purposes
<br />
<br />
Article 2. Purpose of Collection and Use of Personal Information
<br />
The Company uses the personal information it collects for the following purposes.
<br />
<br />
a. Service inquiries: Identifying and confirming the content of inquiries, processing and sending responses, and maintaining smooth communication with the inquiring customer
<br />
b. Customer management: Providing technical support, sales contact information, and contract information
<br />
c. Marketing and advertising: Delivering promotional information about services, products, seminars, and events; providing services and advertisements tailored to demographic characteristics; sending newsletters
<br />
<br />
Article 3. Retention and Use Period of Personal Information
<br />
The Company destroys personal information without delay once the purpose of collection and use has been achieved. However, the following information is retained for the period specified below for the stated reasons. Where retention is required under applicable law, such as the Act on Consumer Protection in Electronic Commerce or the Commercial Act, the Company retains member information for the period prescribed by the relevant law and uses it solely for that retention purpose. The retention periods are as follows.
<br />
<br />
a. Items retained: Name, company name, phone or mobile number, email address, access logs
<br />
b. Basis for retention: Terms of Service and the Act on Consumer Protection in Electronic Commerce
<br />
c. Retention period: 5 years (however, information related to a contract is retained for the duration of the contract even after 5 years have passed from the date of collection)
<br />
Article 4. Procedures and Methods for Destroying Personal Information
<br />
In principle, the Company destroys personal information without delay once the purpose of collection and use has been achieved. The procedures and methods of destruction are as follows.
<br />
<br />
a. Destruction procedure
<br />
Information entered is transferred to a separate database (or a separate document box, for paper records) once its purpose has been achieved, stored for a set period in accordance with internal policy and applicable law (see Retention and Use Period), and then destroyed. Personal information transferred to a separate database is not used for any purpose other than retention unless required by law.
<br />
<br />
b. Destruction method
<br />
Personal information stored in electronic file form is deleted using technical methods that make the records unrecoverable.
<br />
<br />
Article 5. Use of Personal Information
<br />
1. The Company collects the minimum amount of personal information necessary to provide its services, but may request more detailed information when necessary.
<br />
2. The Company may provide personal information to third parties with the user's consent. Even in such cases, personal information is provided to third parties only with the user's consent. If you do not wish your information to be provided, you may choose not to use certain services or participate in certain promotions or events (in which case separate notice will be given).
<br />
<br />
Article 6. Outsourcing of Collected Personal Information
<br />
Where the Company outsources the provision of the specific services above to an external company (the "outsourced company"), it may, with the member's consent, provide the member's personal information necessary for the service to the outsourced company, and will disclose the fact of outsourcing in such cases. The outsourced company will not use the personal information provided for any purpose other than the outsourced purpose, nor provide it to third parties.
<br />
The Company outsources personal information to external companies as needed to perform its services. Details of the Company's personal information processing agencies and outsourced tasks are provided separately.
<br />
<br />
Article 7. Rights of Users and Legal Representatives and How to Exercise Them
<br />
1. The Company designates a related department and a Chief Privacy Officer to protect customers' personal information and handle related complaints, as follows.
<br />
a. Users may contact the Company's Chief Privacy Officer in writing, by phone, or by email to request access, correction, or deletion of their information.
<br />
b. If a user requests correction of an error in their personal information, the Company will not use or provide that information until the correction is completed. If incorrect personal information has already been provided to a third party, the Company will notify the third party of the correction without delay.
<br />
c. Personal information terminated or deleted at the user's request is processed in accordance with Article 3 and is not accessed or used for any other purpose.
<br />
<br />
2. Please keep your personal information accurate and up to date to help prevent unforeseen incidents. Users are responsible for any incidents caused by inaccurate information they enter, and membership may be revoked for entering false information, including identity theft.
<br />
3. Users have both the right to have their personal information protected and the obligation to protect themselves and avoid infringing on others' information. Please take care that your personal information is not leaked, and avoid damaging the personal information of others, including through posts. Failure to meet this responsibility, resulting in damage to another person's information or dignity, may result in punishment under the Act on Promotion of Information and Communications Network Utilization and Information Protection and other applicable laws.
<br />
<br />
Article 8. Installation, Operation, and Refusal of Automatic Personal Information Collection Devices
<br />
The Company does not operate "cookies" or similar devices that repeatedly store and retrieve your information.
<br />
<br />
Article 9. Other Policies on the Handling of Personal Information
<br />
1. Technical and managerial measures for personal information protection
<br />
In handling users' personal information, the Company takes the following technical measures to prevent it from being lost, stolen, leaked, altered, or damaged.
<br />
<br />
a. Users' personal information is protected by passwords, and important data is protected through separate security features such as encrypting files and transmitted data or using file lock functions.
<br />
b. The Company uses antivirus software to prevent damage from computer viruses. The software is updated periodically, and in the event of a new virus, the Company provides the update as soon as it becomes available to prevent personal information from being compromised.
<br />
c. To guard against hacking and other external intrusions, the Company maintains thorough security using intrusion prevention systems and vulnerability analysis systems on each server.
<br />
<br />
The Company also takes the following managerial measures to prevent users' personal information from being lost, stolen, leaked, altered, or damaged.
<br />
a. The Company limits access to users' personal information to those performing marketing duties directly involving the user, the Chief Privacy Officer and staff responsible for personal information management, and others for whom handling personal information is unavoidable in the course of their work.
<br />
b. The Company conducts internal training for staff who handle personal information on new security technologies and their obligations to protect personal information.
<br />
c. Handover of duties among personal information handlers is carried out thoroughly under secure conditions, with clearly defined responsibility for any personal information incidents for both incoming and departing staff.
<br />
d. The Company is not responsible for issues arising from a user's own mistakes or the inherent risks of the internet.
<br />
e. If personal information is lost, leaked, altered, or damaged due to an internal administrator's mistake or a technical management incident, the Company will promptly inform users and take appropriate measures and compensation.
<br />
<br />
2. Policy on Providing Links to Other Sites
<br />
The Company may provide users with links to other companies' websites or materials. In such cases, the Company has no control over the external sites or materials and cannot be held responsible for, or guarantee, the usefulness of the services or materials provided there. If you follow a link included by the Company to another site, that site's privacy policy is unrelated to the Company, so please review the new site's policy.
<br />
<br />
3. Policy on Operating Posts
<br />
The Company values users' posts and does its best to protect them from alteration, damage, or deletion. However, this does not apply in the following cases.
<br />
a. Spam posts (e.g., chain letters, advertisements for specific sites)
<br />
b. Posts that damage another person's reputation by spreading false information with intent to defame them
<br />
c. Posts disclosing another person's personal information without consent
<br />
d. Posts that infringe on the intellectual property or other rights of the Company or a third party
<br />
e. Other posts unrelated to the board's topic
<br />
f. To promote a healthy community, the Company may delete or redact specific parts of posts that disclose another person's information without consent, and may indicate a relocation path for content more appropriate for another board, to avoid confusion.
<br />
g. In other cases, the Company may remove content after an explicit or individual warning.
<br />
h. Rights and responsibilities related to a post fundamentally belong to its author. Information voluntarily disclosed through a post is difficult to protect, so please think carefully before disclosing information.
<br />
<br />
4. Policy on Refusing Unauthorized Collection of Email Addresses
<br />
The Company refuses to allow email addresses posted on its site to be collected without authorization using email harvesting programs or other technical means. Violation of this may result in punishment under the Act on Promotion of Information and Communications Network Utilization and Information Protection and other applicable laws.
<br />
<br />
5. Sending Promotional Information
<br />
The Company does not send promotional information for commercial purposes against a user's explicit refusal to receive it. Where a user has consented to receive product information, newsletters, or similar emails, the Company takes the following measures so users can easily identify them in the subject and body of the email.
<br />
a. Email subject line
<br />
- The Company may choose not to include the word "(Advertisement)" in the subject line, and instead display the main content of the email in the subject.
<br />
b. Email body
<br />
- The Company clearly states the sender's name and email address so users can express their wish to opt out.
<br />
- The Company clearly states an easy way for users to opt out of future emails.
<br />
<br />
Article 10. Civil Complaint Service Related to Personal Information
<br />
The Company designates a related department and a Chief Privacy Officer to protect customers' personal information and handle related complaints. Any complaints regarding personal information protection arising from use of the Company's services may be reported to the Chief Privacy Officer or the responsible department. The Company will respond promptly and fully to all reports.
<br />
<br />
Customer Service Department: Phone: Email: Chief Privacy Officer: Phone: Email:
<br />
<br />
For other reports or consultations regarding personal information infringement, please contact the organizations below.
<br />
<br />
1. Personal Information Dispute Mediation Committee: 1833-6972 (www.kopico.go.kr)
<br />
<br />
2. Personal Information Infringement Report Center: 118 (privacy.kisa.or.kr)
<br />
<br />
3. Supreme Prosecutors' Office: 1301 (www.spo.go.kr)
<br />
<br />
4. National Police Agency: 182 (ecrm.cyber.go.kr)
<br />
<br />
Article 11. Duty of Notification
<br />
Any additions, deletions, or amendments to this Privacy Policy will be announced through the website at least 7 days before the revision takes effect. For significant changes affecting users' rights, such as changes to the collection and use of personal information or provision to third parties, notice will be given at least 30 days in advance.
<br />
<br />
Announcement date: May 26, 2022
<br />
Effective date: May 26, 2022
</>
),
},
};
function MainContact() {
const { lang } = useLanguage();
const t = TEXT[lang];
const sectionRef = useRef(null);
const headRef = useRef(null);
const formRef = useRef(null);
const [isPrivacyOpen, setIsPrivacyOpen] = useState(false);
useEffect(() => {
const ctx = gsap.context(() => {
gsap.set(headRef.current, {
opacity: 0,
x: -80,
y: 20,
});
gsap.set(formRef.current, {
opacity: 0,
x: 90,
y: 20,
scale: 0.96,
});
const tl = gsap.timeline({
scrollTrigger: {
trigger: sectionRef.current,
start: "top 68%",
toggleActions: "play none none reverse",
},
});
tl.to(headRef.current, {
opacity: 1,
x: 0,
y: 0,
duration: 0.9,
ease: "power3.out",
}).to(
formRef.current,
{
opacity: 1,
x: 0,
y: 0,
scale: 1,
duration: 1,
ease: "power3.out",
},
"-=0.55",
);
}, sectionRef);
return () => ctx.revert();
}, []);
return (
<section className="main-contact-section" ref={sectionRef}>
<div className="contact-orb contact-orb--1" />
<div className="contact-orb contact-orb--2" />
<div className="contact-orb contact-orb--3" />
<div className="main-contact-inner">
<div className="main-contact-head" ref={headRef}>
<p className="main-contact-eyebrow">{t.eyebrow}</p>
<h2 className="main-contact-title">{t.title}</h2>
</div>
<form className="main-contact-form" ref={formRef}>
<div className="main-contact-grid">
<label>
<span>
{t.fields.name} <em>*</em>
</span>
<input type="text" placeholder={t.fields.namePh} required />
</label>
<label>
<span>
{t.fields.email} <em>*</em>
</span>
<input type="email" placeholder={t.fields.emailPh} required />
</label>
<label>
<span>{t.fields.phone}</span>
<input type="tel" placeholder={t.fields.phonePh} />
</label>
<label>
<span>{t.fields.website}</span>
<input type="url" placeholder={t.fields.websitePh} />
</label>
<label className="main-contact-full">
<span>
{t.fields.subject} <em>*</em>
</span>
<input type="text" placeholder={t.fields.subjectPh} required />
</label>
<label className="main-contact-full">
<span>
{t.fields.message} <em>*</em>
</span>
<textarea placeholder={t.fields.messagePh} required />
</label>
</div>
<div className="main-contact-form-bottom">
<label className="main-contact-check">
<input type="checkbox" required />
<span>{t.agree}</span>
</label>
<button type="button" className="main-contact-privacy-open" onClick={() => setIsPrivacyOpen(true)}>
{t.viewPolicy}
</button>
</div>
<button type="submit" className="main-contact-submit">
{t.submit}
</button>
</form>
</div>
{isPrivacyOpen && (
<div className="main-contact-modal">
<div className="main-contact-modal-dim" onClick={() => setIsPrivacyOpen(false)} />
<div className="main-contact-modal-card">
<div className="main-contact-modal-head">
<h3>{t.policyTitle}</h3>
<button type="button" onClick={() => setIsPrivacyOpen(false)}>
×
</button>
</div>
<div className="main-contact-modal-body">
<p>{t.policyBody}</p>
</div> </div>
</div> </div>
</div> </div>

87
src/components/main/MainNews.jsx

@ -1,19 +1,25 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { gsap } from "gsap"; import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger"; import { ScrollTrigger } from "gsap/ScrollTrigger";
import { Link } from "react-router-dom"; import { useLanguage } from "../../context/LanguageContext";
gsap.registerPlugin(ScrollTrigger); gsap.registerPlugin(ScrollTrigger);
function MainNews() { const TEXT = {
const sectionRef = useRef(null); ko: {
const headRef = useRef(null); eyebrow: "INSIDE PAL",
const itemsRef = useRef([]); title: (
<>
const news = [ 새로운 항공 서비스와
<br />
기술 소식
</>
),
desc: "항공 IT 플랫폼 구축, 서비스 운영, 기술 개발 관련 소식을 전합니다.",
news: [
{ {
date: "2025.11.14", date: "2025.11.14",
title: "포스맥-팔네트웍스, 미래항공교통산업 진출 전략적 MOU 체결 ", title: "포스맥-팔네트웍스, 미래항공교통산업 진출 전략적 MOU 체결",
desc: "기술 개발, 인프라 구축, 정책 사업 참여 등 성장성이 높은 항공 모빌리티 시장에서 공동 사업모델을 구축합니다.", desc: "기술 개발, 인프라 구축, 정책 사업 참여 등 성장성이 높은 항공 모빌리티 시장에서 공동 사업모델을 구축합니다.",
link: "https://www.gnynews.co.kr/news/articleView.html?idxno=453325", link: "https://www.gnynews.co.kr/news/articleView.html?idxno=453325",
}, },
@ -29,7 +35,47 @@ function MainNews() {
desc: "항공·스마트제조 기업과 협력하며 미래 항공 모빌리티 생태계를 확장합니다.", desc: "항공·스마트제조 기업과 협력하며 미래 항공 모빌리티 생태계를 확장합니다.",
link: "https://www.incheontoday.com/news/articleView.html?idxno=256068", link: "https://www.incheontoday.com/news/articleView.html?idxno=256068",
}, },
]; ],
},
en: {
eyebrow: "INSIDE PAL",
title: (
<>
Latest in Aviation Services
<br />
&amp; Technology
</>
),
desc: "News on our aviation IT platforms, service operations, and technology development.",
news: [
{
date: "2025.11.14",
title: "PosMac and PAL Networks Sign Strategic MOU on Future Air Traffic",
desc: "Building a joint business model in the high-growth air mobility market, spanning technology development, infrastructure, and policy projects.",
link: "https://www.gnynews.co.kr/news/articleView.html?idxno=453325",
},
{
date: "2024.10.22",
title: "PAL Networks Selected for Korea Airports Corporation Drone Traffic Management Project",
desc: "Providing integrated management of drone operations around airports for a safer flight environment.",
link: "https://www.incheonilbo.com/news/articleView.html?idxno=1268774",
},
{
date: "2024.10.31",
title: "Incheon Free Economic Zone Runs K-UAM Confex Aviation & Smart Manufacturing Pavilion",
desc: "Expanding the future air mobility ecosystem in partnership with aviation and smart manufacturing companies.",
link: "https://www.incheontoday.com/news/articleView.html?idxno=256068",
},
],
},
};
function MainNews() {
const { lang } = useLanguage();
const t = TEXT[lang];
const sectionRef = useRef(null);
const headRef = useRef(null);
const itemsRef = useRef([]);
useEffect(() => { useEffect(() => {
const ctx = gsap.context(() => { const ctx = gsap.context(() => {
@ -71,25 +117,14 @@ function MainNews() {
<section className="main-news-section" ref={sectionRef}> <section className="main-news-section" ref={sectionRef}>
<div className="main-news-inner"> <div className="main-news-inner">
<div className="main-news-head" ref={headRef}> <div className="main-news-head" ref={headRef}>
<p className="main-news-eyebrow">INSIDE PAL</p> <p className="main-news-eyebrow">{t.eyebrow}</p>
<h2 className="main-news-title"> <h2 className="main-news-title">{t.title}</h2>
새로운 항공 서비스와 <p className="main-news-desc">{t.desc}</p>
<br />
기술 소식
</h2>
<p className="main-news-desc">
항공 IT 플랫폼 구축, 서비스 운영, 기술 개발 관련 소식을 전합니다.
</p>
</div> </div>
<div className="main-news-list"> <div className="main-news-list">
{news.map((item, index) => ( {t.news.map((item, index) => (
<a <a href={item.link} target="_blank" rel="noopener noreferrer" key={index}>
href={item.link}
target="_blank"
rel="noopener noreferrer"
key={index}
>
<article <article
className="main-news-item" className="main-news-item"
ref={(el) => { ref={(el) => {
@ -102,7 +137,7 @@ function MainNews() {
</div> </div>
<div className="main-news-content"> <div className="main-news-content">
<h3>{item.title}</h3> <h3 className="main-news-content-title-clamp">{item.title}</h3>
<p>{item.desc}</p> <p>{item.desc}</p>
</div> </div>

306
src/components/main/MainSolution.jsx

@ -3,12 +3,91 @@ import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger"; import { ScrollTrigger } from "gsap/ScrollTrigger";
import { Plane, Radio, Ship, Car } from "lucide-react"; import { Plane, Radio, Ship, Car } from "lucide-react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useLanguage } from "../../context/LanguageContext";
gsap.registerPlugin(ScrollTrigger); gsap.registerPlugin(ScrollTrigger);
function FlightMockup({ animate }) { const FLIGHT_TABLE_TEXT = {
ko: {
liveTitle: "실시간 이동체 현황",
thead: ["구분", "사용/현황", "상태", "속도", "고도"],
stats: [
{ label: "전체", sub: "+12 실시간", icon: null },
{ label: "항공기", sub: "운항 중", icon: <Plane size={14} color="#a78bfa" /> },
{ label: "무인기", sub: "운항 중", icon: <Radio size={14} color="#60a5fa" /> },
{ label: "선박", sub: "항해 중", icon: <Ship size={14} color="#34d399" /> },
{ label: "차량", sub: "이동 중", icon: <Car size={14} color="#fb923c" /> },
],
rows: [
{
id: "✈ 항공기",
name: "KAI 123",
status: "운항 중",
statusType: "green",
speed: "820km/h",
alt: "10,200m",
},
{
id: "⬡ 무인기",
name: "DRN-045",
status: "운항 중",
statusType: "green",
speed: "45km/h",
alt: "120m",
},
{
id: "⚓ 선박",
name: "SEA-001",
status: "대기",
statusType: "yellow",
speed: "–",
alt: "–",
},
],
},
en: {
liveTitle: "Live Vehicle Status",
thead: ["Type", "ID", "Status", "Speed", "Altitude"],
stats: [
{ label: "Total", sub: "+12 live", icon: null },
{ label: "Aircraft", sub: "In flight", icon: <Plane size={14} color="#a78bfa" /> },
{ label: "Drone", sub: "In flight", icon: <Radio size={14} color="#60a5fa" /> },
{ label: "Vessel", sub: "At sea", icon: <Ship size={14} color="#34d399" /> },
{ label: "Vehicle", sub: "In transit", icon: <Car size={14} color="#fb923c" /> },
],
rows: [
{
id: "✈ Aircraft",
name: "KAI 123",
status: "In Flight",
statusType: "green",
speed: "820km/h",
alt: "10,200m",
},
{
id: "⬡ Drone",
name: "DRN-045",
status: "In Flight",
statusType: "green",
speed: "45km/h",
alt: "120m",
},
{
id: "⚓ Vessel",
name: "SEA-001",
status: "Standby",
statusType: "yellow",
speed: "–",
alt: "–",
},
],
},
};
function FlightMockup({ animate, lang }) {
const statsRef = useRef([]); const statsRef = useRef([]);
const rowsRef = useRef([]); const rowsRef = useRef([]);
const t = FLIGHT_TABLE_TEXT[lang];
useEffect(() => { useEffect(() => {
if (!animate) return; if (!animate) return;
@ -51,33 +130,18 @@ function FlightMockup({ animate }) {
}); });
}, [animate]); }, [animate]);
const stats = [
{ label: "전체", sub: "+12 실시간", icon: null },
{
label: "항공기",
sub: "운항 중",
icon: <Plane size={14} color="#a78bfa" />,
},
{
label: "무인기",
sub: "운항 중",
icon: <Radio size={14} color="#60a5fa" />,
},
{ label: "선박", sub: "항해 중", icon: <Ship size={14} color="#34d399" /> },
{ label: "차량", sub: "이동 중", icon: <Car size={14} color="#fb923c" /> },
];
return ( return (
<div className="dark-closeup"> <div className="dark-closeup">
<div className="dark-top-bar"> <div className="dark-top-bar">
<div className="dark-top-bar__title"> <div className="dark-top-bar__title">
<span className="dark-top-bar__live" /> <span className="dark-top-bar__live" />
실시간 이동체 현황 {t.liveTitle}
</div> </div>
<div className="dark-top-bar__time">2025.06.10 14:32:07</div> <div className="dark-top-bar__time">2025.06.10 14:32:07</div>
</div> </div>
<div className="dark-stat-grid"> <div className="dark-stat-grid">
{stats.map((s, i) => ( {t.stats.map((s, i) => (
<div key={i} className={`dark-stat-card${i === 0 ? " dark-stat-card--total" : ""}`}> <div key={i} className={`dark-stat-card${i === 0 ? " dark-stat-card--total" : ""}`}>
<div className="dark-stat-card__label">{s.label}</div> <div className="dark-stat-card__label">{s.label}</div>
<div <div
@ -94,38 +158,11 @@ function FlightMockup({ animate }) {
</div> </div>
<div className="dark-table-head"> <div className="dark-table-head">
<span>구분</span> {t.thead.map((label) => (
<span>사용/현황</span> <span key={label}>{label}</span>
<span>상태</span> ))}
<span>속도</span>
<span>고도</span>
</div> </div>
{[ {t.rows.map((row, i) => (
{
id: "✈ 항공기",
name: "KAI 123",
status: "운항 중",
statusType: "green",
speed: "820km/h",
alt: "10,200m",
},
{
id: "⬡ 무인기",
name: "DRN-045",
status: "운항 중",
statusType: "green",
speed: "45km/h",
alt: "120m",
},
{
id: "⚓ 선박",
name: "SEA-001",
status: "대기",
statusType: "yellow",
speed: "–",
alt: "–",
},
].map((row, i) => (
<div <div
key={i} key={i}
className={`dark-table-row dark-table-row--anim${i === 0 ? " dark-table-row--active" : ""}`} className={`dark-table-row dark-table-row--anim${i === 0 ? " dark-table-row--active" : ""}`}
@ -145,8 +182,47 @@ function FlightMockup({ animate }) {
</div> </div>
); );
} }
function IBEMockup({ animate }) {
const IBE_TEXT = {
ko: {
nav: ["검색", "선택", "일정추가", "부가서비스", "결제", "발권"],
fromLabel: "출발지",
toLabel: "도착지",
fromCity: "인천 (ICN)",
toCity: "도쿄 (NRT)",
dateLabel: "출발일",
dateVal: "2024.06.15 (토)",
searchBtn: "검색",
resultLabel: (n) => (
<>
<span>{n}개의</span> 일정 검색됨
</>
),
duration: "2시간 20분",
selectBtn: "선택",
},
en: {
nav: ["Search", "Select", "Add Trip", "Extras", "Payment", "Ticketing"],
fromLabel: "From",
toLabel: "To",
fromCity: "Incheon (ICN)",
toCity: "Tokyo (NRT)",
dateLabel: "Departure",
dateVal: "2024.06.15 (Sat)",
searchBtn: "Search",
resultLabel: (n) => (
<>
<span>{n}</span> flights found
</>
),
duration: "2h 20m",
selectBtn: "Select",
},
};
function IBEMockup({ animate, lang }) {
const priceRef = useRef(null); const priceRef = useRef(null);
const t = IBE_TEXT[lang];
useEffect(() => { useEffect(() => {
if (!animate) return; if (!animate) return;
@ -161,24 +237,29 @@ function IBEMockup({ animate }) {
return () => clearInterval(t); return () => clearInterval(t);
}, [animate]); }, [animate]);
const flights = [
{ dep: "09:00", arr: "11:20", price: null, active: true },
{ dep: "12:30", arr: "14:50", price: "150,000 KRW", active: false },
{ dep: "18:20", arr: "20:40", price: "120,000 KRW", active: false },
];
return ( return (
<div className="light-closeup"> <div className="light-closeup">
<div className="light-header"> <div className="light-header">
<div className="light-header__nav"> <div className="light-header__nav">
<span className="active">검색</span> {t.nav.map((label, i) => (
<span>선택</span> <span key={label} className={i === 0 ? "active" : ""}>
<span>일정추가</span> {label}
<span>부가서비스</span> </span>
<span>결제</span> ))}
<span>발권</span>
</div> </div>
</div> </div>
<div className="light-search-box"> <div className="light-search-box">
<div className="light-search-cities"> <div className="light-search-cities">
<div className="light-search-city"> <div className="light-search-city">
<div className="light-search-city__label">출발지</div> <div className="light-search-city__label">{t.fromLabel}</div>
<div className="light-search-city__val">ICN</div> <div className="light-search-city__val">ICN</div>
<div className="light-search-city__sub">인천 (ICN)</div> <div className="light-search-city__sub">{t.fromCity}</div>
</div> </div>
<div <div
className="light-search-arrow" className="light-search-arrow"
@ -192,26 +273,20 @@ function IBEMockup({ animate }) {
</div> </div>
<div className="light-search-city"> <div className="light-search-city">
<div className="light-search-city__label">도착지</div> <div className="light-search-city__label">{t.toLabel}</div>
<div className="light-search-city__val">NRT</div> <div className="light-search-city__val">NRT</div>
<div className="light-search-city__sub">도쿄 (NRT)</div> <div className="light-search-city__sub">{t.toCity}</div>
</div> </div>
</div> </div>
<div className="light-search-date"> <div className="light-search-date">
<div className="light-search-date__label">출발일</div> <div className="light-search-date__label">{t.dateLabel}</div>
<div className="light-search-date__val">2024.06.15 ()</div> <div className="light-search-date__val">{t.dateVal}</div>
</div> </div>
<div className="light-search-btn">검색</div> <div className="light-search-btn">{t.searchBtn}</div>
</div> </div>
<div className="light-result-label"> <div className="light-result-label">{t.resultLabel(128)}</div>
<span>128개의</span> 일정 검색됨 {flights.map((f, i) => (
</div>
{[
{ dep: "09:00", arr: "11:20", price: null, active: true },
{ dep: "12:30", arr: "14:50", price: "150,000 KRW", active: false },
{ dep: "18:20", arr: "20:40", price: "120,000 KRW", active: false },
].map((f, i) => (
<div key={i} className={`light-flight${f.active ? " light-flight--active" : ""}`}> <div key={i} className={`light-flight${f.active ? " light-flight--active" : ""}`}>
<div className="light-flight__times"> <div className="light-flight__times">
<div> <div>
@ -220,7 +295,7 @@ function IBEMockup({ animate }) {
</div> </div>
<div> <div>
<div className="light-flight__arrow"></div> <div className="light-flight__arrow"></div>
<div className="light-flight__stop">2시간 20</div> <div className="light-flight__stop">{t.duration}</div>
</div> </div>
<div> <div>
<div className="light-flight__time">{f.arr}</div> <div className="light-flight__time">{f.arr}</div>
@ -231,7 +306,7 @@ function IBEMockup({ animate }) {
<div className="light-flight__price" ref={f.active ? priceRef : null}> <div className="light-flight__price" ref={f.active ? priceRef : null}>
{f.active ? "0 KRW" : f.price} {f.active ? "0 KRW" : f.price}
</div> </div>
<div className="light-flight__btn">선택</div> <div className="light-flight__btn">{t.selectBtn}</div>
</div> </div>
</div> </div>
))} ))}
@ -239,13 +314,19 @@ function IBEMockup({ animate }) {
); );
} }
function MainSolution() { const SECTION_TEXT = {
const sectionRef = useRef(null); ko: {
const headRef = useRef(null); eyebrow: "PAL SOLUTION",
const cardsRef = useRef([]); title: (
const [animate, setAnimate] = useState(false); <>
항공 IT
const solutions = [ <br />
서비스 솔루션
</>
),
desc: "항공 운항 관리부터 스마트 관광 예약 플랫폼, 클라우드 인프라까지 다양한 항공·모빌리티 서비스를 하나의 통합 시스템 안에서 제공합니다.",
link: "자세히 보기",
solutions: [
{ {
label: "비행 관리", label: "비행 관리",
title: "비행상황관리 시스템", title: "비행상황관리 시스템",
@ -263,7 +344,48 @@ function MainSolution() {
link: "/solution/ibe", link: "/solution/ibe",
theme: "light", theme: "light",
}, },
]; ],
},
en: {
eyebrow: "PAL SOLUTION",
title: (
<>
Aviation IT
<br />
Service Solutions
</>
),
desc: "From flight operations management to smart tourism booking platforms and cloud infrastructure, we deliver a wide range of aviation and mobility services within a single integrated system.",
link: "Learn More",
solutions: [
{
label: "Flight Management",
title: "Flight Control System",
desc: "Monitor the real-time location and operational status of aircraft, drones, vessels, and vehicles on a single platform. Track altitude, speed, and route history with precision, and respond instantly when anomalies occur.",
mockup: "flight",
link: "/solution/flight-control",
theme: "dark",
},
{
label: "Booking System",
title: "IBE",
titleSub: "Internet Booking Engine",
desc: "A next-generation booking engine that connects the entire process, from flight search to reservation, payment, and ticketing. It flexibly integrates with diverse channels and systems to deliver an optimal booking experience.",
mockup: "ibe",
link: "/solution/ibe",
theme: "light",
},
],
},
};
function MainSolution() {
const { lang, withLang } = useLanguage();
const t = SECTION_TEXT[lang];
const sectionRef = useRef(null);
const headRef = useRef(null);
const cardsRef = useRef([]);
const [animate, setAnimate] = useState(false);
useEffect(() => { useEffect(() => {
const ctx = gsap.context(() => { const ctx = gsap.context(() => {
@ -305,17 +427,13 @@ function MainSolution() {
<section className="main-solution-section" ref={sectionRef}> <section className="main-solution-section" ref={sectionRef}>
<div className="main-solution-inner"> <div className="main-solution-inner">
<div className="main-solution-head" ref={headRef}> <div className="main-solution-head" ref={headRef}>
<p className="main-solution-eyebrow">PAL SOLUTION</p> <p className="main-solution-eyebrow">{t.eyebrow}</p>
<h2 className="main-solution-title"> <h2 className="main-solution-title">{t.title}</h2>
항공 IT <p className="main-solution-desc">{t.desc}</p>
<br />
서비스 솔루션
</h2>
<p className="main-solution-desc">항공 운항 관리부터 스마트 관광 예약 플랫폼, 클라우드 인프라까지 다양한 항공·모빌리티 서비스를 하나의 통합 시스템 안에서 제공합니다.</p>
</div> </div>
<div className="main-solution-grid"> <div className="main-solution-grid">
{solutions.map((item, index) => ( {t.solutions.map((item, index) => (
<article <article
key={index} key={index}
className={`main-solution-card main-solution-card--${item.theme}`} className={`main-solution-card main-solution-card--${item.theme}`}
@ -330,20 +448,20 @@ function MainSolution() {
{item.title.split("\n").map((line, i) => ( {item.title.split("\n").map((line, i) => (
<span key={i}> <span key={i}>
{line} {line}
{i === 0 && <br />} {i === 0 && item.title.includes("\n") && <br />}
</span> </span>
))} ))}
</h3> </h3>
{item.titleSub && <p className="main-solution-card-title-sub">{item.titleSub}</p>} {item.titleSub && <p className="main-solution-card-title-sub">{item.titleSub}</p>}
<p className="main-solution-card-desc">{item.desc}</p> <p className="main-solution-card-desc">{item.desc}</p>
</div> </div>
<Link to={item.link} className="main-solution-card-link"> <Link to={withLang(item.link)} className="main-solution-card-link">
자세히 보기 <span></span> {t.link} <span></span>
</Link> </Link>
</div> </div>
<div className="main-solution-card-right"> <div className="main-solution-card-right">
{item.mockup === "flight" && <FlightMockup animate={animate} />} {item.mockup === "flight" && <FlightMockup animate={animate} lang={lang} />}
{item.mockup === "ibe" && <IBEMockup animate={animate} />} {item.mockup === "ibe" && <IBEMockup animate={animate} lang={lang} />}
</div> </div>
</article> </article>
))} ))}

67
src/components/main/MainUam.jsx

@ -2,9 +2,55 @@ import { useEffect, useRef } from "react";
import { gsap } from "gsap"; import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger"; import { ScrollTrigger } from "gsap/ScrollTrigger";
import uamImg from "../../../public/images/uam-silver.png"; import uamImg from "../../../public/images/uam-silver.png";
import { useLanguage } from "../../context/LanguageContext";
gsap.registerPlugin(ScrollTrigger); gsap.registerPlugin(ScrollTrigger);
const TEXT = {
ko: {
utmLabel: "UTM SYSTEM",
utmTitle: "드론 하늘길에서",
uamLabel: "UAM SYSTEM",
uamTitle: (
<>
도심 항공
<span className="airspace-mobile-br"> </span>
네트워크로
</>
),
networkLabel: "UAM NETWORK",
networkTitle: "Urban Air Mobility",
networkDesc: (
<>
도심 버티포트, 운항 경로, 항공 교통 데이터를 <br />
하나의 네트워크로 연결해 미래형 항공 이동 환경을 구축합니다.
</>
),
},
en: {
utmLabel: "UTM SYSTEM",
utmTitle: "From drone skies",
uamLabel: "UAM SYSTEM",
uamTitle: (
<>
to an urban air
<span className="airspace-mobile-br"> </span>
network.
</>
),
networkLabel: "UAM NETWORK",
networkTitle: "Urban Air Mobility",
networkDesc: (
<>
Vertiports, flight paths, and air traffic data <br />
connected into one network for the future of air mobility.
</>
),
},
};
function MainUam() { function MainUam() {
const { lang } = useLanguage();
const t = TEXT[lang];
const sectionRef = useRef(null); const sectionRef = useRef(null);
const leftRef = useRef(null); const leftRef = useRef(null);
const rightRef = useRef(null); const rightRef = useRef(null);
@ -185,17 +231,13 @@ function MainUam() {
<canvas ref={canvasRef} className="aurora-canvas" /> <canvas ref={canvasRef} className="aurora-canvas" />
<div className="airspace-panel airspace-panel--utm" ref={leftRef}> <div className="airspace-panel airspace-panel--utm" ref={leftRef}>
<p>UTM SYSTEM</p> <p>{t.utmLabel}</p>
<h2>드론 하늘길에서</h2> <h2>{t.utmTitle}</h2>
</div> </div>
<div className="airspace-panel airspace-panel--uam" ref={rightRef}> <div className="airspace-panel airspace-panel--uam" ref={rightRef}>
<p>UAM SYSTEM</p> <p>{t.uamLabel}</p>
<h2> <h2>{t.uamTitle}</h2>
도심 항공
<span className="airspace-mobile-br"> </span>
네트워크로
</h2>
</div> </div>
<div className="airspace-lines"> <div className="airspace-lines">
@ -217,12 +259,9 @@ function MainUam() {
</div> </div>
<div className="airspace-uam-content"> <div className="airspace-uam-content">
<p>UAM NETWORK</p> <p>{t.networkLabel}</p>
<h2>Urban Air Mobility</h2> <h2>{t.networkTitle}</h2>
<span> <span>{t.networkDesc}</span>
도심 버티포트, 운항 경로, 항공 교통 데이터를 <br />
하나의 네트워크로 연결해 미래형 항공 이동 환경을 구축합니다.
</span>
</div> </div>
</section> </section>
); );

284
src/components/main/MainUtm.jsx

@ -1,23 +1,37 @@
import { useRef, useEffect, useState } from "react"; import { useRef, useEffect, useState } from "react";
import { motion, useInView, AnimatePresence } from "framer-motion"; import { motion, useInView, AnimatePresence } from "framer-motion";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useLanguage } from "../../context/LanguageContext";
const ease = [0.22, 1, 0.36, 1]; const ease = [0.22, 1, 0.36, 1];
const FEATURES = [ const FEATURES_KO = [
{ num: "01", label: "비행 계획" }, { num: "01", label: "비행 계획" },
{ num: "02", label: "비행 승인" }, { num: "02", label: "비행 승인" },
{ num: "03", label: "실시간 관제" }, { num: "03", label: "실시간 관제" },
{ num: "04", label: "데이터 관리" }, { num: "04", label: "데이터 관리" },
]; ];
const FLIGHT_PLANS = [ const FEATURES_EN = [
{ num: "01", label: "Flight Plan" },
{ num: "02", label: "Approval" },
{ num: "03", label: "Live Control" },
{ num: "04", label: "Data" },
];
const FLIGHT_PLANS_KO = [
{ id: "FP-001", pilot: "홍길동", route: "서울 → 김포", status: "대기" }, { id: "FP-001", pilot: "홍길동", route: "서울 → 김포", status: "대기" },
{ id: "FP-002", pilot: "김철수", route: "인천 → 수원", status: "대기" }, { id: "FP-002", pilot: "김철수", route: "인천 → 수원", status: "대기" },
{ id: "FP-003", pilot: "이영희", route: "김포 → 부천", status: "승인" }, { id: "FP-003", pilot: "이영희", route: "김포 → 부천", status: "승인" },
]; ];
const DETAIL = { const FLIGHT_PLANS_EN = [
{ id: "FP-001", pilot: "J. Doe", route: "ICN → GMP", status: "Pending" },
{ id: "FP-002", pilot: "K. Kim", route: "ICN → SWU", status: "Pending" },
{ id: "FP-003", pilot: "Y. Lee", route: "GMP → BCN", status: "Approved" },
];
const DETAIL_KO = {
id: "FP-001", id: "FP-001",
pilot: "홍길동", pilot: "홍길동",
pilotId: "hong001", pilotId: "hong001",
@ -27,110 +41,127 @@ const DETAIL = {
date: "2025-06-10 14:30", date: "2025-06-10 14:30",
}; };
const DETAIL_EN = {
id: "FP-001",
pilot: "J. Doe",
pilotId: "jdoe001",
route: "ICN → GMP",
altitude: "100m",
speed: "13m/s",
date: "2025-06-10 14:30",
};
const PHASES = ["list", "detail", "confirm", "done"]; const PHASES = ["list", "detail", "confirm", "done"];
const PHASE_DURATION = { list: 1800, detail: 2200, confirm: 1800, done: 2000 }; const PHASE_DURATION = { list: 1800, detail: 2200, confirm: 1800, done: 2000 };
function UtmSystemPanel({ phase, activeRow }) { const PANEL_TEXT = {
ko: {
panelTitle: "비행계획 승인관리",
badge: (n) => `검색결과 총 ${n}`,
thead: ["계획 ID", "신청자", "경로", "상태"],
detailBtn: "상세보기",
detailTitle: "비행계획 상세",
rowLabels: ["계획 ID", "신청자", "경로", "고도", "신청일시", "상태"],
approveBtn: "승인처리",
toast: "FP-001 승인이 완료되었습니다.",
confirmTitle: "비행계획을 승인하시겠습니까?",
cancel: "취소",
ok: "확인",
statusDone: "승인",
statusWait: "대기",
},
en: {
panelTitle: "Flight Plan Approval",
badge: (n) => `${n} results found`,
thead: ["Plan ID", "Pilot", "Route", "Status"],
detailBtn: "View",
detailTitle: "Flight Plan Details",
rowLabels: ["Plan ID", "Pilot", "Route", "Altitude", "Requested", "Status"],
approveBtn: "Approve",
toast: "FP-001 has been approved.",
confirmTitle: "Approve this flight plan?",
cancel: "Cancel",
ok: "Confirm",
statusDone: "Approved",
statusWait: "Pending",
},
};
function UtmSystemPanel({ phase, activeRow, lang, flightPlans, detail }) {
const t = PANEL_TEXT[lang];
return ( return (
<div className="utm-panel"> <div className="utm-panel">
<div className="utm-panel__header"> <div className="utm-panel__header">
<span className="utm-panel__title">비행계획 승인관리</span> <span className="utm-panel__title">{t.panelTitle}</span>
<span className="utm-panel__badge"> <span className="utm-panel__badge">{t.badge(flightPlans.length)}</span>
검색결과 {FLIGHT_PLANS.length}
</span>
</div> </div>
<div className="utm-panel__table"> <div className="utm-panel__table">
<div className="utm-panel__thead"> <div className="utm-panel__thead">
<span>계획 ID</span> {t.thead.map((label) => (
<span>신청자</span> <span key={label}>{label}</span>
<span>경로</span> ))}
<span>상태</span>
<span></span> <span></span>
</div> </div>
{FLIGHT_PLANS.map((row, i) => ( {flightPlans.map((row, i) => (
<div <div key={row.id} className={`utm-panel__row${activeRow === i ? " utm-panel__row--active" : ""}`}>
key={row.id}
className={`utm-panel__row${activeRow === i ? " utm-panel__row--active" : ""}`}
>
<span className="utm-panel__cell">{row.id}</span> <span className="utm-panel__cell">{row.id}</span>
<span className="utm-panel__cell">{row.pilot}</span> <span className="utm-panel__cell">{row.pilot}</span>
<span className="utm-panel__cell">{row.route}</span> <span className="utm-panel__cell">{row.route}</span>
<span <span className={`utm-panel__status utm-panel__status--${row.status === t.statusDone ? "done" : "wait"}`}>{row.status}</span>
className={`utm-panel__status utm-panel__status--${row.status === "승인" ? "done" : "wait"}`}
>
{row.status}
</span>
<span className="utm-panel__cell"> <span className="utm-panel__cell">
<span <span className={`utm-panel__btn${activeRow === i && phase === "list" ? " utm-panel__btn--hover" : ""}`}>{t.detailBtn}</span>
className={`utm-panel__btn${activeRow === i && phase === "list" ? " utm-panel__btn--hover" : ""}`}
>
상세보기
</span>
</span> </span>
</div> </div>
))} ))}
</div> </div>
{/* 상세: 아래로 펼쳐짐 */} {/* 상세: 아래로 펼쳐짐 */}
<div <div className={`utm-panel__detail${phase === "detail" || phase === "confirm" || phase === "done" ? " utm-panel__detail--show" : ""}`}>
className={`utm-panel__detail${phase === "detail" || phase === "confirm" || phase === "done" ? " utm-panel__detail--show" : ""}`} <div className="utm-panel__detail-title">{t.detailTitle}</div>
>
<div className="utm-panel__detail-title">비행계획 상세</div>
<div className="utm-panel__detail-rows"> <div className="utm-panel__detail-rows">
<div className="utm-panel__detail-row"> <div className="utm-panel__detail-row">
<span>계획 ID</span> <span>{t.rowLabels[0]}</span>
<span>{DETAIL.id}</span> <span>{detail.id}</span>
</div> </div>
<div className="utm-panel__detail-row"> <div className="utm-panel__detail-row">
<span>신청자</span> <span>{t.rowLabels[1]}</span>
<span> <span>
{DETAIL.pilot} ({DETAIL.pilotId}) {detail.pilot} ({detail.pilotId})
</span> </span>
</div> </div>
<div className="utm-panel__detail-row"> <div className="utm-panel__detail-row">
<span>경로</span> <span>{t.rowLabels[2]}</span>
<span>{DETAIL.route}</span> <span>{detail.route}</span>
</div> </div>
<div className="utm-panel__detail-row"> <div className="utm-panel__detail-row">
<span>고도</span> <span>{t.rowLabels[3]}</span>
<span> <span>
{DETAIL.altitude} · {DETAIL.speed} {detail.altitude} · {detail.speed}
</span> </span>
</div> </div>
<div className="utm-panel__detail-row"> <div className="utm-panel__detail-row">
<span>신청일시</span> <span>{t.rowLabels[4]}</span>
<span>{DETAIL.date}</span> <span>{detail.date}</span>
</div> </div>
<div className="utm-panel__detail-row"> <div className="utm-panel__detail-row">
<span>상태</span> <span>{t.rowLabels[5]}</span>
<span <span className={`utm-panel__status utm-panel__status--${phase === "done" ? "done" : "wait"}`}>{phase === "done" ? t.statusDone : t.statusWait}</span>
className={`utm-panel__status utm-panel__status--${phase === "done" ? "done" : "wait"}`}
>
{phase === "done" ? "승인" : "대기"}
</span>
</div> </div>
</div> </div>
{phase === "detail" && ( {phase === "detail" && (
<div className="utm-panel__detail-actions"> <div className="utm-panel__detail-actions">
<span className="utm-panel__approve-btn utm-panel__approve-btn--hover"> <span className="utm-panel__approve-btn utm-panel__approve-btn--hover">{t.approveBtn}</span>
승인처리
</span>
</div> </div>
)} )}
<AnimatePresence> <AnimatePresence>
{phase === "done" && ( {phase === "done" && (
<motion.div <motion.div className="utm-panel__toast" initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0 }} transition={{ duration: 0.35, ease }}>
className="utm-panel__toast"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.35, ease }}
>
<span className="utm-panel__toast-icon"></span> <span className="utm-panel__toast-icon"></span>
FP-001 승인이 완료되었습니다. {t.toast}
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
@ -139,30 +170,16 @@ function UtmSystemPanel({ phase, activeRow }) {
{/* 컨펌: 패널 안에서 블러 오버레이 */} {/* 컨펌: 패널 안에서 블러 오버레이 */}
<AnimatePresence> <AnimatePresence>
{phase === "confirm" && ( {phase === "confirm" && (
<motion.div <motion.div className="utm-panel__confirm-overlay" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.25 }}>
className="utm-panel__confirm-overlay" <motion.div className="utm-confirm" initial={{ opacity: 0, scale: 0.92, y: 16 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.95 }} transition={{ duration: 0.35, ease }}>
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25 }}
>
<motion.div
className="utm-confirm"
initial={{ opacity: 0, scale: 0.92, y: 16 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.35, ease }}
>
<div className="utm-confirm__icon"></div> <div className="utm-confirm__icon"></div>
<div className="utm-confirm__title"> <div className="utm-confirm__title">{t.confirmTitle}</div>
비행계획을 승인하시겠습니까?
</div>
<div className="utm-confirm__desc"> <div className="utm-confirm__desc">
FP-001 · 홍길동 · 서울 김포 {detail.id} · {detail.pilot} · {detail.route}
</div> </div>
<div className="utm-confirm__btns"> <div className="utm-confirm__btns">
<span className="utm-confirm__cancel">취소</span> <span className="utm-confirm__cancel">{t.cancel}</span>
<span className="utm-confirm__ok">확인</span> <span className="utm-confirm__ok">{t.ok}</span>
</div> </div>
</motion.div> </motion.div>
</motion.div> </motion.div>
@ -172,13 +189,61 @@ function UtmSystemPanel({ phase, activeRow }) {
); );
} }
const HERO_TEXT = {
ko: {
eyebrow: "UTM / UATM PLATFORM",
title: (
<>
드론 비행의 모든 과정을
<br />
<em>하나의 플랫폼에서</em>
</>
),
desc: (
<>
비행 계획부터 승인, 실시간 관제, 데이터 관리까지
<br /> 안전하고 효율적인 하늘길을 만듭니다.
</>
),
link: "자세히 보기",
mapAlt: "UTM 관제 지도",
mobileAlt: "UTM 관제 시스템",
},
en: {
eyebrow: "UTM / UATM PLATFORM",
title: (
<>
Every step of drone flight
<br />
<em>on a single platform</em>
</>
),
desc: (
<>
From flight planning to approval, live control, and data management,
<br />
we build safer, more efficient skies.
</>
),
link: "Learn More",
mapAlt: "UTM control map",
mobileAlt: "UTM control system",
},
};
function MainUtm() { function MainUtm() {
const { lang, withLang } = useLanguage();
const ref = useRef(null); const ref = useRef(null);
const inView = useInView(ref, { once: false, margin: "-80px" }); const inView = useInView(ref, { once: false, margin: "-80px" });
const [phase, setPhase] = useState("list"); const [phase, setPhase] = useState("list");
const [activeRow] = useState(0); const [activeRow] = useState(0);
const cursorRef = useRef(null); const cursorRef = useRef(null);
const FEATURES = lang === "en" ? FEATURES_EN : FEATURES_KO;
const flightPlans = lang === "en" ? FLIGHT_PLANS_EN : FLIGHT_PLANS_KO;
const detail = lang === "en" ? DETAIL_EN : DETAIL_KO;
const t = HERO_TEXT[lang];
useEffect(() => { useEffect(() => {
if (!inView) return; if (!inView) return;
let t; let t;
@ -282,42 +347,19 @@ function MainUtm() {
<div className="utm-hero__blob utm-hero__blob--2" /> <div className="utm-hero__blob utm-hero__blob--2" />
<div className="utm-hero__inner"> <div className="utm-hero__inner">
<motion.span <motion.span className="utm-hero__eyebrow" initial={{ opacity: 0, y: 12 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
className="utm-hero__eyebrow" {t.eyebrow}
initial={{ opacity: 0, y: 12 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease }}
>
UTM / UATM PLATFORM
</motion.span> </motion.span>
<motion.h2 <motion.h2 className="utm-hero__title" initial={{ opacity: 0, y: 24 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.7, delay: 0.08, ease }}>
className="utm-hero__title" {t.title}
initial={{ opacity: 0, y: 24 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.7, delay: 0.08, ease }}
>
드론 비행의 모든 과정을
<br />
<em>하나의 플랫폼에서</em>
</motion.h2> </motion.h2>
<motion.p <motion.p className="utm-hero__desc" initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.16, ease }}>
className="utm-hero__desc" {t.desc}
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.16, ease }}
>
비행 계획부터 승인, 실시간 관제, 데이터 관리까지
<br /> 안전하고 효율적인 하늘길을 만듭니다.
</motion.p> </motion.p>
<motion.ul <motion.ul className="utm-hero__chips" initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.24, ease }}>
className="utm-hero__chips"
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.24, ease }}
>
{FEATURES.map((f) => ( {FEATURES.map((f) => (
<li key={f.num} className="utm-hero__chip"> <li key={f.num} className="utm-hero__chip">
<span className="utm-hero__chip-num">{f.num}</span> <span className="utm-hero__chip-num">{f.num}</span>
@ -326,29 +368,19 @@ function MainUtm() {
))} ))}
<li> <li>
<Link to="/utm/intro" className="utm-hero__link"> <Link to={withLang("/utm/intro")} className="utm-hero__link">
자세히 보기 <span></span> {t.link} <span></span>
</Link> </Link>
</li> </li>
</motion.ul> </motion.ul>
<motion.div <motion.div className="utm-hero__showcase" initial={{ opacity: 0, y: 48 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.9, delay: 0.32, ease }}>
className="utm-hero__showcase"
initial={{ opacity: 0, y: 48 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.9, delay: 0.32, ease }}
>
{/* PC 전용 — 애니메이션 */} {/* PC 전용 — 애니메이션 */}
<div className="utm-hero__pc-only"> <div className="utm-hero__pc-only">
<div className="utm-hero__map-wrap"> <div className="utm-hero__map-wrap">
<img <img src={`${import.meta.env.BASE_URL}images/main_utm_img.png`} alt={t.mapAlt} className="utm-hero__map-img" draggable="false" />
src={`${import.meta.env.BASE_URL}images/main_utm_img.png`}
alt="UTM 관제 지도"
className="utm-hero__map-img"
draggable="false"
/>
</div> </div>
<UtmSystemPanel phase={phase} activeRow={activeRow} /> <UtmSystemPanel phase={phase} activeRow={activeRow} lang={lang} flightPlans={flightPlans} detail={detail} />
<div className="utm-cursor" ref={cursorRef}> <div className="utm-cursor" ref={cursorRef}>
<div className="utm-cursor__dot" /> <div className="utm-cursor__dot" />
</div> </div>
@ -356,11 +388,7 @@ function MainUtm() {
{/* 모바일 전용 — 이미지 */} {/* 모바일 전용 — 이미지 */}
<div className="utm-hero__mobile-only"> <div className="utm-hero__mobile-only">
<img <img src={`${import.meta.env.BASE_URL}images/main_utm_mobile.png`} alt={t.mobileAlt} className="utm-hero__mobile-img" />
src={`${import.meta.env.BASE_URL}images/main_utm_mobile.png`}
alt="UTM 관제 시스템"
className="utm-hero__mobile-img"
/>
</div> </div>
</motion.div> </motion.div>
</div> </div>

12
src/components/main/MainVisual.jsx

@ -1,10 +1,12 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { gsap } from "gsap"; import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger"; import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useLanguage } from "../../context/LanguageContext";
gsap.registerPlugin(ScrollTrigger); gsap.registerPlugin(ScrollTrigger);
function MainVisual() { function MainVisual() {
const { lang } = useLanguage();
const sectionRef = useRef(null); const sectionRef = useRef(null);
const bgRef = useRef(null); const bgRef = useRef(null);
const hero2Ref = useRef(null); const hero2Ref = useRef(null);
@ -133,9 +135,19 @@ function MainVisual() {
</div> </div>
<div className="text text-change text-center-hero" ref={text2Ref}> <div className="text text-change text-center-hero" ref={text2Ref}>
{lang === "en" ? (
<>
Designing safer skies through
<br />
aviation data and integrated air traffic control.
</>
) : (
<>
항공 데이터와 통합 관제 기술로 항공 데이터와 통합 관제 기술로
<br /> <br />
안전한 하늘길을 설계합니다 안전한 하늘길을 설계합니다
</>
)}
</div> </div>
</div> </div>

41
src/context/LanguageContext.jsx

@ -0,0 +1,41 @@
import { createContext, useContext, useMemo } from "react";
import { useLocation, useNavigate } from "react-router-dom";
const LanguageContext = createContext(null);
export function LanguageProvider({ children }) {
const { pathname } = useLocation();
const navigate = useNavigate();
const lang = pathname === "/en" || pathname.startsWith("/en/") ? "en" : "ko";
const value = useMemo(() => {
// ko , en /en prefix
const withLang = (path) => {
if (lang !== "en") return path;
return path === "/" ? "/en/main" : `/en${path}`;
};
//
const toggleLang = () => {
if (lang === "en") {
navigate(pathname.replace(/^\/en/, "") || "/main");
} else {
navigate(`/en${pathname}`);
}
};
return { lang, withLang, toggleLang };
}, [lang, pathname, navigate]);
return <LanguageContext.Provider value={value}>{children}</LanguageContext.Provider>;
}
// eslint-disable-next-line react-refresh/only-export-components
export function useLanguage() {
const ctx = useContext(LanguageContext);
if (!ctx) {
throw new Error("useLanguage must be used within LanguageProvider");
}
return ctx;
}

23
src/css/common.css

@ -214,21 +214,21 @@ body{overflow-x:hidden;}
.location-map { width: 100%; height: 480px; border-radius: 20px; overflow: hidden; } .location-map { width: 100%; height: 480px; border-radius: 20px; overflow: hidden; }
.location-map iframe { width: 100%; height: 100%; border: none; display: block; } .location-map iframe { width: 100%; height: 100%; border: none; display: block; }
.location-info-card { display: flex; flex-direction: column; } .location-info-card { display: flex; flex-direction: column; }
.location-info-card h3 { font-size: 12px; font-weight: 700; color: #bbb; letter-spacing: 0.12em; text-transform: uppercase; margin: 0 0 32px; } .location-info-card h3 { font-size: 14px; font-weight: 700; color: #bbb; letter-spacing: 0.12em; text-transform: uppercase; margin: 0 0 32px; }
.location-info-wrapper { align-self: start; position: sticky; top: 100px; max-height: calc(100vh - 200px); overflow-y: auto; } .location-info-wrapper { align-self: start; position: sticky; top: 100px; max-height: calc(100vh - 200px); overflow-y: auto; }
.location-info-list { list-style: none; margin: 0 0 auto; padding: 0; display: flex; flex-direction: column; gap: 24px; } .location-info-list { list-style: none; margin: 0 0 auto; padding: 0; display: flex; flex-direction: column; gap: 24px; }
.location-info-item { display: flex; flex-direction: column; gap: 4px; } .location-info-item { display: flex; flex-direction: column; gap: 4px; }
.location-info-label { font-size: 11px; font-weight: 600; color: #bbb; letter-spacing: 0.1em; text-transform: uppercase; } .location-info-label { font-size: 14px; font-weight: 600; color: #bbb; letter-spacing: 0.1em; text-transform: uppercase; }
.location-info-value { cursor: default; font-size: 14px; font-weight: 500; color: #111; text-decoration: none; line-height: 1.6; } .location-info-value { cursor: default; font-size: 15px; font-weight: 500; color: #111; text-decoration: none; line-height: 1.6; }
.location-hours { border-top: 1px solid #ebebeb; padding-top: 20px; margin-top: 28px; margin-bottom: 20px; } .location-hours { border-top: 1px solid #ebebeb; padding-top: 20px; margin-top: 28px; margin-bottom: 20px; }
.location-hours-eyebrow { margin: 0 0 8px; font-size: 11px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: rgba(17,17,17,.35); } .location-hours-eyebrow { margin: 0 0 8px; font-size: 14px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: rgba(17,17,17,.35); }
.location-hours-label { margin: 0; font-size: 15px; font-weight: 700; color: #111; letter-spacing: -0.02em; line-height: 1.6; } .location-hours-label { margin: 0; font-size: 15px; font-weight: 700; color: #111; letter-spacing: -0.02em; line-height: 1.6; }
.location-hours-text { font-size: 13px; font-weight: 400; color: #bbb; } .location-hours-text { font-size: 13px; font-weight: 400; color: #bbb; }
.location-inquiry-btn { display: flex; align-items: center; justify-content: center; height: 50px; background: var(--color-primary); border-radius: 12px; font-size: 15px; font-weight: 700; color: #fff; text-decoration: none; letter-spacing: -.01em; transition: opacity .2s; } .location-inquiry-btn { display: flex; align-items: center; justify-content: center; height: 50px; background: var(--color-primary); border-radius: 12px; font-size: 15px; font-weight: 700; color: #fff; text-decoration: none; letter-spacing: -.01em; transition: opacity .2s; }
.location-transport { padding-top: 32px; padding-bottom: 0; } .location-transport { padding-top: 32px; padding-bottom: 0; }
.location-transport-title { font-size: 16px; font-weight: 700; color: #111; letter-spacing: -0.03em; margin: 0 0 20px; } .location-transport-title { font-size: 16px; font-weight: 700; color: #111; letter-spacing: -0.03em; margin: 0 0 20px; }
.location-transport-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1.25rem; } .location-transport-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1.25rem; }
.location-transport-item { display: flex; flex-direction: column; gap: 14px; padding: 24px 20px; border-radius: 16px; background: #fff; border: 1px solid #e8e8e8; } .location-transport-item { display: flex; flex-direction: column; gap: 28px; padding: 24px 20px; border-radius: 16px; background: #fff; border: 1px solid #e8e8e8; }
.location-transport-item-top { display: flex; align-items: center; justify-content: space-between; } .location-transport-item-top { display: flex; align-items: center; justify-content: space-between; }
.location-transport-icon { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; } .location-transport-icon { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.location-transport-icon img { width: 24px; height: 24px; object-fit: contain; } .location-transport-icon img { width: 24px; height: 24px; object-fit: contain; }
@ -241,8 +241,8 @@ body{overflow-x:hidden;}
.location-transport-item:nth-child(3) .location-transport-badge { background: #3d51b8; } .location-transport-item:nth-child(3) .location-transport-badge { background: #3d51b8; }
.location-transport-routes { display: flex; flex-direction: column; gap: 10px; } .location-transport-routes { display: flex; flex-direction: column; gap: 10px; }
.location-transport-route { display: flex; flex-direction: column; gap: 3px; padding-left: 10px; border-left: 2px solid #f0f0f0; } .location-transport-route { display: flex; flex-direction: column; gap: 3px; padding-left: 10px; border-left: 2px solid #f0f0f0; }
.location-transport-route-title { font-size: 12px; font-weight: 700; color: #333; margin: 0; } .location-transport-route-title { font-size: 15px; font-weight: 700; color: #333; margin: 0; }
.location-transport-route-desc { font-size: 12px; color: #888; line-height: 1.6; margin: 0; white-space: pre-line; } .location-transport-route-desc { font-size: 14px; color: #888; line-height: 1.6; margin: 0; white-space: pre-line; }
.location-info-section-title { display: none; font-size: 16px; font-weight: 700; color: #111; letter-spacing: -0.03em; margin: 0 0 20px; } .location-info-section-title { display: none; font-size: 16px; font-weight: 700; color: #111; letter-spacing: -0.03em; margin: 0 0 20px; }
@media (max-width: 768px) { @media (max-width: 768px) {
.location-tabs { margin-bottom: 24px; } .location-tabs { margin-bottom: 24px; }
@ -718,7 +718,7 @@ body{overflow-x:hidden;}
.fc-intro__title { font-size: 2rem; font-weight: 800; color: #1a1f3a; line-height: 1.35; letter-spacing: -0.02em; margin: 16px 0 28px; } .fc-intro__title { font-size: 2rem; font-weight: 800; color: #1a1f3a; line-height: 1.35; letter-spacing: -0.02em; margin: 16px 0 28px; }
.fc-intro__desc { font-size:16px; color: #666; line-height: 1.9; margin-bottom: 48px; } .fc-intro__desc { font-size:16px; color: #666; line-height: 1.9; margin-bottom: 48px; }
.fc-intro__icons { display: flex; gap: 32px; } .fc-intro__icons { display: flex; gap: 32px; }
.fc-intro__icon-item { display: flex; flex-direction: column; align-items: center; gap: 10px; } .fc-intro__icon-item { display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center;}
.fc-intro__icon-item img { width: 80px; height: 80px; object-fit: contain; } .fc-intro__icon-item img { width: 80px; height: 80px; object-fit: contain; }
.fc-intro__icon-item span { font-size:14px; font-weight: 600; color: #888; } .fc-intro__icon-item span { font-size:14px; font-weight: 600; color: #888; }
.fc-intro__right { flex: 0 0 52%; } .fc-intro__right { flex: 0 0 52%; }
@ -966,7 +966,7 @@ body{overflow-x:hidden;}
.si_archive__slider:active { cursor: grabbing; } .si_archive__slider:active { cursor: grabbing; }
.si_archive__track { display: flex; gap: 18px; will-change: transform; padding-right: 40px; padding-left: 1px; padding-bottom: 1px;} .si_archive__track { display: flex; gap: 18px; will-change: transform; padding-right: 40px; padding-left: 1px; padding-bottom: 1px;}
.si_archive__card { flex: 0 0 65%; padding: 20px; background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; box-sizing: border-box; display: flex; flex-direction: column; isolation: isolate; } .si_archive__card {max-width:1120px;flex: 0 0 65%; padding: 20px; background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; box-sizing: border-box; display: flex; flex-direction: column; isolation: isolate; }
.si_archive__card-header { display: flex; align-items: center; gap: 12px; } .si_archive__card-header { display: flex; align-items: center; gap: 12px; }
.si_archive__card-img { width: 100%; aspect-ratio: 16/8; flex-shrink: 0; background: #eef2f7; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }.si_archive__card-img img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; border-radius: 8px; } .si_archive__card-img { width: 100%; aspect-ratio: 16/8; flex-shrink: 0; background: #eef2f7; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }.si_archive__card-img img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; border-radius: 8px; }
.si_archive__card-img-placeholder { position:absolute; inset:0; background:linear-gradient(135deg,#cbd5e1 0%,#94a3b8 100%); } .si_archive__card-img-placeholder { position:absolute; inset:0; background:linear-gradient(135deg,#cbd5e1 0%,#94a3b8 100%); }
@ -979,7 +979,7 @@ body{overflow-x:hidden;}
.si_archive__card-tags { display: flex; flex-wrap: wrap; gap: 4px; } .si_archive__card-tags { display: flex; flex-wrap: wrap; gap: 4px; }
.si_archive__tag { padding: 3px 8px; border: 1px solid #d1d5db; border-radius: 999px; font-size: 12px; color: #555; background: #fff; } .si_archive__tag { padding: 3px 8px; border: 1px solid #d1d5db; border-radius: 999px; font-size: 12px; color: #555; background: #fff; }
.si_archive__card-desc { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 0; padding: 0; list-style: none; border-top: 1px solid #f1f1f1; padding-top: 14px; } .si_archive__card-desc { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 0; padding: 0; list-style: none; border-top: 1px solid #f1f1f1; padding-top: 14px; }
.si_archive__card-desc li { display: flex; flex-direction: column; gap: 6px; } .si_archive__card-desc li {display: flex; flex-direction: column; gap: 6px; }
.si_archive__card-desc-icon { width: 28px; height: 28px; background: #f3f4f6; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 14px; color: var(--color-primary); } .si_archive__card-desc-icon { width: 28px; height: 28px; background: #f3f4f6; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 14px; color: var(--color-primary); }
.si_archive__card-desc-title { font-size: 16px; font-weight: 700; color: #111; } .si_archive__card-desc-title { font-size: 16px; font-weight: 700; color: #111; }
.si_archive__card-desc-text { font-size: 14px; line-height: 1.5; color: #888; } .si_archive__card-desc-text { font-size: 14px; line-height: 1.5; color: #888; }
@ -1041,6 +1041,7 @@ body{overflow-x:hidden;}
.si_archive__card { flex: 0 0 calc(100vw - 48px); } .si_archive__card { flex: 0 0 calc(100vw - 48px); }
.si_archive__card-img { aspect-ratio: 8/6; } .si_archive__card-img { aspect-ratio: 8/6; }
.si_archive__card-desc { grid-template-columns: repeat(1, 1fr); } .si_archive__card-desc { grid-template-columns: repeat(1, 1fr); }
.si_archive__card-desc li{flex-direction:column;align-items:start;}
.si_archive__title { font-size: 30px; } .si_archive__title { font-size: 30px; }
.si_archive__desc { font-size: 13px; } .si_archive__desc { font-size: 13px; }
.si_archive__progress { gap: 8px; } .si_archive__progress { gap: 8px; }
@ -1168,7 +1169,7 @@ body{overflow-x:hidden;}
.utm-what__desc { font-size: 16px; color: rgba(255,255,255,0.5); line-height: 1.9; max-width: 640px; margin: 0 auto; word-break: keep-all; } .utm-what__desc { font-size: 16px; color: rgba(255,255,255,0.5); line-height: 1.9; max-width: 640px; margin: 0 auto; word-break: keep-all; }
.utm-what__body { display: grid; grid-template-columns: 220px 1fr 220px; gap: 20px; align-items: center; } .utm-what__body { display: grid; grid-template-columns: 220px 1fr 220px; gap: 20px; align-items: center; }
.utm-what__cards { list-style: none; display: flex; flex-direction: column; justify-content: center; gap: 25px; min-width: 220px; } .utm-what__cards { list-style: none; display: flex; flex-direction: column; justify-content: center; gap: 25px; min-width: 220px; }
.utm-what__card { display: flex; align-items: center; justify-content: flex-start; gap: 12px; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15); border-radius: 10px; padding: 0 20px; height: 80px; white-space: nowrap; width: 100%; } .utm-what__card { display: flex; align-items: center; justify-content: flex-start; gap: 12px; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15); border-radius: 10px; padding: 0 20px; height: 80px; white-space: normal; width: 100%; }
.utm-what__card--right { cursor: default; pointer-events: none; } .utm-what__card--right { cursor: default; pointer-events: none; }
.utm-what__card-icon { width: 28px; height: 28px; min-width: 28px; border-radius: 6px; background: #6366f1; display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: #fff; } .utm-what__card-icon { width: 28px; height: 28px; min-width: 28px; border-radius: 6px; background: #6366f1; display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: #fff; }
.utm-what__card-label { font-size: 15px; font-weight: 600; color: #fff; flex: 1; } .utm-what__card-label { font-size: 15px; font-weight: 600; color: #fff; flex: 1; }

1
src/css/main.css

@ -443,6 +443,7 @@ body{overflow-x:hidden;}
.main-news-meta span{display:block;margin-bottom:10px;font-size:11px;font-weight:800;letter-spacing:.18em;color:#1a1f5e;} .main-news-meta span{display:block;margin-bottom:10px;font-size:11px;font-weight:800;letter-spacing:.18em;color:#1a1f5e;}
.main-news-meta em{font-style:normal;font-size:14px;font-weight:600;color:rgba(16,20,43,.42);} .main-news-meta em{font-style:normal;font-size:14px;font-weight:600;color:rgba(16,20,43,.42);}
.main-news-content h3{margin:0 0 12px;font-size:25px;font-weight:800;line-height:1.35;letter-spacing:-.04em;color:#10142b;word-break:keep-all;} .main-news-content h3{margin:0 0 12px;font-size:25px;font-weight:800;line-height:1.35;letter-spacing:-.04em;color:#10142b;word-break:keep-all;}
.main-news-content-title-clamp{display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis;}
.main-news-content p{margin:0;font-size:15px;line-height:1.75;font-weight:500;color:rgba(16,20,43,.56);word-break:keep-all;} .main-news-content p{margin:0;font-size:15px;line-height:1.75;font-weight:500;color:rgba(16,20,43,.56);word-break:keep-all;}
.main-news-arrow{width:42px;height:42px;border-radius:50%;display:flex;align-items:center;justify-content:center;border:1px solid rgba(26,31,94,.16);color:#1a1f5e;font-size:18px;transition:transform .35s ease,background .35s ease,color .35s ease;} .main-news-arrow{width:42px;height:42px;border-radius:50%;display:flex;align-items:center;justify-content:center;border:1px solid rgba(26,31,94,.16);color:#1a1f5e;font-size:18px;transition:transform .35s ease,background .35s ease,color .35s ease;}
.main-news-item:hover .main-news-arrow{transform:translate(4px,-4px);background:#1a1f5e;color:#ffffff;} .main-news-item:hover .main-news-arrow{transform:translate(4px,-4px);background:#1a1f5e;color:#ffffff;}

3
src/main.jsx

@ -2,6 +2,7 @@ import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { HashRouter } from "react-router-dom"; import { HashRouter } from "react-router-dom";
import App from "./App"; import App from "./App";
import { LanguageProvider } from "./context/LanguageContext";
import "./css/reset.css"; import "./css/reset.css";
import "./css/common.css"; import "./css/common.css";
import "./css/header.css"; import "./css/header.css";
@ -11,7 +12,9 @@ import "./css/main.css";
ReactDOM.createRoot(document.getElementById("root")).render( ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode> <React.StrictMode>
<HashRouter> <HashRouter>
<LanguageProvider>
<App /> <App />
</LanguageProvider>
</HashRouter> </HashRouter>
</React.StrictMode>, </React.StrictMode>,
); );

305
src/pages/business/MaintenancePage.jsx

@ -3,24 +3,27 @@ import { motion, useInView } from "framer-motion";
import { gsap } from "gsap"; import { gsap } from "gsap";
import SubHero from "../../components/SubHero"; import SubHero from "../../components/SubHero";
import useFadeIn from "../../hooks/useFadeIn"; import useFadeIn from "../../hooks/useFadeIn";
import { useLanguage } from "../../context/LanguageContext";
const ease = [0.22, 1, 0.36, 1]; const ease = [0.22, 1, 0.36, 1];
const BUSINESS_NAV = [ const BUSINESS_NAV_KO = [
{ label: "System Integration", to: "/business/si" }, { label: "System Integration", to: "/business/si" },
{ label: "R&D", to: "/business/rnd" }, { label: "R&D", to: "/business/rnd" },
{ label: "운영 · 유지보수", to: "/business/maintenance" }, { label: "운영 · 유지보수", to: "/business/maintenance" },
]; ];
const CIRCLES = [ const BUSINESS_NAV_EN = [
{ label: "System Integration", to: "/business/si" },
{ label: "R&D", to: "/business/rnd" },
{ label: "Maintenance", to: "/business/maintenance" },
];
const CIRCLES_KO = [
{ {
id: "c0", id: "c0",
title: "모니터링", title: "모니터링",
items: [ items: ["시스템·네트워크 전 계층 감시", "이상 징후 선제 포착", "24/7 실시간 알람"],
"시스템·네트워크 전 계층 감시",
"이상 징후 선제 포착",
"24/7 실시간 알람",
],
grad: ["#d94889", "#a855f7"], grad: ["#d94889", "#a855f7"],
}, },
{ {
@ -38,11 +41,7 @@ const CIRCLES = [
{ {
id: "c3", id: "c3",
title: "기술 지원", title: "기술 지원",
items: [ items: ["기술 문의 신속 응대", "가이드 및 교육 제공", "고객 역량 강화 지원"],
"기술 문의 신속 응대",
"가이드 및 교육 제공",
"고객 역량 강화 지원",
],
grad: ["#3b82f6", "#06b6d4"], grad: ["#3b82f6", "#06b6d4"],
}, },
{ {
@ -53,14 +52,54 @@ const CIRCLES = [
}, },
]; ];
const KPI = [ const CIRCLES_EN = [
{
id: "c0",
title: "Monitoring",
items: ["Monitor every layer of systems and networks", "Proactively detect anomalies", "24/7 real-time alerts"],
grad: ["#d94889", "#a855f7"],
},
{
id: "c1",
title: "Incident Response",
items: ["Immediate root-cause analysis", "Rapid system recovery", "Preventive measures against recurrence"],
grad: ["#a855f7", "#6366f1"],
},
{
id: "c2",
title: "Security",
items: ["Regular vulnerability assessments", "Patch and update management", "Blocking external threats"],
grad: ["#6366f1", "#3b82f6"],
},
{
id: "c3",
title: "Technical Support",
items: ["Fast response to technical inquiries", "Guides and training provided", "Supporting customer capability building"],
grad: ["#3b82f6", "#06b6d4"],
},
{
id: "c4",
title: "Optimization",
items: ["Regular report analysis", "Optimizing operational efficiency", "Ongoing service quality improvement"],
grad: ["#06b6d4", "#d94889"],
},
];
const KPI_KO = [
{ value: "99.9%", label: "서비스 가용성" }, { value: "99.9%", label: "서비스 가용성" },
{ value: "24/7", label: "365일 실시간 운영" }, { value: "24/7", label: "365일 실시간 운영" },
{ value: "10m 24s", label: "평균 응답 시간" }, { value: "10m 24s", label: "평균 응답 시간" },
{ value: "100%", label: "SLA 준수율" }, { value: "100%", label: "SLA 준수율" },
]; ];
const SERVICES_CARD = [ const KPI_EN = [
{ value: "99.9%", label: "Service Availability" },
{ value: "24/7", label: "365-Day Real-Time Operation" },
{ value: "10m 24s", label: "Average Response Time" },
{ value: "100%", label: "SLA Compliance Rate" },
];
const SERVICES_CARD_KO = [
{ {
num: "01", num: "01",
title: "모니터링", title: "모니터링",
@ -93,6 +132,64 @@ const SERVICES_CARD = [
}, },
]; ];
const SERVICES_CARD_EN = [
{
num: "01",
title: "Monitoring",
desc: "We monitor every layer of systems, networks, and applications in real time, 24/7, catching anomalies before failures occur.",
img: "./images/mt_icon01.png",
},
{
num: "02",
title: "Incident Response",
desc: "The moment an anomaly is detected, dedicated engineers analyze the cause and restore service quickly, going beyond a quick fix to prevent recurrence.",
img: "./images/mt_icon02.png",
},
{
num: "03",
title: "Security Management",
desc: "Regular vulnerability checks and patch management protect your systems from external threats, maintaining a consistently secure environment.",
img: "./images/mt_icon03.png",
},
{
num: "04",
title: "Technical Support",
desc: "We respond quickly to technical questions that arise during operation, and support customer capability building through guides and training.",
img: "./images/mt_icon04.png",
},
{
num: "05",
title: "Optimization",
desc: "Based on regular reports and data analysis, we continuously raise service quality, improving operational efficiency and stability together.",
img: "./images/mt_icon05.png",
},
];
const PAGE_TEXT = {
ko: {
eyebrow: "OUR SERVICE",
titleLines: ["운영·유지보수는", "서비스의 안정성을", "완성합니다"],
desc: (
<>
PAL Networks는 24/7 통합 모니터링과 체계적인 유지보수로
<br />
시스템의 가용성과 안정성을 지속적으로 보장합니다.
</>
),
},
en: {
eyebrow: "OUR SERVICE",
titleLines: ["Operations and maintenance", "complete the stability", "of our service."],
desc: (
<>
PAL Networks ensures continuous system availability and stability
<br />
through 24/7 integrated monitoring and systematic maintenance.
</>
),
},
};
/* ── KPI 카운트업 ── */ /* ── KPI 카운트업 ── */
function KpiItem({ value, label, inView, delay }) { function KpiItem({ value, label, inView, delay }) {
const valRef = useRef(null); const valRef = useRef(null);
@ -125,12 +222,7 @@ function KpiItem({ value, label, inView, delay }) {
}); });
}, [inView]); // eslint-disable-line }, [inView]); // eslint-disable-line
return ( return (
<motion.div <motion.div className="mt-kpi__item" initial={{ opacity: 0, y: 20 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease, delay }}>
className="mt-kpi__item"
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease, delay }}
>
<span className="mt-kpi__value" ref={valRef}> <span className="mt-kpi__value" ref={valRef}>
{value} {value}
</span> </span>
@ -167,7 +259,7 @@ const DOT_SPEED = 15;
// spring-like easing for circle entrance // spring-like easing for circle entrance
const springEase = [0.34, 1.56, 0.64, 1]; const springEase = [0.34, 1.56, 0.64, 1];
function VennDiagram({ inView }) { function VennDiagram({ inView, circles }) {
const angleRef = useRef(-90); const angleRef = useRef(-90);
const dotAngleRef = useRef(0); const dotAngleRef = useRef(0);
const lastTime = useRef(null); const lastTime = useRef(null);
@ -199,8 +291,7 @@ function VennDiagram({ inView }) {
const a = angleRef.current; const a = angleRef.current;
if (arcRef.current) { if (arcRef.current) {
const offset = const offset = arcLen - (arcLen * ((((a + 90) % 360) + 360) % 360)) / 360;
arcLen - (arcLen * ((((a + 90) % 360) + 360) % 360)) / 360;
arcRef.current.setAttribute("stroke-dashoffset", offset); arcRef.current.setAttribute("stroke-dashoffset", offset);
} }
@ -229,8 +320,7 @@ function VennDiagram({ inView }) {
} }
} }
dotAngleRef.current = dotAngleRef.current = (dotAngleRef.current - DOT_SPEED * delta + 360) % 360;
(dotAngleRef.current - DOT_SPEED * delta + 360) % 360;
const da = dotAngleRef.current; const da = dotAngleRef.current;
DOT_OFFSETS.forEach((offset, k) => { DOT_OFFSETS.forEach((offset, k) => {
const deg = da + offset; const deg = da + offset;
@ -257,34 +347,26 @@ function VennDiagram({ inView }) {
}; };
}, [inView]); }, [inView]);
const centers = CIRCLES.map((_, i) => getCenter(i)); const centers = circles.map((_, i) => getCenter(i));
const initDots = DOT_OFFSETS.map((offset) => { const initDots = DOT_OFFSETS.map((offset) => {
const rad = (offset * Math.PI) / 180; const rad = (offset * Math.PI) / 180;
return { x: CX + OUTER_R * Math.cos(rad), y: CY + OUTER_R * Math.sin(rad) }; return { x: CX + OUTER_R * Math.cos(rad), y: CY + OUTER_R * Math.sin(rad) };
}); });
const dashLen = (ARC_DEG / 360) * (2 * Math.PI * GUIDE_R);
const gapLen = 2 * Math.PI * GUIDE_R - dashLen;
const arcLen = 2 * Math.PI * GUIDE_R; const arcLen = 2 * Math.PI * GUIDE_R;
const dashLen = arcLen * (ARC_DEG / 360);
const gapLen = arcLen - dashLen;
return ( return (
<div className="mt-venn"> <div className="mt-venn">
<motion.svg <motion.svg viewBox={`0 0 ${SVG_W} ${SVG_H}`} className="mt-venn__svg" initial={{ opacity: 0 }} animate={inView ? { opacity: 1 } : {}} transition={{ duration: 0.4 }}>
className="mt-venn__svg"
viewBox={`0 0 ${SVG_W} ${SVG_H}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
initial={{ opacity: 0, x: 80 }}
animate={inView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.9, ease, delay: 0.1 }}
>
<defs> <defs>
<linearGradient id="arcGrad" x1="0%" y1="0%" x2="100%" y2="0%"> <linearGradient id="arcGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#d94889" stopOpacity="0.1" /> <stop offset="0%" stopColor="#d94889" stopOpacity="0.1" />
<stop offset="100%" stopColor="#a855f7" stopOpacity="0.95" /> <stop offset="100%" stopColor="#a855f7" stopOpacity="0.95" />
</linearGradient> </linearGradient>
{CIRCLES.map((c, i) => ( {circles.map((c, i) => (
<radialGradient key={c.id} id={`vg-${i}`} cx="38%" cy="32%" r="68%"> <radialGradient key={c.id} id={`vg-${i}`} cx="38%" cy="32%" r="68%">
<stop offset="0%" stopColor={c.grad[0]} stopOpacity="0.95" /> <stop offset="0%" stopColor={c.grad[0]} stopOpacity="0.95" />
<stop offset="100%" stopColor={c.grad[1]} stopOpacity="0.82" /> <stop offset="100%" stopColor={c.grad[1]} stopOpacity="0.82" />
@ -293,70 +375,19 @@ function VennDiagram({ inView }) {
</defs> </defs>
{/* 바깥 장식 원 */} {/* 바깥 장식 원 */}
<motion.circle <motion.circle cx={CX} cy={CY} r={OUTER_R} stroke="rgba(170,120,210,0.1)" strokeWidth="1" fill="none" initial={{ opacity: 0 }} animate={inView ? { opacity: 1 } : {}} transition={{ duration: 1, ease, delay: 0.35 }} />
cx={CX}
cy={CY}
r={OUTER_R}
stroke="rgba(170,120,210,0.1)"
strokeWidth="1"
fill="none"
initial={{ opacity: 0 }}
animate={inView ? { opacity: 1 } : {}}
transition={{ duration: 1, ease, delay: 0.35 }}
/>
{/* 가이드 원 실선 */} {/* 가이드 원 실선 */}
<motion.circle <motion.circle cx={CX} cy={CY} r={GUIDE_R} stroke="rgba(170,120,210,0.18)" strokeWidth="1.2" fill="none" initial={{ opacity: 0, scale: 0.88 }} animate={inView ? { opacity: 1, scale: 1 } : {}} transition={{ duration: 0.85, ease, delay: 0.4 }} style={{ transformOrigin: `${CX}px ${CY}px` }} />
cx={CX}
cy={CY}
r={GUIDE_R}
stroke="rgba(170,120,210,0.18)"
strokeWidth="1.2"
fill="none"
initial={{ opacity: 0, scale: 0.88 }}
animate={inView ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.85, ease, delay: 0.4 }}
style={{ transformOrigin: `${CX}px ${CY}px` }}
/>
{/* 그라데이션 호 */} {/* 그라데이션 호 */}
<circle <circle ref={arcRef} cx={CX} cy={CY} r={GUIDE_R} stroke="url(#arcGrad)" strokeWidth="4" strokeDasharray={`${dashLen} ${gapLen}`} strokeDashoffset={arcLen} strokeLinecap="round" fill="none" />
ref={arcRef}
cx={CX}
cy={CY}
r={GUIDE_R}
stroke="url(#arcGrad)"
strokeWidth="4"
strokeDasharray={`${dashLen} ${gapLen}`}
strokeDashoffset={arcLen}
strokeLinecap="round"
fill="none"
/>
{/* 도트 3개 */} {/* 도트 3개 */}
{initDots.map((pos, k) => ( {initDots.map((pos, k) => (
<motion.g <motion.g key={`dot-${k}`} initial={{ opacity: 0 }} animate={inView ? { opacity: 1 } : {}} transition={{ duration: 0.6, ease, delay: 0.5 + k * 0.08 }}>
key={`dot-${k}`} <circle ref={(el) => (dotGlowRefs.current[k] = el)} cx={pos.x} cy={pos.y} r="9" fill="#a855f7" opacity="0.18" />
initial={{ opacity: 0 }} <circle ref={(el) => (dotRefs.current[k] = el)} cx={pos.x} cy={pos.y} r="5" fill="#a855f7" opacity="0.7" />
animate={inView ? { opacity: 1 } : {}}
transition={{ duration: 0.6, ease, delay: 0.5 + k * 0.08 }}
>
<circle
ref={(el) => (dotGlowRefs.current[k] = el)}
cx={pos.x}
cy={pos.y}
r="9"
fill="#a855f7"
opacity="0.18"
/>
<circle
ref={(el) => (dotRefs.current[k] = el)}
cx={pos.x}
cy={pos.y}
r="5"
fill="#a855f7"
opacity="0.7"
/>
</motion.g> </motion.g>
))} ))}
@ -391,32 +422,11 @@ function VennDiagram({ inView }) {
/> />
{/* 테두리 */} {/* 테두리 */}
<circle <circle cx={x} cy={y} r={CR} stroke={isActive ? "rgba(255,255,255,0.25)" : "rgba(170,120,210,0.3)"} strokeWidth="1.2" fill="none" style={{ transition: "stroke 0.4s ease" }} />
cx={x}
cy={y}
r={CR}
stroke={
isActive ? "rgba(255,255,255,0.25)" : "rgba(170,120,210,0.3)"
}
strokeWidth="1.2"
fill="none"
style={{ transition: "stroke 0.4s ease" }}
/>
{/* 타이틀만 — 설명 제거, 폰트 24→28px, 수직 중앙 정렬 */} {/* 타이틀만 — 설명 제거, 폰트 24→28px, 수직 중앙 정렬 */}
<text <text x={x} y={y + 6} textAnchor="middle" dominantBaseline="middle" fontSize="24" fontWeight="700" fontFamily="inherit" fill={isActive ? "white" : "#1a1f5e"} opacity={isActive ? "1" : "0.7"} style={{ transition: "fill 0.4s ease, opacity 0.4s ease" }}>
x={x} {circles[i].title}
y={y + 6}
textAnchor="middle"
dominantBaseline="middle"
fontSize="24"
fontWeight="700"
fontFamily="inherit"
fill={isActive ? "white" : "#1a1f5e"}
opacity={isActive ? "1" : "0.7"}
style={{ transition: "fill 0.4s ease, opacity 0.4s ease" }}
>
{CIRCLES[i].title}
</text> </text>
</motion.g> </motion.g>
); );
@ -428,6 +438,13 @@ function VennDiagram({ inView }) {
/* ── 메인 페이지 ── */ /* ── 메인 페이지 ── */
function MaintenancePage() { function MaintenancePage() {
const { lang } = useLanguage();
const BUSINESS_NAV = lang === "en" ? BUSINESS_NAV_EN : BUSINESS_NAV_KO;
const CIRCLES = lang === "en" ? CIRCLES_EN : CIRCLES_KO;
const KPI = lang === "en" ? KPI_EN : KPI_KO;
const SERVICES_CARD = lang === "en" ? SERVICES_CARD_EN : SERVICES_CARD_KO;
const t = PAGE_TEXT[lang];
const ref = useFadeIn(); const ref = useFadeIn();
const introRef = useRef(null); const introRef = useRef(null);
const kpiRef = useRef(null); const kpiRef = useRef(null);
@ -451,17 +468,11 @@ function MaintenancePage() {
<div className="inner-wrap"> <div className="inner-wrap">
<section className="mt-intro" ref={introRef}> <section className="mt-intro" ref={introRef}>
<div className="mt-intro__left"> <div className="mt-intro__left">
<motion.span <motion.span className="fc-eyebrow" initial={{ opacity: 0, y: 14 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
className="fc-eyebrow" {t.eyebrow}
initial={{ opacity: 0, y: 14 }}
animate={introInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease }}
>
OUR SERVICE
</motion.span> </motion.span>
<div className="mt-intro__title-wrap"> <div className="mt-intro__title-wrap">
{["운영·유지보수는", "서비스의 안정성을", "완성합니다"].map( {t.titleLines.map((line, i) => (
(line, i) => (
<div className="mt-title-line" key={i}> <div className="mt-title-line" key={i}>
<motion.h2 <motion.h2
className="mt-intro__title" className="mt-intro__title"
@ -476,22 +487,14 @@ function MaintenancePage() {
{line} {line}
</motion.h2> </motion.h2>
</div> </div>
), ))}
)}
</div> </div>
<motion.p <motion.p className="mt-intro__desc" initial={{ opacity: 0, y: 16 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.7, ease, delay: 0.5 }}>
className="mt-intro__desc" {t.desc}
initial={{ opacity: 0, y: 16 }}
animate={introInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.7, ease, delay: 0.5 }}
>
PAL Networks는 24/7 통합 모니터링과 체계적인 유지보수로
<br />
시스템의 가용성과 안정성을 지속적으로 보장합니다.
</motion.p> </motion.p>
</div> </div>
<div className="mt-intro__right"> <div className="mt-intro__right">
<VennDiagram inView={introInView} /> <VennDiagram inView={introInView} circles={CIRCLES} />
</div> </div>
</section> </section>
</div> </div>
@ -500,13 +503,7 @@ function MaintenancePage() {
<div className="inner-wrap"> <div className="inner-wrap">
<div className="mt-kpi__grid"> <div className="mt-kpi__grid">
{KPI.map((k, i) => ( {KPI.map((k, i) => (
<KpiItem <KpiItem key={i} value={k.value} label={k.label} inView={kpiInView} delay={i * 0.15} />
key={i}
value={k.value}
label={k.label}
inView={kpiInView}
delay={i * 0.15}
/>
))} ))}
</div> </div>
</div> </div>
@ -516,19 +513,9 @@ function MaintenancePage() {
<section className="mt-services" ref={colsRef}> <section className="mt-services" ref={colsRef}>
<div className="mt-services__grid"> <div className="mt-services__grid">
{SERVICES_CARD.map((svc, i) => ( {SERVICES_CARD.map((svc, i) => (
<motion.div <motion.div key={i} className="mt-service-card" initial={{ opacity: 0, y: 32 }} animate={colsInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.65, ease, delay: i * 0.1 }}>
key={i}
className="mt-service-card"
initial={{ opacity: 0, y: 32 }}
animate={colsInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.65, ease, delay: i * 0.1 }}
>
<div className="mt-service-card__img-wrap"> <div className="mt-service-card__img-wrap">
<img <img src={svc.img} alt={svc.title} className="mt-service-card__img" />
src={svc.img}
alt={svc.title}
className="mt-service-card__img"
/>
</div> </div>
<span className="mt-service-card__num">{svc.num}</span> <span className="mt-service-card__num">{svc.num}</span>
<h3 className="mt-service-card__title">{svc.title}</h3> <h3 className="mt-service-card__title">{svc.title}</h3>

358
src/pages/business/RndPage.jsx

@ -1,31 +1,13 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import SubHero from "../../components/SubHero"; import SubHero from "../../components/SubHero";
import useFadeIn from "../../hooks/useFadeIn"; import useFadeIn from "../../hooks/useFadeIn";
import { motion, AnimatePresence, useInView } from "framer-motion"; import { motion, useInView } from "framer-motion";
import { import { Shield, Radio, Cpu, Wind, Eye, BarChart2, Globe, Users, Layers, GraduationCap, Wrench, Building2, Navigation, MapPin, AlertTriangle, Plane, Network, Settings } from "lucide-react";
Shield, import { useLanguage } from "../../context/LanguageContext";
Radio,
Cpu,
Wind,
Eye,
BarChart2,
Globe,
Users,
Layers,
GraduationCap,
Wrench,
Building2,
Navigation,
MapPin,
AlertTriangle,
Plane,
Network,
Settings,
} from "lucide-react";
const ease = [0.25, 0.1, 0.25, 1]; const ease = [0.25, 0.1, 0.25, 1];
const PROJECTS = [ const PROJECTS_KO = [
{ {
id: "01", id: "01",
title: "UTM 드론 교통관리체계", title: "UTM 드론 교통관리체계",
@ -166,9 +148,197 @@ const PROJECTS = [
}, },
]; ];
const PROJECTS_EN = [
{
id: "01",
title: "UTM Drone Traffic Management Framework",
tags: ["Drone", "Traffic Management"],
image: "images/rnd_img6.png",
desc: [
{
icon: <Plane size={14} />,
title: "Low-Altitude Airspace Management",
text: "Research on drone operation systems for airspace below 150m",
},
{
icon: <Network size={14} />,
title: "Traffic Management System",
text: "Developed a safe and efficient drone traffic management system",
},
{
icon: <Settings size={14} />,
title: "Flight Optimization",
text: "Demonstrated maximized safety and efficiency for unmanned aircraft operations",
},
],
},
{
id: "02",
title: "Drone Regulatory Sandbox",
tags: ["Drone", "Regulation/Demonstration"],
image: "images/rnd_img1.png",
desc: [
{
icon: <Shield size={14} />,
title: "Safety Management Framework",
text: "Established a safety management foundation for growing drone use",
},
{
icon: <Radio size={14} />,
title: "Identification Device",
text: "Built and demonstrated a drone identification device and management framework",
},
{
icon: <Cpu size={14} />,
title: "Flight Control System",
text: "Conducted R&D on a drone flight control system",
},
],
},
{
id: "03",
title: "Built Environmental Drone Control System",
tags: ["Drone", "Environment/Air Quality"],
image: "images/rnd_img2.png",
desc: [
{
icon: <Wind size={14} />,
title: "Air Pollution Monitoring",
text: "Monitoring system for air pollution, including fine dust and odor",
},
{
icon: <Eye size={14} />,
title: "Real-Time Monitoring",
text: "Collected real-time pollution data using drone-mounted sensors",
},
{
icon: <BarChart2 size={14} />,
title: "Information System",
text: "Built and demonstrated an air pollution monitoring system",
},
],
},
{
id: "04",
title: "Built Metaverse System",
tags: ["Metaverse", "Digital Transformation"],
image: "images/rnd_img3.png",
desc: [
{
icon: <Globe size={14} />,
title: "Digital Transformation",
text: "Extended reality into a digital-based virtual world",
},
{
icon: <Users size={14} />,
title: "Contactless Environment",
text: "Support system for contactless activities amid COVID-19",
},
{
icon: <Layers size={14} />,
title: "Virtual Space",
text: "Demonstrated a system enabling all activities within virtual space",
},
],
},
{
id: "05",
title: "Aviation Workforce Training Team Operations System",
tags: ["Aviation", "Workforce Training"],
image: "images/rnd_img4.png",
desc: [
{
icon: <GraduationCap size={14} />,
title: "Education & Training Foundation",
text: "Education and training framework for a high-school-based aviation maintenance workforce",
},
{
icon: <Wrench size={14} />,
title: "Field-Ready Workforce",
text: "Supported a stable supply of field-ready aviation maintenance workforce",
},
{
icon: <Building2 size={14} />,
title: "Private-Sector Foundation",
text: "Built and demonstrated a private-sector foundation for basic aviation workforce training",
},
],
},
{
id: "06",
title: "Light Aircraft Navigation & Flight Operations Management System",
tags: ["Light Aircraft", "Flight Safety"],
image: "images/rnd_img5.png",
desc: [
{
icon: <Navigation size={14} />,
title: "Dedicated Navigation",
text: "Real-time safety information navigation dedicated to light aircraft",
},
{
icon: <MapPin size={14} />,
title: "Location Monitoring",
text: "Built a location-based monitoring framework for light aircraft",
},
{
icon: <AlertTriangle size={14} />,
title: "Accident Prevention",
text: "Demonstrated flight safety services amid growing air leisure activity",
},
],
},
];
const AUTO_DELAY = 5000; const AUTO_DELAY = 5000;
const PAGE_TEXT = {
ko: {
titleMobile: "연구개발 수행실적",
titleDesktop: (
<>
연구개발
<br />
수행실적
</>
),
descMobile: "미래 항공 모빌리티를 위한 연구개발 활동과 성과를 기록합니다.",
descDesktop: (
<>
미래 항공 모빌리티를 위한
<br />
연구개발 활동과 성과를 기록합니다.
</>
),
prev: "이전",
next: "다음",
},
en: {
titleMobile: "R&D Track Record",
titleDesktop: (
<>
R&D
<br />
Track Record
</>
),
descMobile: "A record of our R&D activities and achievements for future air mobility.",
descDesktop: (
<>
A record of R&D activities and achievements
<br />
for future air mobility.
</>
),
prev: "Previous",
next: "Next",
},
};
function RndPage() { function RndPage() {
const { lang } = useLanguage();
const PROJECTS = lang === "en" ? PROJECTS_EN : PROJECTS_KO;
const t = PAGE_TEXT[lang];
const basePath = import.meta.env.BASE_URL; const basePath = import.meta.env.BASE_URL;
const ref = useFadeIn(); const ref = useFadeIn();
const sectionRef = useRef(null); const sectionRef = useRef(null);
@ -223,15 +393,13 @@ function RndPage() {
}; };
const handleDragStart = (e) => { const handleDragStart = (e) => {
dragStartX.current = dragStartX.current = e.type === "touchstart" ? e.touches[0].clientX : e.clientX;
e.type === "touchstart" ? e.touches[0].clientX : e.clientX;
dragStartCurrent.current = current; dragStartCurrent.current = current;
}; };
const handleDragEnd = (e) => { const handleDragEnd = (e) => {
if (dragStartX.current === null) return; if (dragStartX.current === null) return;
const endX = const endX = e.type === "touchend" ? e.changedTouches[0].clientX : e.clientX;
e.type === "touchend" ? e.changedTouches[0].clientX : e.clientX;
const diff = dragStartX.current - endX; const diff = dragStartX.current - endX;
if (diff > 50) next(); if (diff > 50) next();
else if (diff < -50) prev(); else if (diff < -50) prev();
@ -254,7 +422,14 @@ function RndPage() {
return () => clearInterval(timerRef.current); return () => clearInterval(timerRef.current);
}, [total]); }, [total]);
const BUSINESS_NAV = [ const BUSINESS_NAV =
lang === "en"
? [
{ label: "System Integration", to: "/business/si" },
{ label: "R&D", to: "/business/rnd" },
{ label: "Maintenance", to: "/business/maintenance" },
]
: [
{ label: "System Integration", to: "/business/si" }, { label: "System Integration", to: "/business/si" },
{ label: "R&D", to: "/business/rnd" }, { label: "R&D", to: "/business/rnd" },
{ label: "운영 · 유지보수", to: "/business/maintenance" }, { label: "운영 · 유지보수", to: "/business/maintenance" },
@ -278,83 +453,29 @@ function RndPage() {
<div className="si_archive__main"> <div className="si_archive__main">
{/* 헤더 */} {/* 헤더 */}
<div className="si_archive__header"> <div className="si_archive__header">
<motion.span <motion.span className="fc-eyebrow" initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
className="fc-eyebrow"
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease }}
>
PROJECT ARCHIVE PROJECT ARCHIVE
</motion.span> </motion.span>
<motion.h2 <motion.h2 className="si_archive__title" initial={{ opacity: 0, y: 20 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.1, ease }}>
className="si_archive__title" {window.innerWidth <= 768 ? t.titleMobile : t.titleDesktop}
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.1, ease }}
>
{window.innerWidth <= 768 ? (
"연구개발 수행실적"
) : (
<>
연구개발
<br />
수행실적
</>
)}
</motion.h2> </motion.h2>
<motion.p <motion.p className="si_archive__desc" initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.2, ease }}>
className="si_archive__desc" {window.innerWidth <= 768 ? t.descMobile : t.descDesktop}
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.2, ease }}
>
{window.innerWidth <= 768 ? (
"미래 항공 모빌리티를 위한 연구개발 활동과 성과를 기록합니다."
) : (
<>
미래 항공 모빌리티를 위한
<br />
연구개발 활동과 성과를 기록합니다.
</>
)}
</motion.p> </motion.p>
{/* 네비게이션 */} {/* 네비게이션 */}
<div className="si_archive__nav"> <div className="si_archive__nav">
<motion.button <motion.button className="si_archive__nav-btn" onClick={prev} aria-label={t.prev} initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.4, ease }}>
className="si_archive__nav-btn"
onClick={prev}
aria-label="이전"
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4, ease }}
>
</motion.button> </motion.button>
<motion.div <motion.div className="si_archive__progress" initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.4, ease }}>
className="si_archive__progress" <span className="si_archive__progress-cur">{String(current + 1).padStart(2, "0")}</span>
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4, ease }}
>
<span className="si_archive__progress-cur">
{String(current + 1).padStart(2, "0")}
</span>
<span className="si_archive__progress-divider">/</span> <span className="si_archive__progress-divider">/</span>
<span className="si_archive__progress-total"> <span className="si_archive__progress-total">{String(total).padStart(2, "0")}</span>
{String(total).padStart(2, "0")}
</span>
</motion.div> </motion.div>
<motion.button <motion.button className="si_archive__nav-btn" onClick={next} aria-label={t.next} initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.4, ease }}>
className="si_archive__nav-btn"
onClick={next}
aria-label="다음"
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4, ease }}
>
</motion.button> </motion.button>
</div> </div>
@ -362,56 +483,27 @@ function RndPage() {
{/* 슬라이더 */} {/* 슬라이더 */}
<div className="si_archive__right"> <div className="si_archive__right">
<motion.div <motion.div initial={{ opacity: 0, y: 30 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.3, ease }}>
initial={{ opacity: 0, y: 30 }} <div className="si_archive__slider" style={{ display: "flex" }} onMouseDown={handleDragStart} onMouseUp={handleDragEnd} onTouchStart={handleDragStart} onTouchEnd={handleDragEnd} ref={sliderRef}>
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.3, ease }}
>
<div
className="si_archive__slider"
style={{ display: "flex" }}
onMouseDown={handleDragStart}
onMouseUp={handleDragEnd}
onTouchStart={handleDragStart}
onTouchEnd={handleDragEnd}
ref={sliderRef}
>
<motion.div <motion.div
className="si_archive__track" className="si_archive__track"
animate={{ animate={{
x: x: window.innerWidth <= 768 ? 0 : cardWidth ? -(current * (cardWidth + 18)) : 0,
window.innerWidth <= 768
? 0
: cardWidth
? -(current * (cardWidth + 18))
: 0,
}} }}
transition={{ duration: 0.55, ease }} transition={{ duration: 0.55, ease }}
style={{ alignItems: "stretch" }} style={{ alignItems: "stretch" }}
> >
{PROJECTS.map((project, idx) => ( {PROJECTS.map((project, idx) => (
<div <div key={project.id} className="si_archive__card" ref={idx === 0 ? cardRef : null}>
key={project.id}
className="si_archive__card"
ref={idx === 0 ? cardRef : null}
>
<div className="si_archive__card-img"> <div className="si_archive__card-img">
<img <img src={`${basePath}${project.image}`} alt={project.title} draggable="false" />
src={`${basePath}${project.image}`}
alt={project.title}
draggable="false"
/>
<div className="si_archive__card-img-placeholder" /> <div className="si_archive__card-img-placeholder" />
</div> </div>
<div className="si_archive__card-body"> <div className="si_archive__card-body">
<div className="si_archive__card-header"> <div className="si_archive__card-header">
<div className="si_archive__card-num"> <div className="si_archive__card-num">{project.id}</div>
{project.id} <h3 className="si_archive__card-title">{project.title}</h3>
</div>
<h3 className="si_archive__card-title">
{project.title}
</h3>
</div> </div>
<div className="si_archive__card-tags"> <div className="si_archive__card-tags">
@ -425,15 +517,9 @@ function RndPage() {
<ul className="si_archive__card-desc"> <ul className="si_archive__card-desc">
{project.desc.map((item, i) => ( {project.desc.map((item, i) => (
<li key={i}> <li key={i}>
<div className="si_archive__card-desc-icon"> <div className="si_archive__card-desc-icon">{item.icon}</div>
{item.icon} <div className="si_archive__card-desc-title">{item.title}</div>
</div> <div className="si_archive__card-desc-text">{item.text}</div>
<div className="si_archive__card-desc-title">
{item.title}
</div>
<div className="si_archive__card-desc-text">
{item.text}
</div>
</li> </li>
))} ))}
</ul> </ul>

406
src/pages/business/SiPage.jsx

@ -1,31 +1,12 @@
import { useRef, useState, useEffect } from "react"; import { useRef, useState, useEffect } from "react";
import { motion, AnimatePresence, useInView } from "framer-motion"; import { motion, useInView } from "framer-motion";
import SubHero from "../../components/SubHero"; import SubHero from "../../components/SubHero";
import useFadeIn from "../../hooks/useFadeIn"; import useFadeIn from "../../hooks/useFadeIn";
import { import { Plane, Globe, UtensilsCrossed, Thermometer, MapPin, Link, QrCode, Users, Siren, Wrench, PlusCircle, Settings, ShieldCheck, Monitor, Building2, ClipboardList, Radio, Database, Navigation } from "lucide-react";
Plane, import { useLanguage } from "../../context/LanguageContext";
Globe,
UtensilsCrossed,
Thermometer,
MapPin,
Link,
QrCode,
Users,
Siren,
Wrench,
PlusCircle,
Settings,
ShieldCheck,
Monitor,
Building2,
ClipboardList,
Radio,
Database,
Navigation,
} from "lucide-react";
const ease = [0.25, 0.1, 0.25, 1]; const ease = [0.25, 0.1, 0.25, 1];
const PROJECTS = [ const PROJECTS_KO = [
{ {
id: "01", id: "01",
title: "KAC UTM 시스템 구축", title: "KAC UTM 시스템 구축",
@ -211,9 +192,244 @@ const PROJECTS = [
], ],
}, },
]; ];
const PROJECTS_EN = [
{
id: "01",
title: "Built KAC UTM System",
tags: ["Aviation/Control", "UTM"],
image: "/images/si_kac_utm.png",
desc: [
{
icon: <Radio size={14} />,
title: "Real-Time Control",
text: "Real-time airspace monitoring and control system for UAVs",
},
{
icon: <Navigation size={14} />,
title: "Flight Path Management",
text: "Flight plan approval and proactive collision risk analysis",
},
{
icon: <Database size={14} />,
title: "Data Integration",
text: "Built an integrated platform for flight data collection and analysis",
},
],
},
{
id: "02",
title: "Built Illegal Drone Detection System",
tags: ["Aviation/Security", "Drone"],
image: "/images/si_injustice_drone.png",
desc: [
{
icon: <ShieldCheck size={14} />,
title: "Illegal Drone Detection",
text: "Real-time detection and identification of unauthorized drones",
},
{
icon: <Siren size={14} />,
title: "Threat Response",
text: "Rapid alert and response upon unauthorized aircraft intrusion",
},
{
icon: <Radio size={14} />,
title: "Control Integration",
text: "Real-time integration with UTM control systems",
},
],
},
{
id: "03",
title: "Built Jeju Pass OTA Aviation Service",
tags: ["Aviation/Travel", "OTA"],
image: "/images/si_img1.png",
desc: [
{
icon: <Plane size={14} />,
title: "Integrated Travel Portal",
text: "Built an integrated service covering car rental, lodging, and flights",
},
{
icon: <Globe size={14} />,
title: "Overseas Market Entry",
text: "Laid the groundwork for overseas markets with international flights",
},
{
icon: <UtensilsCrossed size={14} />,
title: "Comprehensive Service",
text: "Extended Jeju Pass to restaurants and cafes",
},
],
},
{
id: "04",
title: "Built Safe Tourism Health & Safety System",
tags: ["Public/Health & Safety", "Health & Safety/Security"],
image: "/images/si_img2.png",
desc: [
{
icon: <Thermometer size={14} />,
title: "Health Status Management",
text: "Checked and managed health status throughout the stay",
},
{
icon: <MapPin size={14} />,
title: "Movement Tracking",
text: "System for tracking movement paths and managing related information",
},
{
icon: <Link size={14} />,
title: "System Integration",
text: "Integrated with the electronic management system for China-dedicated travel agencies",
},
],
},
{
id: "05",
title: "Built Clean Incheon Access Authentication System",
tags: ["Public/Access Control", "Authentication/Security"],
image: "/images/si_img3.png",
desc: [
{
icon: <QrCode size={14} />,
title: "QR Health Management",
text: "Built visitor health screening management using QR codes",
},
{
icon: <Users size={14} />,
title: "Visitor Management",
text: "Systematic visitor management and access information",
},
{
icon: <Siren size={14} />,
title: "Rapid Response",
text: "Supported rapid health response in case of confirmed cases",
},
],
},
{
id: "06",
title: "SSG.COM Aviation Service Operations & Maintenance",
tags: ["Aviation/E-commerce", "Operations & Maintenance"],
image: "/images/si_img4.png",
desc: [
{
icon: <Wrench size={14} />,
title: "Bug Fixes",
text: "Fixed system errors and improved usability issues",
},
{
icon: <PlusCircle size={14} />,
title: "Feature Additions",
text: "Developed additional features as needed and data extraction",
},
{
icon: <Settings size={14} />,
title: "Operational Stability",
text: "Stabilized operations through system optimization",
},
],
},
{
id: "07",
title: "Built Hyundai Motor Access Authentication System",
tags: ["Corporate/Security", "Authentication/Security"],
image: "/images/si_img5.png",
desc: [
{
icon: <QrCode size={14} />,
title: "QR Visitor Management",
text: "Visitor health screening management system using QR codes",
},
{
icon: <Siren size={14} />,
title: "Access Information",
text: "Accurate access records provided in case of confirmed cases",
},
{
icon: <Monitor size={14} />,
title: "Media Wall Integration",
text: "Customer engagement through media wall integration",
},
],
},
{
id: "08",
title: "Built Hi Air Flight Operations System",
tags: ["Aviation", "Certification/Scheduling"],
image: "/images/si_img6.png",
desc: [
{
icon: <Building2 size={14} />,
title: "Infrastructure",
text: "Essential services and infrastructure for airline operators",
},
{
icon: <Plane size={14} />,
title: "International Route Support",
text: "Supported international route launches and increased ancillary revenue",
},
{
icon: <ClipboardList size={14} />,
title: "System Enhancement",
text: "Enhanced mandatory systems and services required by MOLIT",
},
],
},
];
const AUTO_DELAY = 5000; const AUTO_DELAY = 5000;
const PAGE_TEXT = {
ko: {
titleMobile: "수행사업 아카이브",
titleDesktop: (
<>
수행사업
<br />
아카이브
</>
),
descMobile: "PAL Networks가 구축한 주요 프로젝트를 소개합니다.",
descDesktop: (
<>
PAL Networks가 구축한
<br />
주요 프로젝트를 소개합니다.
</>
),
prev: "이전",
next: "다음",
},
en: {
titleMobile: "Project Archive",
titleDesktop: (
<>
Project
<br />
Archive
</>
),
descMobile: "Key projects built by PAL Networks.",
descDesktop: (
<>
Key projects built
<br />
by PAL Networks.
</>
),
prev: "Previous",
next: "Next",
},
};
function SiPage() { function SiPage() {
const { lang } = useLanguage();
const PROJECTS = lang === "en" ? PROJECTS_EN : PROJECTS_KO;
const t = PAGE_TEXT[lang];
const basePath = import.meta.env.BASE_URL; const basePath = import.meta.env.BASE_URL;
const ref = useFadeIn(); const ref = useFadeIn();
const sectionRef = useRef(null); const sectionRef = useRef(null);
@ -270,15 +486,13 @@ function SiPage() {
}; };
const handleDragStart = (e) => { const handleDragStart = (e) => {
dragStartX.current = dragStartX.current = e.type === "touchstart" ? e.touches[0].clientX : e.clientX;
e.type === "touchstart" ? e.touches[0].clientX : e.clientX;
dragStartCurrent.current = current; dragStartCurrent.current = current;
}; };
const handleDragEnd = (e) => { const handleDragEnd = (e) => {
if (dragStartX.current === null) return; if (dragStartX.current === null) return;
const endX = const endX = e.type === "touchend" ? e.changedTouches[0].clientX : e.clientX;
e.type === "touchend" ? e.changedTouches[0].clientX : e.clientX;
const diff = dragStartX.current - endX; const diff = dragStartX.current - endX;
if (diff > 50) next(); if (diff > 50) next();
else if (diff < -50) prev(); else if (diff < -50) prev();
@ -301,7 +515,14 @@ function SiPage() {
return () => clearInterval(timerRef.current); return () => clearInterval(timerRef.current);
}, [total]); }, [total]);
const BUSINESS_NAV = [ const BUSINESS_NAV =
lang === "en"
? [
{ label: "System Integration", to: "/business/si" },
{ label: "R&D", to: "/business/rnd" },
{ label: "Maintenance", to: "/business/maintenance" },
]
: [
{ label: "System Integration", to: "/business/si" }, { label: "System Integration", to: "/business/si" },
{ label: "R&D", to: "/business/rnd" }, { label: "R&D", to: "/business/rnd" },
{ label: "운영 · 유지보수", to: "/business/maintenance" }, { label: "운영 · 유지보수", to: "/business/maintenance" },
@ -325,83 +546,29 @@ function SiPage() {
<div className="si_archive__main"> <div className="si_archive__main">
{/* 헤더 */} {/* 헤더 */}
<div className="si_archive__header"> <div className="si_archive__header">
<motion.span <motion.span className="fc-eyebrow" initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
className="fc-eyebrow"
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease }}
>
PROJECT ARCHIVE PROJECT ARCHIVE
</motion.span> </motion.span>
<motion.h2 <motion.h2 className="si_archive__title" initial={{ opacity: 0, y: 20 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.1, ease }}>
className="si_archive__title" {window.innerWidth <= 768 ? t.titleMobile : t.titleDesktop}
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.1, ease }}
>
{window.innerWidth <= 768 ? (
"수행사업 아카이브"
) : (
<>
수행사업
<br />
아카이브
</>
)}
</motion.h2> </motion.h2>
<motion.p <motion.p className="si_archive__desc" initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.2, ease }}>
className="si_archive__desc" {window.innerWidth <= 768 ? t.descMobile : t.descDesktop}
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.2, ease }}
>
{window.innerWidth <= 768 ? (
"PAL Networks가 구축한 주요 프로젝트를 소개합니다."
) : (
<>
PAL Networks가 구축한
<br />
주요 프로젝트를 소개합니다.
</>
)}
</motion.p> </motion.p>
{/* 네비게이션 */} {/* 네비게이션 */}
<div className="si_archive__nav"> <div className="si_archive__nav">
<motion.button <motion.button className="si_archive__nav-btn" onClick={prev} aria-label={t.prev} initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.4, ease }}>
className="si_archive__nav-btn"
onClick={prev}
aria-label="이전"
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4, ease }}
>
</motion.button> </motion.button>
<motion.div <motion.div className="si_archive__progress" initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.4, ease }}>
className="si_archive__progress" <span className="si_archive__progress-cur">{String(current + 1).padStart(2, "0")}</span>
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4, ease }}
>
<span className="si_archive__progress-cur">
{String(current + 1).padStart(2, "0")}
</span>
<span className="si_archive__progress-divider">/</span> <span className="si_archive__progress-divider">/</span>
<span className="si_archive__progress-total"> <span className="si_archive__progress-total">{String(total).padStart(2, "0")}</span>
{String(total).padStart(2, "0")}
</span>
</motion.div> </motion.div>
<motion.button <motion.button className="si_archive__nav-btn" onClick={next} aria-label={t.next} initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.4, ease }}>
className="si_archive__nav-btn"
onClick={next}
aria-label="다음"
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4, ease }}
>
</motion.button> </motion.button>
</div> </div>
@ -409,56 +576,27 @@ function SiPage() {
{/* 슬라이더 */} {/* 슬라이더 */}
<div className="si_archive__right"> <div className="si_archive__right">
<motion.div <motion.div initial={{ opacity: 0, y: 30 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.3, ease }}>
initial={{ opacity: 0, y: 30 }} <div className="si_archive__slider" style={{ display: "flex" }} onMouseDown={handleDragStart} onMouseUp={handleDragEnd} onTouchStart={handleDragStart} onTouchEnd={handleDragEnd} ref={sliderRef}>
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.3, ease }}
>
<div
className="si_archive__slider"
style={{ display: "flex" }}
onMouseDown={handleDragStart}
onMouseUp={handleDragEnd}
onTouchStart={handleDragStart}
onTouchEnd={handleDragEnd}
ref={sliderRef}
>
<motion.div <motion.div
className="si_archive__track" className="si_archive__track"
animate={{ animate={{
x: x: window.innerWidth <= 768 ? 0 : cardWidth ? -(current * (cardWidth + 18)) : 0,
window.innerWidth <= 768
? 0
: cardWidth
? -(current * (cardWidth + 18))
: 0,
}} }}
transition={{ duration: 0.55, ease }} transition={{ duration: 0.55, ease }}
style={{ alignItems: "stretch" }} style={{ alignItems: "stretch" }}
> >
{PROJECTS.map((project, idx) => ( {PROJECTS.map((project, idx) => (
<div <div key={project.id} className="si_archive__card" ref={idx === 0 ? cardRef : null}>
key={project.id}
className="si_archive__card"
ref={idx === 0 ? cardRef : null}
>
<div className="si_archive__card-img"> <div className="si_archive__card-img">
<img <img src={`${basePath}images/${project.image.split("/").pop()}`} alt={project.title} draggable="false" />
src={`${basePath}images/${project.image.split("/").pop()}`}
alt={project.title}
draggable="false"
/>
<div className="si_archive__card-img-placeholder" /> <div className="si_archive__card-img-placeholder" />
</div> </div>
<div className="si_archive__card-body"> <div className="si_archive__card-body">
<div className="si_archive__card-header"> <div className="si_archive__card-header">
<div className="si_archive__card-num"> <div className="si_archive__card-num">{project.id}</div>
{project.id} <h3 className="si_archive__card-title">{project.title}</h3>
</div>
<h3 className="si_archive__card-title">
{project.title}
</h3>
</div> </div>
<div className="si_archive__card-tags"> <div className="si_archive__card-tags">
@ -472,15 +610,9 @@ function SiPage() {
<ul className="si_archive__card-desc"> <ul className="si_archive__card-desc">
{project.desc.map((item, i) => ( {project.desc.map((item, i) => (
<li key={i}> <li key={i}>
<div className="si_archive__card-desc-icon"> <div className="si_archive__card-desc-icon">{item.icon}</div>
{item.icon} <div className="si_archive__card-desc-title">{item.title}</div>
</div> <div className="si_archive__card-desc-text">{item.text}</div>
<div className="si_archive__card-desc-title">
{item.title}
</div>
<div className="si_archive__card-desc-text">
{item.text}
</div>
</li> </li>
))} ))}
</ul> </ul>

298
src/pages/company/AboutPage.jsx

@ -5,12 +5,12 @@ import FloatingKeywords from "../../components/FloatingKeywords";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import TopButton from "../../components/TopButton"; import { useLanguage } from "../../context/LanguageContext";
gsap.registerPlugin(ScrollTrigger); gsap.registerPlugin(ScrollTrigger);
const heroRight = <FloatingKeywords />; const heroRight = <FloatingKeywords />;
const COMPANY_NAV = [ const COMPANY_NAV_KO = [
{ label: "회사소개", to: "/company/about" }, { label: "회사소개", to: "/company/about" },
{ label: "인증 및 특허현황", to: "/company/cert" }, { label: "인증 및 특허현황", to: "/company/cert" },
{ label: "연혁", to: "/company/history" }, { label: "연혁", to: "/company/history" },
@ -18,6 +18,14 @@ const COMPANY_NAV = [
{ label: "찾아오시는 길", to: "/company/location" }, { label: "찾아오시는 길", to: "/company/location" },
]; ];
const COMPANY_NAV_EN = [
{ label: "About Us", to: "/company/about" },
{ label: "Certifications & Patents", to: "/company/cert" },
{ label: "History", to: "/company/history" },
{ label: "Clients & Partners", to: "/company/partners" },
{ label: "Location", to: "/company/location" },
];
const STATS = [ const STATS = [
{ num: 160000, suffix: "대+", label: "국내 등록 드론 현황" }, { num: 160000, suffix: "대+", label: "국내 등록 드론 현황" },
{ num: 1500, suffix: "억$+", label: "UAM 시장 규모 (2040)" }, { num: 1500, suffix: "억$+", label: "UAM 시장 규모 (2040)" },
@ -47,7 +55,139 @@ const TECH_CARDS = [
}, },
]; ];
const MISSION_ITEMS_KO = [
{
num: "01",
keyword: "INNOVATION",
title: "혁신",
desc: "효율적이고 혁신적인 기술로 항공산업 발전에 기여합니다.",
},
{
num: "02",
keyword: "TRUST",
title: "신뢰",
desc: "안전하고 신뢰할 수 있는 시스템을 구축합니다.",
},
{
num: "03",
keyword: "COOPERATION",
title: "협력",
desc: "파트너와 함께 성장하는 협력 생태계를 만들어갑니다",
},
];
const MISSION_ITEMS_EN = [
{
num: "01",
keyword: "INNOVATION",
title: "Innovation",
desc: "We contribute to the advancement of the aviation industry through efficient, innovative technology.",
},
{
num: "02",
keyword: "TRUST",
title: "Trust",
desc: "We build safe and reliable systems.",
},
{
num: "03",
keyword: "COOPERATION",
title: "Cooperation",
desc: "We build a collaborative ecosystem that grows together with our partners.",
},
];
const PAGE_TEXT = {
ko: {
heroSub: "가치를 존중하며 신뢰를 바탕으로, 고객과 함께 지속 가능한 미래를 만들어갑니다.",
companyLabel: "About PAL Networks",
companyHeadingBlue: "항공·전문 IT산업분야의",
companyHeadingPlain: "소프트웨어 개발 전문 기업",
companyBody1: "항공·전문 IT산업분야의 소프트웨어 개발 전문 기업으로써 개발, 연구 & 컨설팅 등 기술력 기반으로 전문인력 중심으로 수행하고 있습니다.",
companyBody2: "PAL은 모두 가깝게 오래 사귄 사람을 뜻하는 '친구'라는 의미와 좋은 친구 Good friend, 짝꿍 Mate란 의미입니다.",
companyBody3: "PAL네트웍스는 사람에 따뜻한 가치를 존중하며, 신뢰를 바탕으로 실천하는 기업입니다.",
companyImgAlt: "PAL Networks 소개",
meaningLabel: "Brand Identity",
meaningTitle: "PALNETWORKS의 의미",
meaningCard1Title: "PAL — 친구 · Good friend · Mate",
meaningCard1Desc: "오래 사귄 친구처럼 고객과 함께 성장하는 파트너십을 추구합니다",
meaningCard2Title: "신뢰 · Trust",
meaningCard2Desc: "신뢰를 바탕으로 사람에게 따뜻한 가치를 존중하며 실천하는 기업입니다",
missionLabel: "Mission",
missionTitle: (
<>
항공산업의 기술혁신 선도를 통한
<br />
파트너 고객만족 지원
</>
),
missionDesc: "미션은 효율적이고 혁신적인 기술로 항공산업에 이바지 하는 시스템과 관련기술을 구축 개발하여 대한민국의 현재와 미래 발전에 기여하는 것입니다.",
ctaTitle: (
<>
PAL Networks와
<br />
함께 시작해보세요
</>
),
ctaDesc: (
<>
항공 IT 솔루션에 대한 문의, 협력 제안 무엇이든 환영합니다.
<br />
전문 인력이 빠르게 답변드리겠습니다.
</>
),
ctaLocation: "찾아오시는 길",
ctaInquiry: "문의하기",
},
en: {
heroSub: "Respecting values and building on trust, we create a sustainable future together with our customers.",
companyLabel: "About PAL Networks",
companyHeadingBlue: "In the Aviation & IT Industry,",
companyHeadingPlain: "A Software Development Specialist",
companyBody1: "As a software development specialist in the aviation and specialized IT industry, we carry out development, research, and consulting driven by technical expertise and skilled professionals.",
companyBody2: "PAL means \u2018friend\u2019 \u2014 someone close who has stayed by your side for a long time \u2014 as well as Good Friend and Mate.",
companyBody3: "PAL Networks is a company that respects the warm value of people and acts on a foundation of trust.",
companyImgAlt: "About PAL Networks",
meaningLabel: "Brand Identity",
meaningTitle: "The Meaning of PALNETWORKS",
meaningCard1Title: "PAL — Friend · Good Friend · Mate",
meaningCard1Desc: "We pursue a partnership that grows together with our customers, like a long-time friend.",
meaningCard2Title: "Trust",
meaningCard2Desc: "We are a company built on trust, respecting and practicing warm values toward people.",
missionLabel: "Mission",
missionTitle: (
<>
Leading Technology Innovation
<br />
for Partner Satisfaction
</>
),
missionDesc: "Our mission is to build and develop systems and related technologies that contribute to the aviation industry through efficient, innovative technology, helping advance Korea's present and future.",
ctaTitle: (
<>
Get Started
<br />
with PAL Networks
</>
),
ctaDesc: (
<>
Inquiries about aviation IT solutions or partnership proposals are always welcome.
<br />
Our specialists will respond quickly.
</>
),
ctaLocation: "Location",
ctaInquiry: "Inquiry",
},
};
export default function AboutPage() { export default function AboutPage() {
const { lang, withLang } = useLanguage();
const COMPANY_NAV = lang === "en" ? COMPANY_NAV_EN : COMPANY_NAV_KO;
const MISSION_ITEMS = lang === "en" ? MISSION_ITEMS_EN : MISSION_ITEMS_KO;
const t = PAGE_TEXT[lang];
const basePath = import.meta.env.BASE_URL; const basePath = import.meta.env.BASE_URL;
const ref = useRef(null); const ref = useRef(null);
@ -200,13 +340,7 @@ export default function AboutPage() {
<div className="sub-content"> <div className="sub-content">
<div className="inner-wrap"> <div className="inner-wrap">
<motion.div <motion.div className="ht-header" initial={{ opacity: 0, y: 32 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-80px" }} transition={{ duration: 0.6, ease: [0.4, 0, 0.2, 1] }}>
className="ht-header"
initial={{ opacity: 0, y: 32 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.6, ease: [0.4, 0, 0.2, 1] }}
>
<p className="ht-header-title"> <p className="ht-header-title">
Values First, Values First,
<br /> <br />
@ -214,49 +348,30 @@ export default function AboutPage() {
<span>Trust</span> <b>in Every Stepe</b> <span>Trust</span> <b>in Every Stepe</b>
</em> </em>
</p> </p>
<em className="ht-header-sub"> <em className="ht-header-sub">{t.heroSub}</em>
가치를 존중하며 신뢰를 바탕으로, 고객과 함께 지속 가능한 미래를
만들어갑니다.
</em>
</motion.div> </motion.div>
{/* 회사 소개 */} {/* 회사 소개 */}
<section className="about-company-section"> <section className="about-company-section">
<div className="about-company-inner"> <div className="about-company-inner">
<div className="about-company-text" ref={companyTextRef}> <div className="about-company-text" ref={companyTextRef}>
<p className="about-section-label">About PAL Networks</p> <p className="about-section-label">{t.companyLabel}</p>
<h3 className="about-company-heading"> <h3 className="about-company-heading">
<span className="blue">항공·전문 IT산업분야의</span> <span className="blue">{t.companyHeadingBlue}</span>
<br /> <br />
소프트웨어 개발 전문 기업 {t.companyHeadingPlain}
</h3> </h3>
<p className="about-company-body"> <p className="about-company-body">{t.companyBody1}</p>
항공·전문 IT산업분야의 소프트웨어 개발 전문 기업으로써 개발, <p className="about-company-body">{t.companyBody2}</p>
연구 &amp; 컨설팅 기술력 기반으로 전문인력 중심으로 <p className="about-company-body">{t.companyBody3}</p>
수행하고 있습니다.
</p>
<p className="about-company-body">
PAL은 모두 가깝게 오래 사귄 사람을 뜻하는 '친구'라는 의미와
좋은 친구 Good friend, 짝꿍 Mate란 의미입니다.
</p>
<p className="about-company-body">
PAL네트웍스는 사람에 따뜻한 가치를 존중하며, 신뢰를 바탕으로
실천하는 기업입니다.
</p>
</div> </div>
<div className="about-img-group"> <div className="about-img-group">
<div className="about-img-main-wrap" ref={imgMainRef}> <div className="about-img-main-wrap" ref={imgMainRef}>
<img <img src={`${basePath}images/aboutImg1.png`} alt={t.companyImgAlt} />{" "}
src={`${basePath}images/aboutImg1.png`}
alt="PAL Networks 소개"
/>{" "}
</div> </div>
<div className="about-img-side-wrap" ref={imgSideRef}> <div className="about-img-side-wrap" ref={imgSideRef}>
<img <img src={`${basePath}images/aboutImg1.png`} alt={t.companyImgAlt} />
src={`${basePath}images/aboutImg1.png`}
alt="PAL Networks 소개"
/>
</div> </div>
</div> </div>
</div> </div>
@ -264,31 +379,18 @@ export default function AboutPage() {
{/* PALNETWORKS 의미 */} {/* PALNETWORKS 의미 */}
<section className="about-meaning-section"> <section className="about-meaning-section">
<p className="about-section-label">Brand Identity</p> <p className="about-section-label">{t.meaningLabel}</p>
<h3 className="about-meaning-title">PALNETWORKS의 의미</h3> <h3 className="about-meaning-title">{t.meaningTitle}</h3>
<div className="about-meaning-cards"> <div className="about-meaning-cards">
<div <div className="about-meaning-card" ref={(el) => (meaningCardsRef.current[0] = el)}>
className="about-meaning-card"
ref={(el) => (meaningCardsRef.current[0] = el)}
>
<div className="about-meaning-card-keyword">FRIENDSHIP</div> <div className="about-meaning-card-keyword">FRIENDSHIP</div>
<h4 className="about-meaning-card-title"> <h4 className="about-meaning-card-title">{t.meaningCard1Title}</h4>
PAL 친구 · Good friend · Mate <p className="about-meaning-card-desc">{t.meaningCard1Desc}</p>
</h4>
<p className="about-meaning-card-desc">
오래 사귄 친구처럼 고객과 함께 성장하는 파트너십을 추구합니다
</p>
</div> </div>
<div <div className="about-meaning-card" ref={(el) => (meaningCardsRef.current[1] = el)}>
className="about-meaning-card"
ref={(el) => (meaningCardsRef.current[1] = el)}
>
<div className="about-meaning-card-keyword">RELIABILITY</div> <div className="about-meaning-card-keyword">RELIABILITY</div>
<h4 className="about-meaning-card-title">신뢰 · Trust</h4> <h4 className="about-meaning-card-title">{t.meaningCard2Title}</h4>
<p className="about-meaning-card-desc"> <p className="about-meaning-card-desc">{t.meaningCard2Desc}</p>
신뢰를 바탕으로 사람에게 따뜻한 가치를 존중하며 실천하는
기업입니다
</p>
</div> </div>
</div> </div>
</section> </section>
@ -297,54 +399,19 @@ export default function AboutPage() {
<section className="about-mission-section"> <section className="about-mission-section">
<div className="about-mission-inner"> <div className="about-mission-inner">
<div className="about-mission-left"> <div className="about-mission-left">
<p className="about-section-label">Mission</p> <p className="about-section-label">{t.missionLabel}</p>
<h3 className="about-mission-title"> <h3 className="about-mission-title">{t.missionTitle}</h3>
항공산업의 기술혁신 선도를 통한 <p className="about-mission-desc">{t.missionDesc}</p>
<br />
파트너 고객만족 지원
</h3>
<p className="about-mission-desc">
미션은 효율적이고 혁신적인 기술로 항공산업에 이바지 하는
시스템과 관련기술을 구축 개발하여 대한민국의 현재와 미래
발전에 기여하는 것입니다.
</p>
</div> </div>
<div className="about-mission-right"> <div className="about-mission-right">
{[ {MISSION_ITEMS.map((item, i) => (
{ <div key={i} className="about-mission-item" ref={(el) => (missionItemsRef.current[i] = el)}>
num: "01",
keyword: "INNOVATION",
title: "혁신",
desc: "효율적이고 혁신적인 기술로 항공산업 발전에 기여합니다",
},
{
num: "02",
keyword: "TRUST",
title: "신뢰",
desc: "안전하고 신뢰할 수 있는 시스템을 구축합니다",
},
{
num: "03",
keyword: "COOPERATION",
title: "협력",
desc: "파트너와 함께 성장하는 협력 생태계를 만들어갑니다",
},
].map((item, i) => (
<div
key={i}
className="about-mission-item"
ref={(el) => (missionItemsRef.current[i] = el)}
>
<span className="about-mission-item-num">{item.num}</span> <span className="about-mission-item-num">{item.num}</span>
<div className="about-mission-item-body"> <div className="about-mission-item-body">
<div className="about-mission-item-top"> <div className="about-mission-item-top">
<h4 className="about-mission-item-title"> <h4 className="about-mission-item-title">{item.title}</h4>
{item.title} <span className="about-mission-item-keyword">{item.keyword}</span>
</h4>
<span className="about-mission-item-keyword">
{item.keyword}
</span>
</div> </div>
<p className="about-mission-item-desc">{item.desc}</p> <p className="about-mission-item-desc">{item.desc}</p>
</div> </div>
@ -420,33 +487,16 @@ export default function AboutPage() {
{/* CTA */} {/* CTA */}
<section className="about-cta-section"> <section className="about-cta-section">
<div className="about-cta-bg" /> <div className="about-cta-bg" />
<div <div className="about-cta-img" style={{ backgroundImage: `url(${basePath}images/about_banner.png)` }} />
className="about-cta-img"
style={{ backgroundImage: `url(${basePath}images/about_banner.png)` }}
/>
<div className="about-cta-inner"> <div className="about-cta-inner">
<h3 className="about-cta-title"> <h3 className="about-cta-title">{t.ctaTitle}</h3>
PAL Networks와 <p className="about-cta-desc">{t.ctaDesc}</p>
<br />
함께 시작해보세요
</h3>
<p className="about-cta-desc">
항공 IT 솔루션에 대한 문의, 협력 제안 무엇이든 환영합니다.
<br />
전문 인력이 빠르게 답변드리겠습니다.
</p>
<div className="about-cta-buttons"> <div className="about-cta-buttons">
<Link <Link to={withLang("/company/location")} className="about-cta-btn about-cta-btn--primary">
to="/company/location" {t.ctaLocation}
className="about-cta-btn about-cta-btn--primary"
>
찾아오시는
</Link> </Link>
<Link <Link to={withLang("/contact")} className="about-cta-btn about-cta-btn--outline">
to="/contact" {t.ctaInquiry}
className="about-cta-btn about-cta-btn--outline"
>
문의하기
</Link> </Link>
</div> </div>
</div> </div>

205
src/pages/company/CertPage.jsx

@ -1,10 +1,11 @@
import { useRef } from "react"; import { useRef } from "react";
import SubHero from "../../components/SubHero"; import SubHero from "../../components/SubHero";
import { motion, useInView } from "framer-motion"; import { motion, useInView } from "framer-motion";
import { useLanguage } from "../../context/LanguageContext";
const ease = [0.22, 1, 0.36, 1]; const ease = [0.22, 1, 0.36, 1];
const COMPANY_NAV = [ const COMPANY_NAV_KO = [
{ label: "회사소개", to: "/company/about" }, { label: "회사소개", to: "/company/about" },
{ label: "인증 및 특허현황", to: "/company/cert" }, { label: "인증 및 특허현황", to: "/company/cert" },
{ label: "연혁", to: "/company/history" }, { label: "연혁", to: "/company/history" },
@ -12,7 +13,15 @@ const COMPANY_NAV = [
{ label: "찾아오시는 길", to: "/company/location" }, { label: "찾아오시는 길", to: "/company/location" },
]; ];
const CERTS = [ const COMPANY_NAV_EN = [
{ label: "About Us", to: "/company/about" },
{ label: "Certifications & Patents", to: "/company/cert" },
{ label: "History", to: "/company/history" },
{ label: "Clients & Partners", to: "/company/partners" },
{ label: "Location", to: "/company/location" },
];
const CERTS_KO = [
{ imgs: ["cert01.png"], label: "관광벤처인증" }, { imgs: ["cert01.png"], label: "관광벤처인증" },
{ imgs: ["cert02.png"], label: "관광사업자등록증" }, { imgs: ["cert02.png"], label: "관광사업자등록증" },
{ imgs: ["cert03.png"], label: "국방벤처기업 (1)" }, { imgs: ["cert03.png"], label: "국방벤처기업 (1)" },
@ -25,7 +34,31 @@ const CERTS = [
{ imgs: ["cert10.png"], label: "정보통신공사업등록증" }, { imgs: ["cert10.png"], label: "정보통신공사업등록증" },
{ imgs: ["cert11.png"], label: "항공선도기업" }, { imgs: ["cert11.png"], label: "항공선도기업" },
]; ];
const PATENTS = [
const CERTS_EN = [
{ imgs: ["cert01.png"], label: "Tourism Venture Certification" },
{ imgs: ["cert02.png"], label: "Tourism Business Registration" },
{ imgs: ["cert03.png"], label: "Defense Venture Company (1)" },
{ imgs: ["cert04.png"], label: "Defense Venture Company (2)" },
{
imgs: ["cert05.png"],
label: "Defense Venture Partnership Confirmation",
},
{ imgs: ["cert06.png"], label: "Corporate R&D Institute" },
{
imgs: ["cert07.png"],
label: "Broadcasting & Telecom Equipment KC Certification",
},
{ imgs: ["cert08.png"], label: "Venture Company Certificate" },
{ imgs: ["cert09.png"], label: "Inno-Biz Certificate" },
{
imgs: ["cert10.png"],
label: "Information & Communication Construction Business Registration",
},
{ imgs: ["cert11.png"], label: "Leading Aviation Company" },
];
const PATENTS_KO = [
{ imgs: ["patents01.png"], label: "대기질측정장치 시스템" }, { imgs: ["patents01.png"], label: "대기질측정장치 시스템" },
{ {
imgs: ["patents02.png"], imgs: ["patents02.png"],
@ -37,7 +70,19 @@ const PATENTS = [
}, },
]; ];
const PATENT_APPLICATIONS = [ const PATENTS_EN = [
{ imgs: ["patents01.png"], label: "Air Quality Measurement System" },
{
imgs: ["patents02.png"],
label: "Urban Air Mobility Vehicle Performance Simulation System",
},
{
imgs: ["patents03.png"],
label: "Multi-Protocol Data Relay Device for Unmanned Aerial Vehicles",
},
];
const PATENT_APPLICATIONS_KO = [
{ title: "인터넷 기반의 항공권 예약 방법 및 시스템", date: "2022-02-17" }, { title: "인터넷 기반의 항공권 예약 방법 및 시스템", date: "2022-02-17" },
{ {
title: "드론 식별장치 및 이를 이용한 비행 관제 시스템", title: "드론 식별장치 및 이를 이용한 비행 관제 시스템",
@ -56,27 +101,80 @@ const PATENT_APPLICATIONS = [
}, },
]; ];
const PATENT_APPLICATIONS_EN = [
{
title: "Internet-Based Flight Ticket Reservation Method and System",
date: "2022-02-17",
},
{
title: "Drone Identification Device and Flight Control System Using It",
date: "2022-02-17",
},
{
title: "Education Method and System Using the Metaverse",
date: "2022-02-17",
},
{
title: "Flight Simulation Training Method and System Using the Metaverse",
date: "2022-02-17",
},
{ title: "Air Quality Measurement Device and System", date: "2023-02-27" },
{
title: "Flight Management and Monitoring Device for Unmanned Aerial Vehicles",
date: "2023-06-26",
},
{
title: "Flight Management and Monitoring Device for Unmanned Aerial Vehicles via Identifier Analysis",
date: "2024-01-08",
},
];
const PAGE_TEXT = {
ko: {
heroSub: (
<>
축적된 기술 역량과 지식재산을 기반으로 안정적이고
<br />
신뢰할 있는 서비스를 제공합니다.
</>
),
certTitle: "인증 현황",
certDesc: "팔네트웍스가 보유한 주요 인증 목록입니다.",
patentTitle: "특허 현황",
patentDesc: "팔네트웍스가 보유한 주요 특허 목록입니다.",
applicationTitle: "출원 현황",
thTitle: "발명의 명칭",
thDate: "출원일자",
},
en: {
heroSub: (
<>
Built on accumulated technical expertise and intellectual property,
<br />
we deliver stable and reliable service.
</>
),
certTitle: "Certifications",
certDesc: "A list of key certifications held by PAL Networks.",
patentTitle: "Patents",
patentDesc: "A list of key patents held by PAL Networks.",
applicationTitle: "Patent Applications",
thTitle: "Title of Invention",
thDate: "Application Date",
},
};
function ImgGrid({ items, inView, folder = "cert" }) { function ImgGrid({ items, inView, folder = "cert" }) {
const basePath = import.meta.env.BASE_URL; const basePath = import.meta.env.BASE_URL;
return ( return (
<ul className="cert-grid"> <ul className="cert-grid">
{items.map((item, i) => ( {items.map((item, i) => (
<motion.li <motion.li key={i} className={`cert-grid__item${item.imgs.length > 1 ? " cert-grid__item--wide" : ""}`} initial={{ opacity: 0, y: 32 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: i * 0.08, ease }}>
key={i}
className={`cert-grid__item${item.imgs.length > 1 ? " cert-grid__item--wide" : ""}`}
initial={{ opacity: 0, y: 32 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: i * 0.08, ease }}
>
<div className="cert-grid__card"> <div className="cert-grid__card">
<div className="cert-grid__imgs"> <div className="cert-grid__imgs">
{item.imgs.map((img, j) => ( {item.imgs.map((img, j) => (
<div key={j} className="cert-grid__img-wrap"> <div key={j} className="cert-grid__img-wrap">
<img <img src={`${basePath}images/${folder}/${img}`} alt={item.label} className="cert-grid__img" />
src={`${basePath}images/${folder}/${img}`}
alt={item.label}
className="cert-grid__img"
/>
</div> </div>
))} ))}
</div> </div>
@ -88,6 +186,13 @@ function ImgGrid({ items, inView, folder = "cert" }) {
); );
} }
export default function CertPage() { export default function CertPage() {
const { lang } = useLanguage();
const COMPANY_NAV = lang === "en" ? COMPANY_NAV_EN : COMPANY_NAV_KO;
const CERTS = lang === "en" ? CERTS_EN : CERTS_KO;
const PATENTS = lang === "en" ? PATENTS_EN : PATENTS_KO;
const PATENT_APPLICATIONS = lang === "en" ? PATENT_APPLICATIONS_EN : PATENT_APPLICATIONS_KO;
const t = PAGE_TEXT[lang];
const ref = useRef(null); const ref = useRef(null);
const certRef = useRef(null); const certRef = useRef(null);
const certInView = useInView(certRef, { once: true, margin: "-80px" }); const certInView = useInView(certRef, { once: true, margin: "-80px" });
@ -112,90 +217,50 @@ export default function CertPage() {
<div className="sub-content"> <div className="sub-content">
<div className="inner-wrap"> <div className="inner-wrap">
<motion.div <motion.div className="ht-header" initial={{ opacity: 0, y: 32 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-80px" }} transition={{ duration: 0.6, ease: [0.4, 0, 0.2, 1] }}>
className="ht-header"
initial={{ opacity: 0, y: 32 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.6, ease: [0.4, 0, 0.2, 1] }}
>
<p className="ht-header-title"> <p className="ht-header-title">
Certified <span>Expertise</span>, Certified <span>Expertise</span>,
<br /> <br />
Protected <b>Innovation</b> Protected <b>Innovation</b>
</p> </p>
<em className="ht-header-sub"> <em className="ht-header-sub">{t.heroSub}</em>
축적된 기술 역량과 지식재산을 기반으로 안정적이고
<br />
신뢰할 있는 서비스를 제공합니다.
</em>
</motion.div> </motion.div>
{/* 인증 */} {/* 인증 */}
<section className="cert-section" ref={certRef}> <section className="cert-section" ref={certRef}>
<motion.div <motion.div className="cert-section__header" initial={{ opacity: 0, y: 20 }} animate={certInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
className="cert-section__header"
initial={{ opacity: 0, y: 20 }}
animate={certInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease }}
>
<span className="about-section-label">Certification</span> <span className="about-section-label">Certification</span>
<h2 className="cert-section__title">인증 현황</h2> <h2 className="cert-section__title">{t.certTitle}</h2>
<p className="cert-section__desc"> <p className="cert-section__desc">{t.certDesc}</p>
팔네트웍스가 보유한 주요 인증 목록입니다.
</p>
</motion.div> </motion.div>
<ImgGrid items={CERTS} inView={certInView} /> <ImgGrid items={CERTS} inView={certInView} />
</section> </section>
{/* 특허 */} {/* 특허 */}
<section className="cert-section" ref={patentRef}> <section className="cert-section" ref={patentRef}>
<motion.div <motion.div className="cert-section__header" initial={{ opacity: 0, y: 20 }} animate={patentInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
className="cert-section__header"
initial={{ opacity: 0, y: 20 }}
animate={patentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease }}
>
<span className="about-section-label">Patent</span> <span className="about-section-label">Patent</span>
<h2 className="cert-section__title">특허 현황</h2> <h2 className="cert-section__title">{t.patentTitle}</h2>
<p className="cert-section__desc"> <p className="cert-section__desc">{t.patentDesc}</p>
팔네트웍스가 보유한 주요 특허 목록입니다.
</p>
</motion.div> </motion.div>
<ImgGrid <ImgGrid items={PATENTS} inView={patentInView} folder="cert/patents" />
items={PATENTS}
inView={patentInView}
folder="cert/patents"
/>
</section> </section>
<section className="patent-section" ref={applicationRef}> <section className="patent-section" ref={applicationRef}>
<motion.div <motion.div className="cert-section__header" initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-80px" }} transition={{ duration: 0.6, ease }}>
className="cert-section__header"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.6, ease }}
>
<span className="about-section-label">Patent</span> <span className="about-section-label">Patent</span>
<h2 className="cert-section__title">출원 현황</h2> <h2 className="cert-section__title">{t.applicationTitle}</h2>
</motion.div> </motion.div>
<table className="patent-table"> <table className="patent-table">
<thead> <thead>
<tr> <tr>
<th>발명의 명칭</th> <th>{t.thTitle}</th>
<th>출원일자</th> <th>{t.thDate}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{PATENT_APPLICATIONS.map((item, i) => ( {PATENT_APPLICATIONS.map((item, i) => (
<motion.tr <motion.tr key={i} initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-40px" }} transition={{ duration: 0.5, delay: i * 0.06, ease }}>
key={i}
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{ duration: 0.5, delay: i * 0.06, ease }}
>
<td>{item.title}</td> <td>{item.title}</td>
<td>{item.date}</td> <td>{item.date}</td>
</motion.tr> </motion.tr>

109
src/pages/company/HistoryPage.jsx

@ -1,8 +1,9 @@
import { useRef, useState, useEffect } from "react"; import { useRef, useState, useEffect } from "react";
import { motion, useScroll, useTransform } from "framer-motion"; import { motion, useScroll, useTransform } from "framer-motion";
import SubHero from "../../components/SubHero"; import SubHero from "../../components/SubHero";
import { useLanguage } from "../../context/LanguageContext";
const COMPANY_NAV = [ const COMPANY_NAV_KO = [
{ label: "회사소개", to: "/company/about" }, { label: "회사소개", to: "/company/about" },
{ label: "인증 및 특허현황", to: "/company/cert" }, { label: "인증 및 특허현황", to: "/company/cert" },
{ label: "연혁", to: "/company/history" }, { label: "연혁", to: "/company/history" },
@ -10,9 +11,17 @@ const COMPANY_NAV = [
{ label: "찾아오시는 길", to: "/company/location" }, { label: "찾아오시는 길", to: "/company/location" },
]; ];
const COMPANY_NAV_EN = [
{ label: "About Us", to: "/company/about" },
{ label: "Certifications & Patents", to: "/company/cert" },
{ label: "History", to: "/company/history" },
{ label: "Clients & Partners", to: "/company/partners" },
{ label: "Location", to: "/company/location" },
];
const TABS = ["2020's", "2010's"]; const TABS = ["2020's", "2010's"];
const HISTORY = { const HISTORY_KO = {
"2020's": [ "2020's": [
{ {
year: "2026", year: "2026",
@ -71,6 +80,86 @@ const HISTORY = {
], ],
}; };
const HISTORY_EN = {
"2020's": [
{
year: "2026",
items: ["Selected for Drone Industry Alliance Project Unit (PU) Program\n- Established qualification requirements for dedicated drone traffic management system operator -", "Built KAC Drone Traffic Management System (Korea Airports Corporation)", "Developed Information Sharing System for Public UTM Prototype (Korea Airports Corporation)", "Built DA42NG VR Simulator (Cheongju University)", "Selected for Defense Venture Innovative Technology Support Program (Incheon Defense Venture Center)", "Incheon PAV Prototype Flight Test Support Program (Incheon TechnoPark)", "Incheon RISE (Regional Innovation System for Education) Industry-Academia Technology Development Program (Inha Technical College)\n- Developed prototype for detecting and alerting UTM anomalies based on low-altitude drone flight data", "Incheon Regional Drone Company Voucher Support Program (Incheon TechnoPark)", "Airworthiness Certification Specialist Training Program, outsourced (Defense Acquisition Program Administration)", "Selected for EdTech Project Addressing Education Challenges (Korea Education and Research Information Service)\n- Built customized AI EdTech platform for students with borderline intellectual functioning", "Incheon PAV Prototype Development Support Program (Incheon TechnoPark)\n- K-certification-based Incheon PAV demonstration and localization of core components"],
},
{
year: "2025",
items: ["KAC Drone Traffic Management (UTM) System Build Project (Korea Airports Corporation)", "Built Website for Aerospace Bootcamp Program (Cheongju University)", "Built DIAMOND DA40NG Simulator (Cheongju University)", "Exhibited Flight Control System Using UTM Simulator (Drone Show Korea, BEXCO Busan)", "Selected for PAV Company PR Support Program (Incheon TechnoPark)", "PAV Component Technology Development Support Program (Incheon TechnoPark)", "Operated Website for Airworthiness Certification Training Program (Defense Acquisition Program Administration)", "Developed Information Sharing System for Public UTM Prototype (Korea Airports Corporation)", "Developed and Stabilized IBE (Seom Air)", "Obtained Inno-Biz Certification for Technology-Innovative SMEs"],
},
{
year: "2024",
items: ["Maintenance of Hwagae Garden, Ganghwa Island (Ganghwa County)", "PRIVIA Aviation Development Work (Tidesquare)", "Air Traffic Data System (IB Leaders)", "2024 Military Airworthiness Certification Training Program Website Maintenance (Defense Acquisition Program Administration)", "HILS-Based PAV Component Performance Test System (Inha Technical College)", "PAV Remote Monitoring System (Incheon TechnoPark)", "Developed Drone Terrain-Following Flight Manual (National Fire Research Institute, National Fire Agency)", "Participated in UTM Team Korea (UTK) Working Group (Ministry of Land, Infrastructure and Transport)", "KAC Drone Traffic Management (UTM) System Build Project (Korea Airports Corporation)", "Commissioned Analysis of Urban Air Traffic (UTM, AAM, etc.) Projects (Incheon TechnoPark)"],
},
{
year: "2023",
items: ["Built Flight Reservation System for Seom Air Co., Ltd.", "Signed Agreement for Namwon City Drone Demonstration City Project (Ministry of Land, Infrastructure and Transport)", "Signed MOU between PAL Networks and Vessl", "Pre-planning for UTM Airspace Aircraft Surveillance Technology Development (Incheon TechnoPark)", "Developed Blockchain-Based Drone Traffic Management (UTM) Information Sharing System (Korea Airports Corporation)", "UTM Consulting (SK Telecom)", "Air Mobility (PAV) Component Technology Development Support Program (Incheon TechnoPark)", "Exhibited LAANC/Drone/PAV/UTM Flight Operations Management System (Seoul ADEX 2023)", "Exhibited UTM Flight Operations Management System (2023 K-UTM CONFEX)", "Manufactured and Supplied 3 Types Including PAV GNSS Simulator Set (Inha Technical College)"],
},
{
year: "2022",
items: ["Operated and Maintained QR Code Access System (Hyundai Motor Company)", "Military Airworthiness Certification Training Program, outsourced (Defense Acquisition Program Administration)", "Air Mobility (PAV) Component Technology Development (Incheon TechnoPark)", "Developed Content for XR Metaverse 'Incheon-Yieum' Project Expansion (Incheon TechnoPark)", "Designed Drone Traffic Management (UTM) System", "Research Service for Namwon City Aviation Industry (Drone/UTM) Cluster Development", "Participated in K-UTM CONFEX Exhibition (Unmanned Aircraft Control System)", "Business Agreement with Namwon City on Aviation Industry"],
},
{
year: "2021",
items: ["Operated and Maintained QR Code Access System (Hyundai Motor Company)", "Analyzed PAV Technology and Industry Landscape (Incheon TechnoPark)", "Registered as Incheon Distributor for KT Cloud (KT Corporation)", "Drone Regulatory Sandbox Program \u2013 Built Flight Control System (Korea Institute of Aviation Safety Technology)", "Built Metaverse for KB Kookmin Bank Software Competition (Sapiens 4.0)", "Selected for Drone Demonstration Business Commercialization (Public Service Technology Advancement)", "Built OTA Aviation Service for Kaplix (Jeju Pass)", "Participated in ADEX 2021 Exhibition", "Selected for Incheon PAV Consortium", "Participated in Korea Metaverse Festival (KMF)"],
},
{
year: "2020",
items: ["Maintained Integrated System (Hi Air)", "Selected for VR/AR Convergence Content Demonstration and Development Support Program (Incheon TechnoPark)", "Developed QR Code-Based Visitor Health Screening Mobile Web (Incheon Tourism Organization)", "Developed QR Visitor Log and Automatic Door Control System (Hyundai Motor Company)", "Integrated WebTour (Hi Air)", "Obtained IATA Online Agency Accreditation"],
},
],
"2010's": [
{
year: "2019",
items: ["Signed MOU for IBS (PSS, Crew Scheduling, MRO)", "Developed and Delivered IBS PSS (Hi Air)", "Developed Safety & Security System, E-SMS (Hi Air)", "Developed Weight & Balance System (Hi Air)"],
},
{
year: "2018",
items: ["Built Airline Website Reservation System (Air Philip)", "Precision Inspection of Aeronautical Lighting Facilities Using Drones (Korea Institute of Aviation Safety Technology)", "Registered PAL&TOUR as Overseas Travel Agency", "Certified as Venture Company", "Developed Test UTM System Database (KT Corporation)", "Designated as Incheon Promising Aviation Company by Incheon Metropolitan City"],
},
{
year: "2017",
items: ["Developed Light Aircraft Navigation and Flight Operations Management System (Ministry of Land, Infrastructure and Transport)", "A-CDM Design and RMS Improvement Service (Korea Airports Corporation)", "Signed MOU with IBTP", "Built Airline Maintenance System (Air Philip)", "Relocated Headquarters to Robot Tower"],
},
{
year: "2016",
items: ["Improved and Maintained IT System for Aviation Workforce Training Program (Korea Aviation Association)"],
},
{
year: "2015",
items: ["Operated Website System for Community Service Centers (Incheon Metropolitan City)", "Recognized as Corporate R&D Institute", "ActiveX Removal Project for Airport Portal (Incheon International Airport Corporation)", "Converted to PAL Networks Co., Ltd. (corporate entity)", "Selected for Startup Growth Program by Small and Medium Business Administration (Completed: Successful)"],
},
{
year: "2014",
items: ["Founded 'PAL Networks' as a sole proprietorship", "IT System Enhancement Service for Aviation Workforce Training Program (Korea Aviation Association)", "Built Website for Myanmar International Airlines (Crystal Aviation)", "Registered as Direct Manufacturer", "Built Website for World Book Capital (Incheon Metropolitan City)"],
},
],
};
const PAGE_TEXT = {
ko: {
heroSub: (
<>
항공 IT 소프트웨어 개발부터 드론·UTM·UATM 미래 모빌리티까지,
<br />
하늘길을 개척해온 팔네트웍스의 10 발자취를 담았습니다.
</>
),
},
en: {
heroSub: (
<>
From aviation IT software development to drones, UTM, UATM, and future mobility,
<br />
this is a decade of PAL Networks pioneering the skies.
</>
),
},
};
function YearGroup({ group }) { function YearGroup({ group }) {
const ref = useRef(null); const ref = useRef(null);
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
@ -101,8 +190,8 @@ function YearGroup({ group }) {
); );
} }
function TimelinePanel({ tab }) { function TimelinePanel({ tab, history }) {
const groups = HISTORY[tab]; const groups = history[tab];
const containerRef = useRef(null); const containerRef = useRef(null);
const { scrollYProgress } = useScroll({ const { scrollYProgress } = useScroll({
@ -136,6 +225,10 @@ function TimelinePanel({ tab }) {
} }
export default function HistoryPage() { export default function HistoryPage() {
const { lang } = useLanguage();
const COMPANY_NAV = lang === "en" ? COMPANY_NAV_EN : COMPANY_NAV_KO;
const history = lang === "en" ? HISTORY_EN : HISTORY_KO;
const t = PAGE_TEXT[lang];
const [activeTab, setActiveTab] = useState("2020's"); const [activeTab, setActiveTab] = useState("2020's");
return ( return (
@ -161,11 +254,7 @@ export default function HistoryPage() {
<span>of</span> <b>Aviation Technology</b> <span>of</span> <b>Aviation Technology</b>
</em> </em>
</p> </p>
<em className="ht-header-sub"> <em className="ht-header-sub">{t.heroSub}</em>
항공 IT 소프트웨어 개발부터 드론·UTM·UATM 미래 모빌리티까지,
<br />
하늘길을 개척해온 팔네트웍스의 10 발자취를 담았습니다.
</em>
</motion.div> </motion.div>
{/* 연혁 카드 박스 */} {/* 연혁 카드 박스 */}
@ -177,7 +266,7 @@ export default function HistoryPage() {
</button> </button>
))} ))}
</div> </div>
<TimelinePanel key={activeTab} tab={activeTab} /> <TimelinePanel key={activeTab} tab={activeTab} history={history} />
</div> </div>
</div> </div>
</div> </div>

290
src/pages/company/LocationPage.jsx

@ -3,8 +3,9 @@ import useFadeIn from "../../hooks/useFadeIn";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useState } from "react"; import { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useLanguage } from "../../context/LanguageContext";
const COMPANY_NAV = [ const COMPANY_NAV_KO = [
{ label: "회사소개", to: "/company/about" }, { label: "회사소개", to: "/company/about" },
{ label: "인증 및 특허현황", to: "/company/cert" }, { label: "인증 및 특허현황", to: "/company/cert" },
{ label: "연혁", to: "/company/history" }, { label: "연혁", to: "/company/history" },
@ -12,7 +13,15 @@ const COMPANY_NAV = [
{ label: "찾아오시는 길", to: "/company/location" }, { label: "찾아오시는 길", to: "/company/location" },
]; ];
const INFO = [ const COMPANY_NAV_EN = [
{ label: "About Us", to: "/company/about" },
{ label: "Certifications & Patents", to: "/company/cert" },
{ label: "History", to: "/company/history" },
{ label: "Clients & Partners", to: "/company/partners" },
{ label: "Location", to: "/company/location" },
];
const INFO_KO = [
{ {
label: "주소", label: "주소",
value: "인천광역시 서구 로봇랜드로 155-11\n로봇랜드 14층 1401~2호", value: "인천광역시 서구 로봇랜드로 155-11\n로봇랜드 14층 1401~2호",
@ -26,7 +35,21 @@ const INFO = [
}, },
]; ];
const INFO_MAGOK = [ const INFO_EN = [
{
label: "Address",
value: "14F, 1401-2, Robot Land\n155-11 Robot Land-ro, Seo-gu, Incheon",
},
{ label: "Phone", value: "032-727-5909", href: "tel:032-727-5909" },
{ label: "Fax", value: "032-727-5908" },
{
label: "Email",
value: "help@palnet.co.kr",
href: "mailto:help@palnet.co.kr",
},
];
const INFO_MAGOK_KO = [
{ {
label: "주소", label: "주소",
value: "서울특별시 강서구 공항대로 219\n서울마곡지구센테니아빌딩", value: "서울특별시 강서구 공항대로 219\n서울마곡지구센테니아빌딩",
@ -39,7 +62,20 @@ const INFO_MAGOK = [
}, },
]; ];
const TRANSPORT_INCHEON = [ const INFO_MAGOK_EN = [
{
label: "Address",
value: "Seoul Magok District Centennia Building\n219 Gonghangdae-ro, Gangseo-gu, Seoul",
},
{ label: "Phone", value: "032-727-5909", href: "tel:032-727-5909" },
{
label: "Email",
value: "help@palnet.co.kr",
href: "mailto:help@palnet.co.kr",
},
];
const TRANSPORT_INCHEON_KO = [
{ {
badge: "대중교통", badge: "대중교통",
icon: "/images/subway.png", icon: "/images/subway.png",
@ -87,7 +123,57 @@ const TRANSPORT_INCHEON = [
], ],
}, },
]; ];
const TRANSPORT_MAGOK = [
const TRANSPORT_INCHEON_EN = [
{
badge: "Public Transit",
icon: "/images/subway.png",
items: [
{
title: "Cheongna Int'l City Station (Airport Railroad)",
desc: "Cheongna Int'l City Stn \u2192 Take Trunk Bus 2-1 \u2192 Get off at Cheongna Baekse Nursing Hospital stop \u2192 10-min walk \u2192 Robot Tower",
},
{
title: "Geomam Station (Airport Railroad / Incheon Line 2)",
desc: "Geomam Stn \u2192 Take Trunk Bus 70 \u2192 Get off at Cheongna Robot Land stop \u2192 Robot Tower",
},
],
},
{
badge: "Shuttle Bus",
icon: "/images/bus.png",
items: [
{
title: "Departs from Cheongna Int'l City Station",
desc: "Cheongna Stn Exit 1 \u2192 Robot Tower \u2192 Gajeong Stn Exit 4\nDeparture: 7:40 / 8:10 / 8:40 / 9:10 / 9:40",
},
{
title: "Departs from Gajeong Station A (Exit 4)",
desc: "Departure: 7:00 / 7:40 / 8:30 / 9:30",
},
{
title: "Departs from Gajeong Station B (via Cheongna Complex)",
desc: "Departure: 7:20 / 8:20 / 9:20 / 9:30",
},
],
},
{
badge: "By Car",
icon: "/images/car.png",
items: [
{
title: "From Seoul",
desc: "Seoul Ring Expressway (43.5km) \u2192 Incheon Int'l Airport Expwy (9.4km) \u2192 Cheongjung-ro (4.3km) \u2192 Robot Land-ro 639m \u2192 Incheon Robot Land Robot Tower",
},
{
title: "From Incheon",
desc: "Gyeongin Expwy (10.3km) \u2192 Seogot-ro (0.7km) \u2192 Bongodae-ro (2.4km) \u2192 Turn right toward Gyeongin Port at Seohae Intersection \u2192 Robot Land-ro 346m \u2192 Incheon Robot Land Robot Tower",
},
],
},
];
const TRANSPORT_MAGOK_KO = [
{ {
badge: "지하철", badge: "지하철",
icon: "/images/subway.png", icon: "/images/subway.png",
@ -124,12 +210,92 @@ const TRANSPORT_MAGOK = [
}, },
]; ];
const MAP_INCHEON = const TRANSPORT_MAGOK_EN = [
"https://www.google.com/maps/embed?pb=!1m16!1m12!1m3!1d3164.5077769134846!2d126.60837056230932!3d37.51952592193411!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!2m1!1z7J247LKcIOuhnOu0h-uenOuTnA!5e0!3m2!1sko!2skr!4v1779870074094!5m2!1sko!2skr"; {
const MAP_MAGOK = badge: "Subway",
"https://www.google.com/maps/embed?pb=!1m16!1m12!1m3!1d3162.80690228454!2d126.82939801231092!3d37.559612971923826!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!2m1!1z7IS87YWM64uI7JWE!5e0!3m2!1sko!2skr!4v1779871233362!5m2!1sko!2skr"; icon: "/images/subway.png",
items: [
{
title: "Magok Station (Line 5)",
desc: "Exit 9 \u2192 About 5-min walk (straight toward Gonghangdae-ro, arrive at Seoul Magok District Centennia Building)",
},
],
},
{
badge: "Bus",
icon: "/images/bus.png",
items: [
{
title: "Get off at Magok Station stop",
desc: "Take Trunk Bus 601, 605, or 654 \u2192 About 5-min walk",
},
],
},
{
badge: "By Car",
icon: "/images/car.png",
items: [
{
title: "From Gangnam",
desc: "Olympic-daero \u2192 Banghwa Bridge \u2192 Enter Gonghangdae-ro \u2192 Turn left at Magok Stn intersection \u2192 Arrive at 219 Gonghangdae-ro (approx. 40 min)",
},
{
title: "From Incheon",
desc: "Gyeongin Expwy \u2192 Gayang Bridge \u2192 Enter Gonghangdae-ro \u2192 Turn right at Magok Stn intersection \u2192 Arrive at 219 Gonghangdae-ro (approx. 50 min)",
},
],
},
];
const MAP_INCHEON = "https://www.google.com/maps/embed?pb=!1m16!1m12!1m3!1d3164.5077769134846!2d126.60837056230932!3d37.51952592193411!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!2m1!1z7J247LKcIOuhnOu0h-uenOuTnA!5e0!3m2!1sko!2skr!4v1779870074094!5m2!1sko!2skr";
const MAP_MAGOK = "https://www.google.com/maps/embed?pb=!1m16!1m12!1m3!1d3162.80690228454!2d126.82939801231092!3d37.559612971923826!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!2m1!1z7IS87YWM64uI7JWE!5e0!3m2!1sko!2skr!4v1779871233362!5m2!1sko!2skr";
const PAGE_TEXT = {
ko: {
heroSub: "고객 여러분의 방문을 위해 팔네트웍스의 위치 정보를 안내해드립니다.",
tabIncheon: "인천 본사",
tabMagok: "마곡 지점",
companyInfoLabel: "회사 정보",
hoursLabel: "운영 시간",
hoursText: (
<>
평일 09:00 18:00
<br />
<span className="location-hours-text">··공휴일 휴무</span>
</>
),
inquiryBtn: "문의하기",
transportTitle: "교통편 안내",
mapTitle: "팔네트웍스 위치",
},
en: {
heroSub: "For your visit, here is PAL Networks' location information.",
tabIncheon: "Incheon HQ",
tabMagok: "Magok Branch",
companyInfoLabel: "Company Information",
hoursLabel: "Business Hours",
hoursText: (
<>
Weekdays 09:00 18:00
<br />
<span className="location-hours-text">Closed on weekends & holidays</span>
</>
),
inquiryBtn: "Inquiry",
transportTitle: "Directions",
mapTitle: "PAL Networks Location",
},
};
export default function LocationPage() { export default function LocationPage() {
const { lang, withLang } = useLanguage();
const COMPANY_NAV = lang === "en" ? COMPANY_NAV_EN : COMPANY_NAV_KO;
const INFO = lang === "en" ? INFO_EN : INFO_KO;
const INFO_MAGOK = lang === "en" ? INFO_MAGOK_EN : INFO_MAGOK_KO;
const TRANSPORT_INCHEON = lang === "en" ? TRANSPORT_INCHEON_EN : TRANSPORT_INCHEON_KO;
const TRANSPORT_MAGOK = lang === "en" ? TRANSPORT_MAGOK_EN : TRANSPORT_MAGOK_KO;
const t = PAGE_TEXT[lang];
const ref = useFadeIn(); const ref = useFadeIn();
const [tab, setTab] = useState("incheon"); const [tab, setTab] = useState("incheon");
@ -152,13 +318,7 @@ export default function LocationPage() {
<div className="sub-content"> <div className="sub-content">
<div className="inner-wrap"> <div className="inner-wrap">
<motion.div <motion.div className="ht-header" initial={{ opacity: 0, y: 32 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-80px" }} transition={{ duration: 0.6, ease: [0.4, 0, 0.2, 1] }}>
className="ht-header"
initial={{ opacity: 0, y: 32 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.6, ease: [0.4, 0, 0.2, 1] }}
>
<p className="ht-header-title"> <p className="ht-header-title">
Find Us, Find Us,
<br /> <br />
@ -166,44 +326,24 @@ export default function LocationPage() {
<span>Here</span> <b>We Are</b> <span>Here</span> <b>We Are</b>
</em> </em>
</p> </p>
<em className="ht-header-sub"> <em className="ht-header-sub">{t.heroSub}</em>
고객 여러분의 방문을 위해 팔네트웍스의 위치 정보를 안내해드립니다.
</em>
</motion.div> </motion.div>
<div className="location-tabs"> <div className="location-tabs">
<button <button className={`location-tab ${isIncheon ? "active" : ""}`} onClick={() => setTab("incheon")}>
className={`location-tab ${isIncheon ? "active" : ""}`} {t.tabIncheon}
onClick={() => setTab("incheon")}
>
인천 본사
</button> </button>
<button <button className={`location-tab ${!isIncheon ? "active" : ""}`} onClick={() => setTab("magok")}>
className={`location-tab ${!isIncheon ? "active" : ""}`} {t.tabMagok}
onClick={() => setTab("magok")}
>
마곡 지점
</button> </button>
</div> </div>
<section className="sub-section"> <section className="sub-section">
<div className="location-wrap"> <div className="location-wrap">
{/* 지도 */} {/* 지도 */}
<motion.div <motion.div className="location-map-wrap" key={tab + "-map"} initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4, ease: [0.4, 0, 0.2, 1] }}>
className="location-map-wrap"
key={tab + "-map"}
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: [0.4, 0, 0.2, 1] }}
>
<div className="location-map"> <div className="location-map">
<iframe <iframe src={currentMap} allowFullScreen loading="lazy" referrerPolicy="no-referrer-when-downgrade" title={t.mapTitle} />
src={currentMap}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
title="팔네트웍스 위치"
/>
</div> </div>
</motion.div> </motion.div>
@ -219,7 +359,7 @@ export default function LocationPage() {
delay: 0.15, delay: 0.15,
}} }}
> >
<p className="location-info-section-title">회사 정보</p> <p className="location-info-section-title">{t.companyInfoLabel}</p>
<motion.div <motion.div
key={tab + "-info"} key={tab + "-info"}
className="location-info-card" className="location-info-card"
@ -231,22 +371,17 @@ export default function LocationPage() {
delay: 0.1, delay: 0.1,
}} }}
> >
<h3>{isIncheon ? "인천 본사" : "마곡 지점"}</h3> <h3>{isIncheon ? t.tabIncheon : t.tabMagok}</h3>
<ul className="location-info-list"> <ul className="location-info-list">
{currentInfo.map((item) => ( {currentInfo.map((item) => (
<li key={item.label} className="location-info-item"> <li key={item.label} className="location-info-item">
<span className="location-info-label"> <span className="location-info-label">{item.label}</span>
{item.label}
</span>
{item.href ? ( {item.href ? (
<a className="location-info-value" href={item.href}> <a className="location-info-value" href={item.href}>
{item.value} {item.value}
</a> </a>
) : ( ) : (
<p <p className="location-info-value" style={{ margin: 0, whiteSpace: "pre-line" }}>
className="location-info-value"
style={{ margin: 0, whiteSpace: "pre-line" }}
>
{item.value} {item.value}
</p> </p>
)} )}
@ -254,42 +389,23 @@ export default function LocationPage() {
))} ))}
</ul> </ul>
<div className="location-hours"> <div className="location-hours">
<p className="location-hours-eyebrow">운영 시간</p> <p className="location-hours-eyebrow">{t.hoursLabel}</p>
<p className="location-hours-label"> <p className="location-hours-label">{t.hoursText}</p>
평일 09:00 18:00
<br />
<span className="location-hours-text">
··공휴일 휴무
</span>
</p>
</div> </div>
<Link <Link to={withLang("/contact/inquiry")} className="location-inquiry-btn" onMouseEnter={(e) => (e.currentTarget.style.opacity = ".85")} onMouseLeave={(e) => (e.currentTarget.style.opacity = "1")}>
to="/contact/inquiry" {t.inquiryBtn}
className="location-inquiry-btn"
onMouseEnter={(e) =>
(e.currentTarget.style.opacity = ".85")
}
onMouseLeave={(e) => (e.currentTarget.style.opacity = "1")}
>
문의하기
</Link> </Link>
</motion.div> </motion.div>
</motion.div> </motion.div>
{/* 교통편 */} {/* 교통편 */}
<motion.div <motion.div className="location-transport-wrap" key={tab + "-transport"} initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4, ease: [0.4, 0, 0.2, 1] }}>
className="location-transport-wrap"
key={tab + "-transport"}
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: [0.4, 0, 0.2, 1] }}
>
<div className="location-transport"> <div className="location-transport">
<h3 className="location-transport-title">교통편 안내</h3> <h3 className="location-transport-title">{t.transportTitle}</h3>
<div className="location-transport-list"> <div className="location-transport-list">
{currentTransport.map((t, i) => ( {currentTransport.map((item, i) => (
<motion.div <motion.div
key={t.badge} key={item.badge}
className="location-transport-item" className="location-transport-item"
initial={{ opacity: 0, y: 24 }} initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
@ -302,21 +418,15 @@ export default function LocationPage() {
> >
<div className="location-transport-item-top"> <div className="location-transport-item-top">
<div className="location-transport-icon"> <div className="location-transport-icon">
<img src={t.icon} alt={t.badge} /> <img src={item.icon} alt={item.badge} />
</div> </div>
<span className="location-transport-badge"> <span className="location-transport-badge">{item.badge}</span>
{t.badge}
</span>
</div> </div>
<div className="location-transport-routes"> <div className="location-transport-routes">
{t.items.map((item, j) => ( {item.items.map((route, j) => (
<div key={j} className="location-transport-route"> <div key={j} className="location-transport-route">
<p className="location-transport-route-title"> <p className="location-transport-route-title">{route.title}</p>
{item.title} <p className="location-transport-route-desc">{route.desc}</p>
</p>
<p className="location-transport-route-desc">
{item.desc}
</p>
</div> </div>
))} ))}
</div> </div>

146
src/pages/company/PartnersPage.jsx

@ -6,10 +6,11 @@ import { Link } from "react-router-dom";
import SubHero from "../../components/SubHero"; import SubHero from "../../components/SubHero";
import useFadeIn from "../../hooks/useFadeIn"; import useFadeIn from "../../hooks/useFadeIn";
import { useLanguage } from "../../context/LanguageContext";
gsap.registerPlugin(ScrollTrigger); gsap.registerPlugin(ScrollTrigger);
const COMPANY_NAV = [ const COMPANY_NAV_KO = [
{ label: "회사소개", to: "/company/about" }, { label: "회사소개", to: "/company/about" },
{ label: "인증 및 특허현황", to: "/company/cert" }, { label: "인증 및 특허현황", to: "/company/cert" },
{ label: "연혁", to: "/company/history" }, { label: "연혁", to: "/company/history" },
@ -17,6 +18,14 @@ const COMPANY_NAV = [
{ label: "찾아오시는 길", to: "/company/location" }, { label: "찾아오시는 길", to: "/company/location" },
]; ];
const COMPANY_NAV_EN = [
{ label: "About Us", to: "/company/about" },
{ label: "Certifications & Patents", to: "/company/cert" },
{ label: "History", to: "/company/history" },
{ label: "Clients & Partners", to: "/company/partners" },
{ label: "Location", to: "/company/location" },
];
const CLIENTS = [ const CLIENTS = [
{ id: "airport", logo: "01" }, { id: "airport", logo: "01" },
{ id: "kac", logo: "09" }, { id: "kac", logo: "09" },
@ -28,37 +37,78 @@ const CLIENTS = [
{ id: "molit", logo: "24" }, { id: "molit", logo: "24" },
]; ];
const PARTNERS = [ const PARTNERS = ["02", "03", "04", "05", "06", "07", "08", "10", "11", "12", "13", "14", "18", "19", "22"];
"02",
"03", const PAGE_TEXT = {
"04", ko: {
"05", heroSub: (
"06", <>
"07", 오랜 신뢰, 깊은 협력 <br />
"08", 신뢰를 바탕으로 다양한 기관 파트너와 협력하고 있습니다.
"10", </>
"11", ),
"12", clientsHeading: (
"13", <>
"14", 주요 <br /> 고객사
"18", </>
"19", ),
"22", partnersHeading: (
]; <>
기술 <br /> 협력사
</>
),
countSuffix: "개사",
ctaTitle: (
<>
팔네트웍스와 함께 성장할
<br />
파트너를 찾습니다
</>
),
ctaBtn: "협력 문의하기",
},
en: {
heroSub: (
<>
Long-standing trust, deep collaboration <br />
We work with a wide range of institutions and partners, built on trust.
</>
),
clientsHeading: (
<>
Key <br /> Clients
</>
),
partnersHeading: (
<>
Technology <br /> Partners
</>
),
countSuffix: " Organizations",
ctaTitle: (
<>
Looking for partners to
<br />
grow together with PAL Networks
</>
),
ctaBtn: "Partner With Us",
},
};
function LogoCard({ logo, basePath }) { function LogoCard({ logo, basePath }) {
return ( return (
<div className="partners-logo-cell"> <div className="partners-logo-cell">
<img <img src={`${basePath}images/partner/banner${logo}.png`} alt="" loading="lazy" />
src={`${basePath}images/partner/banner${logo}.png`}
alt=""
loading="lazy"
/>
</div> </div>
); );
} }
export default function PartnersPage() { export default function PartnersPage() {
const { lang, withLang } = useLanguage();
const COMPANY_NAV = lang === "en" ? COMPANY_NAV_EN : COMPANY_NAV_KO;
const t = PAGE_TEXT[lang];
const ref = useFadeIn(); const ref = useFadeIn();
const wrapRef = useRef(null); const wrapRef = useRef(null);
const basePath = import.meta.env.BASE_URL; const basePath = import.meta.env.BASE_URL;
@ -66,14 +116,7 @@ export default function PartnersPage() {
useEffect(() => { useEffect(() => {
const ctx = gsap.context(() => { const ctx = gsap.context(() => {
// //
gsap.utils gsap.utils.toArray([".partners-section-row", ".partners-sidebar", ".partners-grid", ".sub-fade-in"]).forEach((el) => {
.toArray([
".partners-section-row",
".partners-sidebar",
".partners-grid",
".sub-fade-in",
])
.forEach((el) => {
gsap.fromTo( gsap.fromTo(
el, el,
{ opacity: 0, y: 48 }, { opacity: 0, y: 48 },
@ -128,13 +171,7 @@ export default function PartnersPage() {
<div className="sub-content"> <div className="sub-content">
<div className="inner-wrap"> <div className="inner-wrap">
{/* 상단 타이틀 */} {/* 상단 타이틀 */}
<motion.div <motion.div className="ht-header" initial={{ opacity: 0, y: 32 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-80px" }} transition={{ duration: 0.6, ease: [0.4, 0, 0.2, 1] }}>
className="ht-header"
initial={{ opacity: 0, y: 32 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.6, ease: [0.4, 0, 0.2, 1] }}
>
<p className="ht-header-title"> <p className="ht-header-title">
Trusted by Many, Trusted by Many,
<br /> <br />
@ -142,10 +179,7 @@ export default function PartnersPage() {
<span>Built</span> <b>for the Future</b> <span>Built</span> <b>for the Future</b>
</em> </em>
</p> </p>
<em className="ht-header-sub"> <em className="ht-header-sub">{t.heroSub}</em>
오랜 신뢰, 깊은 협력 <br />
신뢰를 바탕으로 다양한 기관 파트너와 협력하고 있습니다.
</em>
</motion.div> </motion.div>
{/* <motion.div {/* <motion.div
initial={{ scaleY: 0, opacity: 0 }} initial={{ scaleY: 0, opacity: 0 }}
@ -167,10 +201,11 @@ export default function PartnersPage() {
<div className="partners-sidebar"> <div className="partners-sidebar">
<p className="partners-eyebrow">Clients</p> <p className="partners-eyebrow">Clients</p>
<div className="partners-heading-row"> <div className="partners-heading-row">
<h2 className="partners-heading"> <h2 className="partners-heading">{t.clientsHeading}</h2>
주요 <br /> 고객사 <p className="partners-count">
</h2> {CLIENTS.length}
<p className="partners-count">{CLIENTS.length}개사</p> {t.countSuffix}
</p>
</div> </div>
</div> </div>
<div className="partners-grid partners-grid--4"> <div className="partners-grid partners-grid--4">
@ -185,10 +220,11 @@ export default function PartnersPage() {
<div className="partners-sidebar"> <div className="partners-sidebar">
<p className="partners-eyebrow">Partners</p> <p className="partners-eyebrow">Partners</p>
<div className="partners-heading-row"> <div className="partners-heading-row">
<h2 className="partners-heading"> <h2 className="partners-heading">{t.partnersHeading}</h2>
기술 <br /> 협력사 <p className="partners-count">
</h2> {PARTNERS.length}
<p className="partners-count">{PARTNERS.length}개사</p> {t.countSuffix}
</p>
</div> </div>
</div> </div>
<div className="partners-grid partners-grid--5"> <div className="partners-grid partners-grid--5">
@ -209,14 +245,10 @@ export default function PartnersPage() {
/> />
<div className="cta-content"> <div className="cta-content">
<p className="cta-eyebrow">Become a Partner</p> <p className="cta-eyebrow">Become a Partner</p>
<h3 className="cta-title"> <h3 className="cta-title">{t.ctaTitle}</h3>
팔네트웍스와 함께 성장할
<br />
파트너를 찾습니다
</h3>
</div> </div>
<Link to="/contact/inquiry" className="cta-btn"> <Link to={withLang("/contact/inquiry")} className="cta-btn">
협력 문의하기 {t.ctaBtn}
</Link> </Link>
</div> </div>
</section> </section>

107
src/pages/contact/InquiryPage.jsx

@ -2,13 +2,80 @@ import { useState } from "react";
import { motion as Motion } from "framer-motion"; import { motion as Motion } from "framer-motion";
import SubHero from "../../components/SubHero"; import SubHero from "../../components/SubHero";
import PrivacyModal from "../../components/PrivacyModal"; import PrivacyModal from "../../components/PrivacyModal";
import { useLanguage } from "../../context/LanguageContext";
const CONTACT_NAV = [ const CONTACT_NAV_KO = [
{ label: "문의하기", to: "/contact/inquiry" }, { label: "문의하기", to: "/contact/inquiry" },
{ label: "채용정보", to: "/contact/recruit" }, { label: "채용정보", to: "/contact/recruit" },
]; ];
const CONTACT_NAV_EN = [
{ label: "Inquiry", to: "/contact/inquiry" },
{ label: "Careers", to: "/contact/recruit" },
];
const TEXT = {
ko: {
title: (
<>
프로젝트 협업이나 기술 도입 문의를 남겨주세요.
<br />
빠르게 검토 연락드리겠습니다.
</>
),
fields: {
name: "이름",
namePh: "이름을 입력해 주세요.",
email: "이메일",
emailPh: "이메일을 입력해 주세요.",
phone: "연락처",
phonePh: "연락처를 입력해 주세요.",
website: "홈페이지",
websitePh: "홈페이지 주소를 입력해 주세요.",
title: "제목",
titlePh: "문의 제목을 입력해 주세요.",
content: "내용",
contentPh: "문의 내용을 입력해 주세요.",
},
agree: "개인정보처리방침에 동의합니다.",
viewPolicy: "개인정보처리방침 보기",
submit: "문의하기",
submitAlert: "문의가 접수되었습니다.",
},
en: {
title: (
<>
Leave us a message about project collaboration or technology adoption.
<br />
We'll review it and get back to you quickly.
</>
),
fields: {
name: "Name",
namePh: "Please enter your name.",
email: "Email",
emailPh: "Please enter your email.",
phone: "Phone",
phonePh: "Please enter your phone number.",
website: "Website",
websitePh: "Please enter your website URL.",
title: "Subject",
titlePh: "Please enter the subject of your inquiry.",
content: "Message",
contentPh: "Please enter your inquiry.",
},
agree: "I agree to the Privacy Policy.",
viewPolicy: "View Privacy Policy",
submit: "Submit",
submitAlert: "Your inquiry has been submitted.",
},
};
export default function InquiryPage() { export default function InquiryPage() {
const { lang } = useLanguage();
const CONTACT_NAV = lang === "en" ? CONTACT_NAV_EN : CONTACT_NAV_KO;
const t = TEXT[lang];
const [isPrivacyOpen, setIsPrivacyOpen] = useState(false); const [isPrivacyOpen, setIsPrivacyOpen] = useState(false);
const [form, setForm] = useState({ const [form, setForm] = useState({
name: "", name: "",
@ -27,7 +94,7 @@ export default function InquiryPage() {
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
alert("문의가 접수되었습니다."); alert(t.submitAlert);
}; };
return ( return (
@ -46,11 +113,7 @@ export default function InquiryPage() {
<div className="inq-wrap"> <div className="inq-wrap">
{/* 타이틀 */} {/* 타이틀 */}
<Motion.div className="inq-head" initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-60px" }} transition={{ duration: 0.5, ease: [0.4, 0, 0.2, 1] }}> <Motion.div className="inq-head" initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-60px" }} transition={{ duration: 0.5, ease: [0.4, 0, 0.2, 1] }}>
<h2 className="inq-title"> <h2 className="inq-title">{t.title}</h2>
프로젝트 협업이나 기술 도입 문의를 남겨주세요.
<br />
빠르게 검토 연락드리겠습니다.
</h2>
{/* <p className="inq-desc"> {/* <p className="inq-desc">
- -
</p> */} </p> */}
@ -61,40 +124,40 @@ export default function InquiryPage() {
<div className="inq-grid"> <div className="inq-grid">
<label className="inq-field"> <label className="inq-field">
<span className="inq-label"> <span className="inq-label">
이름 <em>*</em> {t.fields.name} <em>*</em>
</span> </span>
<input className="inq-input" type="text" name="name" value={form.name} onChange={handleChange} placeholder="이름을 입력해 주세요." required /> <input className="inq-input" type="text" name="name" value={form.name} onChange={handleChange} placeholder={t.fields.namePh} required />
</label> </label>
<label className="inq-field"> <label className="inq-field">
<span className="inq-label"> <span className="inq-label">
이메일 <em>*</em> {t.fields.email} <em>*</em>
</span> </span>
<input className="inq-input" type="email" name="email" value={form.email} onChange={handleChange} placeholder="이메일을 입력해 주세요." required /> <input className="inq-input" type="email" name="email" value={form.email} onChange={handleChange} placeholder={t.fields.emailPh} required />
</label> </label>
<label className="inq-field"> <label className="inq-field">
<span className="inq-label">연락처</span> <span className="inq-label">{t.fields.phone}</span>
<input className="inq-input" type="tel" name="phone" value={form.phone} onChange={handleChange} placeholder="연락처를 입력해 주세요." /> <input className="inq-input" type="tel" name="phone" value={form.phone} onChange={handleChange} placeholder={t.fields.phonePh} />
</label> </label>
<label className="inq-field"> <label className="inq-field">
<span className="inq-label">홈페이지</span> <span className="inq-label">{t.fields.website}</span>
<input className="inq-input" type="url" name="website" value={form.website} onChange={handleChange} placeholder="홈페이지 주소를 입력해 주세요." /> <input className="inq-input" type="url" name="website" value={form.website} onChange={handleChange} placeholder={t.fields.websitePh} />
</label> </label>
<label className="inq-field inq-field--full"> <label className="inq-field inq-field--full">
<span className="inq-label"> <span className="inq-label">
제목 <em>*</em> {t.fields.title} <em>*</em>
</span> </span>
<input className="inq-input" type="text" name="title" value={form.title} onChange={handleChange} placeholder="문의 제목을 입력해 주세요." required /> <input className="inq-input" type="text" name="title" value={form.title} onChange={handleChange} placeholder={t.fields.titlePh} required />
</label> </label>
<label className="inq-field inq-field--full"> <label className="inq-field inq-field--full">
<span className="inq-label"> <span className="inq-label">
내용 <em>*</em> {t.fields.content} <em>*</em>
</span> </span>
<textarea className="inq-textarea" name="content" value={form.content} onChange={handleChange} placeholder="문의 내용을 입력해 주세요." required /> <textarea className="inq-textarea" name="content" value={form.content} onChange={handleChange} placeholder={t.fields.contentPh} required />
</label> </label>
</div> </div>
@ -102,14 +165,14 @@ export default function InquiryPage() {
<div className="inq-agree"> <div className="inq-agree">
<label className="inq-check"> <label className="inq-check">
<input type="checkbox" name="agree" checked={form.agree} onChange={handleChange} required /> <input type="checkbox" name="agree" checked={form.agree} onChange={handleChange} required />
<span>개인정보처리방침에 동의합니다.</span> <span>{t.agree}</span>
</label> </label>
<button type="button" className="inq-privacy-btn" onClick={() => setIsPrivacyOpen(true)}> <button type="button" className="inq-privacy-btn" onClick={() => setIsPrivacyOpen(true)}>
개인정보처리방침 보기 {t.viewPolicy}
</button> </button>
</div> </div>
<button type="submit" className="inq-submit"> <button type="submit" className="inq-submit">
문의하기 {t.submit}
</button> </button>
</div> </div>
</Motion.form> </Motion.form>

87
src/pages/contact/RecruitPage.jsx

@ -1,13 +1,19 @@
import { useState } from "react"; import { useState } from "react";
import { motion as Motion } from "framer-motion"; import { motion as Motion } from "framer-motion";
import SubHero from "../../components/SubHero"; import SubHero from "../../components/SubHero";
import { useLanguage } from "../../context/LanguageContext";
const CONTACT_NAV = [ const CONTACT_NAV_KO = [
{ label: "문의하기", to: "/contact/inquiry" }, { label: "문의하기", to: "/contact/inquiry" },
{ label: "채용정보", to: "/contact/recruit" }, { label: "채용정보", to: "/contact/recruit" },
]; ];
const JOBS = [ const CONTACT_NAV_EN = [
{ label: "Inquiry", to: "/contact/inquiry" },
{ label: "Careers", to: "/contact/recruit" },
];
const JOBS_KO = [
{ {
id: 1, id: 1,
part: "개발", part: "개발",
@ -30,14 +36,65 @@ const JOBS = [
}, },
]; ];
const BENEFITS = [ const JOBS_EN = [
{
id: 1,
part: "Development",
title: "Backend Developer",
type: "Full-time",
career: "3+ years experience",
stack: ["Java", "Spring Boot", "MySQL", "AWS"],
desc: "Responsible for backend development of aviation IT systems and the UAM/UTM platform.",
deadline: "2025.07.31",
},
{
id: 2,
part: "Development",
title: "Frontend Developer",
type: "Full-time",
career: "2+ years experience",
stack: ["React", "TypeScript", "Vite", "Framer Motion"],
desc: "Responsible for UI/UX development for air traffic control systems and web services.",
deadline: "2025.07.31",
},
];
const BENEFITS_KO = [
{ icon: "🏢", title: "유연근무", desc: "자율 출퇴근 및 재택근무 지원" }, { icon: "🏢", title: "유연근무", desc: "자율 출퇴근 및 재택근무 지원" },
{ icon: "📚", title: "자기계발", desc: "도서 구입비 및 교육비 지원" }, { icon: "📚", title: "자기계발", desc: "도서 구입비 및 교육비 지원" },
{ icon: "🍱", title: "식사 지원", desc: "중식 제공 또는 식대 지원" }, { icon: "🍱", title: "식사 지원", desc: "중식 제공 또는 식대 지원" },
{ icon: "💰", title: "성과 보상", desc: "우수 성과자 인센티브 지급" }, { icon: "💰", title: "성과 보상", desc: "우수 성과자 인센티브 지급" },
]; ];
function JobCard({ job, index }) { const BENEFITS_EN = [
{ icon: "🏢", title: "Flexible Work", desc: "Flexible hours and remote work support" },
{ icon: "📚", title: "Self-Development", desc: "Book and education expense support" },
{ icon: "🍱", title: "Meal Support", desc: "Lunch provided or meal allowance" },
{ icon: "💰", title: "Performance Rewards", desc: "Incentives for top performers" },
];
const PAGE_TEXT = {
ko: {
title: "하늘길을 함께 만들어갈 인재를 찾습니다.",
desc: "팔네트웍스는 항공 IT, UAM, 드론 분야의 미래를 함께 개척할 열정 있는 분들을 환영합니다.",
benefitsTitle: "복리후생",
jobsTitle: "채용 공고",
deadlineLabel: (d) => `마감 ${d}`,
apply: "지원하기",
applySubject: (title) => `채용 지원 - ${title}`,
},
en: {
title: "We're looking for talent to help build the skies with us.",
desc: "PAL Networks welcomes passionate people ready to pioneer the future of aviation IT, UAM, and drones with us.",
benefitsTitle: "Benefits",
jobsTitle: "Open Positions",
deadlineLabel: (d) => `Deadline: ${d}`,
apply: "Apply",
applySubject: (title) => `Job Application - ${title}`,
},
};
function JobCard({ job, index, t }) {
return ( return (
<Motion.div className="rc-job-card" initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-40px" }} transition={{ duration: 0.4, delay: index * 0.08, ease: [0.4, 0, 0.2, 1] }}> <Motion.div className="rc-job-card" initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-40px" }} transition={{ duration: 0.4, delay: index * 0.08, ease: [0.4, 0, 0.2, 1] }}>
<div className="rc-job-top"> <div className="rc-job-top">
@ -55,9 +112,9 @@ function JobCard({ job, index }) {
))} ))}
</div> </div>
<div className="rc-job-bottom"> <div className="rc-job-bottom">
<span className="rc-job-deadline">마감 {job.deadline}</span> <span className="rc-job-deadline">{t.deadlineLabel(job.deadline)}</span>
<a href="mailto:1416geun@palnet.co.kr?subject=채용 지원 - {job.title}" className="rc-job-apply"> <a href={`mailto:1416geun@palnet.co.kr?subject=${t.applySubject(job.title)}`} className="rc-job-apply">
지원하기 {t.apply}
</a> </a>
</div> </div>
</Motion.div> </Motion.div>
@ -65,6 +122,12 @@ function JobCard({ job, index }) {
} }
export default function RecruitPage() { export default function RecruitPage() {
const { lang } = useLanguage();
const CONTACT_NAV = lang === "en" ? CONTACT_NAV_EN : CONTACT_NAV_KO;
const JOBS = lang === "en" ? JOBS_EN : JOBS_KO;
const BENEFITS = lang === "en" ? BENEFITS_EN : BENEFITS_KO;
const t = PAGE_TEXT[lang];
return ( return (
<article> <article>
<SubHero <SubHero
@ -80,13 +143,13 @@ export default function RecruitPage() {
<div className="inner-wrap"> <div className="inner-wrap">
{/* 상단 타이틀 */} {/* 상단 타이틀 */}
<Motion.div className="rc-head" initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-60px" }} transition={{ duration: 0.5, ease: [0.4, 0, 0.2, 1] }}> <Motion.div className="rc-head" initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-60px" }} transition={{ duration: 0.5, ease: [0.4, 0, 0.2, 1] }}>
<h2 className="rc-title">하늘길을 함께 만들어갈 인재를 찾습니다.</h2> <h2 className="rc-title">{t.title}</h2>
<p className="rc-desc">팔네트웍스는 항공 IT, UAM, 드론 분야의 미래를 함께 개척할 열정 있는 분들을 환영합니다.</p> <p className="rc-desc">{t.desc}</p>
</Motion.div> </Motion.div>
{/* 복리후생 */} {/* 복리후생 */}
<Motion.div className="rc-benefits" initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-60px" }} transition={{ duration: 0.5, delay: 0.1, ease: [0.4, 0, 0.2, 1] }}> <Motion.div className="rc-benefits" initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-60px" }} transition={{ duration: 0.5, delay: 0.1, ease: [0.4, 0, 0.2, 1] }}>
<h3 className="rc-section-title">복리후생</h3> <h3 className="rc-section-title">{t.benefitsTitle}</h3>
<div className="rc-benefits-grid"> <div className="rc-benefits-grid">
{BENEFITS.map((b, i) => ( {BENEFITS.map((b, i) => (
<div key={i} className="rc-benefit-item"> <div key={i} className="rc-benefit-item">
@ -100,10 +163,10 @@ export default function RecruitPage() {
{/* 채용 공고 */} {/* 채용 공고 */}
<div className="rc-jobs"> <div className="rc-jobs">
<h3 className="rc-section-title">채용 공고</h3> <h3 className="rc-section-title">{t.jobsTitle}</h3>
<div className="rc-jobs-grid"> <div className="rc-jobs-grid">
{JOBS.map((job, i) => ( {JOBS.map((job, i) => (
<JobCard key={job.id} job={job} index={i} /> <JobCard key={job.id} job={job} index={i} t={t} />
))} ))}
</div> </div>
</div> </div>

435
src/pages/solution/FlightControlPage.jsx

@ -1,28 +1,26 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { motion, useInView, AnimatePresence } from "framer-motion"; import { motion, useInView, AnimatePresence } from "framer-motion";
import { import { Radio, Puzzle, Network, Expand, Shield, ArrowUpRight } from "lucide-react";
Radio,
Puzzle,
Network,
Expand,
Shield,
ArrowUpRight,
Package,
Wind,
AlertTriangle,
Ship,
Plane,
} from "lucide-react";
import SubHero from "../../components/SubHero"; import SubHero from "../../components/SubHero";
import useFadeIn from "../../hooks/useFadeIn"; import useFadeIn from "../../hooks/useFadeIn";
import { useLanguage } from "../../context/LanguageContext";
const SOLUTION_NAV = [ const SOLUTION_NAV_KO = [
{ label: "비행상황관리 시스템", to: "/solution/flight-control" }, { label: "비행상황관리 시스템", to: "/solution/flight-control" },
{ label: "IBE", to: "/solution/ibe" }, { label: "IBE", to: "/solution/ibe" },
// { label: " ", to: "/solution/smart-tour" }, // { label: " ", to: "/solution/smart-tour" },
// { label: "KT G-cloud ", to: "/solution/kt-gcloud" }, // { label: "KT G-cloud ", to: "/solution/kt-gcloud" },
]; ];
const SOLUTION_NAV_EN = [
{ label: "Flight Control System", to: "/solution/flight-control" },
{ label: "IBE", to: "/solution/ibe" },
// { label: "Smart Tourism Booking", to: "/solution/smart-tour" },
// { label: "KT G-cloud Incheon Distributor", to: "/solution/kt-gcloud" },
];
// / (FEATURES, STATS)
// .
const FEATURES = [ const FEATURES = [
{ icon: Radio, label: "실시간 모니터링" }, { icon: Radio, label: "실시간 모니터링" },
{ icon: Puzzle, label: "손쉬운 연계모듈" }, { icon: Puzzle, label: "손쉬운 연계모듈" },
@ -38,7 +36,7 @@ const STATS = [
{ value: "N+1", label: "다중 비행체 동시 관제" }, { value: "N+1", label: "다중 비행체 동시 관제" },
]; ];
const DOMAINS = [ const DOMAINS_KO = [
{ {
img: "domain_img1.jpg", img: "domain_img1.jpg",
label: "드론 물류·배송 관제", label: "드론 물류·배송 관제",
@ -66,7 +64,35 @@ const DOMAINS = [
}, },
]; ];
const FUNCTIONS = [ const DOMAINS_EN = [
{
img: "domain_img1.jpg",
label: "Drone Logistics & Delivery Control",
desc: "Optimized urban and regional drone delivery routes with real-time location tracking",
},
{
img: "domain_img2.jpg",
label: "UAM Urban Air Mobility",
desc: "Control system linked to future urban air traffic infrastructure",
},
{
img: "domain_img3.jpg",
label: "Environmental Monitoring Drones",
desc: "Operation and monitoring of drones collecting air quality and weather data",
},
{
img: "domain_img4.jpg",
label: "Anti-Drone Security Control",
desc: "Detection, tracking, and neutralization support for unauthorized drones",
},
{
img: "domain_img5.jpg",
label: "Integrated Vessel & Aircraft Control",
desc: "Combined maritime and aerial identification data collection and integrated situation management",
},
];
const FUNCTIONS_KO = [
{ num: "01", img: "./images/s1-01.jpg", label: "비행가능 지역 및 공역표출" }, { num: "01", img: "./images/s1-01.jpg", label: "비행가능 지역 및 공역표출" },
{ num: "02", img: "./images/s1-02.jpg", label: "비행체 위치 표출" }, { num: "02", img: "./images/s1-02.jpg", label: "비행체 위치 표출" },
{ {
@ -79,7 +105,28 @@ const FUNCTIONS = [
{ num: "06", img: "./images/s1-06.jpg", label: "비정상 상황의 경보 표출" }, { num: "06", img: "./images/s1-06.jpg", label: "비정상 상황의 경보 표출" },
]; ];
const FLOW = [ const FUNCTIONS_EN = [
{
num: "01",
img: "./images/s1-01.jpg",
label: "Display of flyable areas and airspace",
},
{ num: "02", img: "./images/s1-02.jpg", label: "Display of vehicle location" },
{
num: "03",
img: "./images/s1-03.jpg",
label: "Display of flight paths and history",
},
{ num: "04", img: "./images/s1-04.jpg", label: "Display of flight information" },
{ num: "05", img: "./images/s1-05.jpg", label: "Flight plan lookup" },
{
num: "06",
img: "./images/s1-06.jpg",
label: "Alerts for abnormal situations",
},
];
const FLOW_KO = [
{ {
step: "01", step: "01",
label: "비행체 식별", label: "비행체 식별",
@ -95,17 +142,117 @@ const FLOW = [
{ step: "05", label: "경보·대응", desc: "이상 상황 감지 즉시 경보 및 대응" }, { step: "05", label: "경보·대응", desc: "이상 상황 감지 즉시 경보 및 대응" },
]; ];
const FLOW_EN = [
{
step: "01",
label: "Vehicle Identification",
desc: "Real-time signal collection from aircraft, drones, and vessels",
},
{
step: "02",
label: "Data Collection",
desc: "Integrated processing of location, speed, altitude, and ID codes",
},
{
step: "03",
label: "Control Server",
desc: "Analysis and situation assessment by the integrated control server",
},
{
step: "04",
label: "Monitoring",
desc: "Real-time status display on the controller's screen",
},
{
step: "05",
label: "Alert & Response",
desc: "Immediate alert and response upon detecting anomalies",
},
];
const PAGE_TEXT = {
ko: {
introTitle: (
<>
모든 이동체 정보를
<br />
하나의 플랫폼에서
</>
),
introDesc: (
<>
항공기, 무인기, 선박, 지상 이동체의 실시간 정보를 통합 모니터링하고 <br />
상황을 인식하여 신속하고 안전한 운영 의사결정을 지원합니다.
</>
),
introIcons: [
{ img: "fc_aircraft.png", label: "항공기" },
{ img: "fc_drone.png", label: "무인기" },
{ img: "fc_vessel.png", label: "선박" },
{ img: "fc_car.png", label: "차량 등" },
],
monitorAlt: "비행상황관리 시스템",
highlight: [
{
tag: "Real-Time Monitoring",
title: "실시간 이동체 정보 기반\n통합 모니터링",
desc: "전체에 분산된 이동체의 위치와 상태를 실시간으로 추적하여 안전하고 효율적인 운영 환경을 제공합니다.",
},
{
tag: "Situational Awareness",
title: "상황 인식 및\n의사결정 지원",
desc: "다양한 데이터와 고도화된 분석을 통해 상황을 예측하고 최적의 의사결정을 가능하게 합니다.",
},
],
domainsTitle: "적용 분야",
functionsTitle: "주요기능",
flowTitle: "시스템 구성",
},
en: {
introTitle: (
<>
All Vehicle Information
<br />
on a Single Platform
</>
),
introDesc: (
<>
Integrated real-time monitoring of aircraft, drones, vessels, and ground vehicles <br />
supports situational awareness for fast, safe operational decisions.
</>
),
introIcons: [
{ img: "fc_aircraft.png", label: "Aircraft" },
{ img: "fc_drone.png", label: "Drone" },
{ img: "fc_vessel.png", label: "Vessel" },
{ img: "fc_car.png", label: "Vehicle & More" },
],
monitorAlt: "Flight Operations Management System",
highlight: [
{
tag: "Real-Time Monitoring",
title: "Real-Time Vehicle Data\nIntegrated Monitoring",
desc: "Tracks the location and status of vehicles distributed across the field in real time, providing a safe and efficient operating environment.",
},
{
tag: "Situational Awareness",
title: "Situational Awareness &\nDecision Support",
desc: "Advanced analysis of diverse data predicts situations and enables optimal decision-making.",
},
],
domainsTitle: "Applications",
functionsTitle: "Key Features",
flowTitle: "System Architecture",
},
};
const ease = [0.22, 1, 0.36, 1]; const ease = [0.22, 1, 0.36, 1];
function RevealText({ children, delay = 0, className }) { function RevealText({ children, delay = 0, className }) {
return ( return (
<div style={{ overflow: "hidden" }}> <div style={{ overflow: "hidden" }}>
<motion.div <motion.div className={className} initial={{ y: "105%", opacity: 0 }} animate={{ y: "0%", opacity: 1 }} transition={{ duration: 0.75, ease: [0.22, 1, 0.36, 1], delay }}>
className={className}
initial={{ y: "105%", opacity: 0 }}
animate={{ y: "0%", opacity: 1 }}
transition={{ duration: 0.75, ease: [0.22, 1, 0.36, 1], delay }}
>
{children} {children}
</motion.div> </motion.div>
</div> </div>
@ -115,10 +262,7 @@ function RevealText({ children, delay = 0, className }) {
function StaggerWords({ text, delay = 0, className }) { function StaggerWords({ text, delay = 0, className }) {
const words = text.split(" "); const words = text.split(" ");
return ( return (
<span <span className={className} style={{ display: "flex", flexWrap: "wrap", gap: "0 .3em" }}>
className={className}
style={{ display: "flex", flexWrap: "wrap", gap: "0 .3em" }}
>
{words.map((word, i) => ( {words.map((word, i) => (
<span key={i} style={{ overflow: "hidden", display: "inline-block" }}> <span key={i} style={{ overflow: "hidden", display: "inline-block" }}>
<motion.span <motion.span
@ -140,6 +284,13 @@ function StaggerWords({ text, delay = 0, className }) {
} }
function FlightControlPage() { function FlightControlPage() {
const { lang } = useLanguage();
const SOLUTION_NAV = lang === "en" ? SOLUTION_NAV_EN : SOLUTION_NAV_KO;
const DOMAINS = lang === "en" ? DOMAINS_EN : DOMAINS_KO;
const FUNCTIONS = lang === "en" ? FUNCTIONS_EN : FUNCTIONS_KO;
const FLOW = lang === "en" ? FLOW_EN : FLOW_KO;
const t = PAGE_TEXT[lang];
const basePath = import.meta.env.BASE_URL; const basePath = import.meta.env.BASE_URL;
const ref = useFadeIn(); const ref = useFadeIn();
@ -168,11 +319,7 @@ function FlightControlPage() {
return ( return (
<article ref={ref}> <article ref={ref}>
<SubHero <SubHero label="SOLUTION" title={<em>Flight Management</em>} navItems={SOLUTION_NAV} />
label="SOLUTION"
title={<em>Flight Management</em>}
navItems={SOLUTION_NAV}
/>
<div className="sub-content"> <div className="sub-content">
<div className="inner-wrap"> <div className="inner-wrap">
@ -245,68 +392,27 @@ function FlightControlPage() {
{/* 개요 인트로 */} {/* 개요 인트로 */}
<section className="fc-intro" ref={introRef}> <section className="fc-intro" ref={introRef}>
<div className="fc-intro__left"> <div className="fc-intro__left">
<motion.span <motion.span className="fc-eyebrow" initial={{ opacity: 0, y: 16 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
className="fc-eyebrow"
initial={{ opacity: 0, y: 16 }}
animate={introInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease }}
>
Overview Overview
</motion.span> </motion.span>
<motion.h2 <motion.h2 className="fc-intro__title" initial={{ opacity: 0, y: 24 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.7, ease, delay: 0.1 }}>
className="fc-intro__title" {t.introTitle}
initial={{ opacity: 0, y: 24 }}
animate={introInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.7, ease, delay: 0.1 }}
>
모든 이동체 정보를
<br />
하나의 플랫폼에서
</motion.h2> </motion.h2>
<motion.p <motion.p className="fc-intro__desc" initial={{ opacity: 0, y: 16 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease, delay: 0.2 }}>
className="fc-intro__desc" {t.introDesc}
initial={{ opacity: 0, y: 16 }}
animate={introInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease, delay: 0.2 }}
>
항공기, 무인기, 선박, 지상 이동체의 실시간 정보를 통합
모니터링하고 <br />
상황을 인식하여 신속하고 안전한 운영 의사결정을 지원합니다.
</motion.p> </motion.p>
<motion.div <motion.div className="fc-intro__icons" initial={{ opacity: 0, y: 16 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease, delay: 0.3 }}>
className="fc-intro__icons" {t.introIcons.map((item, i) => (
initial={{ opacity: 0, y: 16 }}
animate={introInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease, delay: 0.3 }}
>
{[
{ img: "fc_aircraft.png", label: "항공기" },
{ img: "fc_drone.png", label: "무인기" },
{ img: "fc_vessel.png", label: "선박" },
{ img: "fc_car.png", label: "차량 등" },
].map((item, i) => (
<div key={i} className="fc-intro__icon-item"> <div key={i} className="fc-intro__icon-item">
<img <img src={`${basePath}images/${item.img}`} alt={item.label} />
src={`${basePath}images/${item.img}`}
alt={item.label}
/>
<span>{item.label}</span> <span>{item.label}</span>
</div> </div>
))} ))}
</motion.div> </motion.div>
</div> </div>
<motion.div <motion.div className="fc-intro__right" initial={{ opacity: 0, x: 40 }} animate={introInView ? { opacity: 1, x: 0 } : {}} transition={{ duration: 0.9, ease, delay: 0.15 }}>
className="fc-intro__right" <img src={`${basePath}images/fc_computer.png`} alt={t.monitorAlt} className="fc-intro__monitor" />
initial={{ opacity: 0, x: 40 }}
animate={introInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.9, ease, delay: 0.15 }}
>
<img
src={`${basePath}images/fc_computer.png`}
alt="비행상황관리 시스템"
className="fc-intro__monitor"
/>
</motion.div> </motion.div>
</section> </section>
@ -314,9 +420,7 @@ function FlightControlPage() {
<section className="fc-highlight" ref={highlightRef}> <section className="fc-highlight" ref={highlightRef}>
{[ {[
{ {
tag: "Real-Time Monitoring", ...t.highlight[0],
title: "실시간 이동체 정보 기반\n통합 모니터링",
desc: "전체에 분산된 이동체의 위치와 상태를 실시간으로 추적하여 안전하고 효율적인 운영 환경을 제공합니다.",
img: "fc_tablet.png", img: "fc_tablet.png",
icons: [ icons: [
{ {
@ -352,20 +456,12 @@ function FlightControlPage() {
], ],
}, },
{ {
tag: "Situational Awareness", ...t.highlight[1],
title: "상황 인식 및\n의사결정 지원",
desc: "다양한 데이터와 고도화된 분석을 통해 상황을 예측하고 최적의 의사결정을 가능하게 합니다.",
img: "fc_tablet2.png", img: "fc_tablet2.png",
icons: null, icons: null,
}, },
].map((item, i) => ( ].map((item, i) => (
<motion.div <motion.div key={i} className="fc-highlight__item" initial={{ opacity: 0, y: 40 }} animate={highlightInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.7, ease, delay: i * 0.15 }}>
key={i}
className="fc-highlight__item"
initial={{ opacity: 0, y: 40 }}
animate={highlightInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.7, ease, delay: i * 0.15 }}
>
<div className="fc-highlight__text"> <div className="fc-highlight__text">
<span className="fc-eyebrow">{item.tag}</span> <span className="fc-eyebrow">{item.tag}</span>
<h3 className="fc-highlight__title"> <h3 className="fc-highlight__title">
@ -380,11 +476,7 @@ function FlightControlPage() {
</div> </div>
<div className="fc-highlight__img-wrap"> <div className="fc-highlight__img-wrap">
<div className="fc-highlight__img-scene"> <div className="fc-highlight__img-scene">
<img <img src={`${basePath}images/${item.img}`} alt={item.tag} className="fc-highlight__tablet" />
src={`${basePath}images/${item.img}`}
alt={item.tag}
className="fc-highlight__tablet"
/>
{item.icons && {item.icons &&
item.icons.map((icon, j) => ( item.icons.map((icon, j) => (
<motion.img <motion.img
@ -394,11 +486,7 @@ function FlightControlPage() {
className="fc-highlight__float-icon" className="fc-highlight__float-icon"
style={{ left: icon.x, top: icon.y }} style={{ left: icon.x, top: icon.y }}
initial={{ x: 0, y: 0 }} initial={{ x: 0, y: 0 }}
animate={ animate={highlightInView ? { x: icon.move.x[1], y: icon.move.y[1] } : { x: 0, y: 0 }}
highlightInView
? { x: icon.move.x[1], y: icon.move.y[1] }
: { x: 0, y: 0 }
}
transition={{ transition={{
duration: 2.5, duration: 2.5,
repeat: 0, repeat: 0,
@ -410,30 +498,9 @@ function FlightControlPage() {
{i === 1 && ( {i === 1 && (
<> <>
<motion.img <motion.img src={`${basePath}images/fc_left_tab.png`} alt="" className="fc-situation__left" initial={{ opacity: 0, x: -30 }} animate={highlightInView ? { opacity: 1, x: 0 } : {}} transition={{ duration: 0.8, ease, delay: 0.3 }} />
src={`${basePath}images/fc_left_tab.png`} <motion.img src={`${basePath}images/fc_right_tab.png`} alt="" className="fc-situation__right" initial={{ opacity: 0, x: 30 }} animate={highlightInView ? { opacity: 1, x: 0 } : {}} transition={{ duration: 0.8, ease, delay: 0.5 }} />
alt="" <motion.img src={`${basePath}images/fc_bottom_tab.png`} alt="" className="fc-situation__bottom" initial={{ opacity: 0, y: 30 }} animate={highlightInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.8, ease, delay: 0.7 }} />
className="fc-situation__left"
initial={{ opacity: 0, x: -30 }}
animate={highlightInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.8, ease, delay: 0.3 }}
/>
<motion.img
src={`${basePath}images/fc_right_tab.png`}
alt=""
className="fc-situation__right"
initial={{ opacity: 0, x: 30 }}
animate={highlightInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.8, ease, delay: 0.5 }}
/>
<motion.img
src={`${basePath}images/fc_bottom_tab.png`}
alt=""
className="fc-situation__bottom"
initial={{ opacity: 0, y: 30 }}
animate={highlightInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, ease, delay: 0.7 }}
/>
</> </>
)} )}
</div> </div>
@ -459,28 +526,13 @@ function FlightControlPage() {
{/* 3. 적용 분야 */} {/* 3. 적용 분야 */}
<section className="fc-domains" ref={domainsRef}> <section className="fc-domains" ref={domainsRef}>
<motion.span <motion.span className="fc-section-title" initial={{ opacity: 0, y: 20 }} animate={domainsInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
className="fc-section-title" {t.domainsTitle}
initial={{ opacity: 0, y: 20 }}
animate={domainsInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease }}
>
적용 분야
</motion.span> </motion.span>
<div className="fc-domains__grid"> <div className="fc-domains__grid">
{DOMAINS.map(({ icon: Icon, img, label, desc }, i) => ( {DOMAINS.map(({ img, label, desc }, i) => (
<motion.div <motion.div key={label} className="fc-domain-card" initial={{ opacity: 0, y: 24 }} animate={domainsInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.55, ease, delay: 0.07 * i }}>
key={label} <img className="fc-domain-card__img" src={`${basePath}images/${img}`} alt={label} />
className="fc-domain-card"
initial={{ opacity: 0, y: 24 }}
animate={domainsInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.55, ease, delay: 0.07 * i }}
>
<img
className="fc-domain-card__img"
src={`${basePath}images/${img}`}
alt={label}
/>
<div className="fc-domain-card__overlay"> <div className="fc-domain-card__overlay">
<span className="fc-domain-card__label">{label}</span> <span className="fc-domain-card__label">{label}</span>
<p className="fc-domain-card__desc">{desc}</p> <p className="fc-domain-card__desc">{desc}</p>
@ -492,79 +544,31 @@ function FlightControlPage() {
{/* 4. 주요기능 */} {/* 4. 주요기능 */}
<section className="fc-functions" ref={funcRef}> <section className="fc-functions" ref={funcRef}>
<motion.span <motion.span className="fc-section-title" initial={{ opacity: 0, y: 20 }} animate={funcInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
className="fc-section-title" {t.functionsTitle}
initial={{ opacity: 0, y: 20 }}
animate={funcInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease }}
>
주요기능
</motion.span> </motion.span>
<div className="fc-functions__body"> <div className="fc-functions__body">
<ul className="fc-func-list"> <ul className="fc-func-list">
{FUNCTIONS.map(({ num, label }, i) => ( {FUNCTIONS.map(({ num, label }, i) => (
<motion.li <motion.li key={num} className={`fc-func-item${activeIdx === i ? " is-active" : ""}`} onMouseEnter={() => setActiveIdx(i)} initial={{ opacity: 0, y: 16 }} animate={funcInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.5, ease, delay: 0.05 * i }}>
key={num}
className={`fc-func-item${activeIdx === i ? " is-active" : ""}`}
onMouseEnter={() => setActiveIdx(i)}
initial={{ opacity: 0, y: 16 }}
animate={funcInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, ease, delay: 0.05 * i }}
>
<span className="fc-func-item__num">{num}</span> <span className="fc-func-item__num">{num}</span>
<span className="fc-func-item__label">{label}</span> <span className="fc-func-item__label">{label}</span>
<motion.span <motion.span className="fc-func-item__arrow" initial={{ opacity: 0, x: -6 }} animate={activeIdx === i ? { opacity: 1, x: 0 } : { opacity: 0, x: -6 }} transition={{ duration: 0.2 }}>
className="fc-func-item__arrow"
initial={{ opacity: 0, x: -6 }}
animate={
activeIdx === i
? { opacity: 1, x: 0 }
: { opacity: 0, x: -6 }
}
transition={{ duration: 0.2 }}
>
<ArrowUpRight size={16} strokeWidth={1.5} /> <ArrowUpRight size={16} strokeWidth={1.5} />
</motion.span> </motion.span>
<motion.div <motion.div className="fc-func-item__line" animate={{ scaleX: activeIdx === i ? 1 : 0 }} transition={{ duration: 0.35, ease }} />
className="fc-func-item__line"
animate={{ scaleX: activeIdx === i ? 1 : 0 }}
transition={{ duration: 0.35, ease }}
/>
</motion.li> </motion.li>
))} ))}
</ul> </ul>
<div className="fc-func-display"> <div className="fc-func-display">
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
<motion.div <motion.div key={activeIdx} className="fc-func-display__inner" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.35, ease: "easeInOut" }}>
key={activeIdx} <img src={FUNCTIONS[activeIdx].img} alt={FUNCTIONS[activeIdx].label} className="fc-func-display__img" />
className="fc-func-display__inner"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.35, ease: "easeInOut" }}
>
<img
src={FUNCTIONS[activeIdx].img}
alt={FUNCTIONS[activeIdx].label}
className="fc-func-display__img"
/>
<div className="fc-func-display__caption"> <div className="fc-func-display__caption">
<motion.span <motion.span key={`num-${activeIdx}`} className="fc-func-display__num" initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.25 }}>
key={`num-${activeIdx}`}
className="fc-func-display__num"
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
>
{FUNCTIONS[activeIdx].num} {FUNCTIONS[activeIdx].num}
</motion.span> </motion.span>
<motion.span <motion.span key={`label-${activeIdx}`} className="fc-func-display__label" initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.25, delay: 0.05 }}>
key={`label-${activeIdx}`}
className="fc-func-display__label"
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.05 }}
>
{FUNCTIONS[activeIdx].label} {FUNCTIONS[activeIdx].label}
</motion.span> </motion.span>
</div> </div>
@ -576,23 +580,12 @@ function FlightControlPage() {
{/* 5. 시스템 구성 흐름 */} {/* 5. 시스템 구성 흐름 */}
<section className="fc-flow" ref={flowRef}> <section className="fc-flow" ref={flowRef}>
<motion.span <motion.span className="fc-section-title" initial={{ opacity: 0, y: 20 }} animate={flowInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
className="fc-section-title" {t.flowTitle}
initial={{ opacity: 0, y: 20 }}
animate={flowInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease }}
>
시스템 구성
</motion.span> </motion.span>
<div className="fc-flow__row"> <div className="fc-flow__row">
{FLOW.map(({ step, label, desc }, i) => ( {FLOW.map(({ step, label, desc }, i) => (
<motion.div <motion.div key={step} className="fc-flow__item" initial={{ opacity: 0, y: 20 }} animate={flowInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.5, ease, delay: 0.08 * i }}>
key={step}
className="fc-flow__item"
initial={{ opacity: 0, y: 20 }}
animate={flowInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, ease, delay: 0.08 * i }}
>
<span className="fc-flow__step">{step}</span> <span className="fc-flow__step">{step}</span>
<span className="fc-flow__label">{label}</span> <span className="fc-flow__label">{label}</span>
<p className="fc-flow__desc">{desc}</p> <p className="fc-flow__desc">{desc}</p>

409
src/pages/solution/IbePage.jsx

@ -3,10 +3,166 @@ import SubHero from "../../components/SubHero";
import { Fragment, useRef } from "react"; import { Fragment, useRef } from "react";
import { motion, useInView } from "framer-motion"; import { motion, useInView } from "framer-motion";
import { Search, Armchair, CalendarClock, Wallet, Ticket } from "lucide-react"; import { Search, Armchair, CalendarClock, Wallet, Ticket } from "lucide-react";
import { useLanguage } from "../../context/LanguageContext";
const ease = [0.22, 1, 0.36, 1]; const ease = [0.22, 1, 0.36, 1];
const SOLUTION_NAV_KO = [
{ label: "비행상황관리 시스템", to: "/solution/flight-control" },
{ label: "IBE", to: "/solution/ibe" },
];
const SOLUTION_NAV_EN = [
{ label: "Flight Control System", to: "/solution/flight-control" },
{ label: "IBE", to: "/solution/ibe" },
];
const PAGE_TEXT = {
ko: {
introTitle: (
<>
항공 예약의 모든 과정을
<br />
하나로 연결하는 통합 플랫폼
</>
),
introDesc: (
<>
IBE(Internet Booking Engine) 항공권 검색부터 예약, 결제,
<br />
발권까지 모든 프로세스를 지원하는 차세대 예약 엔진 입니다.
<br />
다양한 채널과 시스템을 유연하게 연동하여 최적의 예약 경험을 제공합니다.
</>
),
introIcons: [
{ img: "ibe_intro_icon1.png", label: "다중 채널 연동" },
{ img: "ibe_intro_icon2.png", label: "실시간 예약/재고" },
{ img: "ibe_intro_icon3.png", label: "안정성과 확장성" },
{ img: "ibe_intro_icon4.png", label: "데이터 기반 운영" },
],
bookingTitle: "직관적이고 간편한 예약 프로세스",
bookingSteps: [
{
num: "01",
icon: Search,
label: "검색",
desc: "출발지, 도착지, 일정,\n탑승객 정보를 입력하여\n최적의 항공편 검색",
},
{
num: "02",
icon: Armchair,
label: "선택",
desc: "운임, 스케줄, 좌석 등\n다양한 옵션을 비교하고\n원하는 항공편 선택",
},
{
num: "03",
icon: CalendarClock,
label: "예약",
desc: "승객 정보 입력 및\n부가 서비스 선택을 통해\n예약을 진행",
},
{
num: "04",
icon: Wallet,
label: "결제",
desc: "다양한 결제 수단을 지원하여\n안전하고 간편하게\n결제 진행",
},
{
num: "05",
icon: Ticket,
label: "발권",
desc: "E-Ticket 발행 후\n예약 정보를 확인하고\n예약 완료",
},
],
channelTitle: "다양한 시스템과의 유연한 연동",
channelDesc: "항공사 GDS, 호텔, 결제, 보험 등 다양한 외부 시스템과 API 기반 연동을 통해 확장성 높은 예약 환경을 제공합니다.",
channelLeft: [
{ img: "ibe_pal_icon1.png", label: "항공사" },
{ img: "ibe_pal_icon2.png", label: "GDS" },
{ img: "ibe_pal_icon3.png", label: "호텔" },
],
channelRight: [
{ img: "ibe_pal_icon4.png", label: "결제 시스템" },
{ img: "ibe_pal_icon5.png", label: "여행사/OTA" },
{ img: "ibe_pal_icon6.png", label: "보험" },
],
},
en: {
introTitle: (
<>
An Integrated Platform Connecting
<br />
Every Step of Flight Booking
</>
),
introDesc: (
<>
IBE (Internet Booking Engine) is a next-generation booking engine
<br />
that supports the entire process, from flight search to reservation, payment, and ticketing.
<br />
It flexibly integrates with diverse channels and systems to deliver an optimal booking experience.
</>
),
introIcons: [
{ img: "ibe_intro_icon1.png", label: "Multi-Channel Integration" },
{ img: "ibe_intro_icon2.png", label: "Real-Time Booking & Inventory" },
{ img: "ibe_intro_icon3.png", label: "Stability & Scalability" },
{ img: "ibe_intro_icon4.png", label: "Data-Driven Operations" },
],
bookingTitle: "An Intuitive, Simple Booking Process",
bookingSteps: [
{
num: "01",
icon: Search,
label: "Search",
desc: "Enter departure, destination,\ndates, and passenger details\nto find the best flight",
},
{
num: "02",
icon: Armchair,
label: "Select",
desc: "Compare fares, schedules,\nand seat options to choose\nthe flight you want",
},
{
num: "03",
icon: CalendarClock,
label: "Reserve",
desc: "Enter passenger details and\nselect add-on services\nto complete the reservation",
},
{
num: "04",
icon: Wallet,
label: "Payment",
desc: "Pay safely and easily\nwith a range of\nsupported payment methods",
},
{
num: "05",
icon: Ticket,
label: "Ticketing",
desc: "Issue an E-Ticket,\nconfirm the booking details,\nand you're all set",
},
],
channelTitle: "Flexible Integration with Diverse Systems",
channelDesc: "API-based integration with a wide range of external systems \u2014 airline GDS, hotels, payment, insurance, and more \u2014 delivers a highly scalable booking environment.",
channelLeft: [
{ img: "ibe_pal_icon1.png", label: "Airlines" },
{ img: "ibe_pal_icon2.png", label: "GDS" },
{ img: "ibe_pal_icon3.png", label: "Hotels" },
],
channelRight: [
{ img: "ibe_pal_icon4.png", label: "Payment Systems" },
{ img: "ibe_pal_icon5.png", label: "Travel Agencies/OTA" },
{ img: "ibe_pal_icon6.png", label: "Insurance" },
],
},
};
function IbePage() { function IbePage() {
const { lang } = useLanguage();
const SOLUTION_NAV = lang === "en" ? SOLUTION_NAV_EN : SOLUTION_NAV_KO;
const t = PAGE_TEXT[lang];
const basePath = import.meta.env.BASE_URL; const basePath = import.meta.env.BASE_URL;
const ref = useFadeIn(); const ref = useFadeIn();
const introRef = useRef(null); const introRef = useRef(null);
@ -16,11 +172,6 @@ function IbePage() {
const bookingRef = useRef(null); const bookingRef = useRef(null);
const bookingInView = useInView(bookingRef, { once: true, margin: "-60px" }); const bookingInView = useInView(bookingRef, { once: true, margin: "-60px" });
const SOLUTION_NAV = [
{ label: "비행상황관리 시스템", to: "/solution/flight-control" },
{ label: "IBE", to: "/solution/ibe" },
];
return ( return (
<article ref={ref}> <article ref={ref}>
<SubHero <SubHero
@ -38,139 +189,48 @@ function IbePage() {
{/* 개요 인트로 */} {/* 개요 인트로 */}
<section className="fc-intro" ref={introRef}> <section className="fc-intro" ref={introRef}>
<div className="fc-intro__left"> <div className="fc-intro__left">
<motion.span <motion.span className="fc-eyebrow" initial={{ opacity: 0, y: 16 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
className="fc-eyebrow"
initial={{ opacity: 0, y: 16 }}
animate={introInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease }}
>
Overview Overview
</motion.span> </motion.span>
<motion.h2 <motion.h2 className="fc-intro__title" initial={{ opacity: 0, y: 24 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.7, ease, delay: 0.1 }}>
className="fc-intro__title" {t.introTitle}
initial={{ opacity: 0, y: 24 }}
animate={introInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.7, ease, delay: 0.1 }}
>
항공 예약의 모든 과정을
<br />
하나로 연결하는 통합 플랫폼
</motion.h2> </motion.h2>
<motion.p <motion.p className="fc-intro__desc" initial={{ opacity: 0, y: 16 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease, delay: 0.2 }}>
className="fc-intro__desc" {t.introDesc}
initial={{ opacity: 0, y: 16 }}
animate={introInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease, delay: 0.2 }}
>
IBE(Internet Booking Engine) 항공권 검색부터 예약, 결제,
<br />
발권까지 모든 프로세스를 지원하는 차세대 예약 엔진 입니다.
<br />
다양한 채널과 시스템을 유연하게 연동하여 최적의 예약 경험을
제공합니다.
</motion.p> </motion.p>
<motion.div <motion.div className="fc-intro__icons" initial={{ opacity: 0, y: 16 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease, delay: 0.3 }}>
className="fc-intro__icons" {t.introIcons.map((item, i) => (
initial={{ opacity: 0, y: 16 }}
animate={introInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease, delay: 0.3 }}
>
{[
{ img: "ibe_intro_icon1.png", label: "다중 채널 연동" },
{ img: "ibe_intro_icon2.png", label: "실시간 예약/재고" },
{ img: "ibe_intro_icon3.png", label: "안정성과 확장성" },
{ img: "ibe_intro_icon4.png", label: "데이터 기반 운영" },
].map((item, i) => (
<div key={i} className="fc-intro__icon-item"> <div key={i} className="fc-intro__icon-item">
<img <img src={`${basePath}images/${item.img}`} alt={item.label} />
src={`${basePath}images/${item.img}`}
alt={item.label}
/>
<span>{item.label}</span> <span>{item.label}</span>
</div> </div>
))} ))}
</motion.div> </motion.div>
</div> </div>
<motion.div <motion.div className="fc-intro__right" initial={{ opacity: 0, x: 40 }} animate={introInView ? { opacity: 1, x: 0 } : {}} transition={{ duration: 0.9, ease, delay: 0.15 }}>
className="fc-intro__right" <img src={`${basePath}images/ibe_computer.png`} alt="IBE" className="fc-intro__monitor" />
initial={{ opacity: 0, x: 40 }}
animate={introInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.9, ease, delay: 0.15 }}
>
<img
src={`${basePath}images/ibe_computer.png`}
alt="IBE"
className="fc-intro__monitor"
/>
</motion.div> </motion.div>
</section> </section>
{/* BOOKING PROCESS */} {/* BOOKING PROCESS */}
<section className="ibe-booking-section" ref={bookingRef}> <section className="ibe-booking-section" ref={bookingRef}>
<motion.span <motion.span className="fc-eyebrow" initial={{ opacity: 0, y: 16 }} animate={bookingInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
className="fc-eyebrow"
initial={{ opacity: 0, y: 16 }}
animate={bookingInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease }}
>
Booking Process Booking Process
</motion.span> </motion.span>
<motion.h2 <motion.h2 className="ibe-booking__title" initial={{ opacity: 0, y: 24 }} animate={bookingInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.7, ease, delay: 0.1 }}>
className="ibe-booking__title" {t.bookingTitle}
initial={{ opacity: 0, y: 24 }}
animate={bookingInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.7, ease, delay: 0.1 }}
>
직관적이고 간편한 예약 프로세스
</motion.h2> </motion.h2>
<div className="ibe-booking__flow"> <div className="ibe-booking__flow">
<svg width="0" height="0" style={{ position: "absolute" }}> <svg width="0" height="0" style={{ position: "absolute" }}>
<defs> <defs>
<linearGradient <linearGradient id="ibe-icon-grad" x1="0%" y1="0%" x2="100%" y2="100%">
id="ibe-icon-grad"
x1="0%"
y1="0%"
x2="100%"
y2="100%"
>
<stop offset="0%" stopColor="#60a5fa" /> <stop offset="0%" stopColor="#60a5fa" />
<stop offset="100%" stopColor="#f472b6" /> <stop offset="100%" stopColor="#f472b6" />
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>
{[ {t.bookingSteps.map((item, i) => {
{
num: "01",
icon: Search,
label: "검색",
desc: "출발지, 도착지, 일정,\n탑승객 정보를 입력하여\n최적의 항공편 검색",
},
{
num: "02",
icon: Armchair,
label: "선택",
desc: "운임, 스케줄, 좌석 등\n다양한 옵션을 비교하고\n원하는 항공편 선택",
},
{
num: "03",
icon: CalendarClock,
label: "예약",
desc: "승객 정보 입력 및\n부가 서비스 선택을 통해\n예약을 진행",
},
{
num: "04",
icon: Wallet,
label: "결제",
desc: "다양한 결제 수단을 지원하여\n안전하고 간편하게\n결제 진행",
},
{
num: "05",
icon: Ticket,
label: "발권",
desc: "E-Ticket 발행 후\n예약 정보를 확인하고\n예약 완료",
},
].map((item, i) => {
const Icon = item.icon; const Icon = item.icon;
return ( return (
<Fragment key={i}> <Fragment key={i}>
@ -185,11 +245,7 @@ function IbePage() {
}} }}
> >
<div className="ibe-booking__circle"> <div className="ibe-booking__circle">
<Icon <Icon size={32} strokeWidth={1.5} stroke="url(#ibe-icon-grad)" />
size={32}
strokeWidth={1.5}
stroke="url(#ibe-icon-grad)"
/>
</div> </div>
<span className="ibe-booking__num">{item.num}</span> <span className="ibe-booking__num">{item.num}</span>
<span className="ibe-booking__label">{item.label}</span> <span className="ibe-booking__label">{item.label}</span>
@ -203,10 +259,7 @@ function IbePage() {
</p> </p>
</motion.div> </motion.div>
{i < 4 && ( {i < 4 && (
<div <div className="ibe-booking__connector" style={{ "--delay": `${i * 0.4}s` }}>
className="ibe-booking__connector"
style={{ "--delay": `${i * 0.4}s` }}
>
<div className="ibe-booking__line-bg" /> <div className="ibe-booking__line-bg" />
<div className="ibe-booking__line-flow" /> <div className="ibe-booking__line-flow" />
</div> </div>
@ -219,67 +272,24 @@ function IbePage() {
{/* MULTI CHANNEL INTEGRATION */} {/* MULTI CHANNEL INTEGRATION */}
<section className="ibe-channel-section" ref={channelRef}> <section className="ibe-channel-section" ref={channelRef}>
<motion.span <motion.span className="fc-eyebrow" initial={{ opacity: 0, y: 16 }} animate={channelInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
className="fc-eyebrow"
initial={{ opacity: 0, y: 16 }}
animate={channelInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease }}
>
Multi Channel Integration Multi Channel Integration
</motion.span> </motion.span>
<motion.h2 <motion.h2 className="ibe-channel__title" initial={{ opacity: 0, y: 24 }} animate={channelInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.7, ease, delay: 0.1 }}>
className="ibe-channel__title" {t.channelTitle}
initial={{ opacity: 0, y: 24 }}
animate={channelInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.7, ease, delay: 0.1 }}
>
다양한 시스템과의 유연한 연동
</motion.h2> </motion.h2>
<motion.p <motion.p className="ibe-channel__desc" initial={{ opacity: 0, y: 16 }} animate={channelInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease, delay: 0.2 }}>
className="ibe-channel__desc" {t.channelDesc}
initial={{ opacity: 0, y: 16 }}
animate={channelInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease, delay: 0.2 }}
>
항공사 GDS, 호텔, 결제, 보험 다양한 외부 시스템과 API 기반
연동을 통해 확장성 높은 예약 환경을 제공합니다.
</motion.p> </motion.p>
<motion.div <motion.div className="ibe-channel__diagram" initial={{ opacity: 0, y: 40 }} animate={channelInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.8, ease, delay: 0.3 }}>
className="ibe-channel__diagram" <svg className="ibe-channel__svg" viewBox="0 0 1540 430" preserveAspectRatio="none">
initial={{ opacity: 0, y: 40 }} <path className="ibe-channel__path" d="M350 95 C520 95 560 185 710 185" />
animate={channelInView ? { opacity: 1, y: 0 } : {}} <path className="ibe-channel__path" d="M350 215 C520 215 560 215 710 215" />
transition={{ duration: 0.8, ease, delay: 0.3 }} <path className="ibe-channel__path" d="M350 335 C520 335 560 245 710 245" />
> <path className="ibe-channel__path" d="M830 185 C980 185 1020 95 1190 95" />
<svg <path className="ibe-channel__path" d="M830 215 C980 215 1020 215 1190 215" />
className="ibe-channel__svg" <path className="ibe-channel__path" d="M830 245 C980 245 1020 335 1190 335" />
viewBox="0 0 1540 430"
preserveAspectRatio="none"
>
<path
className="ibe-channel__path"
d="M350 95 C520 95 560 185 710 185"
/>
<path
className="ibe-channel__path"
d="M350 215 C520 215 560 215 710 215"
/>
<path
className="ibe-channel__path"
d="M350 335 C520 335 560 245 710 245"
/>
<path
className="ibe-channel__path"
d="M830 185 C980 185 1020 95 1190 95"
/>
<path
className="ibe-channel__path"
d="M830 215 C980 215 1020 215 1190 215"
/>
<path
className="ibe-channel__path"
d="M830 245 C980 245 1020 335 1190 335"
/>
<circle cx="350" cy="95" r="4" className="ibe-channel__dot" /> <circle cx="350" cy="95" r="4" className="ibe-channel__dot" />
<circle cx="350" cy="215" r="4" className="ibe-channel__dot" /> <circle cx="350" cy="215" r="4" className="ibe-channel__dot" />
<circle cx="350" cy="335" r="4" className="ibe-channel__dot" /> <circle cx="350" cy="335" r="4" className="ibe-channel__dot" />
@ -290,55 +300,22 @@ function IbePage() {
<div className="ibe-channel__cols-wrap"> <div className="ibe-channel__cols-wrap">
<div className="ibe-channel__col ibe-channel__col--left"> <div className="ibe-channel__col ibe-channel__col--left">
{[ {t.channelLeft.map((item, i) => (
{ img: "ibe_pal_icon1.png", label: "항공사" }, <motion.div key={i} className="ibe-channel__card" initial={{ opacity: 0, x: -24 }} animate={channelInView ? { opacity: 1, x: 0 } : {}} transition={{ duration: 0.6, ease, delay: 0.4 + i * 0.1 }}>
{ img: "ibe_pal_icon2.png", label: "GDS" }, <img src={`${basePath}images/${item.img}`} alt={item.label} />
{ img: "ibe_pal_icon3.png", label: "호텔" },
].map((item, i) => (
<motion.div
key={i}
className="ibe-channel__card"
initial={{ opacity: 0, x: -24 }}
animate={channelInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.6, ease, delay: 0.4 + i * 0.1 }}
>
<img
src={`${basePath}images/${item.img}`}
alt={item.label}
/>
<span>{item.label}</span> <span>{item.label}</span>
</motion.div> </motion.div>
))} ))}
</div> </div>
<div className="ibe-channel__center"> <div className="ibe-channel__center">
<motion.img <motion.img src={`${basePath}images/ibe_pal_item.png`} alt="PAL IBE" className="ibe-channel__center-img" initial={{ opacity: 0, scale: 0.8 }} animate={channelInView ? { opacity: 1, scale: 1 } : {}} transition={{ duration: 0.8, ease, delay: 0.5 }} />
src={`${basePath}images/ibe_pal_item.png`}
alt="PAL IBE"
className="ibe-channel__center-img"
initial={{ opacity: 0, scale: 0.8 }}
animate={channelInView ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.8, ease, delay: 0.5 }}
/>
</div> </div>
<div className="ibe-channel__col ibe-channel__col--right"> <div className="ibe-channel__col ibe-channel__col--right">
{[ {t.channelRight.map((item, i) => (
{ img: "ibe_pal_icon4.png", label: "결제 시스템" }, <motion.div key={i} className="ibe-channel__card" initial={{ opacity: 0, x: 24 }} animate={channelInView ? { opacity: 1, x: 0 } : {}} transition={{ duration: 0.6, ease, delay: 0.4 + i * 0.1 }}>
{ img: "ibe_pal_icon5.png", label: "여행사/OTA" }, <img src={`${basePath}images/${item.img}`} alt={item.label} />
{ img: "ibe_pal_icon6.png", label: "보험" },
].map((item, i) => (
<motion.div
key={i}
className="ibe-channel__card"
initial={{ opacity: 0, x: 24 }}
animate={channelInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.6, ease, delay: 0.4 + i * 0.1 }}
>
<img
src={`${basePath}images/${item.img}`}
alt={item.label}
/>
<span>{item.label}</span> <span>{item.label}</span>
</motion.div> </motion.div>
))} ))}

44
src/pages/utm/CasePage.jsx

@ -1,13 +1,19 @@
import { motion as Motion, useInView } from "framer-motion"; import { motion as Motion, useInView } from "framer-motion";
import { useRef } from "react"; import { useRef } from "react";
import SubHero from "../../components/SubHero"; import SubHero from "../../components/SubHero";
import { useLanguage } from "../../context/LanguageContext";
const UTM_NAV = [ const UTM_NAV_KO = [
{ label: "UTM/UATM 소개", to: "/utm/intro" }, { label: "UTM/UATM 소개", to: "/utm/intro" },
{ label: "도입사례", to: "/utm/case" }, { label: "도입사례", to: "/utm/case" },
]; ];
const CASES = [ const UTM_NAV_EN = [
{ label: "UTM/UATM Overview", to: "/utm/intro" },
{ label: "Case Studies", to: "/utm/case" },
];
const CASES_KO = [
{ {
id: "01", id: "01",
eyebrow: "한국공항공사", eyebrow: "한국공항공사",
@ -37,6 +43,36 @@ const CASES = [
}, },
]; ];
const CASES_EN = [
{
id: "01",
eyebrow: "Korea Airports Corporation",
title: "Built KAC Drone Traffic Management UTM System",
desc: "We developed the information sharing system for the public UTM system and built the drone traffic management system, providing real-time flight approval, collision avoidance, and integrated control data management.",
tags: ["UTM", "Drone Control", "Korea Airports Corporation"],
img: "./images/case01.png",
year: "2024",
},
{
id: "02",
eyebrow: "Ministry of Land, Infrastructure and Transport",
title: "Participated in UAM Team Korea Working Group",
desc: "We joined the UAM Team Korea (UTK) working group led by the Ministry of Land, Infrastructure and Transport, contributing to the development of aircraft surveillance technology for urban air traffic and the UAM flight operations management system.",
tags: ["UAM", "UATM", "Ministry of Land, Infrastructure and Transport"],
img: "./images/case02.png",
year: "2024",
},
{
id: "03",
eyebrow: "Namwon City · Korea Institute of Aviation Safety Technology",
title: "Built Drone Demonstration City and Control System",
desc: "Through the Namwon City Drone Demonstration City agreement and the drone regulatory sandbox program, we built an identification device and flight control system aligned with drone management standards.",
tags: ["Drone", "Demonstration City", "Control System"],
img: "./images/rnd_img1.png",
year: "2023",
},
];
function CaseItem({ item, index }) { function CaseItem({ item, index }) {
const ref = useRef(null); const ref = useRef(null);
const inView = useInView(ref, { once: true, margin: "-10%" }); const inView = useInView(ref, { once: true, margin: "-10%" });
@ -81,6 +117,10 @@ function CaseItem({ item, index }) {
} }
export default function CasePage() { export default function CasePage() {
const { lang } = useLanguage();
const UTM_NAV = lang === "en" ? UTM_NAV_EN : UTM_NAV_KO;
const CASES = lang === "en" ? CASES_EN : CASES_KO;
return ( return (
<article> <article>
<SubHero <SubHero

906
src/pages/utm/IntroPage.jsx

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save