import { useEffect, useRef, useState } from "react"; import SubHero from "../../components/SubHero"; import useFadeIn from "../../hooks/useFadeIn"; import { motion, AnimatePresence, useInView, useScroll, useTransform } from "framer-motion"; import { MapPin, ShieldAlert, Network, BadgeCheck, Cloud, FileCheck, Database, Layers, Navigation, CheckCircle, Radio, Plane, Bell } from "lucide-react"; import { useLanguage } from "../../context/LanguageContext"; const ease = [0.22, 1, 0.36, 1]; const basePath = import.meta.env.BASE_URL; const INTRO_CARDS_KO = [ { label: "비행 계획", icon: Navigation }, { label: "비행 승인", icon: CheckCircle }, { label: "실시간 관제", icon: Radio }, { label: "데이터 관리", icon: Database }, ]; const INTRO_CARDS_EN = [ { label: "Flight Plan", icon: Navigation }, { label: "Approval", icon: CheckCircle }, { label: "Live Control", icon: Radio }, { label: "Data", icon: Database }, ]; const UTM_WHAT_LEFT_KO = [ { icon: MapPin, label: "실시간 위치 감시" }, { icon: FileCheck, label: "비행 계획 및 경로 승인" }, { icon: Network, label: "공역 설정·지오펜싱" }, { icon: Cloud, label: "기상 정보 자동 연계" }, ]; const UTM_WHAT_LEFT_EN = [ { icon: MapPin, label: "Real-Time Location Tracking" }, { icon: FileCheck, label: "Flight Plan & Route Approval" }, { icon: Network, label: "Airspace & Geofencing" }, { icon: Cloud, label: "Automated Weather Integration" }, ]; const UTM_WHAT_RIGHT_KO = [ { icon: BadgeCheck, label: "불법 기체 실시간 감지" }, { icon: Database, label: "비행 데이터 통합 관리" }, { icon: Layers, label: "UAM·드론 시스템 연동" }, { icon: ShieldAlert, label: "충돌 위험 사전 분석" }, ]; const UTM_WHAT_RIGHT_EN = [ { icon: BadgeCheck, label: "Real-Time Unauthorized Aircraft Detection" }, { icon: Database, label: "Integrated Flight Data Management" }, { icon: Layers, label: "UAM & Drone System Integration" }, { icon: ShieldAlert, label: "Proactive Collision Risk Analysis" }, ]; const INTRO_FLIGHT_PLANS_KO = [ { id: "FP-001", pilot: "홍길동", route: "서울 → 김포", status: "대기" }, { id: "FP-002", pilot: "김철수", route: "인천 → 수원", status: "대기" }, ]; const INTRO_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" }, ]; const AIRSPACES_KO = [ { id: "AZ-001", name: "비행금지구역", type: "금지", color: "#ef4444", top: "30%", left: "45%", size: 80, }, { id: "AZ-002", name: "비행제한구역", type: "제한", color: "#f97316", top: "55%", left: "62%", size: 60, }, { id: "AZ-003", name: "비행허용구역", type: "허용", color: "#6366f1", top: "25%", left: "25%", size: 70, }, { id: "AZ-004", name: "초경량비행구역", type: "허용", color: "#22c55e", top: "60%", left: "30%", size: 50, }, ]; const AIRSPACES_EN = [ { id: "AZ-001", name: "No-Fly Zone", type: "Prohibited", color: "#ef4444", top: "30%", left: "45%", size: 80, }, { id: "AZ-002", name: "Restricted Zone", type: "Restricted", color: "#f97316", top: "55%", left: "62%", size: 60, }, { id: "AZ-003", name: "Permitted Zone", type: "Permitted", color: "#6366f1", top: "25%", left: "25%", size: 70, }, { id: "AZ-004", name: "Ultralight Zone", type: "Permitted", color: "#22c55e", top: "60%", left: "30%", size: 50, }, ]; const INTRO_DETAIL_KO = { id: "FP-001", pilot: "홍길동", pilotId: "hong001", route: "서울 → 김포", altitude: "100m", speed: "13m/s", date: "2025-06-10 14:30", }; const INTRO_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 INTRO_PHASES = ["list", "detail", "confirm", "done"]; const INTRO_PHASE_DURATION = { list: 1800, detail: 2200, confirm: 1800, done: 2000, }; const TAB_DURATION = [5000, 7500, 7500, 5000]; // 실시간감시, 비행계획, 공역, 날씨 const FP_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", }, }; const AIRSPACE_POPUP_TEXT = { ko: { zoneName: "구역명", zoneId: "구역 ID", altRange: "고도 범위", radius: "반경", status: "상태", altRangeVal: "0 ~ 150m", }, en: { zoneName: "Zone Name", zoneId: "Zone ID", altRange: "Altitude Range", radius: "Radius", status: "Status", altRangeVal: "0 - 150m", }, }; const WEATHER_TEXT = { ko: { title: "기상 정보", desc: "맑음 · 비행 적합", windSpeedLabel: "풍속", windDirLabel: "풍향", windDirVal: "북동", humidityLabel: "습도", visibilityLabel: "시정", ceilingLabel: "운고", precipLabel: "강수", }, en: { title: "Weather", desc: "Clear · Suitable for Flight", windSpeedLabel: "Wind Speed", windDirLabel: "Wind Direction", windDirVal: "NE", humidityLabel: "Humidity", visibilityLabel: "Visibility", ceilingLabel: "Ceiling", precipLabel: "Precipitation", }, }; const EVO_ITEMS_KO = [ { img: `${basePath}images/utm_intro_drone.png`, alt: "DRONE", name: "DRONE", desc: ( <> 개별 드론 운용 단계.
단순 비행 임무 수행 중심으로 체계적 관리 체계 부재. ), tag: "단일 기체 운용", side: "left", active: false, }, { img: `${basePath}images/utm_intro_utm.png`, alt: "UTM", name: "UTM", desc: ( <> 무인기 교통관리 시스템 도입.
비행 경로 승인·충돌 방지·공역 관리 체계화. ), tag: "공역 관리 시스템", side: "right", active: false, }, { img: `${basePath}images/utm_intro_uam.png`, alt: "UAM", name: "UAM", desc: ( <> 도심항공모빌리티 등장.
유인 eVTOL 기체가 도심 저고도 공역을 비행. ), tag: "도심 항공 모빌리티", side: "left", active: false, }, { img: `${basePath}images/utm_intro_uatm.png`, alt: "UATM", name: "UATM", desc: ( <> UAM과 UTM의 완전한 통합.
기체 운용부터 관제까지 하나의 플랫폼으로 실현. ), tag: "통합 관제 플랫폼", side: "right", active: true, }, ]; const EVO_ITEMS_EN = [ { img: `${basePath}images/utm_intro_drone.png`, alt: "DRONE", name: "DRONE", desc: ( <> Individual drone operation stage.
Focused on simple flight missions, without a systematic management framework. ), tag: "Single Aircraft Operation", side: "left", active: false, }, { img: `${basePath}images/utm_intro_utm.png`, alt: "UTM", name: "UTM", desc: ( <> Introduction of unmanned traffic management systems.
Systematic flight path approval, collision avoidance, and airspace management. ), tag: "Airspace Management System", side: "right", active: false, }, { img: `${basePath}images/utm_intro_uam.png`, alt: "UAM", name: "UAM", desc: ( <> Emergence of urban air mobility.
Manned eVTOL aircraft fly through low-altitude urban airspace. ), tag: "Urban Air Mobility", side: "left", active: false, }, { img: `${basePath}images/utm_intro_uatm.png`, alt: "UATM", name: "UATM", desc: ( <> Full integration of UAM and UTM.
From aircraft operation to air traffic control, realized on a single platform. ), tag: "Integrated Control Platform", side: "right", active: true, }, ]; const SECTORS_KO = [ { id: 0, num: "01", name: "UAM 기체 운용", color: "#E8193C", tags: ["eVTOL 기체 직접 운용", "실 운용 데이터 기반 검증", "국내 유일 통합 사업자", "안전 관리 체계 구축"], }, { id: 1, num: "02", name: "UTM 관제 기술", color: "#1A1F5E", tags: ["실시간 공역 감시·관제", "충돌 위험 사전 분석", "비행 경로 승인 시스템", "지오펜싱·공역 설정"], }, { id: 2, num: "03", name: "통합 항공 데이터 플랫폼", color: "#3D3D3D", tags: ["비행 데이터 수집·분석", "UAM·드론 시스템 연동", "기상 정보 자동 연계", "통합 데이터 관리"], }, ]; const SECTORS_EN = [ { id: 0, num: "01", name: "UAM Aircraft Operation", color: "#E8193C", tags: ["Direct eVTOL aircraft operation", "Verified through real operational data", "Korea's only integrated operator", "Established safety management system"], }, { id: 1, num: "02", name: "UTM Control Technology", color: "#1A1F5E", tags: ["Real-time airspace monitoring & control", "Proactive collision risk analysis", "Flight path approval system", "Geofencing & airspace configuration"], }, { id: 2, num: "03", name: "Integrated Aviation Data Platform", color: "#3D3D3D", tags: ["Flight data collection & analysis", "UAM & drone system integration", "Automated weather integration", "Integrated data management"], }, ]; const PAGE_TEXT = { ko: { overviewTitle: ( <> PAL Networks의 UTM/UATM은
도심 항공 관제의 {""} 새로운 기준입니다. ), overviewDesc: ( <> UAM 기체 운용과 UTM 관제 플랫폼을 동시에 보유한 국내 유일의 통합 사업자입니다.
실 운용 데이터로 검증된 시스템으로 안전한 저고도 공역 관리를 실현합니다. ), whatTitle: ( <> UTM, 무엇을 할 수 있나요? ), whatDesc: ( <> UTM은 단순한 관제 시스템을 넘어, 도심 저고도 공역 전체를 디지털로 운영하는 통합 플랫폼입니다.
실시간 감시부터 충돌 위험 분석, 기상 연계, 공역 관리까지 — 비행의 모든 순간을 안전하게 연결합니다. ), mobileImgAlt: (i) => `UTM 기능 ${i}`, evoTitle: ( <> UTM에서 UATM으로,
하늘길의 패러다임이 바뀝니다 ), evoDesc: "드론 한 대의 비행에서 시작된 기술이, 이제 도심 전체의 하늘을 통합 관제하는 UATM으로 진화했습니다. PAL Networks는 이 변화의 최전선에서 기체와 관제를 함께 운영합니다.", capTitle: ( <> PAL Networks의
세 가지 핵심 역량 ), capDesc: "기체 운용부터 관제, 데이터까지 — 하나의 사업자가 모두 보유한 통합 역량입니다.", }, en: { overviewTitle: ( <> PAL Networks' UTM/UATM is
the new standard for urban air traffic control. ), overviewDesc: ( <> We are Korea's only integrated operator with both UAM aircraft operations and a UTM control platform.
Our system, proven through real operational data, delivers safe low-altitude airspace management. ), whatTitle: ( <> UTM, what can it do? ), whatDesc: ( <> UTM is more than a control system — it's an integrated platform that digitally operates the entire urban low-altitude airspace.
From real-time monitoring to collision risk analysis, weather integration, and airspace management, it safely connects every moment of flight. ), mobileImgAlt: (i) => `UTM Feature ${i}`, evoTitle: ( <> From UTM to UATM,
the paradigm of the skies is changing ), evoDesc: "What began as technology for flying a single drone has evolved into UATM, integrating control over an entire city's skies. PAL Networks operates at the forefront of this shift, running both aircraft and air traffic control together.", capTitle: ( <> PAL Networks'
Three Core Capabilities ), capDesc: "From aircraft operation to control and data — integrated capabilities held by a single operator.", }, }; /* ──────────────────────────────────────── EvoItem ──────────────────────────────────────── */ function EvoItem({ item, index }) { const itemRef = useRef(null); const itemInView = useInView(itemRef, { once: true, margin: "-80px" }); const itemActive = useInView(itemRef, { margin: "-45% 0px -45% 0px" }); return ( {item.side === "left" ? ( <>
{item.alt}
{item.name}
{item.desc}
{item.tag}
{String(index + 1).padStart(2, "0")}
) : ( <>
{String(index + 1).padStart(2, "0")}
{item.alt}
{item.name}
{item.desc}
{item.tag}
)} ); } /* ──────────────────────────────────────── IntroPage ──────────────────────────────────────── */ function IntroPage() { const { lang } = useLanguage(); const ref = useFadeIn(); const INTRO_CARDS = lang === "en" ? INTRO_CARDS_EN : INTRO_CARDS_KO; const UTM_WHAT_LEFT = lang === "en" ? UTM_WHAT_LEFT_EN : UTM_WHAT_LEFT_KO; const UTM_WHAT_RIGHT = lang === "en" ? UTM_WHAT_RIGHT_EN : UTM_WHAT_RIGHT_KO; const INTRO_FLIGHT_PLANS = lang === "en" ? INTRO_FLIGHT_PLANS_EN : INTRO_FLIGHT_PLANS_KO; const AIRSPACES = lang === "en" ? AIRSPACES_EN : AIRSPACES_KO; const INTRO_DETAIL = lang === "en" ? INTRO_DETAIL_EN : INTRO_DETAIL_KO; const EVO_ITEMS = lang === "en" ? EVO_ITEMS_EN : EVO_ITEMS_KO; const SECTORS = lang === "en" ? SECTORS_EN : SECTORS_KO; const fp = FP_PANEL_TEXT[lang]; const azp = AIRSPACE_POPUP_TEXT[lang]; const wx = WEATHER_TEXT[lang]; const t = PAGE_TEXT[lang]; const UTM_NAV = lang === "en" ? [ { label: "UTM/UATM Overview", to: "/utm/intro" }, { label: "Case Studies", to: "/utm/case" }, ] : [ { label: "UTM/UATM 소개", to: "/utm/intro" }, { label: "도입사례", to: "/utm/case" }, ]; const [activeIndex, setActiveIndex] = useState(0); const [fpPhase, setFpPhase] = useState("list"); const fpCursorRef = useRef(null); const fpPanelRef = useRef(null); const introRef = useRef(null); const introInView = useInView(introRef, { once: true, margin: "-60px" }); const whatRef = useRef(null); const whatInView = useInView(whatRef, { once: true, margin: "-100px" }); const evoRef = useRef(null); const evoInView = useInView(evoRef, { once: true, margin: "-100px" }); const evoTrackRef = useRef(null); const capRef = useRef(null); const capInView = useInView(capRef, { once: true, margin: "-80px" }); const { scrollYProgress } = useScroll({ target: evoTrackRef, offset: ["start center", "end center"], }); const lineHeight = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]); const [selectedAirspace, setSelectedAirspace] = useState(null); const airspaceCursorRef = useRef(null); const airspaceAreaRef = useRef(null); useEffect(() => { if (activeIndex !== 1) return; let timer; const run = (current) => { const next = INTRO_PHASES[(INTRO_PHASES.indexOf(current) + 1) % INTRO_PHASES.length]; timer = setTimeout(() => { setFpPhase(next); run(next); }, INTRO_PHASE_DURATION[current]); }; timer = setTimeout(() => run("list"), 1000); return () => { clearTimeout(timer); setFpPhase("list"); }; }, [activeIndex]); useEffect(() => { if (activeIndex !== 1) return; const panel = fpPanelRef.current; const cursor = fpCursorRef.current; if (!panel || !cursor) return; let timers = []; function getPos(selector) { const el = panel.querySelector(selector); if (!el) return null; const pr = panel.getBoundingClientRect(); const er = el.getBoundingClientRect(); return { x: er.left - pr.left + er.width / 2, y: er.top - pr.top + er.height / 2, }; } function moveTo(x, y) { cursor.style.transform = `translate(${x}px, ${y}px)`; } function click() { cursor.classList.add("utm-cursor--click"); setTimeout(() => cursor.classList.remove("utm-cursor--click"), 250); } function after(ms, fn) { const t = setTimeout(fn, ms); timers.push(t); } function loop() { timers.forEach(clearTimeout); timers = []; setFpPhase("list"); after(1000, () => { const p = getPos(".utm-panel__row--active .utm-panel__btn"); if (p) moveTo(p.x, p.y); }); after(2000, () => { click(); after(200, () => setFpPhase("detail")); }); after(3200, () => { const p = getPos(".utm-panel__approve-btn"); if (p) moveTo(p.x, p.y); }); after(4200, () => { click(); after(200, () => setFpPhase("confirm")); }); after(5000, () => { const p = getPos(".utm-confirm__ok"); if (p) moveTo(p.x, p.y); }); after(5800, () => { click(); after(200, () => setFpPhase("done")); }); after(7500, () => loop()); } moveTo(180, 80); after(500, () => loop()); return () => { timers.forEach(clearTimeout); timers = []; cursor.classList.remove("utm-cursor--click"); setFpPhase("list"); }; }, [activeIndex]); useEffect(() => { if (activeIndex !== 2) return; setTimeout(() => setSelectedAirspace(null), 0); // ← 이렇게 변경 const cursor = airspaceCursorRef.current; const area = airspaceAreaRef.current; if (!cursor || !area) return; let timers = []; function getPos(selector) { const el = area.querySelector(selector); if (!el) return null; const ar = area.getBoundingClientRect(); const er = el.getBoundingClientRect(); return { x: er.left - ar.left + er.width / 2, y: er.top - ar.top + er.height / 2, }; } function moveTo(x, y) { cursor.style.transform = `translate(${x}px, ${y}px)`; } function click() { cursor.classList.add("utm-cursor--click"); setTimeout(() => cursor.classList.remove("utm-cursor--click"), 250); } function after(ms, fn) { const t = setTimeout(fn, ms); timers.push(t); } function loop() { timers.forEach(clearTimeout); timers = []; setSelectedAirspace(null); after(800, () => { const p = getPos(".utm-airspace--0"); if (p) moveTo(p.x, p.y); }); after(1800, () => { click(); after(200, () => setSelectedAirspace(AIRSPACES[0])); }); after(4000, () => { setSelectedAirspace(null); }); after(4600, () => { const p = getPos(".utm-airspace--2"); if (p) moveTo(p.x, p.y); }); after(5400, () => { click(); after(200, () => setSelectedAirspace(AIRSPACES[2])); }); after(7500, () => loop()); } moveTo(100, 100); after(500, () => loop()); return () => { timers.forEach(clearTimeout); setSelectedAirspace(null); cursor.classList.remove("utm-cursor--click"); }; }, [activeIndex, AIRSPACES]); useEffect(() => { const t = setTimeout(() => { setActiveIndex((prev) => (prev + 1) % UTM_WHAT_LEFT.length); }, TAB_DURATION[activeIndex]); return () => clearTimeout(t); }, [activeIndex, UTM_WHAT_LEFT.length]); return (
UTM/UATM } navItems={UTM_NAV} />
{/* ── 01 Overview ── */}
Overview {t.overviewTitle} {t.overviewDesc}
    {INTRO_CARDS.map((card, i) => ( {String(i + 1).padStart(2, "0")}

    {card.label}

    ))}
{/* ── 02 What is UTM ── */}
What is UTM {t.whatTitle} {t.whatDesc}
{UTM_WHAT_LEFT.map(({ icon: Icon, label }, i) => (
  • setActiveIndex(i)} style={{ cursor: "pointer" }}> {label}
  • ))}
    {/* PC 전용 — 애니메이션 맵 */}
    {activeIndex === 1 && (
    {fp.panelTitle} {fp.badge(INTRO_FLIGHT_PLANS.length)}
    {fp.thead.map((label) => ( {label} ))}
    {INTRO_FLIGHT_PLANS.map((row, i) => (
    {row.id} {row.pilot} {row.route} {row.status} {fp.detailBtn}
    ))}
    {fp.detailTitle}
    {fp.rowLabels[0]} {INTRO_DETAIL.id}
    {fp.rowLabels[1]} {INTRO_DETAIL.pilot} ({INTRO_DETAIL.pilotId})
    {fp.rowLabels[2]} {INTRO_DETAIL.route}
    {fp.rowLabels[3]} {INTRO_DETAIL.altitude} · {INTRO_DETAIL.speed}
    {fp.rowLabels[4]} {INTRO_DETAIL.date}
    {fp.rowLabels[5]} {fpPhase === "done" ? fp.statusDone : fp.statusWait}
    {fpPhase === "detail" && (
    {fp.approveBtn}
    )} {fpPhase === "done" && ( {fp.toast} )}
    {fpPhase === "confirm" && (
    ⚠️
    {fp.confirmTitle}
    {INTRO_DETAIL.id} · {INTRO_DETAIL.pilot} · {INTRO_DETAIL.route}
    {fp.cancel} {fp.ok}
    )}
    )} {activeIndex === 2 && ( {AIRSPACES.map((az, i) => (
    ))} {selectedAirspace && (
    {selectedAirspace.id} {selectedAirspace.type}
    {azp.zoneName} {selectedAirspace.name}
    {azp.zoneId} {selectedAirspace.id}
    {azp.altRange} {azp.altRangeVal}
    {azp.radius} {selectedAirspace.size * 10}m
    {azp.status} {selectedAirspace.type}
    )}
    )} {activeIndex === 3 && (
    {wx.title} 2025-06-10 14:32
    ☀️
    18°
    {wx.desc}
    {wx.windSpeedLabel} 3.2 m/s
    {wx.windDirLabel} {wx.windDirVal}
    {wx.humidityLabel} 62%
    {wx.visibilityLabel} 10km
    {wx.ceilingLabel} 1,500m
    {wx.precipLabel} 0mm
    )}
    drone PA001
    COLL-ACR-001 13m/s / 100m
    drone BLE
    DJI-TEST-0005 LTE · 9m/s / 100m
    drone LTE
    FPA502 338m / 172k/m / 81°
    126.745021/37.287339
    {/* 모바일 전용 — 탭 이미지 */}
    {UTM_WHAT_LEFT.map((_, i) => ( {t.mobileImgAlt(i ))}
    {UTM_WHAT_RIGHT.map(({ icon: Icon, label }) => (
  • {label}
  • ))}
    {/* ── 03 Evolution ── */}
    Evolution

    {t.evoTitle}

    {t.evoDesc}

    {EVO_ITEMS.map((item, i) => ( ))}
    {/* ── 04 Capabilities ── */}
    Capabilities {t.capTitle} {t.capDesc}
      {SECTORS.map((s, i) => ( {s.num}
      {s.name}
        {s.tags.map((tag) => (
      • {tag}
      • ))}
      ))}
    ); } export default IntroPage;