|
|
|
|
@ -1,7 +1,13 @@
|
|
|
|
|
import { useRef } from "react"; |
|
|
|
|
import { useEffect, useRef, useState } from "react"; |
|
|
|
|
import SubHero from "../../components/SubHero"; |
|
|
|
|
import useFadeIn from "../../hooks/useFadeIn"; |
|
|
|
|
import { motion, useInView, useScroll, useTransform } from "framer-motion"; |
|
|
|
|
import { |
|
|
|
|
motion, |
|
|
|
|
AnimatePresence, |
|
|
|
|
useInView, |
|
|
|
|
useScroll, |
|
|
|
|
useTransform, |
|
|
|
|
} from "framer-motion"; |
|
|
|
|
import { |
|
|
|
|
MapPin, |
|
|
|
|
ShieldAlert, |
|
|
|
|
@ -29,17 +35,78 @@ const INTRO_CARDS = [
|
|
|
|
|
]; |
|
|
|
|
const UTM_WHAT_LEFT = [ |
|
|
|
|
{ icon: MapPin, label: "실시간 위치 감시" }, |
|
|
|
|
{ icon: ShieldAlert, label: "충돌 위험 사전 분석" }, |
|
|
|
|
{ icon: Cloud, label: "기상 정보 자동 연계" }, |
|
|
|
|
{ icon: FileCheck, label: "비행 계획 및 경로 승인" }, |
|
|
|
|
{ icon: Network, label: "공역 설정·지오펜싱" }, |
|
|
|
|
{ icon: Cloud, label: "기상 정보 자동 연계" }, |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
const UTM_WHAT_RIGHT = [ |
|
|
|
|
{ icon: FileCheck, label: "비행 계획 및 경로 승인" }, |
|
|
|
|
{ icon: BadgeCheck, label: "불법 기체 실시간 감지" }, |
|
|
|
|
{ icon: Database, label: "비행 데이터 통합 관리" }, |
|
|
|
|
{ icon: Layers, label: "UAM·드론 시스템 연동" }, |
|
|
|
|
{ icon: ShieldAlert, label: "충돌 위험 사전 분석" }, |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
const INTRO_FLIGHT_PLANS = [ |
|
|
|
|
{ id: "FP-001", pilot: "홍길동", route: "서울 → 김포", status: "대기" }, |
|
|
|
|
{ id: "FP-002", pilot: "김철수", route: "인천 → 수원", status: "대기" }, |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
const AIRSPACES = [ |
|
|
|
|
{ |
|
|
|
|
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 INTRO_DETAIL = { |
|
|
|
|
id: "FP-001", |
|
|
|
|
pilot: "홍길동", |
|
|
|
|
pilotId: "hong001", |
|
|
|
|
route: "서울 → 김포", |
|
|
|
|
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 EVO_ITEMS = [ |
|
|
|
|
{ |
|
|
|
|
@ -219,6 +286,11 @@ function IntroPage() {
|
|
|
|
|
{ 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" }); |
|
|
|
|
@ -239,6 +311,177 @@ function IntroPage() {
|
|
|
|
|
}); |
|
|
|
|
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 t; |
|
|
|
|
const run = (current) => { |
|
|
|
|
const next = |
|
|
|
|
INTRO_PHASES[(INTRO_PHASES.indexOf(current) + 1) % INTRO_PHASES.length]; |
|
|
|
|
t = setTimeout(() => { |
|
|
|
|
setFpPhase(next); |
|
|
|
|
run(next); |
|
|
|
|
}, INTRO_PHASE_DURATION[current]); |
|
|
|
|
}; |
|
|
|
|
t = setTimeout(() => run("list"), 1000); |
|
|
|
|
return () => { |
|
|
|
|
clearTimeout(t); |
|
|
|
|
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]); |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<article ref={ref}> |
|
|
|
|
<SubHero |
|
|
|
|
@ -373,9 +616,15 @@ function IntroPage() {
|
|
|
|
|
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 }) => ( |
|
|
|
|
<li key={label} className="utm-what__card"> |
|
|
|
|
{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> |
|
|
|
|
@ -393,7 +642,7 @@ function IntroPage() {
|
|
|
|
|
<div |
|
|
|
|
className="utm-what__img-wrap" |
|
|
|
|
style={{ |
|
|
|
|
backgroundImage: `url(${basePath}images/what_utm_img2.png)`, |
|
|
|
|
backgroundImage: `url(${basePath}images/utm_what_img2.png)`, |
|
|
|
|
}} |
|
|
|
|
> |
|
|
|
|
<div className="utm-map-menu"> |
|
|
|
|
@ -403,9 +652,12 @@ function IntroPage() {
|
|
|
|
|
<button className="utm-map-menu__btn"> |
|
|
|
|
<ShieldAlert size={16} /> |
|
|
|
|
</button> |
|
|
|
|
<button className="utm-map-menu__btn"> |
|
|
|
|
<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> |
|
|
|
|
@ -420,9 +672,361 @@ function IntroPage() {
|
|
|
|
|
</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"> |
|
|
|
|
비행계획 승인관리 |
|
|
|
|
</span> |
|
|
|
|
<span className="utm-panel__badge"> |
|
|
|
|
검색결과 총 {INTRO_FLIGHT_PLANS.length}건 |
|
|
|
|
</span> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<div className="utm-panel__table"> |
|
|
|
|
<div className="utm-panel__thead"> |
|
|
|
|
<span>계획 ID</span> |
|
|
|
|
<span>신청자</span> |
|
|
|
|
<span>경로</span> |
|
|
|
|
<span>상태</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 === "승인" ? "done" : "wait"}`} |
|
|
|
|
> |
|
|
|
|
{row.status} |
|
|
|
|
</span> |
|
|
|
|
<span className="utm-panel__cell"> |
|
|
|
|
<span |
|
|
|
|
className={`utm-panel__btn${i === 0 && fpPhase === "list" ? " utm-panel__btn--hover" : ""}`} |
|
|
|
|
> |
|
|
|
|
상세보기 |
|
|
|
|
</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"> |
|
|
|
|
비행계획 상세 |
|
|
|
|
</div> |
|
|
|
|
<div className="utm-panel__detail-rows"> |
|
|
|
|
<div className="utm-panel__detail-row"> |
|
|
|
|
<span>계획 ID</span> |
|
|
|
|
<span>{INTRO_DETAIL.id}</span> |
|
|
|
|
</div> |
|
|
|
|
<div className="utm-panel__detail-row"> |
|
|
|
|
<span>신청자</span> |
|
|
|
|
<span> |
|
|
|
|
{INTRO_DETAIL.pilot} ({INTRO_DETAIL.pilotId}) |
|
|
|
|
</span> |
|
|
|
|
</div> |
|
|
|
|
<div className="utm-panel__detail-row"> |
|
|
|
|
<span>경로</span> |
|
|
|
|
<span>{INTRO_DETAIL.route}</span> |
|
|
|
|
</div> |
|
|
|
|
<div className="utm-panel__detail-row"> |
|
|
|
|
<span>고도</span> |
|
|
|
|
<span> |
|
|
|
|
{INTRO_DETAIL.altitude} · {INTRO_DETAIL.speed} |
|
|
|
|
</span> |
|
|
|
|
</div> |
|
|
|
|
<div className="utm-panel__detail-row"> |
|
|
|
|
<span>신청일시</span> |
|
|
|
|
<span>{INTRO_DETAIL.date}</span> |
|
|
|
|
</div> |
|
|
|
|
<div className="utm-panel__detail-row"> |
|
|
|
|
<span>상태</span> |
|
|
|
|
<span |
|
|
|
|
className={`utm-panel__status utm-panel__status--${fpPhase === "done" ? "done" : "wait"}`} |
|
|
|
|
> |
|
|
|
|
{fpPhase === "done" ? "승인" : "대기"} |
|
|
|
|
</span> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
{fpPhase === "detail" && ( |
|
|
|
|
<div className="utm-panel__detail-actions"> |
|
|
|
|
<span className="utm-panel__approve-btn utm-panel__approve-btn--hover"> |
|
|
|
|
승인처리 |
|
|
|
|
</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-001 승인이 완료되었습니다. |
|
|
|
|
</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"> |
|
|
|
|
비행계획을 승인하시겠습니까? |
|
|
|
|
</div> |
|
|
|
|
<div className="utm-confirm__desc"> |
|
|
|
|
FP-001 · 홍길동 · 서울 → 김포 |
|
|
|
|
</div> |
|
|
|
|
<div className="utm-confirm__btns"> |
|
|
|
|
<span className="utm-confirm__cancel"> |
|
|
|
|
취소 |
|
|
|
|
</span> |
|
|
|
|
<span className="utm-confirm__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>구역명</span> |
|
|
|
|
<span>{selectedAirspace.name}</span> |
|
|
|
|
</div> |
|
|
|
|
<div className="utm-airspace-popup__row"> |
|
|
|
|
<span>구역 ID</span> |
|
|
|
|
<span>{selectedAirspace.id}</span> |
|
|
|
|
</div> |
|
|
|
|
<div className="utm-airspace-popup__row"> |
|
|
|
|
<span>고도 범위</span> |
|
|
|
|
<span>0 ~ 150m</span> |
|
|
|
|
</div> |
|
|
|
|
<div className="utm-airspace-popup__row"> |
|
|
|
|
<span>반경</span> |
|
|
|
|
<span>{selectedAirspace.size * 10}m</span> |
|
|
|
|
</div> |
|
|
|
|
<div className="utm-airspace-popup__row"> |
|
|
|
|
<span>상태</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>기상 정보</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"> |
|
|
|
|
맑음 · 비행 적합 |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<div className="utm-weather-panel__grid"> |
|
|
|
|
<div className="utm-weather-panel__item"> |
|
|
|
|
<span className="utm-weather-panel__item-label"> |
|
|
|
|
풍속 |
|
|
|
|
</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"> |
|
|
|
|
풍향 |
|
|
|
|
</span> |
|
|
|
|
<span className="utm-weather-panel__item-val"> |
|
|
|
|
북동 |
|
|
|
|
</span> |
|
|
|
|
</div> |
|
|
|
|
<div className="utm-weather-panel__item"> |
|
|
|
|
<span className="utm-weather-panel__item-label"> |
|
|
|
|
습도 |
|
|
|
|
</span> |
|
|
|
|
<span className="utm-weather-panel__item-val"> |
|
|
|
|
62% |
|
|
|
|
</span> |
|
|
|
|
</div> |
|
|
|
|
<div className="utm-weather-panel__item"> |
|
|
|
|
<span className="utm-weather-panel__item-label"> |
|
|
|
|
시정 |
|
|
|
|
</span> |
|
|
|
|
<span className="utm-weather-panel__item-val"> |
|
|
|
|
10km |
|
|
|
|
</span> |
|
|
|
|
</div> |
|
|
|
|
<div className="utm-weather-panel__item"> |
|
|
|
|
<span className="utm-weather-panel__item-label"> |
|
|
|
|
운고 |
|
|
|
|
</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"> |
|
|
|
|
강수 |
|
|
|
|
</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%" }} |
|
|
|
|
style={{ |
|
|
|
|
top: "35%", |
|
|
|
|
left: "48%", |
|
|
|
|
display: activeIndex === 2 ? "none" : "flex", |
|
|
|
|
}} |
|
|
|
|
> |
|
|
|
|
<div className="drone__badge drone__badge--red"> |
|
|
|
|
<div className="drone__badge-top"> |
|
|
|
|
@ -445,7 +1049,11 @@ function IntroPage() {
|
|
|
|
|
|
|
|
|
|
<div |
|
|
|
|
className="drone drone--2" |
|
|
|
|
style={{ top: "25%", left: "22%" }} |
|
|
|
|
style={{ |
|
|
|
|
top: "25%", |
|
|
|
|
left: "22%", |
|
|
|
|
display: activeIndex === 2 ? "none" : "flex", |
|
|
|
|
}} |
|
|
|
|
> |
|
|
|
|
<div className="drone__badge drone__badge--blue"> |
|
|
|
|
<div className="drone__badge-top"> |
|
|
|
|
@ -468,7 +1076,11 @@ function IntroPage() {
|
|
|
|
|
|
|
|
|
|
<div |
|
|
|
|
className="drone drone--3" |
|
|
|
|
style={{ top: "58%", left: "72%" }} |
|
|
|
|
style={{ |
|
|
|
|
top: "58%", |
|
|
|
|
left: "72%", |
|
|
|
|
display: activeIndex === 2 ? "none" : "flex", |
|
|
|
|
}} |
|
|
|
|
> |
|
|
|
|
<div className="drone__badge drone__badge--orange"> |
|
|
|
|
<div className="drone__badge-top"> |
|
|
|
|
@ -504,10 +1116,10 @@ function IntroPage() {
|
|
|
|
|
key={label} |
|
|
|
|
className="utm-what__card utm-what__card--right" |
|
|
|
|
> |
|
|
|
|
<span className="utm-what__card-label">{label}</span> |
|
|
|
|
<span className="utm-what__card-icon"> |
|
|
|
|
<Icon size={16} /> |
|
|
|
|
</span> |
|
|
|
|
<span className="utm-what__card-label">{label}</span> |
|
|
|
|
</li> |
|
|
|
|
))} |
|
|
|
|
</motion.ul> |
|
|
|
|
|