feat: add fleet relation sort toggle
This commit is contained in:
부모
03d728589f
커밋
96d8a03f93
@ -121,6 +121,30 @@ body {
|
|||||||
margin-bottom: 6px;
|
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 */
|
/* Type grid */
|
||||||
.tg {
|
.tg {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -215,6 +239,21 @@ body {
|
|||||||
background: var(--card);
|
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 {
|
.vi .dot {
|
||||||
width: 7px;
|
width: 7px;
|
||||||
height: 7px;
|
height: 7px;
|
||||||
@ -488,6 +527,13 @@ body {
|
|||||||
margin-bottom: 4px;
|
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 {
|
.fleet-owner {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -495,6 +541,10 @@ body {
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fleet-owner.hl {
|
||||||
|
color: rgba(245, 158, 11, 1);
|
||||||
|
}
|
||||||
|
|
||||||
.fleet-vessel {
|
.fleet-vessel {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -503,6 +553,14 @@ body {
|
|||||||
padding: 1px 0;
|
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 */
|
/* Toggles */
|
||||||
.tog {
|
.tog {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -650,6 +708,50 @@ body {
|
|||||||
animation: map-loader-fill 1.2s ease-in-out infinite;
|
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 {
|
@keyframes map-loader-spin {
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
|
|||||||
@ -48,6 +48,7 @@ function fmtLocal(iso: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Bbox = [number, number, number, number]; // [lonMin, latMin, lonMax, latMax]
|
type Bbox = [number, number, number, number]; // [lonMin, latMin, lonMax, latMax]
|
||||||
|
type FleetRelationSortMode = "count" | "range";
|
||||||
|
|
||||||
function inBbox(lon: number, lat: number, bbox: Bbox) {
|
function inBbox(lon: number, lat: number, bbox: Bbox) {
|
||||||
const [lonMin, latMin, lonMax, latMax] = bbox;
|
const [lonMin, latMin, lonMax, latMax] = bbox;
|
||||||
@ -114,6 +115,7 @@ export function DashboardPage() {
|
|||||||
zones: true,
|
zones: true,
|
||||||
fleetCircles: true,
|
fleetCircles: true,
|
||||||
});
|
});
|
||||||
|
const [fleetRelationSortMode, setFleetRelationSortMode] = useState<FleetRelationSortMode>("count");
|
||||||
|
|
||||||
const [fleetFocus, setFleetFocus] = useState<{ id: string | number; center: [number, number]; zoom?: number } | undefined>(undefined);
|
const [fleetFocus, setFleetFocus] = useState<{ id: string | number; center: [number, number]; zoom?: number } | undefined>(undefined);
|
||||||
|
|
||||||
@ -346,11 +348,33 @@ export function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sb" style={{ maxHeight: 260, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
<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 style={{ color: "var(--accent)", fontSize: 8 }}>
|
||||||
</span>
|
{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>
|
||||||
<div style={{ overflowY: "auto", minHeight: 0 }}>
|
<div style={{ overflowY: "auto", minHeight: 0 }}>
|
||||||
<RelationsPanel
|
<RelationsPanel
|
||||||
@ -371,6 +395,7 @@ export function DashboardPage() {
|
|||||||
setHoveredFleetOwnerKey(null);
|
setHoveredFleetOwnerKey(null);
|
||||||
setHoveredFleetMmsiSet((prev) => (prev.length === 0 ? prev : []));
|
setHoveredFleetMmsiSet((prev) => (prev.length === 0 ? prev : []));
|
||||||
}}
|
}}
|
||||||
|
fleetSortMode={fleetRelationSortMode}
|
||||||
hoveredFleetOwnerKey={hoveredFleetOwnerKey}
|
hoveredFleetOwnerKey={hoveredFleetOwnerKey}
|
||||||
hoveredFleetMmsiSet={hoveredFleetMmsiSet}
|
hoveredFleetMmsiSet={hoveredFleetMmsiSet}
|
||||||
onContextMenuFleet={handleFleetContextMenu}
|
onContextMenuFleet={handleFleetContextMenu}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { VESSEL_TYPES } from "../../entities/vessel/model/meta";
|
|||||||
import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types";
|
import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types";
|
||||||
import { haversineNm } from "../../shared/lib/geo/haversineNm";
|
import { haversineNm } from "../../shared/lib/geo/haversineNm";
|
||||||
|
|
||||||
|
type FleetSortMode = "count" | "range";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedVessel: DerivedLegacyVessel | null;
|
selectedVessel: DerivedLegacyVessel | null;
|
||||||
vessels: DerivedLegacyVessel[];
|
vessels: DerivedLegacyVessel[];
|
||||||
@ -18,6 +20,7 @@ type Props = {
|
|||||||
hoveredFleetOwnerKey?: string | null;
|
hoveredFleetOwnerKey?: string | null;
|
||||||
hoveredFleetMmsiSet?: number[];
|
hoveredFleetMmsiSet?: number[];
|
||||||
onContextMenuFleet?: (ownerKey: string, mmsis: number[]) => void;
|
onContextMenuFleet?: (ownerKey: string, mmsis: number[]) => void;
|
||||||
|
fleetSortMode?: FleetSortMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RelationsPanel({
|
export function RelationsPanel({
|
||||||
@ -35,6 +38,7 @@ export function RelationsPanel({
|
|||||||
hoveredFleetOwnerKey,
|
hoveredFleetOwnerKey,
|
||||||
hoveredFleetMmsiSet,
|
hoveredFleetMmsiSet,
|
||||||
onContextMenuFleet,
|
onContextMenuFleet,
|
||||||
|
fleetSortMode = "count",
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const handlePrimaryAction = (e: MouseEvent, mmsi: number) => {
|
const handlePrimaryAction = (e: MouseEvent, mmsi: number) => {
|
||||||
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
||||||
@ -242,10 +246,27 @@ export function RelationsPanel({
|
|||||||
else group.set(v.ownerKey, [v]);
|
else group.set(v.ownerKey, [v]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const topFleets = Array.from(group.entries())
|
const topFleets = useMemo(() => {
|
||||||
.map(([ownerKey, vs]) => ({ ownerKey, vs }))
|
const toFleetMeta = Array.from(group.entries())
|
||||||
.filter((x) => x.vs.length >= 3)
|
.map(([ownerKey, vs]) => {
|
||||||
.sort((a, b) => b.vs.length - a.vs.length);
|
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) {
|
if (topFleets.length === 0) {
|
||||||
return <div style={{ fontSize: 11, color: "var(--muted)" }}>(표시 중인 선단(3척 이상) 없음)</div>;
|
return <div style={{ fontSize: 11, color: "var(--muted)" }}>(표시 중인 선단(3척 이상) 없음)</div>;
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user