구역 기반 선박 항적 검색 기능 추가. 사용자가 지도에 최대 3개 구역을 그리고 ANY/ALL/SEQUENTIAL 조건으로 해당 구역을 통과한 선박의 항적을 조회·재생할 수 있다. 신규 패키지 (src/areaSearch/): - stores: areaSearchStore, areaSearchAnimationStore (재생 제어) - services: areaSearchApi (REST API + hitDetails 타임스탬프/위치 보간) - components: AreaSearchPage, ZoneDrawPanel, AreaSearchTimeline, AreaSearchTooltip - hooks: useAreaSearchLayer (Deck.gl 레이어), useZoneDraw (OL Draw) - utils: areaSearchLayerRegistry, csvExport (BOM+UTF-8 엑셀 호환) - types: areaSearch.types (상수, 색상, 모드) 주요 기능: - 폴리곤/사각형/원 구역 그리기 + 드래그 순서 변경 - 구역별 색상 구분 (빨강/청록/황색) - 시간 기반 애니메이션 재생 (TripsLayer 궤적 + 가상선박 이동) - 선종/개별 선박 필터링, 항적 표시/궤적 표시 토글 - 호버 툴팁 (국기 SVG, 구역별 진입/진출 시각·위치) - CSV 내보내기 (신호원, 식별번호, 국적 ISO 변환, 구역 통과 정보) 기존 파일 수정: - SideNav/Sidebar: gnb8 '항적분석' 메뉴 활성화 - useShipLayer: areaSearch 레이어 병합 - MapContainer: useAreaSearchLayer 훅 + 호버 핸들러 + 타임라인 렌더링 - trackLayer: layerIds 파라미터 추가 (area search/track query 레이어 ID 분리) - ShipLegend: 항적분석 모드 선종 카운트 지원 - countryCodeUtils: MMSI MID→ISO alpha-2 매핑 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
102 lines
1.6 KiB
SCSS
102 lines
1.6 KiB
SCSS
.area-search-tooltip {
|
|
position: fixed;
|
|
z-index: 200;
|
|
pointer-events: none;
|
|
background: rgba(20, 24, 32, 0.95);
|
|
border-radius: 6px;
|
|
padding: 10px 14px;
|
|
min-width: 180px;
|
|
max-width: 340px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
|
color: #fff;
|
|
font-size: 12px;
|
|
|
|
&__header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
&__kind {
|
|
display: inline-block;
|
|
padding: 1px 5px;
|
|
background: rgba(255, 255, 255, 0.15);
|
|
border-radius: 3px;
|
|
font-size: 10px;
|
|
color: #adb5bd;
|
|
}
|
|
|
|
&__flag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
|
|
img {
|
|
width: 16px;
|
|
height: 12px;
|
|
object-fit: contain;
|
|
vertical-align: middle;
|
|
}
|
|
}
|
|
|
|
&__name {
|
|
font-weight: 700;
|
|
font-size: 13px;
|
|
color: #fff;
|
|
}
|
|
|
|
&__info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
color: #ced4da;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
&__sep {
|
|
color: rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
&__zones {
|
|
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
|
margin-top: 4px;
|
|
padding-top: 5px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
&__zone {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1px;
|
|
}
|
|
|
|
&__zone-name {
|
|
font-weight: 700;
|
|
font-size: 11px;
|
|
margin-bottom: 1px;
|
|
}
|
|
|
|
&__zone-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
color: #ced4da;
|
|
font-size: 11px;
|
|
padding-left: 2px;
|
|
}
|
|
|
|
&__zone-label {
|
|
font-weight: 600;
|
|
font-size: 9px;
|
|
color: #868e96;
|
|
min-width: 24px;
|
|
}
|
|
|
|
&__pos {
|
|
color: #74b9ff;
|
|
font-size: 10px;
|
|
}
|
|
}
|