- 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
577 lines
19 KiB
HTML
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>
|