release: 2026-03-19 (26건 커밋) #105

병합
jhkang develop 에서 main 로 26 commits 를 머지했습니다 2026-03-19 18:13:19 +09:00
Showing only changes of commit 7564f42918 - Show all commits

파일 보기

@ -82,10 +82,18 @@ const SAT_MAP_STYLE: StyleSpecification = {
layers: [{ id: 'carto-dark-layer', type: 'raster', source: 'carto-dark', minzoom: 0, maxzoom: 22 }],
}
/** 좌표 문자열 파싱 ("33.24°N 126.50°E" → {lat, lon}) */
function parseCoord(coordStr: string): { lat: number; lon: number } | null {
const m = coordStr.match(/([\d.]+)°N\s+([\d.]+)°E/)
if (!m) return null
return { lat: parseFloat(m[1]), lon: parseFloat(m[2]) }
}
type SatModalPhase = 'none' | 'provider' | 'blacksky' | 'up42'
export function SatelliteRequest() {
const [requests, setRequests] = useState(satRequests)
const [mainTab, setMainTab] = useState<'list' | 'map'>('list')
const [statusFilter, setStatusFilter] = useState('전체')
const [modalPhase, setModalPhase] = useState<SatModalPhase>('none')
const [selectedRequest, setSelectedRequest] = useState<SatRequest | null>(null)
@ -190,6 +198,27 @@ export function SatelliteRequest() {
<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="flex gap-1.5 mb-4">
<button
onClick={() => setMainTab('list')}
className="px-4 py-2 rounded text-[11px] font-bold font-korean cursor-pointer border transition-colors"
style={mainTab === 'list'
? { background: 'rgba(59,130,246,.12)', borderColor: 'rgba(59,130,246,.3)', color: 'var(--blue)' }
: { background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t3)' }
}
>📋 </button>
<button
onClick={() => setMainTab('map')}
className="px-4 py-2 rounded text-[11px] font-bold font-korean cursor-pointer border transition-colors"
style={mainTab === 'map'
? { background: 'rgba(59,130,246,.12)', borderColor: 'rgba(59,130,246,.3)', color: 'var(--blue)' }
: { background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t3)' }
}
>🗺 </button>
</div>
{mainTab === 'list' && (<>
{/* 요약 통계 */}
<div className="grid grid-cols-4 gap-3 mb-5">
{stats.map((s, i) => (
@ -337,6 +366,78 @@ export function SatelliteRequest() {
</div>
</div>
</div>
</>)}
{/* ═══ 촬영 히스토리 지도 뷰 ═══ */}
{mainTab === 'map' && (
<div className="bg-bg-2 border border-border rounded-md overflow-hidden" style={{ height: 'calc(100vh - 240px)' }}>
<Map
initialViewState={{ longitude: 127.5, latitude: 34.5, zoom: 7 }}
mapStyle={SAT_MAP_STYLE}
style={{ width: '100%', height: '100%' }}
attributionControl={false}
>
{/* 촬영 구역 폴리곤 + 마커 */}
{requests.map(r => {
const coord = parseCoord(r.zoneCoord)
if (!coord) return null
const areaKm = parseFloat(r.zoneArea) || 10
const delta = Math.sqrt(areaKm) * 0.005
const statusColor = r.status === '촬영중' ? '#eab308' : r.status === '완료' ? '#22c55e' : r.status === '취소' ? '#ef4444' : '#3b82f6'
return (
<Source key={r.id} id={`zone-${r.id}`} type="geojson" data={{
type: 'Feature', properties: {},
geometry: { type: 'Polygon', coordinates: [[
[coord.lon - delta, coord.lat - delta],
[coord.lon + delta, coord.lat - delta],
[coord.lon + delta, coord.lat + delta],
[coord.lon - delta, coord.lat + delta],
[coord.lon - delta, coord.lat - delta],
]] },
}}>
<Layer id={`zone-fill-${r.id}`} type="fill" paint={{ 'fill-color': statusColor, 'fill-opacity': 0.15 }} />
<Layer id={`zone-line-${r.id}`} type="line" paint={{ 'line-color': statusColor, 'line-width': 1.5 }} />
</Source>
)
})}
</Map>
{/* 지도 위 범례 */}
<div className="absolute top-3 right-3 px-3 py-2.5 rounded-lg border border-border z-10" style={{ background: 'rgba(18,25,41,.9)', backdropFilter: 'blur(8px)' }}>
<div className="text-[10px] font-bold text-text-2 font-korean mb-2"> </div>
{[
{ label: '촬영중', color: '#eab308' },
{ label: '대기', color: '#3b82f6' },
{ label: '완료', color: '#22c55e' },
{ label: '취소', color: '#ef4444' },
].map(item => (
<div key={item.label} className="flex items-center gap-2 mb-1">
<div className="w-3 h-3 rounded-sm" style={{ background: item.color, opacity: 0.4, border: `1px solid ${item.color}` }} />
<span className="text-[9px] text-text-3 font-korean">{item.label}</span>
</div>
))}
<div className="text-[8px] text-text-3 font-korean mt-1.5 pt-1.5 border-t border-border"> {requests.length}</div>
</div>
{/* 지도 위 요청 리스트 (좌측) */}
<div className="absolute top-3 left-3 w-[240px] max-h-[50vh] overflow-y-auto rounded-lg border border-border z-10" style={{ background: 'rgba(18,25,41,.92)', backdropFilter: 'blur(8px)', scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
<div className="px-3 py-2 border-b border-border text-[10px] font-bold text-text-2 font-korean sticky top-0" style={{ background: 'rgba(18,25,41,.95)' }}>📋 ({requests.length})</div>
{requests.map(r => {
const statusColor = r.status === '촬영중' ? '#eab308' : r.status === '완료' ? '#22c55e' : r.status === '취소' ? '#ef4444' : '#3b82f6'
return (
<div key={r.id} className="px-3 py-2 border-b cursor-pointer hover:bg-bg-hover/30 transition-colors" style={{ borderColor: 'rgba(255,255,255,.04)' }}>
<div className="flex items-center justify-between mb-0.5">
<span className="text-[10px] font-mono text-text-2">{r.id}</span>
<span className="text-[8px] font-bold px-1.5 py-px rounded-full" style={{ background: `${statusColor}20`, color: statusColor }}>{r.status}</span>
</div>
<div className="text-[9px] text-text-1 font-korean truncate">{r.zone}</div>
<div className="text-[8px] text-text-3 font-mono mt-0.5">{r.satellite} · {r.resolution}</div>
</div>
)
})}
</div>
</div>
)}
{/* ═══ 모달: 제공자 선택 ═══ */}
{modalPhase !== 'none' && (