feat: add fleet relation sort toggle
This commit is contained in:
부모
03d728589f
커밋
96d8a03f93
@ -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,11 +348,33 @@ export function DashboardPage() {
|
||||
</div>
|
||||
|
||||
<div className="sb" style={{ maxHeight: 260, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
<div className="sb-t">
|
||||
선단 연관관계{" "}
|
||||
<span style={{ color: "var(--accent)", fontSize: 8 }}>
|
||||
{selectedLegacyVessel ? `${selectedLegacyVessel.permitNo} 연관` : "(선박 클릭 시 표시)"}
|
||||
</span>
|
||||
<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
|
||||
@ -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>;
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user