signal-batch/src/main/resources/static/gis-monitoring.html
HeungTak Lee 89482d854f feat: Add V2 REST API with WebSocket-compatible responses
- Add GisControllerV2/GisServiceV2 for CompactVesselTrack responses
- Add nationalCode and shipKindCode fields to REST API responses
- Add flexible DateTime parsing support (multiple formats)
- Add TrackConverter utility for track data conversion
- Update SwaggerConfig with V2 API endpoints and unified tags
- Update ProdDataSourceConfig for prod-mpr profile support
- Enhance Swagger documentation for all DTOs
2026-01-20 13:38:31 +09:00

577 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GIS 모니터링 - 선박 항적</title>
<!-- Local CSS -->
<link href="/libs/css/bootstrap.min.css" rel="stylesheet">
<link href="/libs/css/bootstrap-icons.css" rel="stylesheet">
<link href="/libs/css/maplibre-gl.css" rel="stylesheet">
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
}
#mapContainer {
width: 100vw;
height: 100vh;
position: relative;
background: #1a1a1a;
}
.control-panel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.95);
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
z-index: 1000;
max-width: 400px;
min-width: 350px;
}
.map-controls {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
background: white;
padding: 10px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
width: 280px;
}
.vessel-tooltip {
position: absolute;
background: rgba(255, 255, 255, 0.95);
padding: 10px;
border-radius: 4px;
font-size: 12px;
pointer-events: none;
z-index: 9999;
max-width: 300px;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.legend {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.95);
padding: 15px;
border-radius: 4px;
font-size: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
z-index: 1002;
}
.legend-item {
display: flex;
align-items: center;
margin: 5px 0;
}
.legend-color {
width: 20px;
height: 20px;
margin-right: 8px;
border: 1px solid #333;
}
.legend-gradient {
width: 200px;
height: 20px;
margin: 10px 0;
background: linear-gradient(to right, #e0e0e0 0%, #90EE90 25%, #FFD700 50%, #FFA500 75%, #FF4500 100%);
border: 1px solid #333;
}
.legend-labels {
display: flex;
justify-content: space-between;
font-size: 11px;
margin-top: 5px;
}
/* Hexagon Legend Styles */
.hexagon-legend {
display: none;
}
.hexagon-legend .legend-gradient {
background: linear-gradient(to right,
rgba(1, 152, 189, 0.8) 0%,
rgba(73, 227, 206, 0.8) 25%,
rgba(216, 254, 181, 0.8) 50%,
rgba(254, 237, 177, 0.8) 65%,
rgba(254, 173, 84, 0.8) 80%,
rgba(209, 55, 78, 0.8) 100%);
}
/* Layer Toggle Button Group */
.layer-toggle-group {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
padding: 5px;
}
.layer-toggle-group .btn {
min-width: 100px;
}
.layer-toggle-group .btn.active {
box-shadow: inset 0 2px 4px rgba(0,0,0,0.2);
}
.track-info-panel {
position: absolute;
top: 10px;
left: 420px;
width: 350px;
max-height: 400px;
background: rgba(255, 255, 255, 0.95);
border-radius: 4px;
padding: 15px;
overflow-y: auto;
display: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
z-index: 1001;
}
.vessel-item {
padding: 8px;
border-bottom: 1px solid #ddd;
cursor: pointer;
display: flex;
align-items: center;
transition: background 0.2s;
}
.vessel-item:hover {
background: #f0f0f0;
}
.vessel-item.selected {
background: #e3f2fd;
}
.vessel-checkbox {
margin-right: 10px;
}
.vessel-info {
flex: 1;
}
.sort-controls {
display: flex;
gap: 10px;
margin-bottom: 10px;
align-items: center;
}
.filter-controls {
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
}
.selected-count {
background: #007bff;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
margin-left: 10px;
}
/* Sequential Passage Panel Styles */
.sequential-panel {
position: absolute;
bottom: 20px;
right: 20px;
width: 380px;
background: rgba(255, 255, 255, 0.95);
border-radius: 4px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
z-index: 1001;
display: none;
}
.zone-selector {
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
background: #f8f9fa;
}
.zone-tag {
display: inline-block;
background: #007bff;
color: white;
padding: 4px 8px;
border-radius: 4px;
margin: 2px;
font-size: 12px;
}
.zone-tag .remove-zone {
margin-left: 5px;
cursor: pointer;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-spinner {
color: white;
font-size: 48px;
}
.context-menu {
position: absolute;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
padding: 0;
margin: 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
z-index: 10000;
display: none;
min-width: 150px;
}
.context-menu-item {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #eee;
font-size: 14px;
}
.context-menu-item:hover {
background: #f0f0f0;
}
.context-menu-item:last-child {
border-bottom: none;
}
.context-menu-item.disabled {
color: #999;
cursor: not-allowed;
}
.context-menu-item.disabled:hover {
background: white;
}
</style>
</head>
<body>
<div id="mapContainer">
<!-- Layer Toggle Buttons -->
<div class="layer-toggle-group btn-group" role="group" aria-label="Layer toggle">
<button type="button" class="btn btn-primary active" id="gridLayerBtn" onclick="GISApp.setVisualizationMode('grid')">
<i class="bi bi-grid-3x3"></i> Grid
</button>
<button type="button" class="btn btn-outline-primary" id="hexagonLayerBtn" onclick="GISApp.setVisualizationMode('hexagon')">
<i class="bi bi-hexagon"></i> Hexagon
</button>
</div>
<div class="control-panel">
<h5 class="mb-3">GIS 모니터링</h5>
<div class="mb-2">
<label class="form-label">조회 기간</label>
<select class="form-select form-select-sm" id="gisTimeRange">
<option value="60" selected>최근 1시간</option>
<option value="180">최근 3시간</option>
<option value="360">최근 6시간</option>
<option value="720">최근 12시간</option>
<option value="1440">최근 24시간</option>
</select>
</div>
<div class="mb-2">
<label class="form-label">레이어 유형</label>
<select class="form-select form-select-sm" id="gisLayerType">
<option value="haegu">대해구</option>
<option value="area">사용자 정의 구역</option>
<option value="both" selected>전체</option>
</select>
</div>
<div class="mb-2">
<label class="form-label">항적 조회 기간</label>
<select class="form-select form-select-sm" id="trackPeriod">
<option value="60">최근 1시간</option>
<option value="180">최근 3시간</option>
<option value="360">최근 6시간</option>
<option value="720">최근 12시간</option>
<option value="1440">최근 24시간</option>
</select>
</div>
<button class="btn btn-primary btn-sm w-100 mb-2" onclick="GISApp.refreshData()">
<i class="bi bi-arrow-clockwise"></i> 새로고침
</button>
<button class="btn btn-success btn-sm w-100" onclick="GISApp.toggleSequentialPanel()">
<i class="bi bi-diagram-3"></i> 순차 통과 분석
</button>
<!-- Vessel List Section -->
<div id="vesselListSection" style="display: none;">
<hr>
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="d-flex align-items-center">
<h6 id="vesselListTitle" class="mb-0">구역 내 선박</h6>
<span class="selected-count" id="selectedCount" style="display: none;">0</span>
</div>
<button class="btn btn-sm btn-secondary" onclick="GISApp.hideVesselList()">
<i class="bi bi-chevron-up"></i>
</button>
</div>
<!-- Sort Controls -->
<div class="sort-controls">
<label class="form-label mb-0 me-2">정렬:</label>
<select class="form-select form-select-sm" id="sortBy" style="width: auto;">
<option value="id">ID</option>
<option value="speed">속력</option>
<option value="distance">거리</option>
</select>
<select class="form-select form-select-sm" id="sortOrder" style="width: auto;">
<option value="asc">오름차순</option>
<option value="desc">내림차순</option>
</select>
</div>
<!-- Action Buttons -->
<div class="mb-2">
<button class="btn btn-sm btn-primary" onclick="GISApp.selectAllVessels()">
<i class="bi bi-check-all"></i> 전체 선택
</button>
<button class="btn btn-sm btn-secondary" onclick="GISApp.deselectAllVessels()">
<i class="bi bi-x-square"></i> 선택 해제
</button>
<button class="btn btn-sm btn-success" onclick="GISApp.showSelectedTracks()">
<i class="bi bi-geo-alt"></i> 항적 표시
</button>
</div>
<div id="vesselListContent" style="max-height: 400px; overflow-y: auto;"></div>
</div>
</div>
<div class="map-controls">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="showTracks">
<label class="form-check-label" for="showTracks">
선박 항적 표시
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="showAllTracks">
<label class="form-check-label" for="showAllTracks">
전체 항적 표시
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="showHeatmap">
<label class="form-check-label" for="showHeatmap">
밀도 히트맵 표시
</label>
</div>
<!-- Track Filters -->
<div class="filter-controls">
<h6>필터 설정</h6>
<div class="mb-2">
<label class="form-label small mb-1">속력 범위 (kts)</label>
<div class="d-flex align-items-center gap-2">
<input type="number" class="form-control form-control-sm" id="minSpeedFilter"
min="0" max="50" value="0" step="0.5" style="width: 70px;">
<span>-</span>
<input type="number" class="form-control form-control-sm" id="maxSpeedFilter"
min="0" max="50" value="50" step="0.5" style="width: 70px;">
</div>
</div>
<div class="mb-2">
<label class="form-label small mb-1">거리 범위 (nm)</label>
<div class="d-flex align-items-center gap-2">
<input type="number" class="form-control form-control-sm" id="minDistFilter"
min="0" max="200" value="0" step="1" style="width: 70px;">
<span>-</span>
<input type="number" class="form-control form-control-sm" id="maxDistFilter"
min="0" max="200" value="200" step="1" style="width: 70px;">
</div>
</div>
<button class="btn btn-sm btn-primary w-100" onclick="GISApp.applyFilters()">
<i class="bi bi-funnel"></i> 필터 적용
</button>
<button class="btn btn-sm btn-secondary w-100 mt-1" onclick="GISApp.resetFilters()">
<i class="bi bi-arrow-counterclockwise"></i> 필터 초기화
</button>
</div>
</div>
<div class="track-info-panel" id="trackInfoPanel">
<h5 id="trackPanelTitle">구역 정보</h5>
<div id="trackPanelContent"></div>
</div>
<!-- Sequential Passage Panel -->
<div class="sequential-panel" id="sequentialPanel">
<h5 class="mb-3">순차 통과 분석</h5>
<div class="mb-3">
<label class="form-label">통과 모드</label>
<select class="form-select form-select-sm" id="passageMode">
<option value="sequential">순차 통과</option>
<option value="all">전체 구역</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">구역 유형</label>
<select class="form-select form-select-sm" id="passageZoneType">
<option value="GRID">대해구</option>
<option value="AREA">사용자 정의 구역</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">조회 기간</label>
<div class="row g-2">
<div class="col-6">
<input type="datetime-local" class="form-control form-control-sm" id="passageStartTime">
</div>
<div class="col-6">
<input type="datetime-local" class="form-control form-control-sm" id="passageEndTime">
</div>
</div>
</div>
<div class="zone-selector">
<label class="form-label">선택 구역 (최대 3개)</label>
<div id="selectedZones" class="mb-2"></div>
<div class="input-group input-group-sm">
<input type="text" class="form-control" id="zoneInput" placeholder="구역 ID 입력">
<button class="btn btn-outline-secondary" onclick="GISApp.addZone()">
<i class="bi bi-plus"></i> 추가
</button>
</div>
</div>
<button class="btn btn-primary btn-sm w-100 mb-2" onclick="GISApp.searchSequentialPassage()">
<i class="bi bi-search"></i> 선박 검색
</button>
<button class="btn btn-secondary btn-sm w-100" onclick="GISApp.toggleSequentialPanel()">
<i class="bi bi-x"></i> 닫기
</button>
<div id="sequentialResults" class="mt-3" style="max-height: 300px; overflow-y: auto;"></div>
</div>
<div class="legend" id="gridLegend">
<h6>범례</h6>
<div class="mb-2"><strong>선박 수</strong></div>
<div class="legend-gradient"></div>
<div class="legend-labels">
<span>0</span>
<span>10</span>
<span>50</span>
<span>100</span>
<span>200+</span>
</div>
<hr class="my-2">
<div class="legend-item">
<div class="legend-color" style="background: rgba(0, 100, 200, 0.5); border: 2px solid #0064C8;"></div>
<span>대해구</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: rgba(200, 100, 0, 0.5); border: 2px solid #C86400;"></div>
<span>사용자 정의 구역</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #00ff00;"></div>
<span>선박 항적</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #0080ff;"></div>
<span>선택된 항적</span>
</div>
</div>
<!-- Hexagon Layer Legend -->
<div class="legend hexagon-legend" id="hexagonLegend">
<h6>Hexagon 범례</h6>
<div class="mb-2"><strong>선박 밀도</strong></div>
<div class="legend-gradient"></div>
<div class="legend-labels" id="hexagonLegendLabels">
<span>0</span>
<span>5</span>
<span>10</span>
<span>20</span>
<span>50+</span>
</div>
<hr class="my-2">
<div id="hexagonStats">
<div><strong>총 선박 수:</strong> <span id="hexTotalVessels">0</span></div>
<div><strong>셀 당 최대:</strong> <span id="hexMaxCount">0</span></div>
</div>
<div class="mt-2">
<small class="text-muted"><i class="bi bi-info-circle"></i> Hexagon 클릭 시 선박 목록 표시</small>
</div>
</div>
</div>
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-spinner">
<i class="bi bi-hourglass-split"></i>
</div>
</div>
<div class="vessel-tooltip" id="vesselTooltip" style="display: none;"></div>
<!-- Context Menu -->
<div class="context-menu" id="contextMenu">
<div class="context-menu-item" id="addToSequential">
<i class="bi bi-plus-circle"></i> 순차 통과에 추가
</div>
<div class="context-menu-item" id="viewAreaDetails">
<i class="bi bi-info-circle"></i> 상세 정보 보기
</div>
</div>
<!-- Local JS -->
<script src="/libs/js/jquery-3.6.0.min.js"></script>
<script src="/libs/js/bootstrap.bundle.min.js"></script>
<script src="/libs/js/maplibre-gl.js"></script>
<script src="/libs/js/deck.gl.min.js"></script>
<script src="/js/gis-monitoring.js"></script>
</body>
</html>