feat: add fleet relation sort toggle

This commit is contained in:
htlee 2026-02-15 15:25:10 +09:00
부모 03d728589f
커밋 96d8a03f93
3개의 변경된 파일157개의 추가작업 그리고 9개의 파일을 삭제

파일 보기

@ -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);

파일 보기

@ -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<FleetRelationSortMode>("count");
const [fleetFocus, setFleetFocus] = useState<{ id: string | number; center: [number, number]; zoom?: number } | undefined>(undefined);
@ -346,12 +348,34 @@ export function DashboardPage() {
</div>
<div className="sb" style={{ maxHeight: 260, display: "flex", flexDirection: "column", overflow: "hidden" }}>
<div className="sb-t">
<div className="sb-t sb-t-row">
<div>
{" "}
<span style={{ color: "var(--accent)", fontSize: 8 }}>
{selectedLegacyVessel ? `${selectedLegacyVessel.permitNo} 연관` : "(선박 클릭 시 표시)"}
</span>
</div>
<div className="relation-sort">
<label className="relation-sort__option">
<input
type="radio"
name="fleet-relation-sort"
checked={fleetRelationSortMode === "count"}
onChange={() => setFleetRelationSortMode("count")}
/>
</label>
<label className="relation-sort__option">
<input
type="radio"
name="fleet-relation-sort"
checked={fleetRelationSortMode === "range"}
onChange={() => setFleetRelationSortMode("range")}
/>
</label>
</div>
</div>
<div style={{ overflowY: "auto", minHeight: 0 }}>
<RelationsPanel
selectedVessel={selectedLegacyVessel}
@ -371,6 +395,7 @@ export function DashboardPage() {
setHoveredFleetOwnerKey(null);
setHoveredFleetMmsiSet((prev) => (prev.length === 0 ? prev : []));
}}
fleetSortMode={fleetRelationSortMode}
hoveredFleetOwnerKey={hoveredFleetOwnerKey}
hoveredFleetMmsiSet={hoveredFleetMmsiSet}
onContextMenuFleet={handleFleetContextMenu}

파일 보기

@ -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 <div style={{ fontSize: 11, color: "var(--muted)" }}>( (3 ) )</div>;