ship-gis/src/areaSearch/components/StsContactDetailModal.scss
LHT 4945606c1c feat: STS 분석 기능 구현 및 항적분석 고도화
- STS(Ship-to-Ship) 접촉 분석 기능 전체 구현
  - API 연동 (vessel-contacts), 스토어, 레이어 훅, 레이어 레지스트리
  - 접촉 쌍 그룹핑, 그룹 카드 목록, 상세 모달 (그리드 레이아웃)
  - ScatterplotLayer 접촉 포인트 + 위험도 색상
- 항적분석 탭 UI 분리 (구역분석 / STS분석)
  - AreaSearchPage → AreaSearchTab, StsAnalysisTab 추출
  - 탭 전환 시 결과 초기화 확인, 구역 클리어
- 지도 호버 하이라이트 구현 (구역분석 + STS)
  - MapContainer pointermove에 STS 레이어 ID 핸들러 추가
  - STS 쌍 항적 동시 하이라이트 (vesselId → groupIndex 매핑)
  - 목록↔지도 호버 연동 자동 스크롤
  - pickingRadius 12→20 확대
- 재생 컨트롤러(AreaSearchTimeline) STS 지원
  - 항적/궤적 토글 activeTab 기반 스토어 분기
  - 닫기 시 양쪽 스토어 + 레이어 정리
- 패널 닫기 초기화 수정 (isOpen 감지, clearResults로 탭 보존)
- 조회 중 로딩 오버레이 (LoadingOverlay 공통 컴포넌트)
- 항적분석 다중 방문 대응, 선박 상세 모달, 구역 편집 기능
- trackLayer updateTriggers Set 직렬화, highlightedVesselIds 지원

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 06:20:46 +09:00

320 lines
5.3 KiB
SCSS

.sts-detail-overlay {
position: fixed;
inset: 0;
z-index: 300;
background: rgba(0, 0, 0, 0.6);
}
.sts-detail-modal {
position: fixed;
z-index: 301;
width: 680px;
max-height: 90vh;
display: flex;
flex-direction: column;
background: rgba(20, 24, 32, 0.98);
border-radius: 8px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
color: #fff;
overflow: hidden;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
cursor: move;
user-select: none;
}
&__title {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
flex: 1;
}
&__arrow {
color: #4a9eff;
font-weight: 700;
font-size: 14px;
flex-shrink: 0;
}
&__vessel-badge {
display: inline-flex;
align-items: center;
gap: 5px;
min-width: 0;
}
&__kind {
padding: 2px 6px;
background: rgba(255, 255, 255, 0.15);
border-radius: 3px;
font-size: 10px;
color: #adb5bd;
flex-shrink: 0;
}
&__flag {
width: 18px;
height: 13px;
object-fit: contain;
flex-shrink: 0;
}
&__name {
font-weight: 700;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__close {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: #868e96;
font-size: 20px;
cursor: pointer;
border-radius: 4px;
flex-shrink: 0;
&:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
}
&__content {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
}
&__map {
width: 100%;
height: 480px;
flex-shrink: 0;
background: #0d1117;
.ol-scale-line {
bottom: 8px;
left: 8px;
}
}
&__risk-bar {
height: 3px;
flex-shrink: 0;
}
&__section {
padding: 10px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
&:last-child {
border-bottom: none;
}
}
&__section-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 700;
color: #ced4da;
margin: 0 0 8px 0;
}
&__track-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
&__row {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #ced4da;
padding: 2px 0;
}
&__label {
font-weight: 600;
font-size: 11px;
color: #868e96;
min-width: 60px;
flex-shrink: 0;
}
&__pos {
color: #74b9ff;
font-size: 11px;
}
// ========== 그리드 레이아웃 ==========
&__summary-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
}
&__stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
}
&__stat-item {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px 8px;
background: rgba(255, 255, 255, 0.04);
border-radius: 4px;
.stat-label {
font-size: 10px;
font-weight: 600;
color: #868e96;
}
.stat-value {
font-size: 12px;
color: #ced4da;
}
}
&__vessel-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
}
&__vessel-grid-item {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px 8px;
background: rgba(255, 255, 255, 0.04);
border-radius: 4px;
.vessel-item-label {
font-size: 10px;
font-weight: 600;
color: #868e96;
}
.vessel-item-value {
font-size: 12px;
color: #ced4da;
}
}
// ========== 접촉 이력 리스트 ==========
&__contact-list {
display: flex;
flex-direction: column;
gap: 4px;
}
&__contact-item {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
font-size: 11px;
color: #ced4da;
background: rgba(255, 255, 255, 0.03);
border-radius: 3px;
}
&__contact-num {
font-weight: 700;
color: #4a9eff;
min-width: 20px;
}
&__contact-sep {
color: #495057;
font-size: 10px;
}
&__indicators {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
&__badge {
display: inline-block;
padding: 3px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: 600;
&--lowSpeedContact {
background: rgba(46, 204, 113, 0.15);
color: #2ecc71;
}
&--differentVesselTypes {
background: rgba(243, 156, 18, 0.15);
color: #f39c12;
}
&--differentNationalities {
background: rgba(52, 152, 219, 0.15);
color: #3498db;
}
&--nightTimeContact {
background: rgba(155, 89, 182, 0.15);
color: #9b59b6;
}
}
&__footer {
display: flex;
justify-content: flex-end;
padding: 10px 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}
&__save-btn {
padding: 6px 16px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
background: transparent;
color: #ced4da;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
&:hover {
border-color: #4a9eff;
color: #fff;
}
}
}