diff --git a/public/images/utm_what_img2.png b/public/images/utm_what_img2.png new file mode 100644 index 0000000..9111c32 Binary files /dev/null and b/public/images/utm_what_img2.png differ diff --git a/public/images/what_utm_img2.png b/public/images/what_utm_img2.png deleted file mode 100644 index a76eb31..0000000 Binary files a/public/images/what_utm_img2.png and /dev/null differ diff --git a/src/css/common.css b/src/css/common.css index 0835f87..e486939 100644 --- a/src/css/common.css +++ b/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; } diff --git a/src/pages/utm/IntroPage.jsx b/src/pages/utm/IntroPage.jsx index 98a9c20..0bc930d 100644 --- a/src/pages/utm/IntroPage.jsx +++ b/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,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 (
- {UTM_WHAT_LEFT.map(({ icon: Icon, label }) => ( -
  • + {UTM_WHAT_LEFT.map(({ icon: Icon, label }, i) => ( +
  • setActiveIndex(i)} + style={{ cursor: "pointer" }} + > @@ -393,7 +642,7 @@ function IntroPage() {
    @@ -403,9 +652,12 @@ function IntroPage() { - + @@ -420,9 +672,361 @@ function IntroPage() {
    + + {activeIndex === 1 && ( + +
    + + 비행계획 승인관리 + + + 검색결과 총 {INTRO_FLIGHT_PLANS.length}건 + +
    + +
    +
    + 계획 ID + 신청자 + 경로 + 상태 + +
    + {INTRO_FLIGHT_PLANS.map((row, i) => ( +
    + + {row.id} + + + {row.pilot} + + + {row.route} + + + {row.status} + + + + 상세보기 + + +
    + ))} +
    + +
    +
    + 비행계획 상세 +
    +
    +
    + 계획 ID + {INTRO_DETAIL.id} +
    +
    + 신청자 + + {INTRO_DETAIL.pilot} ({INTRO_DETAIL.pilotId}) + +
    +
    + 경로 + {INTRO_DETAIL.route} +
    +
    + 고도 + + {INTRO_DETAIL.altitude} · {INTRO_DETAIL.speed} + +
    +
    + 신청일시 + {INTRO_DETAIL.date} +
    +
    + 상태 + + {fpPhase === "done" ? "승인" : "대기"} + +
    +
    + + {fpPhase === "detail" && ( +
    + + 승인처리 + +
    + )} + + + {fpPhase === "done" && ( + + + ✓ + + FP-001 승인이 완료되었습니다. + + )} + +
    + + + {fpPhase === "confirm" && ( + + +
    ⚠️
    +
    + 비행계획을 승인하시겠습니까? +
    +
    + FP-001 · 홍길동 · 서울 → 김포 +
    +
    + + 취소 + + + 확인 + +
    +
    +
    + )} +
    + + {/* 커서 */} +
    +
    +
    + + )} + + + {/* 공역 탭 */} + + {activeIndex === 2 && ( + + {AIRSPACES.map((az, i) => ( +
    + ))} + + {/* 공역 정보 팝업 */} + + {selectedAirspace && ( + +
    + + {selectedAirspace.id} + + + {selectedAirspace.type} + +
    +
    +
    + 구역명 + {selectedAirspace.name} +
    +
    + 구역 ID + {selectedAirspace.id} +
    +
    + 고도 범위 + 0 ~ 150m +
    +
    + 반경 + {selectedAirspace.size * 10}m +
    +
    + 상태 + + {selectedAirspace.type} + +
    +
    +
    + )} +
    + + {/* 커서 */} +
    +
    +
    + + )} + + + {/* 기상 정보 자동 연계 */} + + + {activeIndex === 3 && ( + +
    + + 기상 정보 + + 2025-06-10 14:32 + +
    + +
    +
    ☀️
    +
    18°
    +
    + 맑음 · 비행 적합 +
    +
    + +
    +
    + + 풍속 + + + 3.2 m/s + +
    +
    + + 풍향 + + + 북동 + +
    +
    + + 습도 + + + 62% + +
    +
    + + 시정 + + + 10km + +
    +
    + + 운고 + + + 1,500m + +
    +
    + + 강수 + + + 0mm + +
    +
    +
    + )} +
    @@ -445,7 +1049,11 @@ function IntroPage() {
    @@ -468,7 +1076,11 @@ function IntroPage() {
    @@ -504,10 +1116,10 @@ function IntroPage() { key={label} className="utm-what__card utm-what__card--right" > - {label} + {label}
  • ))}