feat(vesselList): 선박 목록 전체 표시 + 검색 필터 추가
- 80척 제한(slice) 제거, 전체 선박 스크롤 표시 - 헤더에 검색 인풋 추가 (2글자 이상 입력 시 실시간 필터링) - 검색 대상: 등록번호, 선박명, 호출부호, MMSI - 정규화: 대소문자/공백/특수문자 무시 like 매칭 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
705cc696f9
커밋
9c211f4ab6
@ -223,6 +223,26 @@ body {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.vessel-search-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 20px;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
background: var(--card);
|
||||
color: var(--fg);
|
||||
font-size: 9px;
|
||||
outline: none;
|
||||
}
|
||||
.vessel-search-input::placeholder {
|
||||
color: var(--muted);
|
||||
opacity: 0.6;
|
||||
}
|
||||
.vessel-search-input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.vi {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -118,6 +118,7 @@ export function DashboardPage() {
|
||||
const [baseMap, _setBaseMap] = useState<BaseMapId>("enhanced");
|
||||
const [projection, setProjection] = useState<MapProjectionId>("mercator");
|
||||
const [mapStyleSettings, setMapStyleSettings] = useState<MapStyleSettings>(DEFAULT_MAP_STYLE_SETTINGS);
|
||||
const [vesselSearchQuery, setVesselSearchQuery] = useState("");
|
||||
|
||||
const [overlays, setOverlays] = useState<MapToggleState>({
|
||||
pairLines: true,
|
||||
@ -445,14 +446,24 @@ export function DashboardPage() {
|
||||
</div>
|
||||
|
||||
<div className="sb" style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column" }}>
|
||||
<div className="sb-t">
|
||||
선박 목록{" "}
|
||||
<span style={{ color: "var(--accent)", fontSize: 8 }}>
|
||||
({legacyVesselsFiltered.length}척)
|
||||
<div className="sb-t" style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<span style={{ whiteSpace: "nowrap" }}>
|
||||
선박 목록{" "}
|
||||
<span style={{ color: "var(--accent)", fontSize: 8 }}>
|
||||
({legacyVesselsFiltered.length}척)
|
||||
</span>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="vessel-search-input"
|
||||
placeholder="선박 검색..."
|
||||
value={vesselSearchQuery}
|
||||
onChange={(e) => setVesselSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<VesselList
|
||||
vessels={legacyVesselsFiltered}
|
||||
searchQuery={vesselSearchQuery}
|
||||
selectedMmsi={selectedMmsi}
|
||||
highlightedMmsiSet={activeHighlightedMmsiSet}
|
||||
onSelectMmsi={setSelectedMmsi}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { useMemo } from "react";
|
||||
import { VESSEL_TYPES } from "../../entities/vessel/model/meta";
|
||||
import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types";
|
||||
import type { MouseEvent } from "react";
|
||||
|
||||
type Props = {
|
||||
vessels: DerivedLegacyVessel[];
|
||||
searchQuery: string;
|
||||
selectedMmsi: number | null;
|
||||
highlightedMmsiSet?: number[];
|
||||
onToggleHighlightMmsi: (mmsi: number) => void;
|
||||
@ -12,8 +14,29 @@ type Props = {
|
||||
onClearHover?: () => void;
|
||||
};
|
||||
|
||||
function isFiniteNumber(x: unknown): x is number {
|
||||
return typeof x === "number" && Number.isFinite(x);
|
||||
}
|
||||
|
||||
const STRIP_RE = /[\s\-.,_]/g;
|
||||
|
||||
function normalize(s: string): string {
|
||||
return s.replace(STRIP_RE, "").toLowerCase();
|
||||
}
|
||||
|
||||
function matchesQuery(v: DerivedLegacyVessel, normalizedQuery: string): boolean {
|
||||
if (normalize(v.permitNo).includes(normalizedQuery)) return true;
|
||||
if (normalize(v.name).includes(normalizedQuery)) return true;
|
||||
if (v.legacy.shipNameRoman && normalize(v.legacy.shipNameRoman).includes(normalizedQuery)) return true;
|
||||
if (v.legacy.shipNameCn && normalize(v.legacy.shipNameCn).includes(normalizedQuery)) return true;
|
||||
if (v.legacy.callSign && normalize(v.legacy.callSign).includes(normalizedQuery)) return true;
|
||||
if (v.mmsi && normalize(String(v.mmsi)).includes(normalizedQuery)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function VesselList({
|
||||
vessels,
|
||||
searchQuery,
|
||||
selectedMmsi,
|
||||
highlightedMmsiSet = [],
|
||||
onToggleHighlightMmsi,
|
||||
@ -29,14 +52,16 @@ export function VesselList({
|
||||
onSelectMmsi(mmsi);
|
||||
};
|
||||
|
||||
function isFiniteNumber(x: unknown): x is number {
|
||||
return typeof x === "number" && Number.isFinite(x);
|
||||
}
|
||||
const sorted = useMemo(() => {
|
||||
const normalizedQuery = searchQuery.length >= 2 ? normalize(searchQuery) : "";
|
||||
const filtered = normalizedQuery
|
||||
? vessels.filter((v) => matchesQuery(v, normalizedQuery))
|
||||
: vessels;
|
||||
|
||||
const sorted = vessels
|
||||
.slice()
|
||||
.sort((a, b) => (isFiniteNumber(b.sog) ? b.sog : -1) - (isFiniteNumber(a.sog) ? a.sog : -1))
|
||||
.slice(0, 80);
|
||||
return filtered
|
||||
.slice()
|
||||
.sort((a, b) => (isFiniteNumber(b.sog) ? b.sog : -1) - (isFiniteNumber(a.sog) ? a.sog : -1));
|
||||
}, [vessels, searchQuery]);
|
||||
|
||||
return (
|
||||
<div className="vlist">
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user