feat(admin): 지도 베이스 관리 패널, 레이어 패널 추가 및 보고서 기능 개선 #103
@ -396,6 +396,103 @@ router.post('/satellite/:sn/status', requireAuth, requirePermission('aerial', 'C
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// UP42 위성 패스 조회 (실시간 위성 목록 + 궤도)
|
||||
// ============================================================
|
||||
|
||||
/** 한국 주변 위성 패스 시뮬레이션 데이터 (UP42 API 연동 시 교체) */
|
||||
function generateKoreaSatellitePasses() {
|
||||
const now = new Date();
|
||||
const passes = [
|
||||
{
|
||||
id: 'pass-kmp3a-1', satellite: 'KOMPSAT-3A', provider: 'KARI', type: 'optical',
|
||||
resolution: '0.5m', color: '#a855f7',
|
||||
startTime: new Date(now.getTime() + 2 * 3600000).toISOString(),
|
||||
endTime: new Date(now.getTime() + 2 * 3600000 + 14 * 60000).toISOString(),
|
||||
maxElevation: 72, direction: 'descending',
|
||||
orbit: [
|
||||
{ lat: 42.0, lon: 126.5 }, { lat: 40.5, lon: 127.0 }, { lat: 39.0, lon: 127.4 },
|
||||
{ lat: 37.5, lon: 127.8 }, { lat: 36.0, lon: 128.1 }, { lat: 34.5, lon: 128.4 },
|
||||
{ lat: 33.0, lon: 128.6 }, { lat: 31.5, lon: 128.8 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pass-pneo-1', satellite: 'Pléiades Neo', provider: 'Airbus', type: 'optical',
|
||||
resolution: '0.3m', color: '#06b6d4',
|
||||
startTime: new Date(now.getTime() + 3.5 * 3600000).toISOString(),
|
||||
endTime: new Date(now.getTime() + 3.5 * 3600000 + 12 * 60000).toISOString(),
|
||||
maxElevation: 65, direction: 'ascending',
|
||||
orbit: [
|
||||
{ lat: 30.0, lon: 130.0 }, { lat: 31.5, lon: 129.2 }, { lat: 33.0, lon: 128.5 },
|
||||
{ lat: 34.5, lon: 127.8 }, { lat: 36.0, lon: 127.1 }, { lat: 37.5, lon: 126.4 },
|
||||
{ lat: 39.0, lon: 125.8 }, { lat: 40.5, lon: 125.2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pass-s1-1', satellite: 'Sentinel-1 SAR', provider: 'ESA', type: 'sar',
|
||||
resolution: '20m', color: '#f59e0b',
|
||||
startTime: new Date(now.getTime() + 5 * 3600000).toISOString(),
|
||||
endTime: new Date(now.getTime() + 5 * 3600000 + 18 * 60000).toISOString(),
|
||||
maxElevation: 58, direction: 'descending',
|
||||
orbit: [
|
||||
{ lat: 43.0, lon: 124.0 }, { lat: 41.0, lon: 125.0 }, { lat: 39.0, lon: 126.0 },
|
||||
{ lat: 37.0, lon: 126.8 }, { lat: 35.0, lon: 127.5 }, { lat: 33.0, lon: 128.0 },
|
||||
{ lat: 31.0, lon: 128.5 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pass-wv3-1', satellite: 'Maxar WorldView-3', provider: 'Maxar', type: 'optical',
|
||||
resolution: '0.31m', color: '#3b82f6',
|
||||
startTime: new Date(now.getTime() + 8 * 3600000).toISOString(),
|
||||
endTime: new Date(now.getTime() + 8 * 3600000 + 10 * 60000).toISOString(),
|
||||
maxElevation: 80, direction: 'descending',
|
||||
orbit: [
|
||||
{ lat: 41.0, lon: 129.5 }, { lat: 39.5, lon: 129.0 }, { lat: 38.0, lon: 128.5 },
|
||||
{ lat: 36.5, lon: 128.0 }, { lat: 35.0, lon: 127.5 }, { lat: 33.5, lon: 127.0 },
|
||||
{ lat: 32.0, lon: 126.5 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pass-skysat-1', satellite: 'SkySat', provider: 'Planet', type: 'optical',
|
||||
resolution: '0.5m', color: '#22c55e',
|
||||
startTime: new Date(now.getTime() + 12 * 3600000).toISOString(),
|
||||
endTime: new Date(now.getTime() + 12 * 3600000 + 8 * 60000).toISOString(),
|
||||
maxElevation: 55, direction: 'ascending',
|
||||
orbit: [
|
||||
{ lat: 31.0, lon: 127.0 }, { lat: 32.5, lon: 126.5 }, { lat: 34.0, lon: 126.0 },
|
||||
{ lat: 35.5, lon: 125.5 }, { lat: 37.0, lon: 125.0 }, { lat: 38.5, lon: 124.5 },
|
||||
{ lat: 40.0, lon: 124.0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pass-s2-1', satellite: 'Sentinel-2', provider: 'ESA', type: 'optical',
|
||||
resolution: '10m', color: '#ec4899',
|
||||
startTime: new Date(now.getTime() + 18 * 3600000).toISOString(),
|
||||
endTime: new Date(now.getTime() + 18 * 3600000 + 20 * 60000).toISOString(),
|
||||
maxElevation: 62, direction: 'descending',
|
||||
orbit: [
|
||||
{ lat: 42.0, lon: 128.0 }, { lat: 40.0, lon: 128.0 }, { lat: 38.0, lon: 128.0 },
|
||||
{ lat: 36.0, lon: 128.0 }, { lat: 34.0, lon: 128.0 }, { lat: 32.0, lon: 128.0 },
|
||||
],
|
||||
},
|
||||
];
|
||||
return passes;
|
||||
}
|
||||
|
||||
// GET /api/aerial/satellite/passes — 한국 주변 실시간 위성 패스 목록 (UP42 API 연동 준비)
|
||||
router.get('/satellite/passes', requireAuth, requirePermission('aerial', 'READ'), async (_req, res) => {
|
||||
try {
|
||||
// TODO: UP42 API 연동 시 아래 코드를 실제 API 호출로 교체
|
||||
// const token = await getUp42Token()
|
||||
// const passes = await fetchUp42Catalog(token, { bbox: [124, 33, 132, 39] })
|
||||
const passes = generateKoreaSatellitePasses();
|
||||
res.json({ passes, source: 'simulation', note: 'UP42 API 연동 시 실제 데이터로 교체 예정' });
|
||||
} catch (err) {
|
||||
console.error('[aerial] 위성 패스 조회 오류:', err);
|
||||
res.status(500).json({ error: '위성 패스 조회 실패' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// OIL INFERENCE 라우트
|
||||
// ============================================================
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useState, useRef, useEffect, useCallback } from 'react'
|
||||
import { Map, Source, Layer } from '@vis.gl/react-maplibre'
|
||||
import type { StyleSpecification } from 'maplibre-gl'
|
||||
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||
import { fetchSatellitePasses } from '../services/aerialApi'
|
||||
import type { SatellitePass } from '../services/aerialApi'
|
||||
|
||||
interface SatRequest {
|
||||
id: string
|
||||
@ -59,13 +64,23 @@ const up42Satellites = [
|
||||
{ id: 'cop-dem', name: 'Copernicus DEM', res: '10m', type: 'elevation' as const, color: '#64748b', cloud: 0 },
|
||||
]
|
||||
|
||||
const up42Passes = [
|
||||
{ sat: 'KOMPSAT-3A', time: '오늘 14:10–14:24', res: '0.5m', cloud: '≤10%', note: '최우선 추천', color: '#a855f7' },
|
||||
{ sat: 'Pléiades Neo', time: '오늘 14:38–14:52', res: '0.3m', cloud: '≤15%', note: '초고해상도', color: '#06b6d4' },
|
||||
{ sat: 'Sentinel-1 SAR', time: '오늘 16:55–17:08', res: '20m', cloud: '야간/우천 가능', note: 'SAR', color: '#f59e0b' },
|
||||
{ sat: 'KOMPSAT-3', time: '내일 09:12', res: '1.0m', cloud: '≤15%', note: '', color: '#a855f7' },
|
||||
{ sat: 'Maxar WV-3', time: '내일 13:20', res: '0.5m', cloud: '≤20%', note: '', color: '#3b82f6' },
|
||||
]
|
||||
// up42Passes — 실시간 패스로 대체됨 (satPasses from API)
|
||||
|
||||
const SAT_MAP_STYLE: StyleSpecification = {
|
||||
version: 8,
|
||||
sources: {
|
||||
'carto-dark': {
|
||||
type: 'raster',
|
||||
tiles: [
|
||||
'https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png',
|
||||
'https://b.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png',
|
||||
'https://c.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png',
|
||||
],
|
||||
tileSize: 256,
|
||||
},
|
||||
},
|
||||
layers: [{ id: 'carto-dark-layer', type: 'raster', source: 'carto-dark', minzoom: 0, maxzoom: 22 }],
|
||||
}
|
||||
|
||||
type SatModalPhase = 'none' | 'provider' | 'blacksky' | 'up42'
|
||||
|
||||
@ -77,10 +92,24 @@ export function SatelliteRequest() {
|
||||
// UP42 sub-tab
|
||||
const [up42SubTab, setUp42SubTab] = useState<'optical' | 'sar' | 'elevation'>('optical')
|
||||
const [up42SelSat, setUp42SelSat] = useState<string | null>(null)
|
||||
const [up42SelPass, setUp42SelPass] = useState<number | null>(null)
|
||||
const [up42SelPass, setUp42SelPass] = useState<string | null>(null)
|
||||
const [satPasses, setSatPasses] = useState<SatellitePass[]>([])
|
||||
const [satPassesLoading, setSatPassesLoading] = useState(false)
|
||||
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const loadSatPasses = useCallback(async () => {
|
||||
setSatPassesLoading(true)
|
||||
try {
|
||||
const passes = await fetchSatellitePasses()
|
||||
setSatPasses(passes)
|
||||
} catch {
|
||||
setSatPasses([])
|
||||
} finally {
|
||||
setSatPassesLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
||||
@ -91,6 +120,11 @@ export function SatelliteRequest() {
|
||||
return () => document.removeEventListener('mousedown', handler)
|
||||
}, [modalPhase])
|
||||
|
||||
// UP42 모달 열릴 때 위성 패스 로드
|
||||
useEffect(() => {
|
||||
if (modalPhase === 'up42') loadSatPasses()
|
||||
}, [modalPhase, loadSatPasses])
|
||||
|
||||
const allRequests = showMoreCompleted ? satRequests : satRequests.filter(r => r.status !== '완료' || r.id === 'SAT-003')
|
||||
|
||||
const filtered = allRequests.filter(r => {
|
||||
@ -672,83 +706,121 @@ export function SatelliteRequest() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 오른쪽: 지도 + AOI + 패스 */}
|
||||
{/* 오른쪽: 궤도 지도 + 패스 목록 */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden min-w-0">
|
||||
{/* 지도 영역 (placeholder) */}
|
||||
<div className="flex-1 relative" style={{ background: '#0a0e18' }}>
|
||||
{/* 검색바 */}
|
||||
<div className="absolute top-3 left-3 right-3 flex items-center gap-2 px-3 py-2 rounded-lg z-10 border border-[#21262d]" style={{ background: 'rgba(13,17,23,.9)', backdropFilter: 'blur(8px)' }}>
|
||||
<span className="text-[#8690a6] text-[13px]">🔍</span>
|
||||
<input type="text" placeholder="위치 또는 좌표 입력..." className="flex-1 bg-transparent border-none outline-none text-[11px] font-korean text-[#e2e8f0]" />
|
||||
</div>
|
||||
{/* 지도 영역 — 위성 궤도 표시 */}
|
||||
<div className="flex-1 relative">
|
||||
<Map
|
||||
initialViewState={{ longitude: 128, latitude: 36, zoom: 5.5 }}
|
||||
mapStyle={SAT_MAP_STYLE}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
attributionControl={false}
|
||||
>
|
||||
{/* 한국 영역 AOI 박스 */}
|
||||
<Source id="korea-aoi" type="geojson" data={{
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: { type: 'Polygon', coordinates: [[[124, 33], [132, 33], [132, 39], [124, 39], [124, 33]]] },
|
||||
}}>
|
||||
<Layer id="korea-aoi-fill" type="fill" paint={{ 'fill-color': '#3b82f6', 'fill-opacity': 0.05 }} />
|
||||
<Layer id="korea-aoi-line" type="line" paint={{ 'line-color': '#3b82f6', 'line-width': 1.5, 'line-dasharray': [4, 3] }} />
|
||||
</Source>
|
||||
|
||||
{/* 지도 placeholder */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl mb-2 opacity-20">🗺</div>
|
||||
<div className="text-[11px] font-korean opacity-40 text-[#64748b]">지도 영역 — AOI를 그려 위성 패스를 확인하세요</div>
|
||||
{/* 위성 궤도 라인 */}
|
||||
{satPasses.map(pass => (
|
||||
<Source key={pass.id} id={`orbit-${pass.id}`} type="geojson" data={{
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: { type: 'LineString', coordinates: pass.orbit.map(p => [p.lon, p.lat]) },
|
||||
}}>
|
||||
<Layer
|
||||
id={`orbit-line-${pass.id}`}
|
||||
type="line"
|
||||
paint={{
|
||||
'line-color': pass.color,
|
||||
'line-width': up42SelPass === pass.id ? 3 : 1.5,
|
||||
'line-opacity': up42SelPass === pass.id ? 1 : up42SelPass ? 0.2 : 0.6,
|
||||
'line-dasharray': pass.type === 'sar' ? [6, 4] : [1],
|
||||
}}
|
||||
/>
|
||||
{/* 궤도 위 위성 위치 (중간점) */}
|
||||
{(up42SelPass === pass.id || !up42SelPass) && (
|
||||
<Layer
|
||||
id={`orbit-point-${pass.id}`}
|
||||
type="circle"
|
||||
filter={['==', '$type', 'LineString']}
|
||||
paint={{}}
|
||||
/>
|
||||
)}
|
||||
</Source>
|
||||
))}
|
||||
</Map>
|
||||
|
||||
{/* 범례 오버레이 */}
|
||||
<div className="absolute top-3 left-3 px-3 py-2 rounded-lg z-10 border border-[#21262d]" style={{ background: 'rgba(13,17,23,.9)', backdropFilter: 'blur(8px)' }}>
|
||||
<div className="text-[9px] font-bold text-[#64748b] mb-1.5">🛰 위성 궤도</div>
|
||||
{satPasses.slice(0, 4).map(p => (
|
||||
<div key={p.id} className="flex items-center gap-1.5 mb-1">
|
||||
<div className="w-3 h-[2px] rounded-sm" style={{ background: p.color }} />
|
||||
<span className="text-[8px] text-[#94a3b8]">{p.satellite}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-1.5 mt-1.5 pt-1.5 border-t border-[#21262d]">
|
||||
<div className="w-3 h-3 rounded border border-[#3b82f6]" style={{ background: 'rgba(59,130,246,.1)' }} />
|
||||
<span className="text-[8px] text-[#64748b]">한국 영역 AOI</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AOI 도구 버튼 (오른쪽 사이드) */}
|
||||
<div className="absolute top-14 right-3 flex flex-col gap-1 p-1.5 rounded-lg z-10 border border-[#21262d]" style={{ background: 'rgba(13,17,23,.9)' }}>
|
||||
<div className="text-[7px] font-bold text-center mb-0.5 text-[#64748b]">ADD</div>
|
||||
{[
|
||||
{ icon: '⬜', title: '사각형 AOI' },
|
||||
{ icon: '🔷', title: '다각형 AOI' },
|
||||
{ icon: '⭕', title: '원형 AOI' },
|
||||
{ icon: '📁', title: '파일 업로드' },
|
||||
].map((t, i) => (
|
||||
<button key={i} className="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer border-none bg-[#161b22] text-[#8690a6]" title={t.title}>{t.icon}</button>
|
||||
))}
|
||||
<div className="h-px my-0.5 bg-[#21262d]" />
|
||||
<div className="text-[7px] font-bold text-center mb-0.5 text-[#64748b]">AOI</div>
|
||||
<button className="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer border-none bg-[#161b22] text-[#8690a6]" title="저장된 AOI">💾</button>
|
||||
<button className="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer border-none bg-[#161b22] text-[#ef4444]" title="AOI 삭제">🗑</button>
|
||||
</div>
|
||||
|
||||
{/* 줌 컨트롤 */}
|
||||
<div className="absolute bottom-3 right-3 flex flex-col rounded-md overflow-hidden z-10 border border-[#21262d]">
|
||||
<button className="w-7 h-7 flex items-center justify-center text-sm cursor-pointer border-none bg-[#161b22] text-[#8690a6]">+</button>
|
||||
<button className="w-7 h-7 flex items-center justify-center text-sm cursor-pointer border-none border-t border-t-[#21262d] bg-[#161b22] text-[#8690a6]">−</button>
|
||||
</div>
|
||||
|
||||
{/* 이 지역 검색 버튼 */}
|
||||
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 z-10">
|
||||
<button className="px-4 py-2 rounded-full text-[10px] font-semibold cursor-pointer font-korean text-white border-none" style={{ background: 'rgba(59,130,246,.9)', boxShadow: '0 2px 12px rgba(59,130,246,.3)' }}>🔍 이 지역 검색</button>
|
||||
</div>
|
||||
{/* 로딩 */}
|
||||
{satPassesLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center z-10" style={{ background: 'rgba(0,0,0,.5)' }}>
|
||||
<div className="text-[11px] text-[#60a5fa] font-korean animate-pulse">🛰 위성 패스 조회 중...</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 위성 패스 타임라인 */}
|
||||
<div className="border-t border-[#21262d] px-4 py-3 shrink-0" style={{ background: 'rgba(13,17,23,.95)' }}>
|
||||
<div className="text-[10px] font-bold font-korean mb-2 text-[#e2e8f0]">🛰 오늘 가용 위성 패스 — 선택된 AOI 통과 예정</div>
|
||||
<div className="border-t border-[#21262d] px-4 py-3 shrink-0 max-h-[200px] overflow-y-auto" style={{ background: 'rgba(13,17,23,.95)', scrollbarWidth: 'thin', scrollbarColor: '#21262d transparent' }}>
|
||||
<div className="text-[10px] font-bold font-korean mb-2 text-[#e2e8f0]">
|
||||
🛰 한국 주변 실시간 위성 패스 ({satPasses.length}개)
|
||||
<span className="text-[8px] text-[#64748b] font-normal ml-2">클릭하여 궤도 확인</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{up42Passes.map((p, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => setUp42SelPass(up42SelPass === i ? null : i)}
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer transition-colors"
|
||||
style={{
|
||||
background: up42SelPass === i ? 'rgba(59,130,246,.1)' : '#161b22',
|
||||
border: up42SelPass === i ? '1px solid rgba(59,130,246,.3)' : '1px solid #21262d',
|
||||
}}
|
||||
>
|
||||
<div className="w-1.5 h-5 rounded-full shrink-0" style={{ background: p.color }} />
|
||||
<div className="flex-1 flex items-center gap-3 min-w-0">
|
||||
<span className="text-[10px] font-bold font-korean min-w-[100px] text-[#e2e8f0]">{p.sat}</span>
|
||||
<span className="text-[9px] font-bold font-mono min-w-[110px] text-[#60a5fa]">{p.time}</span>
|
||||
<span className="text-[9px] font-mono text-cyan-500">{p.res}</span>
|
||||
<span className="text-[8px] font-mono text-[#64748b]">{p.cloud}</span>
|
||||
</div>
|
||||
{p.note && (
|
||||
{satPasses.map(pass => {
|
||||
const start = new Date(pass.startTime)
|
||||
const timeStr = `${start.getHours().toString().padStart(2, '0')}:${start.getMinutes().toString().padStart(2, '0')}`
|
||||
const diffH = Math.max(0, (start.getTime() - Date.now()) / 3600000)
|
||||
const urgency = diffH < 3 ? '긴급' : diffH < 8 ? '예정' : '내일'
|
||||
return (
|
||||
<div
|
||||
key={pass.id}
|
||||
onClick={() => setUp42SelPass(up42SelPass === pass.id ? null : pass.id)}
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer transition-colors"
|
||||
style={{
|
||||
background: up42SelPass === pass.id ? 'rgba(59,130,246,.1)' : '#161b22',
|
||||
border: up42SelPass === pass.id ? `1px solid ${pass.color}40` : '1px solid #21262d',
|
||||
}}
|
||||
>
|
||||
<div className="w-1.5 h-6 rounded-full shrink-0" style={{ background: pass.color }} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] font-bold font-korean text-[#e2e8f0]">{pass.satellite}</span>
|
||||
<span className="text-[8px] text-[#64748b]">{pass.provider}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-[9px] font-bold font-mono text-[#60a5fa]">{timeStr}</span>
|
||||
<span className="text-[9px] font-mono" style={{ color: pass.color }}>{pass.resolution}</span>
|
||||
<span className="text-[8px] font-mono text-[#64748b]">EL {pass.maxElevation}° · {pass.direction === 'ascending' ? '↗ 상승' : '↘ 하강'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="px-1.5 py-px rounded text-[8px] font-bold shrink-0" style={{
|
||||
background: p.note === '최우선 추천' ? 'rgba(34,197,94,.1)' : p.note === '초고해상도' ? 'rgba(6,182,212,.1)' : p.note === 'SAR' ? 'rgba(245,158,11,.1)' : 'rgba(99,102,241,.1)',
|
||||
color: p.note === '최우선 추천' ? '#22c55e' : p.note === '초고해상도' ? '#06b6d4' : p.note === 'SAR' ? '#f59e0b' : '#818cf8',
|
||||
}}>{p.note}</span>
|
||||
)}
|
||||
{up42SelPass === i && <span className="text-xs text-blue-500">✓</span>}
|
||||
</div>
|
||||
))}
|
||||
background: urgency === '긴급' ? 'rgba(239,68,68,.1)' : urgency === '예정' ? 'rgba(6,182,212,.1)' : 'rgba(100,116,139,.1)',
|
||||
color: urgency === '긴급' ? '#ef4444' : urgency === '예정' ? '#06b6d4' : '#64748b',
|
||||
}}>{urgency}</span>
|
||||
{up42SelPass === pass.id && <span className="text-xs text-blue-500">✓</span>}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -759,7 +831,7 @@ export function SatelliteRequest() {
|
||||
<div className="text-[9px] font-korean text-[#64748b]">원하는 위성을 찾지 못했나요? <span className="text-[#60a5fa] cursor-pointer">태스킹 주문 생성</span> 또는 <span className="text-[#60a5fa] cursor-pointer">자세히 보기 ↗</span></div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-korean mr-1.5 text-[#8690a6]">
|
||||
선택: {up42SelSat ? up42Satellites.find(s => s.id === up42SelSat)?.name : '없음'}
|
||||
선택: {up42SelPass ? satPasses.find(p => p.id === up42SelPass)?.satellite : up42SelSat ? up42Satellites.find(s => s.id === up42SelSat)?.name : '없음'}
|
||||
</span>
|
||||
<button onClick={() => setModalPhase('provider')} className="px-4 py-2 rounded-lg border border-[#21262d] text-[11px] font-semibold cursor-pointer font-korean text-[#94a3b8] bg-[#161b22]">← 뒤로</button>
|
||||
<button
|
||||
|
||||
@ -132,3 +132,26 @@ export async function stopDroneStreamApi(id: string): Promise<{ success: boolean
|
||||
const response = await api.post<{ success: boolean }>(`/aerial/drone/streams/${id}/stop`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UP42 위성 패스 조회
|
||||
// ============================================================
|
||||
|
||||
export interface SatellitePass {
|
||||
id: string;
|
||||
satellite: string;
|
||||
provider: string;
|
||||
type: 'optical' | 'sar' | 'elevation';
|
||||
resolution: string;
|
||||
color: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxElevation: number;
|
||||
direction: 'ascending' | 'descending';
|
||||
orbit: Array<{ lat: number; lon: number }>;
|
||||
}
|
||||
|
||||
export async function fetchSatellitePasses(): Promise<SatellitePass[]> {
|
||||
const response = await api.get<{ passes: SatellitePass[] }>('/aerial/satellite/passes');
|
||||
return response.data.passes;
|
||||
}
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user