|
|
|
@ -27,14 +27,15 @@ const DETAIL = { |
|
|
|
date: "2025-06-10 14:30", |
|
|
|
date: "2025-06-10 14:30", |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const PHASES = ["list", "detail", "confirm", "done"]; |
|
|
|
|
|
|
|
const PHASE_DURATION = { list: 1800, detail: 2200, confirm: 1800, done: 2000 }; |
|
|
|
|
|
|
|
|
|
|
|
function UtmSystemPanel({ phase, activeRow }) { |
|
|
|
function UtmSystemPanel({ phase, activeRow }) { |
|
|
|
return ( |
|
|
|
return ( |
|
|
|
<div className="utm-panel"> |
|
|
|
<div className="utm-panel"> |
|
|
|
<div className="utm-panel__header"> |
|
|
|
<div className="utm-panel__header"> |
|
|
|
<span className="utm-panel__title">비행계획 승인관리</span> |
|
|
|
<span className="utm-panel__title">비행계획 승인관리</span> |
|
|
|
<span className="utm-panel__badge"> |
|
|
|
<span className="utm-panel__badge">검색결과 총 {FLIGHT_PLANS.length}건</span> |
|
|
|
검색결과 총 {FLIGHT_PLANS.length}건 |
|
|
|
|
|
|
|
</span> |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<div className="utm-panel__table"> |
|
|
|
<div className="utm-panel__table"> |
|
|
|
@ -46,32 +47,20 @@ function UtmSystemPanel({ phase, activeRow }) { |
|
|
|
<span></span> |
|
|
|
<span></span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
{FLIGHT_PLANS.map((row, i) => ( |
|
|
|
{FLIGHT_PLANS.map((row, i) => ( |
|
|
|
<div |
|
|
|
<div key={row.id} className={`utm-panel__row${activeRow === i ? " utm-panel__row--active" : ""}`}> |
|
|
|
key={row.id} |
|
|
|
|
|
|
|
className={`utm-panel__row${activeRow === i ? " utm-panel__row--active" : ""}`} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
<span className="utm-panel__cell">{row.id}</span> |
|
|
|
<span className="utm-panel__cell">{row.id}</span> |
|
|
|
<span className="utm-panel__cell">{row.pilot}</span> |
|
|
|
<span className="utm-panel__cell">{row.pilot}</span> |
|
|
|
<span className="utm-panel__cell">{row.route}</span> |
|
|
|
<span className="utm-panel__cell">{row.route}</span> |
|
|
|
<span |
|
|
|
<span className={`utm-panel__status utm-panel__status--${row.status === "승인" ? "done" : "wait"}`}>{row.status}</span> |
|
|
|
className={`utm-panel__status utm-panel__status--${row.status === "승인" ? "done" : "wait"}`} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
{row.status} |
|
|
|
|
|
|
|
</span> |
|
|
|
|
|
|
|
<span className="utm-panel__cell"> |
|
|
|
<span className="utm-panel__cell"> |
|
|
|
<span |
|
|
|
<span className={`utm-panel__btn${activeRow === i && phase === "list" ? " utm-panel__btn--hover" : ""}`}>상세보기</span> |
|
|
|
className={`utm-panel__btn${activeRow === i && phase === "list" ? " utm-panel__btn--hover" : ""}`} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
상세보기 |
|
|
|
|
|
|
|
</span> |
|
|
|
|
|
|
|
</span> |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
))} |
|
|
|
))} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<div |
|
|
|
{/* 상세: 아래로 펼쳐짐 */} |
|
|
|
className={`utm-panel__detail${phase === "detail" || phase === "confirm" || phase === "done" ? " utm-panel__detail--show" : ""}`} |
|
|
|
<div className={`utm-panel__detail${phase === "detail" || phase === "confirm" || phase === "done" ? " utm-panel__detail--show" : ""}`}> |
|
|
|
> |
|
|
|
|
|
|
|
<div className="utm-panel__detail-title">비행계획 상세</div> |
|
|
|
<div className="utm-panel__detail-title">비행계획 상세</div> |
|
|
|
<div className="utm-panel__detail-rows"> |
|
|
|
<div className="utm-panel__detail-rows"> |
|
|
|
<div className="utm-panel__detail-row"> |
|
|
|
<div className="utm-panel__detail-row"> |
|
|
|
@ -100,31 +89,19 @@ function UtmSystemPanel({ phase, activeRow }) { |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div className="utm-panel__detail-row"> |
|
|
|
<div className="utm-panel__detail-row"> |
|
|
|
<span>상태</span> |
|
|
|
<span>상태</span> |
|
|
|
<span |
|
|
|
<span className={`utm-panel__status utm-panel__status--${phase === "done" ? "done" : "wait"}`}>{phase === "done" ? "승인" : "대기"}</span> |
|
|
|
className={`utm-panel__status utm-panel__status--${phase === "done" ? "done" : "wait"}`} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
{phase === "done" ? "승인" : "대기"} |
|
|
|
|
|
|
|
</span> |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
{phase === "detail" && ( |
|
|
|
{phase === "detail" && ( |
|
|
|
<div className="utm-panel__detail-actions"> |
|
|
|
<div className="utm-panel__detail-actions"> |
|
|
|
<span className="utm-panel__approve-btn utm-panel__approve-btn--hover"> |
|
|
|
<span className="utm-panel__approve-btn utm-panel__approve-btn--hover">승인처리</span> |
|
|
|
승인처리 |
|
|
|
|
|
|
|
</span> |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
)} |
|
|
|
|
|
|
|
|
|
|
|
<AnimatePresence> |
|
|
|
<AnimatePresence> |
|
|
|
{phase === "done" && ( |
|
|
|
{phase === "done" && ( |
|
|
|
<motion.div |
|
|
|
<motion.div className="utm-panel__toast" initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0 }} transition={{ duration: 0.35, ease }}> |
|
|
|
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> |
|
|
|
<span className="utm-panel__toast-icon">✓</span> |
|
|
|
FP-001 승인이 완료되었습니다. |
|
|
|
FP-001 승인이 완료되었습니다. |
|
|
|
</motion.div> |
|
|
|
</motion.div> |
|
|
|
@ -132,29 +109,14 @@ function UtmSystemPanel({ phase, activeRow }) { |
|
|
|
</AnimatePresence> |
|
|
|
</AnimatePresence> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* 컨펌: 패널 안에서 블러 오버레이 */} |
|
|
|
<AnimatePresence> |
|
|
|
<AnimatePresence> |
|
|
|
{phase === "confirm" && ( |
|
|
|
{phase === "confirm" && ( |
|
|
|
<motion.div |
|
|
|
<motion.div className="utm-panel__confirm-overlay" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.25 }}> |
|
|
|
className="utm-panel__confirm-overlay" |
|
|
|
<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 }}> |
|
|
|
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__icon">⚠️</div> |
|
|
|
<div className="utm-confirm__title"> |
|
|
|
<div className="utm-confirm__title">비행계획을 승인하시겠습니까?</div> |
|
|
|
비행계획을 승인하시겠습니까? |
|
|
|
<div className="utm-confirm__desc">FP-001 · 홍길동 · 서울 → 김포</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="utm-confirm__desc"> |
|
|
|
|
|
|
|
FP-001 · 홍길동 · 서울 → 김포 |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="utm-confirm__btns"> |
|
|
|
<div className="utm-confirm__btns"> |
|
|
|
<span className="utm-confirm__cancel">취소</span> |
|
|
|
<span className="utm-confirm__cancel">취소</span> |
|
|
|
<span className="utm-confirm__ok">확인</span> |
|
|
|
<span className="utm-confirm__ok">확인</span> |
|
|
|
@ -174,7 +136,21 @@ function MainUtm() { |
|
|
|
const [activeRow] = useState(0); |
|
|
|
const [activeRow] = useState(0); |
|
|
|
const cursorRef = useRef(null); |
|
|
|
const cursorRef = useRef(null); |
|
|
|
|
|
|
|
|
|
|
|
// ← useEffect 딱 하나만! |
|
|
|
useEffect(() => { |
|
|
|
|
|
|
|
if (!inView) return; |
|
|
|
|
|
|
|
let t; |
|
|
|
|
|
|
|
const run = (current) => { |
|
|
|
|
|
|
|
const next = PHASES[(PHASES.indexOf(current) + 1) % PHASES.length]; |
|
|
|
|
|
|
|
t = setTimeout(() => { |
|
|
|
|
|
|
|
setPhase(next); |
|
|
|
|
|
|
|
run(next); |
|
|
|
|
|
|
|
}, PHASE_DURATION[current]); |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
t = setTimeout(() => run("list"), 1000); |
|
|
|
|
|
|
|
return () => clearTimeout(t); |
|
|
|
|
|
|
|
}, [inView]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 커서 이동 로직 |
|
|
|
useEffect(() => { |
|
|
|
useEffect(() => { |
|
|
|
if (!inView) return; |
|
|
|
if (!inView) return; |
|
|
|
|
|
|
|
|
|
|
|
@ -182,13 +158,7 @@ function MainUtm() { |
|
|
|
const cursor = cursorRef.current; |
|
|
|
const cursor = cursorRef.current; |
|
|
|
if (!showcase || !cursor) return; |
|
|
|
if (!showcase || !cursor) return; |
|
|
|
|
|
|
|
|
|
|
|
let stopped = false; |
|
|
|
let timers = []; |
|
|
|
const ids = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function sid(id) { |
|
|
|
|
|
|
|
ids.push(id); |
|
|
|
|
|
|
|
return id; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getPos(selector) { |
|
|
|
function getPos(selector) { |
|
|
|
const el = ref.current?.querySelector(selector); |
|
|
|
const el = ref.current?.querySelector(selector); |
|
|
|
@ -205,110 +175,86 @@ function MainUtm() { |
|
|
|
cursor.style.transform = `translate(${x}px, ${y}px)`; |
|
|
|
cursor.style.transform = `translate(${x}px, ${y}px)`; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function doClick() { |
|
|
|
function click() { |
|
|
|
cursor.classList.add("utm-cursor--click"); |
|
|
|
cursor.classList.add("utm-cursor--click"); |
|
|
|
sid(setTimeout(() => cursor.classList.remove("utm-cursor--click"), 250)); |
|
|
|
setTimeout(() => cursor.classList.remove("utm-cursor--click"), 250); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function schedule(ms, fn) { |
|
|
|
function after(ms, fn) { |
|
|
|
sid( |
|
|
|
const t = setTimeout(fn, ms); |
|
|
|
setTimeout(() => { |
|
|
|
timers.push(t); |
|
|
|
if (!stopped) fn(); |
|
|
|
|
|
|
|
}, ms), |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function run() { |
|
|
|
function loop() { |
|
|
|
if (stopped) return; |
|
|
|
timers.forEach(clearTimeout); |
|
|
|
|
|
|
|
timers = []; |
|
|
|
|
|
|
|
|
|
|
|
setPhase("list"); |
|
|
|
setPhase("list"); |
|
|
|
moveTo(180, 160); |
|
|
|
after(1000, () => { |
|
|
|
|
|
|
|
|
|
|
|
// 상세보기로 이동 |
|
|
|
|
|
|
|
schedule(1000, () => { |
|
|
|
|
|
|
|
const p = getPos(".utm-panel__row--active .utm-panel__btn"); |
|
|
|
const p = getPos(".utm-panel__row--active .utm-panel__btn"); |
|
|
|
if (p) moveTo(p.x, p.y); |
|
|
|
if (p) moveTo(p.x, p.y); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 상세보기 클릭 → detail |
|
|
|
after(2000, () => { |
|
|
|
schedule(2000, () => doClick()); |
|
|
|
click(); |
|
|
|
schedule(2200, () => setPhase("detail")); |
|
|
|
after(200, () => setPhase("detail")); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 승인처리로 이동 |
|
|
|
after(3200, () => { |
|
|
|
schedule(3200, () => { |
|
|
|
|
|
|
|
const p = getPos(".utm-panel__approve-btn"); |
|
|
|
const p = getPos(".utm-panel__approve-btn"); |
|
|
|
if (p) moveTo(p.x, p.y); |
|
|
|
if (p) moveTo(p.x, p.y); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 승인처리 클릭 → confirm |
|
|
|
after(4200, () => { |
|
|
|
schedule(4200, () => doClick()); |
|
|
|
click(); |
|
|
|
schedule(4400, () => setPhase("confirm")); |
|
|
|
after(200, () => setPhase("confirm")); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 확인 버튼으로 이동 |
|
|
|
after(5000, () => { |
|
|
|
schedule(5000, () => { |
|
|
|
|
|
|
|
const p = getPos(".utm-confirm__ok"); |
|
|
|
const p = getPos(".utm-confirm__ok"); |
|
|
|
if (p) moveTo(p.x, p.y); |
|
|
|
if (p) moveTo(p.x, p.y); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 확인 클릭 → done |
|
|
|
after(5800, () => { |
|
|
|
schedule(5800, () => doClick()); |
|
|
|
click(); |
|
|
|
schedule(6000, () => setPhase("done")); |
|
|
|
after(200, () => setPhase("done")); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 반복 |
|
|
|
after(7500, () => loop()); |
|
|
|
schedule(7500, () => run()); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
run(); |
|
|
|
moveTo(180, 160); |
|
|
|
|
|
|
|
after(500, () => loop()); |
|
|
|
|
|
|
|
|
|
|
|
return () => { |
|
|
|
return () => { |
|
|
|
stopped = true; |
|
|
|
timers.forEach(clearTimeout); |
|
|
|
ids.forEach(clearTimeout); |
|
|
|
timers = []; |
|
|
|
cursor.classList.remove("utm-cursor--click"); |
|
|
|
cursor.classList.remove("utm-cursor--click"); |
|
|
|
|
|
|
|
setPhase("list"); // ← cleanup에서만 호출 |
|
|
|
}; |
|
|
|
}; |
|
|
|
}, [inView]); |
|
|
|
}, [inView]); |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
return ( |
|
|
|
<section className="utm-hero" ref={ref}> |
|
|
|
<section className="utm-hero" ref={ref}> |
|
|
|
<div className="utm-hero__blob utm-hero__blob--1" /> |
|
|
|
<div className="utm-hero__blob utm-hero__blob--1" /> |
|
|
|
<div className="utm-hero__blob utm-hero__blob--2" /> |
|
|
|
<div className="utm-hero__blob utm-hero__blob--2" /> |
|
|
|
|
|
|
|
|
|
|
|
<div className="utm-hero__inner"> |
|
|
|
<div className="utm-hero__inner"> |
|
|
|
<motion.span |
|
|
|
<motion.span className="utm-hero__eyebrow" initial={{ opacity: 0, y: 12 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}> |
|
|
|
className="utm-hero__eyebrow" |
|
|
|
|
|
|
|
initial={{ opacity: 0, y: 12 }} |
|
|
|
|
|
|
|
animate={inView ? { opacity: 1, y: 0 } : {}} |
|
|
|
|
|
|
|
transition={{ duration: 0.6, ease }} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
UTM / UATM PLATFORM |
|
|
|
UTM / UATM PLATFORM |
|
|
|
</motion.span> |
|
|
|
</motion.span> |
|
|
|
|
|
|
|
|
|
|
|
<motion.h2 |
|
|
|
<motion.h2 className="utm-hero__title" initial={{ opacity: 0, y: 24 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.7, delay: 0.08, ease }}> |
|
|
|
className="utm-hero__title" |
|
|
|
|
|
|
|
initial={{ opacity: 0, y: 24 }} |
|
|
|
|
|
|
|
animate={inView ? { opacity: 1, y: 0 } : {}} |
|
|
|
|
|
|
|
transition={{ duration: 0.7, delay: 0.08, ease }} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
드론 비행의 모든 과정을 |
|
|
|
드론 비행의 모든 과정을 |
|
|
|
<br /> |
|
|
|
<br /> |
|
|
|
<em>하나의 플랫폼에서</em> |
|
|
|
<em>하나의 플랫폼에서</em> |
|
|
|
</motion.h2> |
|
|
|
</motion.h2> |
|
|
|
|
|
|
|
|
|
|
|
<motion.p |
|
|
|
<motion.p className="utm-hero__desc" initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.16, ease }}> |
|
|
|
className="utm-hero__desc" |
|
|
|
|
|
|
|
initial={{ opacity: 0, y: 16 }} |
|
|
|
|
|
|
|
animate={inView ? { opacity: 1, y: 0 } : {}} |
|
|
|
|
|
|
|
transition={{ duration: 0.6, delay: 0.16, ease }} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
비행 계획부터 승인, 실시간 관제, 데이터 관리까지 |
|
|
|
비행 계획부터 승인, 실시간 관제, 데이터 관리까지 |
|
|
|
<br />더 안전하고 효율적인 하늘길을 만듭니다. |
|
|
|
<br />더 안전하고 효율적인 하늘길을 만듭니다. |
|
|
|
</motion.p> |
|
|
|
</motion.p> |
|
|
|
|
|
|
|
|
|
|
|
<motion.ul |
|
|
|
<motion.ul className="utm-hero__chips" initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.24, ease }}> |
|
|
|
className="utm-hero__chips" |
|
|
|
|
|
|
|
initial={{ opacity: 0, y: 16 }} |
|
|
|
|
|
|
|
animate={inView ? { opacity: 1, y: 0 } : {}} |
|
|
|
|
|
|
|
transition={{ duration: 0.6, delay: 0.24, ease }} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
{FEATURES.map((f) => ( |
|
|
|
{FEATURES.map((f) => ( |
|
|
|
<li key={f.num} className="utm-hero__chip"> |
|
|
|
<li key={f.num} className="utm-hero__chip"> |
|
|
|
<span className="utm-hero__chip-num">{f.num}</span> |
|
|
|
<span className="utm-hero__chip-num">{f.num}</span> |
|
|
|
@ -317,23 +263,14 @@ function MainUtm() { |
|
|
|
))} |
|
|
|
))} |
|
|
|
</motion.ul> |
|
|
|
</motion.ul> |
|
|
|
|
|
|
|
|
|
|
|
<motion.div |
|
|
|
<motion.div className="utm-hero__showcase" initial={{ opacity: 0, y: 48 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.9, delay: 0.32, ease }}> |
|
|
|
className="utm-hero__showcase" |
|
|
|
|
|
|
|
initial={{ opacity: 0, y: 48 }} |
|
|
|
|
|
|
|
animate={inView ? { opacity: 1, y: 0 } : {}} |
|
|
|
|
|
|
|
transition={{ duration: 0.9, delay: 0.32, ease }} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
<div className="utm-hero__map-wrap"> |
|
|
|
<div className="utm-hero__map-wrap"> |
|
|
|
<img |
|
|
|
<img src={utmMapImg} alt="UTM 관제 지도" className="utm-hero__map-img" draggable="false" /> |
|
|
|
src={utmMapImg} |
|
|
|
|
|
|
|
alt="UTM 관제 지도" |
|
|
|
|
|
|
|
className="utm-hero__map-img" |
|
|
|
|
|
|
|
draggable="false" |
|
|
|
|
|
|
|
/> |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<UtmSystemPanel phase={phase} activeRow={activeRow} /> |
|
|
|
<UtmSystemPanel phase={phase} activeRow={activeRow} /> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* 커서 */} |
|
|
|
<div className="utm-cursor" ref={cursorRef}> |
|
|
|
<div className="utm-cursor" ref={cursorRef}> |
|
|
|
<div className="utm-cursor__dot" /> |
|
|
|
<div className="utm-cursor__dot" /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|