wing-ops/frontend/src/tabs/aerial/components/SatelliteRequest.tsx
htlee b00bb56af3 refactor(css): Phase 3 인라인 스타일 → Tailwind 대규모 변환 (486건)
대형 파일 집중 변환:
- SatelliteRequest: 134→66 (hex 색상 일괄 변환)
- IncidentsView: 141→90, MediaModal: 97→38
- HNSScenarioView: 78→38, HNSView: 49→31
- LoginPage, MapView, PredictionInputSection 등 중소 파일 8개

변환 패턴: hex 색상→text-[#hex], CSS 변수→Tailwind 유틸리티,
flex/grid/padding/fontSize/fontWeight/overflow 등 정적 속성 className 이동

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:06:15 +09:00

786 lines
54 KiB
TypeScript
Raw Blame 히스토리

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useRef, useEffect } from 'react'
interface SatRequest {
id: string
zone: string
zoneCoord: string
zoneArea: string
satellite: string
requestDate: string
expectedReceive: string
resolution: string
status: '촬영중' | '대기' | '완료'
provider?: string
purpose?: string
requester?: string
}
const satRequests: SatRequest[] = [
{ id: 'SAT-004', zone: '제주 서귀포 해상 (유출 해역 중심)', zoneCoord: '33.24°N 126.50°E', zoneArea: '15km²', satellite: 'KOMPSAT-3A', requestDate: '02-20 08:14', expectedReceive: '02-20 14:30', resolution: '0.5m', status: '촬영중', provider: 'KARI', purpose: '유출유 확산 모니터링', requester: '방제과 김해양' },
{ id: 'SAT-005', zone: '가파도 북쪽 해안선', zoneCoord: '33.17°N 126.27°E', zoneArea: '8km²', satellite: 'KOMPSAT-3', requestDate: '02-20 09:02', expectedReceive: '02-21 09:00', resolution: '1.0m', status: '대기', provider: 'KARI', purpose: '해안선 오염 확인', requester: '방제과 이민수' },
{ id: 'SAT-006', zone: '마라도 주변 해역', zoneCoord: '33.11°N 126.27°E', zoneArea: '12km²', satellite: 'Sentinel-2', requestDate: '02-20 09:30', expectedReceive: '02-21 11:00', resolution: '10m', status: '대기', provider: 'ESA Copernicus', purpose: '수질 분석용 다분광 촬영', requester: '환경분석팀 박수진' },
{ id: 'SAT-007', zone: '대정읍 해안 오염 확산 구역', zoneCoord: '33.21°N 126.10°E', zoneArea: '20km²', satellite: 'KOMPSAT-3A', requestDate: '02-20 10:05', expectedReceive: '02-22 08:00', resolution: '0.5m', status: '대기', provider: 'KARI', purpose: '확산 예측 모델 검증', requester: '방제과 김해양' },
{ id: 'SAT-003', zone: '제주 남방 100해리 해상', zoneCoord: '33.00°N 126.50°E', zoneArea: '25km²', satellite: 'Sentinel-1', requestDate: '02-19 14:00', expectedReceive: '02-19 23:00', resolution: '20m', status: '완료', provider: 'ESA Copernicus', purpose: 'SAR 유막 탐지', requester: '환경분석팀 박수진' },
{ id: 'SAT-002', zone: '여수 오동도 인근 해역', zoneCoord: '34.73°N 127.68°E', zoneArea: '18km²', satellite: 'KOMPSAT-3A', requestDate: '02-18 11:30', expectedReceive: '02-18 17:45', resolution: '0.5m', status: '완료', provider: 'KARI', purpose: '유출 초기 범위 확인', requester: '방제과 김해양' },
{ id: 'SAT-001', zone: '통영 해역 남측', zoneCoord: '34.85°N 128.43°E', zoneArea: '30km²', satellite: 'Sentinel-1', requestDate: '02-17 09:00', expectedReceive: '02-17 21:00', resolution: '20m', status: '완료', provider: 'ESA Copernicus', purpose: '야간 SAR 유막 모니터링', requester: '환경분석팀 박수진' },
]
const satellites = [
{ name: 'KOMPSAT-3A', desc: '해상도 0.5m · 광학 / IR · 촬영 가능', status: '가용', statusColor: 'var(--green)', borderColor: 'rgba(34,197,94,.2)', pulse: true },
{ name: 'KOMPSAT-3', desc: '해상도 1.0m · 광학 · 임무 중', status: '임무중', statusColor: 'var(--yellow)', borderColor: 'rgba(234,179,8,.2)', pulse: true },
{ name: 'Sentinel-1 (ESA)', desc: '해상도 20m · SAR · 야간/우천 가능', status: '가용', statusColor: 'var(--green)', borderColor: 'var(--bd)', pulse: false },
{ name: 'Sentinel-2 (ESA)', desc: '해상도 10m · 다분광 · 수질 분석 적합', status: '가용', statusColor: 'var(--green)', borderColor: 'var(--bd)', pulse: false },
]
const passSchedules = [
{ time: '14:10 14:24', desc: 'KOMPSAT-3A 패스 (제주 남방)', today: true },
{ time: '16:55 17:08', desc: 'Sentinel-1 패스 (제주 전역)', today: true },
{ time: '내일 09:12', desc: 'KOMPSAT-3 패스 (가파도~마라도)', today: false },
{ time: '내일 10:40', desc: 'Sentinel-2 패스 (제주 서측)', today: false },
]
// UP42 위성 카탈로그 데이터
const up42Satellites = [
{ id: 'mwl-hd15', name: 'Maxar WorldView Legion HD15', res: '0.3m', type: 'optical' as const, color: '#3b82f6', cloud: 15 },
{ id: 'pneo-hd15', name: 'Pléiades Neo HD15', res: '0.3m', type: 'optical' as const, color: '#06b6d4', cloud: 10 },
{ id: 'mwl', name: 'Maxar WorldView Legion', res: '0.5m', type: 'optical' as const, color: '#3b82f6', cloud: 20 },
{ id: 'mwv3', name: 'Maxar WorldView-3', res: '0.5m', type: 'optical' as const, color: '#3b82f6', cloud: 20 },
{ id: 'pneo', name: 'Pléiades Neo', res: '0.5m', type: 'optical' as const, color: '#06b6d4', cloud: 15 },
{ id: 'bj3n', name: 'Beijing-3N', res: '0.5m', type: 'optical' as const, color: '#f97316', cloud: 20, delay: true },
{ id: 'skysat', name: 'SkySat', res: '0.7m', type: 'optical' as const, color: '#22c55e', cloud: 15 },
{ id: 'kmp3a', name: 'KOMPSAT-3A', res: '0.5m', type: 'optical' as const, color: '#a855f7', cloud: 10 },
{ id: 'kmp3', name: 'KOMPSAT-3', res: '1.0m', type: 'optical' as const, color: '#a855f7', cloud: 15 },
{ id: 'spot7', name: 'SPOT 7', res: '1.5m', type: 'optical' as const, color: '#eab308', cloud: 20 },
{ id: 's2', name: 'Sentinel-2', res: '10m', type: 'optical' as const, color: '#ec4899', cloud: 20 },
{ id: 's1', name: 'Sentinel-1 SAR', res: '20m', type: 'sar' as const, color: '#f59e0b', cloud: 0 },
{ id: 'alos2', name: 'ALOS-2 PALSAR-2', res: '3m', type: 'sar' as const, color: '#f59e0b', cloud: 0 },
{ id: 'rcm', name: 'RCM (Radarsat)', res: '3m', type: 'sar' as const, color: '#f59e0b', cloud: 0 },
{ id: 'srtm', name: 'SRTM DEM', res: '30m', type: 'elevation' as const, color: '#64748b', cloud: 0 },
{ id: 'cop-dem', name: 'Copernicus DEM', res: '10m', type: 'elevation' as const, color: '#64748b', cloud: 0 },
]
const up42Passes = [
{ sat: 'KOMPSAT-3A', time: '오늘 14:1014:24', res: '0.5m', cloud: '≤10%', note: '최우선 추천', color: '#a855f7' },
{ sat: 'Pléiades Neo', time: '오늘 14:3814:52', res: '0.3m', cloud: '≤15%', note: '초고해상도', color: '#06b6d4' },
{ sat: 'Sentinel-1 SAR', time: '오늘 16:5517: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' },
]
type SatModalPhase = 'none' | 'provider' | 'blacksky' | 'up42'
export function SatelliteRequest() {
const [statusFilter, setStatusFilter] = useState('전체')
const [modalPhase, setModalPhase] = useState<SatModalPhase>('none')
const [selectedRequest, setSelectedRequest] = useState<SatRequest | null>(null)
const [showMoreCompleted, setShowMoreCompleted] = useState(false)
// 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 modalRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const handler = (e: MouseEvent) => {
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
setModalPhase('none')
}
}
if (modalPhase !== 'none') document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [modalPhase])
const allRequests = showMoreCompleted ? satRequests : satRequests.filter(r => r.status !== '완료' || r.id === 'SAT-003')
const filtered = allRequests.filter(r => {
if (statusFilter === '전체') return true
if (statusFilter === '대기') return r.status === '대기'
if (statusFilter === '진행') return r.status === '촬영중'
if (statusFilter === '완료') return r.status === '완료'
return true
})
const statusBadge = (s: SatRequest['status']) => {
if (s === '촬영중') return (
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-semibold font-korean" style={{ background: 'rgba(234,179,8,.15)', border: '1px solid rgba(234,179,8,.3)', color: 'var(--yellow)' }}>
<span className="w-[5px] h-[5px] rounded-full inline-block animate-pulse" style={{ background: 'var(--yellow)' }} />
</span>
)
if (s === '대기') return (
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-semibold font-korean" style={{ background: 'rgba(59,130,246,.15)', border: '1px solid rgba(59,130,246,.3)', color: 'var(--blue)' }}> </span>
)
return (
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-semibold font-korean" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)', color: 'var(--green)' }}> </span>
)
}
const stats = [
{ value: '3', label: '요청 대기', color: 'var(--blue)' },
{ value: '1', label: '촬영 진행 중', color: 'var(--yellow)' },
{ value: '7', label: '수신 완료', color: 'var(--green)' },
{ value: '0.5m', label: '최고 해상도', color: 'var(--cyan)' },
]
const filters = ['전체', '대기', '진행', '완료']
const up42Filtered = up42Satellites.filter(s => s.type === up42SubTab)
// ── 섹션 헤더 헬퍼 (BlackSky 폼) ──
const sectionHeader = (num: number, label: string) => (
<div className="text-[11px] font-bold font-korean mb-2.5 flex items-center gap-1.5 text-[#818cf8]">
<div className="w-[18px] h-[18px] rounded-[5px] flex items-center justify-center text-[9px] font-bold text-[#818cf8]" style={{ background: 'rgba(99,102,241,.12)' }}>{num}</div>
{label}
</div>
)
const bsInput = "w-full px-3 py-2 rounded-md text-[11px] font-korean outline-none box-border"
const bsInputStyle = { border: '1px solid #21262d', background: '#161b22', color: '#e2e8f0' }
return (
<div className="overflow-y-auto py-5 px-6">
{/* 헤더 */}
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-[10px] flex items-center justify-center text-xl" style={{ background: 'linear-gradient(135deg,rgba(59,130,246,.2),rgba(168,85,247,.2))', border: '1px solid rgba(59,130,246,.3)' }}>🛰</div>
<div>
<div className="text-base font-bold font-korean text-text-1"> </div>
<div className="text-[11px] text-text-3 font-korean mt-0.5"> </div>
</div>
</div>
<button onClick={() => setModalPhase('provider')} className="px-4 py-2.5 text-white border-none rounded-sm text-[13px] font-semibold cursor-pointer font-korean flex items-center gap-1.5" style={{ background: 'linear-gradient(135deg,var(--blue),var(--purple))' }}>🛰 </button>
</div>
{/* 요약 통계 */}
<div className="grid grid-cols-4 gap-3 mb-5">
{stats.map((s, i) => (
<div key={i} className="bg-bg-2 border border-border rounded-md p-3.5 text-center">
<div className="text-[22px] font-bold font-mono" style={{ color: s.color }}>{s.value}</div>
<div className="text-[10px] text-text-3 mt-1 font-korean">{s.label}</div>
</div>
))}
</div>
{/* 요청 목록 */}
<div className="bg-bg-2 border border-border rounded-md overflow-hidden mb-5">
<div className="flex items-center justify-between px-4 py-3.5 border-b border-border">
<div className="text-[13px] font-bold font-korean text-text-1">📋 </div>
<div className="flex gap-1.5">
{filters.map(f => (
<button
key={f}
onClick={() => setStatusFilter(f)}
className="px-2.5 py-1 rounded text-[10px] font-semibold cursor-pointer font-korean border"
style={statusFilter === f
? { background: 'rgba(59,130,246,.15)', color: 'var(--blue)', borderColor: 'rgba(59,130,246,.3)' }
: { background: 'var(--bg3)', color: 'var(--t2)', borderColor: 'var(--bd)' }
}
>{f}</button>
))}
</div>
</div>
{/* 헤더 행 */}
<div className="grid gap-0 px-4 py-2 bg-bg-3 border-b border-border" style={{ gridTemplateColumns: '60px 1fr 100px 100px 120px 80px 90px' }}>
{['번호', '촬영 구역', '위성', '요청일시', '예상수신', '해상도', '상태'].map(h => (
<div key={h} className="text-[9px] font-bold text-text-3 font-korean uppercase tracking-wider">{h}</div>
))}
</div>
{/* 데이터 행 */}
{filtered.map(r => (
<div key={r.id}>
<div
onClick={() => setSelectedRequest(selectedRequest?.id === r.id ? null : r)}
className="grid gap-0 px-4 py-3 border-b items-center cursor-pointer hover:bg-bg-hover/30 transition-colors"
style={{
gridTemplateColumns: '60px 1fr 100px 100px 120px 80px 90px',
borderColor: 'rgba(255,255,255,.04)',
background: selectedRequest?.id === r.id ? 'rgba(99,102,241,.06)' : r.status === '촬영중' ? 'rgba(234,179,8,.03)' : 'transparent',
opacity: r.status === '완료' ? 0.7 : 1,
}}
>
<div className="text-[11px] font-mono text-text-2">{r.id}</div>
<div>
<div className="text-xs font-semibold text-text-1 font-korean">{r.zone}</div>
<div className="text-[10px] text-text-3 font-mono mt-0.5">{r.zoneCoord} · {r.zoneArea}</div>
</div>
<div className="text-[11px] font-semibold text-text-1 font-korean">{r.satellite}</div>
<div className="text-[10px] text-text-2 font-mono">{r.requestDate}</div>
<div className="text-[10px] font-semibold font-mono" style={{ color: r.status === '촬영중' ? 'var(--yellow)' : 'var(--t2)' }}>{r.expectedReceive}</div>
<div className="text-[11px] font-bold font-mono" style={{ color: r.status === '완료' ? 'var(--t3)' : 'var(--cyan)' }}>{r.resolution}</div>
<div>{statusBadge(r.status)}</div>
</div>
{/* 상세 정보 패널 */}
{selectedRequest?.id === r.id && (
<div className="px-4 py-3 border-b" style={{ borderColor: 'rgba(255,255,255,.04)', background: 'rgba(99,102,241,.03)' }}>
<div className="grid grid-cols-4 gap-3 mb-2">
{[
['제공자', r.provider || '-'],
['요청 목적', r.purpose || '-'],
['요청자', r.requester || '-'],
['촬영 면적', r.zoneArea],
].map(([k, v], i) => (
<div key={i} className="px-2.5 py-2 bg-bg-0 rounded">
<div className="text-[8px] font-bold text-text-3 font-korean mb-1 uppercase">{k}</div>
<div className="text-[10px] font-semibold text-text-1 font-korean">{v}</div>
</div>
))}
</div>
<div className="flex gap-2">
<button className="px-3 py-1.5 text-[10px] font-semibold font-korean rounded border cursor-pointer hover:bg-bg-hover transition-colors" style={{ background: 'rgba(6,182,212,.08)', borderColor: 'rgba(6,182,212,.2)', color: 'var(--cyan)' }}>📍 </button>
{r.status === '완료' && (
<button className="px-3 py-1.5 text-[10px] font-semibold font-korean rounded border cursor-pointer hover:bg-bg-hover transition-colors" style={{ background: 'rgba(34,197,94,.08)', borderColor: 'rgba(34,197,94,.2)', color: 'var(--green)' }}>📥 </button>
)}
{r.status === '대기' && (
<button className="px-3 py-1.5 text-[10px] font-semibold font-korean rounded border cursor-pointer hover:bg-bg-hover transition-colors" style={{ background: 'rgba(239,68,68,.08)', borderColor: 'rgba(239,68,68,.2)', color: 'var(--red)' }}> </button>
)}
</div>
</div>
)}
</div>
))}
<div
onClick={() => setShowMoreCompleted(!showMoreCompleted)}
className="text-center py-2.5 text-[10px] text-text-3 font-korean cursor-pointer hover:text-text-2 transition-colors"
>
{showMoreCompleted ? '▲ 완료 목록 접기' : '▼ 이전 완료 목록 더보기 (6건)'}
</div>
</div>
{/* 위성 궤도 정보 */}
<div className="grid grid-cols-2 gap-3.5">
{/* 가용 위성 현황 */}
<div className="bg-bg-2 border border-border rounded-md p-4">
<div className="text-xs font-bold text-text-1 font-korean mb-3">🛰 </div>
<div className="flex flex-col gap-2">
{satellites.map((sat, i) => (
<div key={i} className="flex items-center gap-2.5 px-3 py-2 bg-bg-3 rounded-md" style={{ border: `1px solid ${sat.borderColor}` }}>
<div className={`w-2 h-2 rounded-full shrink-0 ${sat.pulse ? 'animate-pulse' : ''}`} style={{ background: sat.statusColor }} />
<div className="flex-1">
<div className="text-[11px] font-semibold text-text-1 font-korean">{sat.name}</div>
<div className="text-[9px] text-text-3 font-korean">{sat.desc}</div>
</div>
<div className="text-[10px] font-bold font-korean" style={{ color: sat.statusColor }}>{sat.status}</div>
</div>
))}
</div>
</div>
{/* 오늘 촬영 가능 시간 */}
<div className="bg-bg-2 border border-border rounded-md p-4">
<div className="text-xs font-bold text-text-1 font-korean mb-3"> (KST)</div>
<div className="flex flex-col gap-1.5">
{passSchedules.map((ps, i) => (
<div
key={i}
className="flex items-center gap-2 px-2.5 py-[7px] rounded-[5px]"
style={{
background: ps.today ? 'rgba(34,197,94,.05)' : 'rgba(59,130,246,.05)',
border: ps.today ? '1px solid rgba(34,197,94,.15)' : '1px solid rgba(59,130,246,.15)',
}}
>
<span className="text-[10px] font-bold font-mono min-w-[90px]" style={{ color: ps.today ? 'var(--cyan)' : 'var(--blue)' }}>{ps.time}</span>
<span className="text-[10px] text-text-1 font-korean">{ps.desc}</span>
</div>
))}
</div>
</div>
</div>
{/* ═══ 모달: 제공자 선택 ═══ */}
{modalPhase !== 'none' && (
<div className="fixed inset-0 z-[9999] flex items-center justify-center" style={{ background: 'rgba(5,8,18,.75)', backdropFilter: 'blur(8px)' }}>
<div ref={modalRef}>
{/* ── 제공자 선택 ── */}
{modalPhase === 'provider' && (
<div className="border rounded-2xl w-[640px] overflow-hidden" style={{ background: 'var(--bg1)', borderColor: 'rgba(99,102,241,.3)', boxShadow: '0 24px 80px rgba(0,0,0,.6)' }}>
{/* 헤더 */}
<div className="px-7 pt-6 pb-4 relative overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-0.5" style={{ background: 'linear-gradient(90deg,#6366f1,#3b82f6,#06b6d4)' }} />
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl flex items-center justify-center text-xl" style={{ background: 'linear-gradient(135deg,rgba(99,102,241,.15),rgba(59,130,246,.08))' }}>🛰</div>
<div>
<div className="text-base font-bold text-text-1 font-korean"> </div>
<div className="text-[10px] text-text-3 font-korean mt-0.5"> </div>
</div>
</div>
<button onClick={() => setModalPhase('none')} className="text-lg cursor-pointer text-text-3 p-1 bg-transparent border-none hover:text-text-1 transition-colors"></button>
</div>
</div>
{/* 제공자 카드 */}
<div className="px-7 pb-6 flex flex-col gap-3.5">
{/* BlackSky (Maxar) */}
<div
onClick={() => setModalPhase('blacksky')}
className="cursor-pointer bg-bg-2 border border-border rounded-xl p-5 relative overflow-hidden hover:border-[rgba(99,102,241,.5)] hover:bg-[rgba(99,102,241,.04)] transition-all"
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2.5">
<div className="w-[42px] h-[42px] rounded-[10px] flex items-center justify-center border border-[rgba(99,102,241,.3)]" style={{ background: 'linear-gradient(135deg,#1a1a2e,#16213e)' }}>
<span className="text-[11px] font-extrabold font-mono text-[#818cf8] tracking-[-0.5px]">B<span className="text-[#a78bfa]">Sky</span></span>
</div>
<div>
<div className="text-sm font-bold text-text-1 font-korean">BlackSky</div>
<div className="text-[9px] text-text-3 font-korean mt-px">Maxar Electro-Optical API</div>
</div>
</div>
<div className="flex items-center gap-1.5">
<span className="px-2 py-0.5 rounded-[10px] text-[8px] font-semibold text-green-500" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)' }}>API </span>
<span className="text-base text-text-3"></span>
</div>
</div>
<div className="grid grid-cols-4 gap-2 mb-2.5">
{[
['유형', 'EO (광학)', '#818cf8'],
['해상도', '~1m', 'var(--t1)'],
['재방문', '≤1시간', 'var(--t1)'],
['납기', '90분 이내', '#22c55e'],
].map(([k, v, c], i) => (
<div key={i} className="text-center p-1.5 bg-bg-0 rounded-md">
<div className="text-[7px] text-text-3 font-korean mb-0.5">{k}</div>
<div className="text-[10px] font-bold font-mono" style={{ color: c }}>{v}</div>
</div>
))}
</div>
<div className="text-[9px] text-text-3 font-korean leading-relaxed"> . . Dawn-to-Dusk .</div>
<div className="mt-2 text-[8px] text-text-3 font-mono">API: <span className="text-[#818cf8]">eapi.maxar.com/e1so/rapidoc</span></div>
</div>
{/* UP42 (EO + SAR) */}
<div
onClick={() => setModalPhase('up42')}
className="cursor-pointer bg-bg-2 border border-border rounded-xl p-5 relative overflow-hidden hover:border-[rgba(59,130,246,.5)] hover:bg-[rgba(59,130,246,.04)] transition-all"
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2.5">
<div className="w-[42px] h-[42px] rounded-[10px] flex items-center justify-center border border-[rgba(59,130,246,.3)]" style={{ background: 'linear-gradient(135deg,#0a1628,#162a50)' }}>
<span className="text-[13px] font-extrabold font-mono text-[#60a5fa] tracking-[-0.5px]">up<sup className="text-[7px] align-super">42</sup></span>
</div>
<div>
<div className="text-sm font-bold text-text-1 font-korean">UP42 EO + SAR</div>
<div className="text-[9px] text-text-3 font-korean mt-px">Optical · SAR · Elevation </div>
</div>
</div>
<div className="flex items-center gap-1.5">
<span className="px-2 py-0.5 rounded-[10px] text-[8px] font-semibold text-green-500" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)' }}>API </span>
<span className="text-base text-text-3"></span>
</div>
</div>
<div className="grid grid-cols-4 gap-2 mb-2.5">
{[
['유형', 'EO + SAR', '#60a5fa'],
['해상도', '0.3~5m', 'var(--t1)'],
['위성 수', '16+ 컬렉션', 'var(--t1)'],
['야간/악천후', 'SAR 가능', '#22c55e'],
].map(([k, v, c], i) => (
<div key={i} className="text-center p-1.5 bg-bg-0 rounded-md">
<div className="text-[7px] text-text-3 font-korean mb-0.5">{k}</div>
<div className="text-[10px] font-bold font-mono" style={{ color: c }}>{v}</div>
</div>
))}
</div>
<div className="flex gap-1.5 mb-2.5 flex-wrap">
{['Pléiades Neo', 'SPOT 6/7'].map((t, i) => (
<span key={i} className="px-1.5 py-px rounded text-[8px] font-korean text-[#60a5fa]" style={{ background: 'rgba(59,130,246,.08)', border: '1px solid rgba(59,130,246,.15)' }}>{t}</span>
))}
{['TerraSAR-X', 'Capella SAR', 'ICEYE'].map((t, i) => (
<span key={i} className="px-1.5 py-px rounded text-[8px] font-korean" style={{ background: 'rgba(6,182,212,.08)', border: '1px solid rgba(6,182,212,.15)', color: 'var(--cyan)' }}>{t}</span>
))}
<span className="px-1.5 py-px rounded text-[8px] font-korean" style={{ background: 'rgba(139,148,158,.08)', border: '1px solid rgba(139,148,158,.15)', color: 'var(--t3)' }}>+11 more</span>
</div>
<div className="text-[9px] text-text-3 font-korean leading-relaxed">(EO) + (SAR) . · SAR . .</div>
<div className="mt-2 text-[8px] text-text-3 font-mono">API: <span className="text-[#60a5fa]">up42.com</span></div>
</div>
</div>
{/* 하단 */}
<div className="px-7 pb-5 flex items-center justify-between">
<div className="text-[9px] text-text-3 font-korean leading-relaxed">💡 촬영: BlackSky (90 ) · /악천후: UP42 SAR </div>
<button onClick={() => setModalPhase('none')} className="px-4 py-2 rounded-lg border text-[11px] font-semibold cursor-pointer font-korean bg-bg-3 text-text-2 border-border"></button>
</div>
</div>
)}
{/* ── BlackSky 긴급 촬영 요청 ── */}
{modalPhase === 'blacksky' && (
<div className="border rounded-[14px] w-[860px] max-h-[90vh] flex flex-col overflow-hidden border-[rgba(99,102,241,.3)]" style={{ background: '#0d1117', boxShadow: '0 24px 80px rgba(0,0,0,.7)' }}>
{/* 헤더 */}
<div className="px-6 py-4 border-b border-[#21262d] flex items-center justify-between shrink-0 relative">
<div className="absolute top-0 left-0 right-0 h-0.5" style={{ background: 'linear-gradient(90deg,#6366f1,#818cf8,#a78bfa)' }} />
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-[rgba(99,102,241,.3)]" style={{ background: 'linear-gradient(135deg,#1a1a2e,#16213e)' }}>
<span className="text-[10px] font-extrabold font-mono text-[#818cf8]">B<span className="text-[#a78bfa]">Sky</span></span>
</div>
<div>
<div className="text-[15px] font-bold font-korean text-[#e2e8f0]">BlackSky </div>
<div className="text-[9px] font-korean mt-0.5 text-[#64748b]">Maxar E1SO RapiDoc API · </div>
</div>
</div>
<div className="flex items-center gap-2">
<span className="px-3 py-1 rounded-md text-[9px] font-semibold font-korean text-[#818cf8]" style={{ background: 'rgba(99,102,241,.1)', border: '1px solid rgba(99,102,241,.25)' }}>API Docs </span>
<button onClick={() => setModalPhase('none')} className="text-lg cursor-pointer p-1 bg-transparent border-none text-[#64748b]"></button>
</div>
</div>
{/* 본문 */}
<div className="flex-1 overflow-y-auto px-6 py-5 flex flex-col gap-4" style={{ scrollbarWidth: 'thin', scrollbarColor: '#21262d transparent' }}>
{/* API 상태 */}
<div className="flex items-center gap-2.5 px-3.5 py-2.5 rounded-lg" style={{ background: 'rgba(34,197,94,.06)', border: '1px solid rgba(34,197,94,.15)' }}>
<div className="w-2 h-2 rounded-full" style={{ background: '#22c55e', boxShadow: '0 0 6px rgba(34,197,94,.5)' }} />
<span className="text-[10px] font-semibold font-korean text-green-500">API Connected</span>
<span className="text-[9px] font-mono text-[#64748b]">eapi.maxar.com/e1so/rapidoc · Latency: 142ms</span>
<span className="ml-auto text-[8px] font-mono text-[#64748b]">Quota: 47/50 </span>
</div>
{/* ① 태스킹 유형 */}
<div>
{sectionHeader(1, '태스킹 유형 · 우선순위')}
<div className="grid grid-cols-3 gap-2.5">
<div>
<label className="block text-[9px] font-korean mb-1 text-[#64748b]"> <span className="text-red-400">*</span></label>
<select className={bsInput} style={bsInputStyle}>
<option> (Emergency)</option>
<option> (Standard)</option>
<option> (Archive)</option>
</select>
</div>
<div>
<label className="block text-[9px] font-korean mb-1 text-[#64748b]"> <span className="text-red-400">*</span></label>
<select className={bsInput} style={bsInputStyle}>
<option>P1 (90 )</option>
<option>P2 (6 )</option>
<option>P3 (24 )</option>
</select>
</div>
<div>
<label className="block text-[9px] font-korean mb-1 text-[#64748b]"> </label>
<select className={bsInput} style={bsInputStyle}>
<option>Single Collect</option>
<option>Multi-pass Monitoring</option>
<option>Continuous ( )</option>
</select>
</div>
</div>
</div>
{/* ② AOI 지정 */}
<div>
{sectionHeader(2, '관심 영역 (AOI)')}
<div className="grid grid-cols-3 gap-2.5 items-end">
<div>
<label className="block text-[9px] font-korean mb-1 text-[#64748b]"> <span className="text-red-400">*</span></label>
<input type="text" defaultValue="34.5832" className={bsInput} style={bsInputStyle} />
</div>
<div>
<label className="block text-[9px] font-korean mb-1 text-[#64748b]"> <span className="text-red-400">*</span></label>
<input type="text" defaultValue="128.4217" className={bsInput} style={bsInputStyle} />
</div>
<button className="px-3.5 py-2 rounded-md text-[10px] font-semibold cursor-pointer font-korean whitespace-nowrap text-[#818cf8]" style={{ border: '1px solid rgba(99,102,241,.3)', background: 'rgba(99,102,241,.08)' }}>📍 AOI </button>
</div>
<div className="mt-2 grid grid-cols-3 gap-2.5">
<div>
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">AOI (km)</label>
<input type="number" defaultValue={10} step={1} min={1} className={bsInput} style={bsInputStyle} />
</div>
<div>
<label className="block text-[9px] font-korean mb-1 text-[#64748b]"> (%)</label>
<input type="number" defaultValue={20} step={5} min={0} max={100} className={bsInput} style={bsInputStyle} />
</div>
<div>
<label className="block text-[9px] font-korean mb-1 text-[#64748b]"> Off-nadir (°)</label>
<input type="number" defaultValue={25} step={5} min={0} max={45} className={bsInput} style={bsInputStyle} />
</div>
</div>
</div>
{/* ③ 촬영 기간 */}
<div>
{sectionHeader(3, '촬영 기간 · 반복')}
<div className="grid grid-cols-3 gap-2.5">
<div>
<label className="block text-[9px] font-korean mb-1 text-[#64748b]"> <span className="text-red-400">*</span></label>
<input type="datetime-local" defaultValue="2026-02-26T08:00" className={bsInput} style={bsInputStyle} />
</div>
<div>
<label className="block text-[9px] font-korean mb-1 text-[#64748b]"> <span className="text-red-400">*</span></label>
<input type="datetime-local" defaultValue="2026-02-27T20:00" className={bsInput} style={bsInputStyle} />
</div>
<div>
<label className="block text-[9px] font-korean mb-1 text-[#64748b]"> </label>
<select className={bsInput} style={bsInputStyle}>
<option>1 ()</option>
<option> ( )</option>
<option> 6</option>
<option> 12</option>
<option> 1</option>
</select>
</div>
</div>
</div>
{/* ④ 산출물 설정 */}
<div>
{sectionHeader(4, '산출물 설정')}
<div className="grid grid-cols-2 gap-2.5">
<div>
<label className="block text-[9px] font-korean mb-1 text-[#64748b]"> <span className="text-red-400">*</span></label>
<select className={bsInput} style={bsInputStyle}>
<option>Ortho-Rectified ()</option>
<option>Pan-sharpened ()</option>
<option>Basic L1 ()</option>
</select>
</div>
<div>
<label className="block text-[9px] font-korean mb-1 text-[#64748b]"> </label>
<select className={bsInput} style={bsInputStyle}>
<option>GeoTIFF</option>
<option>NITF</option>
<option>JPEG2000</option>
</select>
</div>
</div>
<div className="mt-2 flex flex-wrap gap-3">
{[
{ label: '유출유 탐지 분석 (자동)', checked: true },
{ label: 'GIS 상황판 자동 오버레이', checked: true },
{ label: '변화탐지 (Change Detection)', checked: false },
{ label: '웹훅 알림', checked: false },
].map((opt, i) => (
<label key={i} className="flex items-center gap-1 text-[9px] cursor-pointer font-korean text-[#94a3b8]">
<input type="checkbox" defaultChecked={opt.checked} style={{ accentColor: '#818cf8', transform: 'scale(.85)' }} /> {opt.label}
</label>
))}
</div>
</div>
{/* ⑤ 연계 사고 · 비고 */}
<div>
{sectionHeader(5, '연계 사고 · 비고')}
<div className="grid grid-cols-2 gap-2.5 mb-2">
<div>
<label className="block text-[9px] font-korean mb-1 text-[#64748b]"> </label>
<select className={bsInput} style={bsInputStyle}>
<option>OIL-2024-0892 · M/V STELLAR DAISY</option>
<option>HNS-2024-041 · </option>
<option>RSC-2024-0127 · M/V SEA GUARDIAN</option>
<option value=""> </option>
</select>
</div>
<div>
<label className="block text-[9px] font-korean mb-1 text-[#64748b]"></label>
<input type="text" placeholder="소속 / 이름" className={bsInput} style={bsInputStyle} />
</div>
</div>
<textarea
placeholder="촬영 요청 목적, 특이사항, 관심 대상 등을 기록합니다..."
className="w-full h-[50px] px-3 py-2.5 rounded-md text-[10px] font-korean outline-none resize-y leading-relaxed box-border border border-[#21262d] text-[#e2e8f0] bg-[#161b22]"
/>
</div>
</div>
{/* 하단 버튼 */}
<div className="px-6 py-3.5 border-t border-[#21262d] flex items-center gap-2 shrink-0">
<div className="flex-1 text-[9px] font-korean leading-relaxed text-[#64748b]">
<span className="text-red-400">*</span> · P1 90
</div>
<button onClick={() => setModalPhase('provider')} className="px-5 py-2.5 rounded-lg border border-[#21262d] text-xs font-semibold cursor-pointer font-korean text-[#94a3b8] bg-[#161b22]"> </button>
<button onClick={() => setModalPhase('none')} className="px-7 py-2.5 rounded-lg border-none text-xs font-bold cursor-pointer font-korean text-white" style={{ background: 'linear-gradient(135deg,#6366f1,#818cf8)', boxShadow: '0 4px 16px rgba(99,102,241,.35)' }}>🛰 BlackSky </button>
</div>
</div>
)}
{/* ── UP42 카탈로그 주문 ── */}
{modalPhase === 'up42' && (
<div className="border rounded-[14px] w-[920px] max-h-[90vh] flex flex-col overflow-hidden border-[rgba(59,130,246,.3)]" style={{ background: '#0d1117', boxShadow: '0 24px 80px rgba(0,0,0,.7)' }}>
{/* 헤더 */}
<div className="px-6 py-4 border-b border-[#21262d] flex items-center justify-between shrink-0 relative">
<div className="absolute top-0 left-0 right-0 h-0.5" style={{ background: 'linear-gradient(90deg,#3b82f6,#06b6d4,#22c55e)' }} />
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-[rgba(59,130,246,.3)]" style={{ background: 'linear-gradient(135deg,#0a1628,#162a50)' }}>
<span className="text-[13px] font-extrabold font-mono text-[#60a5fa] tracking-[-0.5px]">up<sup className="text-[7px] align-super">42</sup></span>
</div>
<div>
<div className="text-[15px] font-bold font-korean text-[#e2e8f0]"> </div>
<div className="text-[9px] font-korean mt-0.5 text-[#64748b]"> (AOI) </div>
</div>
</div>
<div className="flex items-center gap-2">
<span className="px-3 py-1 rounded-full text-[10px] font-semibold font-korean" style={{ background: 'rgba(234,179,8,.1)', border: '1px solid rgba(234,179,8,.25)', color: '#eab308' }}> Beijing-3N 2.152.23</span>
<button onClick={() => setModalPhase('none')} className="text-lg cursor-pointer p-1 bg-transparent border-none text-[#64748b]"></button>
</div>
</div>
{/* 본문 (좌: 사이드바, 우: 지도+AOI) */}
<div className="flex-1 flex overflow-hidden">
{/* 왼쪽: 위성 카탈로그 */}
<div className="flex flex-col overflow-hidden border-r border-[#21262d]" style={{ width: 320, minWidth: 320, background: '#0d1117' }}>
{/* Optical / SAR / Elevation 탭 */}
<div className="flex border-b border-[#21262d] shrink-0">
{(['optical', 'sar', 'elevation'] as const).map(t => (
<button
key={t}
onClick={() => setUp42SubTab(t)}
className="flex-1 py-2 text-[10px] font-bold cursor-pointer border-none font-korean transition-colors"
style={up42SubTab === t
? { background: 'rgba(59,130,246,.1)', color: '#60a5fa', borderBottom: '2px solid #3b82f6' }
: { background: 'transparent', color: '#64748b' }
}
>{t === 'optical' ? 'Optical' : t === 'sar' ? 'SAR' : 'Elevation'}</button>
))}
</div>
{/* 필터 바 */}
<div className="flex items-center gap-1.5 px-3 py-2 border-b border-[#21262d] shrink-0">
<span className="px-2 py-0.5 rounded text-[9px] font-semibold text-[#60a5fa]" style={{ background: 'rgba(59,130,246,.1)', border: '1px solid rgba(59,130,246,.2)' }}>Filters </span>
<span className="px-2 py-0.5 rounded text-[9px] font-semibold text-[#818cf8]" style={{ background: 'rgba(99,102,241,.1)', border: '1px solid rgba(99,102,241,.2)' }}> 20% </span>
<span className="ml-auto text-[9px] font-mono text-[#64748b]"> </span>
</div>
{/* 컬렉션 수 */}
<div className="px-3 py-1.5 border-b border-[#21262d] text-[9px] font-korean shrink-0 text-[#64748b]">
<b className="text-[#e2e8f0]">{up42Filtered.length}</b>
</div>
{/* 위성 목록 */}
<div className="flex-1 overflow-y-auto" style={{ scrollbarWidth: 'thin', scrollbarColor: '#21262d transparent' }}>
{up42Filtered.map(sat => (
<div
key={sat.id}
onClick={() => setUp42SelSat(up42SelSat === sat.id ? null : sat.id)}
className="flex items-center gap-2.5 px-3 py-2.5 border-b border-[#161b22] cursor-pointer transition-colors"
style={{
background: up42SelSat === sat.id ? 'rgba(59,130,246,.08)' : 'transparent',
}}
>
<div className="w-1 h-8 rounded-full shrink-0" style={{ background: sat.color }} />
<div className="flex-1 min-w-0">
<div className="text-[11px] font-semibold truncate font-korean text-[#e2e8f0]">{sat.name}</div>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-[9px] font-bold font-mono" style={{ color: sat.color }}>{sat.res}</span>
{sat.cloud > 0 && <span className="text-[8px] font-mono text-[#64748b]"> {sat.cloud}%</span>}
{'delay' in sat && sat.delay && <span className="text-[8px] font-bold" style={{ color: '#eab308' }}> </span>}
</div>
</div>
{up42SelSat === sat.id && <span className="text-[12px] text-blue-500"></span>}
</div>
))}
</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>
{/* 지도 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>
</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>
</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="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" className="text-cyan-500">{p.res}</span>
<span className="text-[8px] font-mono text-[#64748b]">{p.cloud}</span>
</div>
{p.note && (
<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>
))}
</div>
</div>
</div>
</div>
{/* 푸터 */}
<div className="px-6 py-3 border-t border-[#21262d] flex items-center justify-between shrink-0">
<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 : '없음'}
</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
onClick={() => setModalPhase('none')}
className="px-6 py-2 rounded-lg border-none text-[11px] font-bold cursor-pointer font-korean text-white transition-opacity"
style={{
background: up42SelSat ? 'linear-gradient(135deg,#3b82f6,#06b6d4)' : '#21262d',
opacity: up42SelSat ? 1 : 0.5,
color: up42SelSat ? '#fff' : '#64748b',
boxShadow: up42SelSat ? '0 4px 16px rgba(59,130,246,.35)' : 'none',
}}
>🛰 </button>
</div>
</div>
</div>
)}
</div>
</div>
)}
</div>
)
}