import { VESSEL_TYPES } from "../../entities/vessel/model/meta"; import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types"; import { haversineNm } from "../../shared/lib/geo/haversineNm"; type Props = { selectedVessel: DerivedLegacyVessel | null; vessels: DerivedLegacyVessel[]; fleetVessels: DerivedLegacyVessel[]; onSelectMmsi: (mmsi: number) => void; }; export function RelationsPanel({ selectedVessel, vessels, fleetVessels, onSelectMmsi }: Props) { if (selectedVessel) { const v = selectedVessel; const meta = VESSEL_TYPES[v.shipCode]; const ownerLabel = v.ownerCn || v.ownerRoman || v.ownerKey || "-"; const sameOwner = v.ownerKey ? vessels.filter((v2) => v2.ownerKey === v.ownerKey && v2.mmsi !== v.mmsi) : []; const pair = v.pairPermitNo ? vessels.find((v2) => v2.permitNo === v.pairPermitNo) ?? null : null; const fcNearby = vessels.filter((fc) => fc.shipCode === "FC" && fc.mmsi !== v.mmsi && haversineNm(fc.lat, fc.lon, v.lat, v.lon) < 5); return (
{meta.icon} {v.permitNo} {v.shipCode}
소유주:{" "} {ownerLabel}
{pair ? ( <>
⛓ 쌍끌이 쌍
{(() => { const dist = haversineNm(v.lat, v.lon, pair.lat, pair.lon); const warn = dist > 3; const pairMeta = VESSEL_TYPES[pair.shipCode]; return ( <>
onSelectMmsi(v.mmsi)}> {v.permitNo}
{warn ? "⚠" : "⟷"}
onSelectMmsi(pair.mmsi)}> {pair.permitNo} {dist.toFixed(2)}NM
정상 범위: 0.3~1.0NM | {warn ? "⚠ 이격 경고" : "✓ 정상 동기화"}
); })()} ) : null} {fcNearby.length && v.shipCode !== "FC" ? ( <>
🚛 근접 운반선
{fcNearby.slice(0, 6).map((fc) => { const dist = haversineNm(v.lat, v.lon, fc.lat, fc.lon); const isSameOwner = !!v.ownerKey && v.ownerKey === fc.ownerKey; const warn = dist < 0.5; return (
onSelectMmsi(fc.mmsi)}> {fc.permitNo} {dist.toFixed(1)}NM {isSameOwner ? ( 동일소유주 ) : null} {warn ? ( 환적의심 ) : null}
); })} ) : null} {sameOwner.length ? ( <>
🏢 동일 소유주 선단 ({sameOwner.length + 1}척)
{sameOwner.slice(0, 8).map((sv) => { const m = VESSEL_TYPES[sv.shipCode]; return (
onSelectMmsi(sv.mmsi)} style={{ cursor: "pointer" }}>
{sv.shipCode} {sv.permitNo} {sv.sog ?? "?"}kt {sv.state.label}
); })} {sameOwner.length > 8 ?
... +{sameOwner.length - 8}척
: null} ) : null}
); } // No vessel selected: show top fleets if (fleetVessels.length === 0) { return
(현재 지도에 표시중인 대상 선박이 없습니다)
; } const group = new Map(); for (const v of fleetVessels) { if (!v.ownerKey) continue; const list = group.get(v.ownerKey); if (list) list.push(v); else group.set(v.ownerKey, [v]); } const topFleets = Array.from(group.entries()) .map(([ownerKey, vs]) => ({ ownerKey, vs })) .filter((x) => x.vs.length >= 3) .sort((a, b) => b.vs.length - a.vs.length) .slice(0, 5); if (topFleets.length === 0) { return
(표시 중인 선단(3척 이상) 없음)
; } return (
{topFleets.map(({ ownerKey, vs }) => { const displayOwner = vs.find((v) => v.ownerCn)?.ownerCn || vs.find((v) => v.ownerRoman)?.ownerRoman || ownerKey; const displayTitle = ownerKey && displayOwner !== ownerKey ? `${displayOwner} (${ownerKey})` : displayOwner; const codes: Record = {}; for (const v of vs) codes[v.shipCode] = (codes[v.shipCode] ?? 0) + 1; return (
🏢{" "} {displayOwner} {" "} {vs.length}척
{Object.entries(codes).map(([c, n]) => { const meta = VESSEL_TYPES[c as keyof typeof VESSEL_TYPES]; return ( {c}×{n} ); })}
{vs.slice(0, 18).map((v) => { const m = VESSEL_TYPES[v.shipCode]; const text = v.shipCode === "FC" ? "F" : v.shipCode === "PT" ? "M" : v.shipCode === "PT-S" ? "S" : v.shipCode[0]; return (
onSelectMmsi(v.mmsi)} style={{ cursor: "pointer", width: 16, height: 16, borderRadius: v.shipCode === "FC" ? 2 : "50%", background: m.color, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 6, color: "#fff", border: "1px solid rgba(255,255,255,.2)", }} title={`${v.permitNo} ${v.shipCode} ${v.sog ?? "?"}kt`} > {text}
{v.pairPermitNo && (v.shipCode === "PT" || v.shipCode === "PT-S") ? : null}
); })} {vs.length > 18 ? +{vs.length - 18} : null}
); })}
); }