release: 2026-03-16 (81건 커밋) #93

병합
jhkang develop 에서 main 로 30 commits 를 머지했습니다 2026-03-16 18:36:00 +09:00
Showing only changes of commit 615f7f9277 - Show all commits

파일 보기

@ -7,11 +7,11 @@ import type { DroneStreamItem } from '../services/aerialApi'
import { CCTVPlayer } from './CCTVPlayer'
import type { CCTVPlayerHandle } from './CCTVPlayer'
/** 드론 위치 좌표 (함정 모항 기준) */
const DRONE_COORDS: Record<string, { lat: number; lon: number }> = {
'busan-1501': { lat: 35.0796, lon: 129.0756 },
'incheon-3008': { lat: 37.4541, lon: 126.5986 },
'mokpo-3015': { lat: 34.7780, lon: 126.3780 },
/** 함정 위치 + 드론 비행 위치 */
const DRONE_POSITIONS: Record<string, { ship: { lat: number; lon: number }; drone: { lat: number; lon: number } }> = {
'busan-1501': { ship: { lat: 35.0796, lon: 129.0756 }, drone: { lat: 35.1100, lon: 129.1100 } },
'incheon-3008': { ship: { lat: 37.4541, lon: 126.5986 }, drone: { lat: 37.4800, lon: 126.5600 } },
'mokpo-3015': { ship: { lat: 34.7780, lon: 126.3780 }, drone: { lat: 34.8050, lon: 126.4100 } },
}
const DRONE_MAP_STYLE: StyleSpecification = {
@ -266,55 +266,61 @@ export function RealtimeDrone() {
attributionControl={false}
>
{streams.map(stream => {
const coord = DRONE_COORDS[stream.id]
if (!coord) return null
const si = statusInfo(stream.status)
const pos = DRONE_POSITIONS[stream.id]
if (!pos) return null
const statusColor = stream.status === 'streaming' ? '#22c55e' : stream.status === 'starting' ? '#06b6d4' : stream.status === 'error' ? '#ef4444' : '#94a3b8'
return (
<Marker
key={stream.id}
longitude={coord.lon}
latitude={coord.lat}
anchor="bottom"
longitude={(pos.ship.lon + pos.drone.lon) / 2}
latitude={(pos.ship.lat + pos.drone.lat) / 2}
anchor="center"
onClick={e => { e.originalEvent.stopPropagation(); setMapPopup(stream) }}
>
<div className="flex flex-col items-center cursor-pointer group" title={stream.shipName}>
{/* 드론 SVG 아이콘 */}
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" className="drop-shadow-lg transition-transform group-hover:scale-110">
<div className="cursor-pointer group" title={stream.shipName}>
<svg width="120" height="80" viewBox="0 0 120 80" fill="none" className="drop-shadow-lg transition-transform group-hover:scale-105" style={{ overflow: 'visible' }}>
{/* 연결선 (점선) */}
<line x1="30" y1="55" x2="85" y2="28" stroke={statusColor} strokeWidth="1.5" strokeDasharray="4 3" opacity="0.6" />
{/* 함정 삼각형 (좌하단) */}
<polygon points="30,45 20,65 40,65" fill="rgba(0,0,0,.5)" stroke={statusColor} strokeWidth="1.5" />
<text x="30" y="59" textAnchor="middle" fill="#fff" fontSize="7" fontWeight="bold"></text>
{/* 함정명 */}
<rect x="5" y="67" width="50" height="13" rx="3" fill="rgba(0,0,0,.7)" />
<text x="30" y="76" textAnchor="middle" fill="#fff" fontSize="7" fontFamily="sans-serif" fontWeight="bold">{stream.shipName.replace(/서 /, ' ')}</text>
{/* 드론 원형 아이콘 (우상단) */}
<circle cx="85" cy="28" r="16" fill="rgba(0,0,0,.6)" stroke={statusColor} strokeWidth="2" />
{/* 드론 내부 — 십자 프로펠러 */}
<line x1="73" y1="28" x2="97" y2="28" stroke={statusColor} strokeWidth="1" opacity="0.4" />
<line x1="85" y1="16" x2="85" y2="40" stroke={statusColor} strokeWidth="1" opacity="0.4" />
{/* 프로펠러 4개 */}
<circle cx="73" cy="28" r="4" fill={statusColor} opacity="0.25" />
<circle cx="97" cy="28" r="4" fill={statusColor} opacity="0.25" />
<circle cx="85" cy="16" r="4" fill={statusColor} opacity="0.25" />
<circle cx="85" cy="40" r="4" fill={statusColor} opacity="0.25" />
{/* 본체 */}
<ellipse cx="16" cy="16" rx="5" ry="3" fill={si.color} opacity="0.9" />
{/* 팔 4개 */}
<line x1="11" y1="13" x2="5" y2="7" stroke={si.color} strokeWidth="1.5" />
<line x1="21" y1="13" x2="27" y2="7" stroke={si.color} strokeWidth="1.5" />
<line x1="11" y1="19" x2="5" y2="25" stroke={si.color} strokeWidth="1.5" />
<line x1="21" y1="19" x2="27" y2="25" stroke={si.color} strokeWidth="1.5" />
{/* 프로펠러 */}
<circle cx="5" cy="7" r="3" fill={si.color} opacity="0.3" />
<circle cx="27" cy="7" r="3" fill={si.color} opacity="0.3" />
<circle cx="5" cy="25" r="3" fill={si.color} opacity="0.3" />
<circle cx="27" cy="25" r="3" fill={si.color} opacity="0.3" />
{/* 카메라 */}
<circle cx="16" cy="16" r="1.5" fill="#fff" />
{/* 송출중 표시 */}
<circle cx="85" cy="28" r="5" fill={statusColor} opacity="0.8" />
<circle cx="85" cy="28" r="2.5" fill="#fff" opacity="0.9" />
{/* 송출중 LED */}
{stream.status === 'streaming' && (
<circle cx="16" cy="10" r="2" fill="#ef4444">
<animate attributeName="opacity" values="1;0.3;1" dur="1.5s" repeatCount="indefinite" />
<circle cx="95" cy="18" r="3" fill="#ef4444">
<animate attributeName="opacity" values="1;0.2;1" dur="1.2s" repeatCount="indefinite" />
</circle>
)}
{/* 드론 이름 */}
<rect x="62" y="46" width="46" height="12" rx="3" fill="rgba(0,0,0,.7)" />
<text x="85" y="55" textAnchor="middle" fill={statusColor} fontSize="7" fontFamily="sans-serif" fontWeight="bold">{stream.droneModel.split(' ').slice(-1)[0]}</text>
</svg>
{/* 라벨 */}
<div className="px-1.5 py-px rounded text-[8px] font-bold font-korean whitespace-nowrap mt-0.5"
style={{ background: 'rgba(0,0,0,.7)', color: '#fff' }}>
{stream.shipName}
</div>
</div>
</Marker>
)
})}
{/* 드론 클릭 팝업 */}
{mapPopup && DRONE_COORDS[mapPopup.id] && (
{mapPopup && DRONE_POSITIONS[mapPopup.id] && (
<Popup
longitude={DRONE_COORDS[mapPopup.id].lon}
latitude={DRONE_COORDS[mapPopup.id].lat}
longitude={DRONE_POSITIONS[mapPopup.id].drone.lon}
latitude={DRONE_POSITIONS[mapPopup.id].drone.lat}
anchor="bottom"
onClose={() => setMapPopup(null)}
closeOnClick={false}