You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1293 lines
49 KiB

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: (
<>
개별 드론 운용 단계.
<br />
단순 비행 임무 수행 중심으로 체계적 관리 체계 부재.
</>
),
tag: "단일 기체 운용",
side: "left",
active: false,
},
{
img: `${basePath}images/utm_intro_utm.png`,
alt: "UTM",
name: "UTM",
desc: (
<>
무인기 교통관리 시스템 도입.
<br />
비행 경로 승인·충돌 방지·공역 관리 체계화.
</>
),
tag: "공역 관리 시스템",
side: "right",
active: false,
},
{
img: `${basePath}images/utm_intro_uam.png`,
alt: "UAM",
name: "UAM",
desc: (
<>
도심항공모빌리티 등장.
<br />
유인 eVTOL 기체가 도심 저고도 공역을 비행.
</>
),
tag: "도심 항공 모빌리티",
side: "left",
active: false,
},
{
img: `${basePath}images/utm_intro_uatm.png`,
alt: "UATM",
name: "UATM",
desc: (
<>
UAM과 UTM의 완전한 통합.
<br />
기체 운용부터 관제까지 하나의 플랫폼으로 실현.
</>
),
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.
<br />
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.
<br />
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.
<br />
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.
<br />
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은
<br />
<em>도심 항공 관제의 {""}</em>
<em>새로운 기준입니다.</em>
</>
),
overviewDesc: (
<>
UAM 기체 운용과 UTM 관제 플랫폼을 동시에 보유한 국내 유일의 통합 사업자입니다.
<br /> 운용 데이터로 검증된 시스템으로 <strong>안전한 저고도 공역 관리</strong> .
</>
),
whatTitle: (
<>
UTM, <em>무엇을 있나요?</em>
</>
),
whatDesc: (
<>
UTM은 단순한 관제 시스템을 넘어, 도심 저고도 공역 전체를 디지털로 운영하는 통합 플랫폼입니다.
<br />
실시간 감시부터 충돌 위험 분석, 기상 연계, 공역 관리까지 비행의 모든 순간을 안전하게 연결합니다.
</>
),
mobileImgAlt: (i) => `UTM 기능 ${i}`,
evoTitle: (
<>
UTM에서 UATM으로,
<br />
<em>하늘길의 패러다임이 바뀝니다</em>
</>
),
evoDesc: "드론 한 대의 비행에서 시작된 기술이, 이제 도심 전체의 하늘을 통합 관제하는 UATM으로 진화했습니다. PAL Networks는 이 변화의 최전선에서 기체와 관제를 함께 운영합니다.",
capTitle: (
<>
PAL Networks의 <br />
<em> 가지 핵심 역량</em>
</>
),
capDesc: "기체 운용부터 관제, 데이터까지 — 하나의 사업자가 모두 보유한 통합 역량입니다.",
},
en: {
overviewTitle: (
<>
PAL Networks' UTM/UATM is
<br />
<em>the new standard </em>
<em>for urban air traffic control.</em>
</>
),
overviewDesc: (
<>
We are Korea's only integrated operator with both UAM aircraft operations and a UTM control platform.
<br />
Our system, proven through real operational data, delivers <strong>safe low-altitude airspace management</strong>.
</>
),
whatTitle: (
<>
UTM, <em>what can it do?</em>
</>
),
whatDesc: (
<>
UTM is more than a control system it's an integrated platform that digitally operates the entire urban low-altitude airspace.
<br />
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,
<br />
<em>the paradigm of the skies is changing</em>
</>
),
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' <br />
<em>Three Core Capabilities</em>
</>
),
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 (
<motion.div ref={itemRef} className={`utm-evo__item${item.active ? " utm-evo__item--active" : ""}${itemActive ? " utm-evo__item--highlight" : ""}`} initial={{ opacity: 0, y: 40 }} animate={itemInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
{item.side === "left" ? (
<>
<div className="utm-evo__card-wrap utm-evo__card-wrap--left">
<div className="utm-evo__card">
<img src={item.img} alt={item.alt} className="utm-evo__card-img" />
<div className="utm-evo__card-body">
<div className="utm-evo__card-name">{item.name}</div>
<div className="utm-evo__card-desc">{item.desc}</div>
<span className="utm-evo__card-tag">{item.tag}</span>
</div>
</div>
</div>
<div className="utm-evo__dot-wrap">
<div className="utm-evo__dot">
<span>{String(index + 1).padStart(2, "0")}</span>
</div>
</div>
<div className="utm-evo__empty" />
</>
) : (
<>
<div className="utm-evo__empty" />
<div className="utm-evo__dot-wrap">
<div className="utm-evo__dot">
<span>{String(index + 1).padStart(2, "0")}</span>
</div>
</div>
<div className="utm-evo__card-wrap utm-evo__card-wrap--right">
<div className="utm-evo__card">
<img src={item.img} alt={item.alt} className="utm-evo__card-img" />
<div className="utm-evo__card-body">
<div className="utm-evo__card-name">{item.name}</div>
<div className="utm-evo__card-desc">{item.desc}</div>
<span className="utm-evo__card-tag">{item.tag}</span>
</div>
</div>
</div>
</>
)}
</motion.div>
);
}
/* ────────────────────────────────────────
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 (
<article ref={ref}>
<SubHero
label="UTM/UATM"
title={
<>
<em>UTM/UATM</em>
</>
}
navItems={UTM_NAV}
/>
<div className="sub-content">
{/* ── 01 Overview ── */}
<div className="inner-wrap">
<section className="utm-intro" ref={introRef}>
<div className="utm-intro__top">
<motion.span className="fc-eyebrow" initial={{ opacity: 0, y: 16 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
Overview
</motion.span>
<motion.h2 className="utm-intro__title" initial={{ opacity: 0, y: 20 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.7, delay: 0.1, ease }}>
{t.overviewTitle}
</motion.h2>
<motion.p className="utm-intro__desc" initial={{ opacity: 0, y: 16 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.2, ease }}>
{t.overviewDesc}
</motion.p>
</div>
<ul className="utm-intro__cards">
{INTRO_CARDS.map((card, i) => (
<motion.li key={card.label} className="utm-intro__card" initial={{ opacity: 0, y: 60 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.8, delay: 0.2 + i * 0.12, ease }}>
<span className="utm-intro__card-num">{String(i + 1).padStart(2, "0")}</span>
<span className="utm-intro__card-icon">
<svg width="0" height="0">
<defs>
<linearGradient id="icon-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#C850C0" />
<stop offset="100%" stopColor="#4158D0" />
</linearGradient>
</defs>
</svg>
<card.icon size={36} strokeWidth={1.5} stroke="url(#icon-grad)" />
</span>
<p className="utm-intro__card-label">{card.label}</p>
</motion.li>
))}
</ul>
</section>
</div>
{/* ── 02 What is UTM ── */}
<section className="utm-what" ref={whatRef}>
<div className="inner-wrap">
<motion.div initial={{ opacity: 0, y: 40 }} animate={whatInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.8, ease }}>
<div className="utm-what__top">
<motion.span className="utm-what__eyebrow" initial={{ opacity: 0, y: 16 }} animate={whatInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
What is UTM
</motion.span>
<motion.h2 className="utm-what__title" initial={{ opacity: 0, y: 20 }} animate={whatInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.7, delay: 0.1, ease }}>
{t.whatTitle}
</motion.h2>
<motion.p className="utm-what__desc" initial={{ opacity: 0, y: 16 }} animate={whatInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.2, ease }}>
{t.whatDesc}
</motion.p>
</div>
<div className="utm-what__body">
<motion.ul className="utm-what__cards" initial={{ opacity: 0, x: -24 }} animate={whatInView ? { opacity: 1, x: 0 } : {}} transition={{ duration: 0.7, delay: 0.3, ease }} style={{ pointerEvents: "auto" }}>
{UTM_WHAT_LEFT.map(({ icon: Icon, label }, i) => (
<li key={label} className={`utm-what__card${activeIndex === i ? " utm-what__card--active" : ""}`} onClick={() => setActiveIndex(i)} style={{ cursor: "pointer" }}>
<span className="utm-what__card-icon">
<Icon size={16} />
</span>
<span className="utm-what__card-label">{label}</span>
</li>
))}
</motion.ul>
<motion.div className="utm-what__mockup" initial={{ opacity: 0, y: 24 }} animate={whatInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.8, delay: 0.2, ease }}>
{/* PC 전용 — 애니메이션 맵 */}
<div
className="utm-what__img-wrap utm-what__pc-only"
style={{
backgroundImage: `url(${basePath}images/utm_what_img2.png)`,
}}
>
<div className="utm-map-menu">
<button className="utm-map-menu__btn">
<Plane size={16} />
</button>
<button className="utm-map-menu__btn">
<ShieldAlert size={16} />
</button>
<button className={`utm-map-menu__btn${activeIndex === 3 ? " utm-map-menu__btn--active" : ""}`}>
<Cloud size={16} />
</button>
<button className="utm-map-menu__btn">
<Layers size={16} />
</button>
<button className="utm-map-menu__btn">
<Radio size={16} />
</button>
<button className="utm-map-menu__btn">
<Database size={16} />
</button>
<button className="utm-map-menu__btn">
<Bell size={16} />
</button>
</div>
<AnimatePresence>
{activeIndex === 1 && (
<motion.div className="utm-panel utm-fp-panel" ref={fpPanelRef} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 20 }} transition={{ duration: 0.35, ease }}>
<div className="utm-panel__header">
<span className="utm-panel__title">{fp.panelTitle}</span>
<span className="utm-panel__badge">{fp.badge(INTRO_FLIGHT_PLANS.length)}</span>
</div>
<div className="utm-panel__table">
<div className="utm-panel__thead">
{fp.thead.map((label) => (
<span key={label}>{label}</span>
))}
<span></span>
</div>
{INTRO_FLIGHT_PLANS.map((row, i) => (
<div key={row.id} className={`utm-panel__row${i === 0 ? " utm-panel__row--active" : ""}`}>
<span className="utm-panel__cell">{row.id}</span>
<span className="utm-panel__cell">{row.pilot}</span>
<span className="utm-panel__cell">{row.route}</span>
<span className={`utm-panel__status utm-panel__status--${row.status === fp.statusDone ? "done" : "wait"}`}>{row.status}</span>
<span className="utm-panel__cell">
<span className={`utm-panel__btn${i === 0 && fpPhase === "list" ? " utm-panel__btn--hover" : ""}`}>{fp.detailBtn}</span>
</span>
</div>
))}
</div>
<div className={`utm-panel__detail${fpPhase === "detail" || fpPhase === "confirm" || fpPhase === "done" ? " utm-panel__detail--show" : ""}`}>
<div className="utm-panel__detail-title">{fp.detailTitle}</div>
<div className="utm-panel__detail-rows">
<div className="utm-panel__detail-row">
<span>{fp.rowLabels[0]}</span>
<span>{INTRO_DETAIL.id}</span>
</div>
<div className="utm-panel__detail-row">
<span>{fp.rowLabels[1]}</span>
<span>
{INTRO_DETAIL.pilot} ({INTRO_DETAIL.pilotId})
</span>
</div>
<div className="utm-panel__detail-row">
<span>{fp.rowLabels[2]}</span>
<span>{INTRO_DETAIL.route}</span>
</div>
<div className="utm-panel__detail-row">
<span>{fp.rowLabels[3]}</span>
<span>
{INTRO_DETAIL.altitude} · {INTRO_DETAIL.speed}
</span>
</div>
<div className="utm-panel__detail-row">
<span>{fp.rowLabels[4]}</span>
<span>{INTRO_DETAIL.date}</span>
</div>
<div className="utm-panel__detail-row">
<span>{fp.rowLabels[5]}</span>
<span className={`utm-panel__status utm-panel__status--${fpPhase === "done" ? "done" : "wait"}`}>{fpPhase === "done" ? fp.statusDone : fp.statusWait}</span>
</div>
</div>
{fpPhase === "detail" && (
<div className="utm-panel__detail-actions">
<span className="utm-panel__approve-btn utm-panel__approve-btn--hover">{fp.approveBtn}</span>
</div>
)}
<AnimatePresence>
{fpPhase === "done" && (
<motion.div 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>
{fp.toast}
</motion.div>
)}
</AnimatePresence>
</div>
<AnimatePresence>
{fpPhase === "confirm" && (
<motion.div className="utm-panel__confirm-overlay" 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__title">{fp.confirmTitle}</div>
<div className="utm-confirm__desc">
{INTRO_DETAIL.id} · {INTRO_DETAIL.pilot} · {INTRO_DETAIL.route}
</div>
<div className="utm-confirm__btns">
<span className="utm-confirm__cancel">{fp.cancel}</span>
<span className="utm-confirm__ok">{fp.ok}</span>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
<div className="utm-cursor" ref={fpCursorRef}>
<div className="utm-cursor__dot" />
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{activeIndex === 2 && (
<motion.div className="utm-airspace-layer" ref={airspaceAreaRef} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.4 }}>
{AIRSPACES.map((az, i) => (
<div
key={az.id}
className={`utm-airspace utm-airspace--${i}`}
style={{
top: az.top,
left: az.left,
width: az.size * 2,
height: az.size * 2,
borderColor: az.color,
background: `${az.color}22`,
}}
/>
))}
<AnimatePresence>
{selectedAirspace && (
<motion.div
className="utm-airspace-popup"
style={{
top: selectedAirspace.top,
left: `calc(${selectedAirspace.left} + ${selectedAirspace.size}px + 10px)`,
}}
initial={{ opacity: 0, scale: 0.9, y: -10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.25, ease }}
>
<div
className="utm-airspace-popup__header"
style={{
borderColor: selectedAirspace.color,
}}
>
<span className="utm-airspace-popup__id">{selectedAirspace.id}</span>
<span
className="utm-airspace-popup__badge"
style={{
background: `${selectedAirspace.color}22`,
color: selectedAirspace.color,
}}
>
{selectedAirspace.type}
</span>
</div>
<div className="utm-airspace-popup__rows">
<div className="utm-airspace-popup__row">
<span>{azp.zoneName}</span>
<span>{selectedAirspace.name}</span>
</div>
<div className="utm-airspace-popup__row">
<span>{azp.zoneId}</span>
<span>{selectedAirspace.id}</span>
</div>
<div className="utm-airspace-popup__row">
<span>{azp.altRange}</span>
<span>{azp.altRangeVal}</span>
</div>
<div className="utm-airspace-popup__row">
<span>{azp.radius}</span>
<span>{selectedAirspace.size * 10}m</span>
</div>
<div className="utm-airspace-popup__row">
<span>{azp.status}</span>
<span
style={{
color: selectedAirspace.color,
fontWeight: 700,
}}
>
{selectedAirspace.type}
</span>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div className="utm-cursor" ref={airspaceCursorRef}>
<div className="utm-cursor__dot" />
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{activeIndex === 3 && (
<motion.div className="utm-weather-panel" initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} transition={{ duration: 0.35, ease }}>
<div className="utm-weather-panel__header">
<Cloud size={14} />
<span>{wx.title}</span>
<span className="utm-weather-panel__time">2025-06-10 14:32</span>
</div>
<div className="utm-weather-panel__main">
<div className="utm-weather-panel__icon">☀</div>
<div className="utm-weather-panel__temp">18°</div>
<div className="utm-weather-panel__desc">{wx.desc}</div>
</div>
<div className="utm-weather-panel__grid">
<div className="utm-weather-panel__item">
<span className="utm-weather-panel__item-label">{wx.windSpeedLabel}</span>
<span className="utm-weather-panel__item-val">3.2 m/s</span>
</div>
<div className="utm-weather-panel__item">
<span className="utm-weather-panel__item-label">{wx.windDirLabel}</span>
<span className="utm-weather-panel__item-val">{wx.windDirVal}</span>
</div>
<div className="utm-weather-panel__item">
<span className="utm-weather-panel__item-label">{wx.humidityLabel}</span>
<span className="utm-weather-panel__item-val">62%</span>
</div>
<div className="utm-weather-panel__item">
<span className="utm-weather-panel__item-label">{wx.visibilityLabel}</span>
<span className="utm-weather-panel__item-val">10km</span>
</div>
<div className="utm-weather-panel__item">
<span className="utm-weather-panel__item-label">{wx.ceilingLabel}</span>
<span className="utm-weather-panel__item-val">1,500m</span>
</div>
<div className="utm-weather-panel__item">
<span className="utm-weather-panel__item-label">{wx.precipLabel}</span>
<span className="utm-weather-panel__item-val">0mm</span>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div
className="drone drone--1"
style={{
top: "35%",
left: "48%",
display: activeIndex === 2 ? "none" : "flex",
}}
>
<div className="drone__badge drone__badge--red">
<div className="drone__badge-top">
<img src={`${basePath}images/red_drone.png`} alt="drone" width="16" height="16" />
<span className="drone__badge-conn">PA001</span>
</div>
<span className="drone__badge-id">COLL-ACR-001</span>
<span className="drone__badge-status">13m/s / 100m</span>
</div>
<div className="drone__ping drone__ping--red" />
<div className="drone__ping drone__ping--red drone__ping--delay" />
</div>
<div
className="drone drone--2"
style={{
top: "25%",
left: "22%",
display: activeIndex === 2 ? "none" : "flex",
}}
>
<div className="drone__badge drone__badge--blue">
<div className="drone__badge-top">
<img src={`${basePath}images/blue_drone.png`} alt="drone" width="16" height="16" />
<span className="drone__badge-conn">BLE</span>
</div>
<span className="drone__badge-id">DJI-TEST-0005</span>
<span className="drone__badge-status">LTE · 9m/s / 100m</span>
</div>
<div className="drone__ping drone__ping--blue" />
<div className="drone__ping drone__ping--blue drone__ping--delay" />
</div>
<div
className="drone drone--3"
style={{
top: "58%",
left: "72%",
display: activeIndex === 2 ? "none" : "flex",
}}
>
<div className="drone__badge drone__badge--orange">
<div className="drone__badge-top">
<img src={`${basePath}images/orange_drone.png`} alt="drone" width="16" height="16" />
<span className="drone__badge-conn">LTE</span>
</div>
<span className="drone__badge-id">FPA502</span>
<span className="drone__badge-status">
338m / 172k/m / 81°
<br />
126.745021/37.287339
</span>
</div>
<div className="drone__ping drone__ping--orange" />
<div className="drone__ping drone__ping--orange drone__ping--delay" />
</div>
</div>
{/* 모바일 전용 — 탭 이미지 */}
<div className="utm-what__mobile-only">
<div className="utm-what__m-img-wrap">
{UTM_WHAT_LEFT.map((_, i) => (
<img key={i} src={`${basePath}images/utm-mobile-img${i + 1}.png`} alt={t.mobileImgAlt(i + 1)} className={`utm-what__m-img${activeIndex === i ? " utm-what__m-img--active" : ""}`} />
))}
</div>
</div>
</motion.div>
<motion.ul className="utm-what__cards utm-what__cards--right" initial={{ opacity: 0, x: 24 }} animate={whatInView ? { opacity: 1, x: 0 } : {}} transition={{ duration: 0.7, delay: 0.3, ease }}>
{UTM_WHAT_RIGHT.map(({ icon: Icon, label }) => (
<li key={label} className="utm-what__card utm-what__card--right">
<span className="utm-what__card-icon">
<Icon size={16} />
</span>
<span className="utm-what__card-label">{label}</span>
</li>
))}
</motion.ul>
</div>
</motion.div>
</div>
</section>
{/* ── 03 Evolution ── */}
<div className="inner-wrap">
<section className="utm-evo" ref={evoRef}>
<motion.div className="utm-evo__left" initial={{ opacity: 0, x: -32 }} animate={evoInView ? { opacity: 1, x: 0 } : {}} transition={{ duration: 0.7, ease }}>
<motion.span className="fc-eyebrow" initial={{ opacity: 0, y: 16 }} animate={evoInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
Evolution
</motion.span>
<h2 className="utm-evo__title">{t.evoTitle}</h2>
<p className="utm-evo__desc">{t.evoDesc}</p>
</motion.div>
<div className="utm-evo__right">
<div className="utm-evo__track" ref={evoTrackRef}>
<div className="utm-evo__line-bg" />
<motion.div className="utm-evo__line-fill" style={{ height: lineHeight }} />
{EVO_ITEMS.map((item, i) => (
<EvoItem key={item.name} item={item} index={i} />
))}
</div>
</div>
</section>
</div>
{/* ── 04 Capabilities ── */}
<section className="utm-cap" ref={capRef}>
<div className="inner-wrap">
<div className="utm-cap__top">
<motion.span className="fc-eyebrow" initial={{ opacity: 0, y: 16 }} animate={capInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}>
Capabilities
</motion.span>
<motion.h2 className="utm-cap__title" initial={{ opacity: 0, y: 20 }} animate={capInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.7, delay: 0.1, ease }}>
{t.capTitle}
</motion.h2>
<motion.p className="utm-cap__desc" initial={{ opacity: 0, y: 16 }} animate={capInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.2, ease }}>
{t.capDesc}
</motion.p>
</div>
<ul className="utm-cap__cards">
{SECTORS.map((s, i) => (
<motion.li key={s.id} className="utm-cap__card" style={{ "--cap-color": s.color }} initial={{ opacity: 0, y: 48 }} animate={capInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.7, delay: 0.2 + i * 0.12, ease }}>
<span className="utm-cap__card-num">{s.num}</span>
<div className="utm-cap__card-body">
<strong className="utm-cap__card-name">{s.name}</strong>
<ul className="utm-cap__card-tags">
{s.tags.map((tag) => (
<li key={tag}>{tag}</li>
))}
</ul>
</div>
</motion.li>
))}
</ul>
</div>
</section>
</div>
</article>
);
}
export default IntroPage;