From 96d8a03f93e7236f1bdf60899c72205ae87db45a Mon Sep 17 00:00:00 2001 From: htlee Date: Sun, 15 Feb 2026 15:25:10 +0900 Subject: [PATCH] feat: add fleet relation sort toggle --- apps/web/src/app/styles.css | 102 ++++++++++++++++++ .../web/src/pages/dashboard/DashboardPage.tsx | 35 +++++- .../src/widgets/relations/RelationsPanel.tsx | 29 ++++- 3 files changed, 157 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/styles.css b/apps/web/src/app/styles.css index de52f4f..b5b2d94 100644 --- a/apps/web/src/app/styles.css +++ b/apps/web/src/app/styles.css @@ -121,6 +121,30 @@ body { margin-bottom: 6px; } +.sb-t-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.relation-sort { + display: flex; + align-items: center; + gap: 6px; + font-size: 8px; + color: var(--muted); + white-space: nowrap; +} + +.relation-sort__option { + display: inline-flex; + align-items: center; + gap: 3px; + cursor: pointer; + user-select: none; +} + /* Type grid */ .tg { display: grid; @@ -215,6 +239,21 @@ body { background: var(--card); } +.vi.sel { + background: rgba(14, 234, 255, 0.16); + border-color: rgba(14, 234, 255, 0.55); +} + +.vi.hl { + background: rgba(245, 158, 11, 0.16); + border: 1px solid rgba(245, 158, 11, 0.4); +} + +.vi.sel.hl { + background: linear-gradient(90deg, rgba(14, 234, 255, 0.16), rgba(245, 158, 11, 0.16)); + border-color: rgba(14, 234, 255, 0.7); +} + .vi .dot { width: 7px; height: 7px; @@ -488,6 +527,13 @@ body { margin-bottom: 4px; } +.fleet-card.hl, +.fleet-card:hover { + border-color: rgba(245, 158, 11, 0.75); + background: rgba(251, 191, 36, 0.09); + box-shadow: 0 0 0 1px rgba(245, 158, 11, 0.25) inset; +} + .fleet-owner { font-size: 10px; font-weight: 700; @@ -495,6 +541,10 @@ body { margin-bottom: 4px; } +.fleet-owner.hl { + color: rgba(245, 158, 11, 1); +} + .fleet-vessel { display: flex; align-items: center; @@ -503,6 +553,14 @@ body { padding: 1px 0; } +.fleet-vessel.hl { + color: rgba(245, 158, 11, 1); +} + +.fleet-dot.hl { + box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.45); +} + /* Toggles */ .tog { display: flex; @@ -650,6 +708,50 @@ body { animation: map-loader-fill 1.2s ease-in-out infinite; } +.maplibre-tooltip-popup .maplibregl-popup-content { + color: #f8fafc !important; + background: rgba(2, 6, 23, 0.98) !important; + border: 1px solid rgba(148, 163, 184, 0.4) !important; + box-shadow: 0 8px 26px rgba(2, 6, 23, 0.55) !important; + border-radius: 8px !important; + font-size: 11px !important; + line-height: 1.35 !important; + padding: 7px 9px !important; + color: #f8fafc !important; + min-width: 180px; +} + +.maplibre-tooltip-popup .maplibregl-popup-tip { + border-top-color: rgba(2, 6, 23, 0.97) !important; +} + +.maplibre-tooltip-popup__content { + color: #f8fafc; + font-family: Pretendard, Inter, ui-sans-serif, -apple-system, Segoe UI, sans-serif; + font-size: 11px; + line-height: 1.35; +} + +.maplibre-tooltip-popup__content div, +.maplibre-tooltip-popup__content span, +.maplibre-tooltip-popup__content p { + color: inherit; +} + +.maplibre-tooltip-popup__content div { + word-break: break-word; +} + +.maplibre-tooltip-popup .maplibregl-popup-content div, +.maplibre-tooltip-popup .maplibregl-popup-content span, +.maplibre-tooltip-popup .maplibregl-popup-content p { + color: inherit !important; +} + +.maplibre-tooltip-popup .maplibregl-popup-close-button { + color: #94a3b8 !important; +} + @keyframes map-loader-spin { to { transform: rotate(360deg); diff --git a/apps/web/src/pages/dashboard/DashboardPage.tsx b/apps/web/src/pages/dashboard/DashboardPage.tsx index 61630b2..0ae6c5c 100644 --- a/apps/web/src/pages/dashboard/DashboardPage.tsx +++ b/apps/web/src/pages/dashboard/DashboardPage.tsx @@ -48,6 +48,7 @@ function fmtLocal(iso: string | null) { } type Bbox = [number, number, number, number]; // [lonMin, latMin, lonMax, latMax] +type FleetRelationSortMode = "count" | "range"; function inBbox(lon: number, lat: number, bbox: Bbox) { const [lonMin, latMin, lonMax, latMax] = bbox; @@ -114,6 +115,7 @@ export function DashboardPage() { zones: true, fleetCircles: true, }); + const [fleetRelationSortMode, setFleetRelationSortMode] = useState("count"); const [fleetFocus, setFleetFocus] = useState<{ id: string | number; center: [number, number]; zoom?: number } | undefined>(undefined); @@ -346,11 +348,33 @@ export function DashboardPage() {
-
- 선단 연관관계{" "} - - {selectedLegacyVessel ? `${selectedLegacyVessel.permitNo} 연관` : "(선박 클릭 시 표시)"} - +
+
+ 선단 연관관계{" "} + + {selectedLegacyVessel ? `${selectedLegacyVessel.permitNo} 연관` : "(선박 클릭 시 표시)"} + +
+
+ + +
(prev.length === 0 ? prev : [])); }} + fleetSortMode={fleetRelationSortMode} hoveredFleetOwnerKey={hoveredFleetOwnerKey} hoveredFleetMmsiSet={hoveredFleetMmsiSet} onContextMenuFleet={handleFleetContextMenu} diff --git a/apps/web/src/widgets/relations/RelationsPanel.tsx b/apps/web/src/widgets/relations/RelationsPanel.tsx index 1572940..3b38527 100644 --- a/apps/web/src/widgets/relations/RelationsPanel.tsx +++ b/apps/web/src/widgets/relations/RelationsPanel.tsx @@ -3,6 +3,8 @@ import { VESSEL_TYPES } from "../../entities/vessel/model/meta"; import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types"; import { haversineNm } from "../../shared/lib/geo/haversineNm"; +type FleetSortMode = "count" | "range"; + type Props = { selectedVessel: DerivedLegacyVessel | null; vessels: DerivedLegacyVessel[]; @@ -18,6 +20,7 @@ type Props = { hoveredFleetOwnerKey?: string | null; hoveredFleetMmsiSet?: number[]; onContextMenuFleet?: (ownerKey: string, mmsis: number[]) => void; + fleetSortMode?: FleetSortMode; }; export function RelationsPanel({ @@ -35,6 +38,7 @@ export function RelationsPanel({ hoveredFleetOwnerKey, hoveredFleetMmsiSet, onContextMenuFleet, + fleetSortMode = "count", }: Props) { const handlePrimaryAction = (e: MouseEvent, mmsi: number) => { if (e.shiftKey || e.ctrlKey || e.metaKey) { @@ -242,10 +246,27 @@ export function RelationsPanel({ 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); + const topFleets = useMemo(() => { + const toFleetMeta = Array.from(group.entries()) + .map(([ownerKey, vs]) => { + const lon = vs.reduce((sum, v) => sum + v.lon, 0) / vs.length; + const lat = vs.reduce((sum, v) => sum + v.lat, 0) / vs.length; + const radiusNm = vs.reduce((max, v) => { + const d = haversineNm(lat, lon, v.lat, v.lon); + return Math.max(max, d); + }, 0); + + return { ownerKey, vs, radiusNm }; + }) + .filter((x) => x.vs.length >= 3); + + return toFleetMeta.sort((a, b) => { + if (fleetSortMode === "range") { + return b.radiusNm - a.radiusNm || b.vs.length - a.vs.length; + } + return b.vs.length - a.vs.length || b.radiusNm - a.radiusNm; + }); + }, [fleetSortMode, group]); if (topFleets.length === 0) { return
(표시 중인 선단(3척 이상) 없음)
;