Browse Source

feat : intro utm 기능 추가

remotes/origin/main
이시연 2 weeks ago
parent
commit
9022fcf936
  1. BIN
      public/images/utm_what_img2.png
  2. BIN
      public/images/what_utm_img2.png
  3. 53
      src/css/common.css
  4. 638
      src/pages/utm/IntroPage.jsx

BIN
public/images/utm_what_img2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
public/images/what_utm_img2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

53
src/css/common.css

@ -1122,7 +1122,7 @@ body{overflow-x:hidden;}
.utm-intro__card-label { font-size: 18px; font-weight: 600; color: #222; line-height: 1.4; word-break: keep-all; }
.utm-what { background: linear-gradient(135deg, #0f1729 0%, #1a1040 40%, #0d1f3c 70%, #0f1729 100%); padding: 100px 0; }
.utm-what { background: linear-gradient(135deg, #07091a 0%, #1d203f 50%, #07091a 100%); padding: 100px 0; }
.utm-what__top { text-align: center; margin-bottom: 64px; }
.utm-what__title { font-size: 40px; font-weight: 700; line-height: 1.35; color: #fff; margin-bottom: 20px; word-break: keep-all; }
.utm-what__title em { background: linear-gradient(90deg, #6366f1, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-style: normal; }
@ -1132,14 +1132,18 @@ body{overflow-x:hidden;}
.utm-what__desc { font-size: 14px; color: rgba(255,255,255,0.5); line-height: 1.9; max-width: 600px; 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__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: center; gap: 12px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; padding: 0 16px; height: 80px; transition: background 0.2s, border-color 0.2s; white-space: nowrap; }
.utm-what__card:hover { background: #6366f1; border-color: #6366f1; }
.utm-what__card--right { flex-direction: row-reverse; }
.utm-what__card-icon { width: 28px; height: 28px; border-radius: 6px; background: rgba(99,102,241,0.15); border: 0.5px solid rgba(99,102,241,0.3); display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: #a78bfa; }
.utm-what__card-label { font-size: 15px; font-weight: 500; color: rgba(255,255,255,0.7); line-height: 1; white-space: nowrap; text-align: center; }
.utm-what__card--right { flex-direction: row-reverse; }
.utm-what__card:hover .utm-what__card-label { color: #fff; }
.utm-what__card:hover .utm-what__card-icon { background: rgba(255,255,255,0.2); border-color: rgba(255,255,255,0.3); color: #fff; }
.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--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-label { font-size: 15px; font-weight: 600; color: #fff; flex: 1; }
.utm-what__card:hover { background: #fff; border-color: #fff; }
.utm-what__card:hover .utm-what__card-label { color: #1a1f5e; }
.utm-what__card:hover .utm-what__card-icon { background: #6366f1; color: #fff; }
.utm-what__card--active { background: #6366f1 !important; border-color: #6366f1 !important; }
.utm-what__card--active .utm-what__card-label { color: #fff !important; }
.utm-what__card--active .utm-what__card-icon { background: rgba(255,255,255,0.2) !important; color: #fff !important; }
.utm-what__mockup { flex: 1; display: flex; align-items: center; }
.utm-what__list { list-style: none; display: flex; flex-direction: column; gap: 14px; }
.utm-what__list li { display: flex; align-items: center; gap: 12px; font-size: 14px; color: rgba(255,255,255,0.75); font-weight: 500; }
@ -1149,7 +1153,7 @@ body{overflow-x:hidden;}
.utm-what__img-wrap { position: relative; width: 100%; min-height: 560px; background-size: contain; background-position: center; background-repeat: no-repeat; }
.utm-what__paths { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.utm-map-menu { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 6px; z-index: 10; }
.utm-map-menu { position: absolute; left: 12px; top: 34%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 6px; z-index: 10; }
.utm-map-menu__btn { width: 36px; height: 36px; background: rgba(15,18,32,0.75); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: rgba(255,255,255,0.6); transition: background 0.2s, color 0.2s, border-color 0.2s; }
.utm-map-menu__btn--alert { color: #FF5656; border-color: rgba(255,86,86,0.3); background: rgba(255,86,86,0.15); }
@ -1189,6 +1193,33 @@ body{overflow-x:hidden;}
.drone__ping--orange { border: 1.5px solid rgba(249,115,22,0.6); }
.drone__ping--delay { animation-delay: 1s; }
.utm-fp-panel { position: absolute; bottom: 62px !important; left: 60px !important; z-index: 20 !important; }
.utm-airspace-layer { position: absolute; inset: 0; z-index: 15; }
.utm-airspace { position: absolute; border-radius: 50%; border: 2px dashed; transform: translate(-50%, -50%); cursor: pointer; transition: filter 0.2s; }
.utm-airspace:hover { filter: brightness(1.3); }
.utm-airspace-popup { position: absolute; width: 200px; background: #1a1d2e; border-radius: 12px; overflow: hidden; box-shadow: 0 8px 32px rgba(0,0,0,0.4); z-index: 30; transform: translateY(-50%); }
.utm-airspace-popup__header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid; }
.utm-airspace-popup__id { font-size: 12px; font-weight: 700; color: #fff; }
.utm-airspace-popup__badge { font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 4px; }
.utm-airspace-popup__rows { display: flex; flex-direction: column; padding: 8px 0; }
.utm-airspace-popup__row { display: flex; justify-content: space-between; padding: 5px 14px; font-size: 11px; }
.utm-airspace-popup__row span:first-child { color: rgba(255,255,255,0.4); }
.utm-airspace-popup__row span:last-child { color: rgba(255,255,255,0.85); font-weight: 500; }
.utm-map-menu__btn--active { background: #6366f1; border-color: #6366f1; color: #fff; }
.utm-weather-panel { position: absolute; top: 45px; left: 60px; background: #1a1d2e; border-radius: 16px; overflow: hidden; box-shadow: 0 24px 60px rgba(0,0,0,0.45), 0 0 0 1px rgba(99,102,241,0.2); z-index: 20; border: 1px solid rgba(99,102,241,0.2); padding: 20px; min-width: 240px; }
.utm-weather-panel__header { display: flex; align-items: center; gap: 6px; font-size: 10px; color: #fff; margin-bottom: 12px; }
.utm-weather-panel__time { margin-left: auto; font-size: 9px; color: rgba(255,255,255,0.2); }
.utm-weather-panel__icon { font-size: 32px; margin-bottom: 4px; }
.utm-weather-panel__temp { font-size: 52px; font-weight: 800; color: #fff; line-height: 1; letter-spacing: -3px; margin-bottom: 6px; }
.utm-weather-panel__desc { font-size: 11px; color:#fff; background:#6366f1; padding: 3px 10px; border-radius: 999px; margin-bottom: 16px; display: inline-block; border: 1px solid rgba(99,102,241,0.2); }
.utm-weather-panel__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 10px; }
.utm-weather-panel__item { background: rgba(99,102,241,0.06); border: 1px solid rgba(99,102,241,0.12); border-radius: 8px; padding: 8px 10px; }
.utm-weather-panel__item-label { font-size: 9px; color: rgba(255,255,255,0.35); margin-bottom: 4px; display: block; letter-spacing: 0.04em; }
.utm-weather-panel__item-val { font-size: 13px; font-weight: 700; color: #fff; }
.utm-weather-panel__status { display: flex; align-items: center; gap: 6px; padding: 8px 10px; background: rgba(52,211,153,0.1); border-radius: 8px; font-size: 11px; font-weight: 600; color: #34d399; border: 1px solid rgba(52,211,153,0.2); }
.utm-weather-panel__status-dot { width: 6px; height: 6px; border-radius: 50%; background: #34d399; flex-shrink: 0; box-shadow: 0 0 6px rgba(52,211,153,0.8); animation: dotBlink 2s ease-in-out infinite; }ather-panel__icon { font-size: 36px; margin-bottom: 4px; }
@keyframes alertAppear { from{opacity:0; transform:translateY(-8px);} to{opacity:1; transform:translateY(0);} }
@keyframes dotBlink { 0%,100%{opacity:1;} 50%{opacity:0.2;} }
@ -1303,7 +1334,6 @@ body{overflow-x:hidden;}
.utm-what__body { grid-template-columns: 1fr; gap: 20px; }
.utm-what__cards { flex-direction: row; flex-wrap: wrap; gap: 10px; justify-content: flex-start;}
.utm-what__card { height: 52px; flex: 1 1 calc(50% - 5px); min-width: 140px; justify-content: flex-start;}
.utm-what__card--right { flex-direction: row; }
.utm-what__card--right .utm-what__card-icon { order: -1; }
.utm-what__mockup { order: -1; }
.utm-what__img-wrap { min-height: 300px; }
@ -1350,7 +1380,6 @@ body{overflow-x:hidden;}
.utm-what__title { font-size: 22px; }
.utm-what__card { flex: 1 1 calc(50% - 5px); min-width: 0; justify-content: flex-start;}
.utm-what__img-wrap { min-height: 240px; }
.utm-what__card--right { flex-direction: row;}
.utm-what__card--right .utm-what__card-icon { order: -1; }
.drone--1 { left: 35% !important; }
.drone--2 { left: 5% !important; }

638
src/pages/utm/IntroPage.jsx

@ -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,18 +35,79 @@ 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 = [
{
img: `${basePath}images/utm_intro_drone.png`,
@ -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 }, i) => (
<li
key={label}
className={`utm-what__card${activeIndex === i ? " utm-what__card--active" : ""}`}
onClick={() => setActiveIndex(i)}
style={{ cursor: "pointer" }}
>
{UTM_WHAT_LEFT.map(({ icon: Icon, label }) => (
<li key={label} className="utm-what__card">
<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>

Loading…
Cancel
Save