Compare commits

...

1 커밋

작성자 SHA1 메시지 날짜
9c211f4ab6 feat(vesselList): 선박 목록 전체 표시 + 검색 필터 추가
- 80척 제한(slice) 제거, 전체 선박 스크롤 표시
- 헤더에 검색 인풋 추가 (2글자 이상 입력 시 실시간 필터링)
- 검색 대상: 등록번호, 선박명, 호출부호, MMSI
- 정규화: 대소문자/공백/특수문자 무시 like 매칭

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 08:06:36 +09:00
3개의 변경된 파일67개의 추가작업 그리고 11개의 파일을 삭제

파일 보기

@ -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">