마커 클릭 선택 패널 추가
This commit is contained in:
@@ -14,6 +14,7 @@ const state = {
|
||||
drivers: new Map(),
|
||||
locations: new Map(),
|
||||
selectedDriverId: "",
|
||||
activeDetailMode: "",
|
||||
vehicleSearch: "",
|
||||
vehicleStatusFilter: "all",
|
||||
eventSource: null,
|
||||
@@ -51,6 +52,12 @@ const elements = {
|
||||
routeFocusPanel: document.getElementById("routeFocusPanel"),
|
||||
routeFocusTitle: document.getElementById("routeFocusTitle"),
|
||||
routeFocusMeta: document.getElementById("routeFocusMeta"),
|
||||
markerActionPanel: document.getElementById("markerActionPanel"),
|
||||
markerActionEyebrow: document.getElementById("markerActionEyebrow"),
|
||||
markerActionTitle: document.getElementById("markerActionTitle"),
|
||||
markerActionMeta: document.getElementById("markerActionMeta"),
|
||||
markerActionSummary: document.getElementById("markerActionSummary"),
|
||||
markerActionCloseButton: document.getElementById("markerActionCloseButton"),
|
||||
driverForm: document.getElementById("driverForm"),
|
||||
driverNameInput: document.getElementById("driverNameInput"),
|
||||
vehicleNoInput: document.getElementById("vehicleNoInput"),
|
||||
@@ -245,6 +252,19 @@ function bindActions() {
|
||||
elements.driversTab.addEventListener("click", () => setActiveView("drivers"));
|
||||
elements.driverForm.addEventListener("submit", submitDriverForm);
|
||||
elements.historyCloseButton.addEventListener("click", closeHistoryDrawer);
|
||||
elements.markerActionCloseButton.addEventListener("click", closeMarkerActionPanel);
|
||||
elements.markerActionPanel.addEventListener("click", (event) => {
|
||||
const actionButton = event.target.closest("[data-marker-action]");
|
||||
if (!actionButton) return;
|
||||
|
||||
const location = state.locations.get(state.selectedDriverId);
|
||||
if (!location) {
|
||||
closeMarkerActionPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
runMarkerAction(actionButton.dataset.markerAction, location);
|
||||
});
|
||||
elements.vehicleSearchInput.addEventListener("input", () => {
|
||||
state.vehicleSearch = elements.vehicleSearchInput.value.trim();
|
||||
render();
|
||||
@@ -390,10 +410,9 @@ function replaceLocations(locations) {
|
||||
if (state.selectedDriverId) {
|
||||
const selectedLocation = state.locations.get(state.selectedDriverId);
|
||||
if (selectedLocation) {
|
||||
renderRoutePreview(selectedLocation, { fit: false });
|
||||
showInfoOverlay(selectedLocation);
|
||||
renderRouteFocusPanel(selectedLocation);
|
||||
refreshSelectedDetail(selectedLocation);
|
||||
} else {
|
||||
closeMarkerActionPanel();
|
||||
clearRouteOverlay();
|
||||
clearInfoOverlay();
|
||||
renderRouteFocusPanel(null);
|
||||
@@ -442,10 +461,8 @@ function applyLocationUpdates(locations) {
|
||||
? state.locations.get(state.selectedDriverId)
|
||||
: null;
|
||||
|
||||
if (selectedLocation && !elements.historyDrawer.classList.contains("is-hidden")) {
|
||||
renderRoutePreview(selectedLocation, { fit: false });
|
||||
showInfoOverlay(selectedLocation);
|
||||
renderRouteFocusPanel(selectedLocation);
|
||||
if (selectedLocation) {
|
||||
refreshSelectedDetail(selectedLocation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -905,13 +922,13 @@ function updateLeafletMarker(location) {
|
||||
marker: L.marker(leafletPosition, { icon, title: location.vehicleNo || location.driverId }),
|
||||
currentPosition: targetPosition
|
||||
};
|
||||
marker.marker.on("click", () => focusLocation(location));
|
||||
marker.marker.on("click", () => openMarkerActionPanel(location));
|
||||
marker.marker.addTo(state.map);
|
||||
state.markers.set(location.driverId, marker);
|
||||
} else {
|
||||
marker.marker.setIcon(icon);
|
||||
marker.marker.off("click");
|
||||
marker.marker.on("click", () => focusLocation(location));
|
||||
marker.marker.on("click", () => openMarkerActionPanel(location));
|
||||
animateMarkerPosition(marker, targetPosition);
|
||||
}
|
||||
}
|
||||
@@ -924,7 +941,10 @@ function createTruckMarkerElement(location, status, heading) {
|
||||
markerElement.title = `${vehicleType.label} ${location.vehicleNo || location.driverId}`;
|
||||
markerElement.setAttribute("aria-label", `${vehicleType.label} ${location.vehicleNo || location.driverId} 위치 보기`);
|
||||
markerElement.innerHTML = createTruckMarkerHtml(location, vehicleType, heading);
|
||||
markerElement.addEventListener("click", () => focusLocation(location));
|
||||
markerElement.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
openMarkerActionPanel(location);
|
||||
});
|
||||
return markerElement;
|
||||
}
|
||||
|
||||
@@ -1117,6 +1137,8 @@ function centerMapOnLocation(location, options = {}) {
|
||||
}
|
||||
|
||||
function renderRoutePreview(location, options = {}) {
|
||||
if (!state.map || !state.mapProvider) return false;
|
||||
|
||||
clearRouteOverlay();
|
||||
|
||||
const routePath = getRoutePath(location);
|
||||
@@ -1236,7 +1258,206 @@ function createDestinationMarkerHtml(location) {
|
||||
`;
|
||||
}
|
||||
|
||||
function refreshSelectedDetail(location) {
|
||||
if (!elements.markerActionPanel.classList.contains("is-hidden")) {
|
||||
renderMarkerActionPanel(location);
|
||||
}
|
||||
|
||||
if (state.activeDetailMode === "route") {
|
||||
renderRouteFocusPanel(location);
|
||||
renderRoutePreview(location, { fit: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.activeDetailMode === "cargo") {
|
||||
renderCargoFocusPanel(location);
|
||||
showInfoOverlay(location);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.activeDetailMode === "history") {
|
||||
renderRouteFocusPanel(location);
|
||||
renderRoutePreview(location, { fit: false });
|
||||
if (state.infoOverlay) {
|
||||
showInfoOverlay(location);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.activeDetailMode === "center") {
|
||||
renderCurrentLocationPanel(location);
|
||||
showInfoOverlay(location);
|
||||
}
|
||||
}
|
||||
|
||||
function openMarkerActionPanel(location) {
|
||||
state.selectedDriverId = location.driverId;
|
||||
state.activeDetailMode = "menu";
|
||||
elements.historyDrawer.classList.add("is-hidden");
|
||||
clearRouteOverlay();
|
||||
clearInfoOverlay();
|
||||
renderRouteFocusPanel(null);
|
||||
renderMarkerActionPanel(location);
|
||||
elements.markerActionPanel.classList.remove("is-hidden");
|
||||
render();
|
||||
}
|
||||
|
||||
function hideMarkerActionPanel() {
|
||||
elements.markerActionPanel.classList.add("is-hidden");
|
||||
}
|
||||
|
||||
function closeMarkerActionPanel() {
|
||||
hideMarkerActionPanel();
|
||||
if (state.activeDetailMode === "menu") {
|
||||
state.activeDetailMode = "";
|
||||
}
|
||||
state.selectedDriverId = "";
|
||||
render();
|
||||
}
|
||||
|
||||
function renderMarkerActionPanel(location) {
|
||||
const status = getLocationStatus(location);
|
||||
const vehicleType = getVehicleType(location);
|
||||
const metrics = getRouteMetrics(location);
|
||||
const driver = location.driverName || location.driverId;
|
||||
const title = location.vehicleNo || location.driverId;
|
||||
const distance = metrics ? formatDistance(metrics.remainingDistanceKm) : "-";
|
||||
const eta = metrics ? formatEtaTime(metrics.estimatedArrivalAt) : "-";
|
||||
|
||||
elements.markerActionEyebrow.textContent = "차량 선택";
|
||||
elements.markerActionTitle.textContent = `${title} · ${vehicleType.label}`;
|
||||
elements.markerActionMeta.textContent = [driver, status.label, formatSpeed(location.speed)].filter(Boolean).join(" · ");
|
||||
elements.markerActionSummary.replaceChildren(
|
||||
createMarkerActionSummaryItem("화물", getCargoSummary(location)),
|
||||
createMarkerActionSummaryItem("목적지", getRouteSummary(location)),
|
||||
createMarkerActionSummaryItem("남은 거리", distance),
|
||||
createMarkerActionSummaryItem("도착 예정", eta)
|
||||
);
|
||||
}
|
||||
|
||||
function createMarkerActionSummaryItem(label, value) {
|
||||
const item = document.createElement("span");
|
||||
const labelElement = document.createElement("b");
|
||||
const valueElement = document.createElement("em");
|
||||
labelElement.textContent = label;
|
||||
valueElement.textContent = value || "-";
|
||||
item.append(labelElement, valueElement);
|
||||
return item;
|
||||
}
|
||||
|
||||
function runMarkerAction(action, location) {
|
||||
if (action === "route") {
|
||||
showVehicleRoute(location);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "cargo") {
|
||||
showVehicleCargo(location);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "history") {
|
||||
showVehicleHistory(location);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "center") {
|
||||
showVehicleCenter(location);
|
||||
}
|
||||
}
|
||||
|
||||
function showVehicleRoute(location) {
|
||||
state.selectedDriverId = location.driverId;
|
||||
state.activeDetailMode = "route";
|
||||
hideMarkerActionPanel();
|
||||
elements.historyDrawer.classList.add("is-hidden");
|
||||
clearInfoOverlay();
|
||||
renderRouteFocusPanel(location);
|
||||
render();
|
||||
|
||||
if (!state.map || !state.mapProvider) return;
|
||||
|
||||
if (!renderRoutePreview(location, { fit: true })) {
|
||||
centerMapOnLocation(location, { zoom: true });
|
||||
}
|
||||
}
|
||||
|
||||
function showVehicleCargo(location) {
|
||||
state.selectedDriverId = location.driverId;
|
||||
state.activeDetailMode = "cargo";
|
||||
hideMarkerActionPanel();
|
||||
elements.historyDrawer.classList.add("is-hidden");
|
||||
clearRouteOverlay();
|
||||
renderCargoFocusPanel(location);
|
||||
render();
|
||||
showInfoOverlay(location);
|
||||
}
|
||||
|
||||
function showVehicleHistory(location) {
|
||||
state.selectedDriverId = location.driverId;
|
||||
state.activeDetailMode = "history";
|
||||
hideMarkerActionPanel();
|
||||
clearInfoOverlay();
|
||||
renderRouteFocusPanel(location);
|
||||
render();
|
||||
renderRoutePreview(location, { fit: false });
|
||||
loadLocationHistory(location);
|
||||
}
|
||||
|
||||
function showVehicleCenter(location) {
|
||||
state.selectedDriverId = location.driverId;
|
||||
state.activeDetailMode = "center";
|
||||
hideMarkerActionPanel();
|
||||
elements.historyDrawer.classList.add("is-hidden");
|
||||
clearRouteOverlay();
|
||||
renderCurrentLocationPanel(location);
|
||||
render();
|
||||
|
||||
if (state.map && state.mapProvider) {
|
||||
centerMapOnLocation(location, { zoom: true });
|
||||
}
|
||||
showInfoOverlay(location);
|
||||
}
|
||||
|
||||
function setRouteFocusEyebrow(label) {
|
||||
const eyebrow = elements.routeFocusPanel.querySelector(".section-eyebrow");
|
||||
if (eyebrow) {
|
||||
eyebrow.textContent = label;
|
||||
}
|
||||
}
|
||||
|
||||
function setRouteFocusContent(eyebrow, title, meta) {
|
||||
setRouteFocusEyebrow(eyebrow);
|
||||
elements.routeFocusPanel.classList.add("is-active");
|
||||
elements.routeFocusTitle.textContent = title;
|
||||
elements.routeFocusMeta.textContent = meta;
|
||||
}
|
||||
|
||||
function renderCargoFocusPanel(location) {
|
||||
const vehicleType = getVehicleType(location);
|
||||
const product = getCargoName(location) || "미등록";
|
||||
const quantity = getCargoQuantity(location) || "수량 미등록";
|
||||
const weight = getCargoWeight(location) || "중량 미등록";
|
||||
const route = getRouteSummary(location);
|
||||
setRouteFocusContent(
|
||||
"화물 정보",
|
||||
`${location.vehicleNo || location.driverId} · ${vehicleType.label}`,
|
||||
`${product} · ${quantity} · ${weight} · ${route}`
|
||||
);
|
||||
}
|
||||
|
||||
function renderCurrentLocationPanel(location) {
|
||||
const vehicleType = getVehicleType(location);
|
||||
const coordinate = `${formatCoordinate(location.latitude)}, ${formatCoordinate(location.longitude)}`;
|
||||
setRouteFocusContent(
|
||||
"현재 위치",
|
||||
`${location.vehicleNo || location.driverId} · ${vehicleType.label}`,
|
||||
`${formatSpeed(location.speed)} · ${coordinate} · ${formatDateTime(location.receivedAt)}`
|
||||
);
|
||||
}
|
||||
|
||||
function renderRouteFocusPanel(location) {
|
||||
setRouteFocusEyebrow("선택 경로");
|
||||
if (!location) {
|
||||
elements.routeFocusPanel.classList.remove("is-active");
|
||||
elements.routeFocusTitle.textContent = "차량 선택";
|
||||
@@ -1258,6 +1479,8 @@ function renderRouteFocusPanel(location) {
|
||||
}
|
||||
|
||||
function focusLocation(location) {
|
||||
state.activeDetailMode = "history";
|
||||
hideMarkerActionPanel();
|
||||
loadLocationHistory(location);
|
||||
renderRouteFocusPanel(location);
|
||||
render();
|
||||
@@ -1272,6 +1495,8 @@ function focusLocation(location) {
|
||||
|
||||
async function loadLocationHistory(location) {
|
||||
state.selectedDriverId = location.driverId;
|
||||
state.activeDetailMode = "history";
|
||||
hideMarkerActionPanel();
|
||||
elements.historyDrawer.classList.remove("is-hidden");
|
||||
elements.historyTitle.textContent = `${location.vehicleNo || location.driverId} 위치 이력`;
|
||||
elements.historySummary.textContent = "조회 중";
|
||||
@@ -1321,7 +1546,9 @@ function renderHistory(location, records) {
|
||||
|
||||
function closeHistoryDrawer() {
|
||||
state.selectedDriverId = "";
|
||||
state.activeDetailMode = "";
|
||||
elements.historyDrawer.classList.add("is-hidden");
|
||||
hideMarkerActionPanel();
|
||||
clearRouteOverlay();
|
||||
clearInfoOverlay();
|
||||
renderRouteFocusPanel(null);
|
||||
@@ -1329,6 +1556,8 @@ function closeHistoryDrawer() {
|
||||
}
|
||||
|
||||
function showInfoOverlay(location) {
|
||||
if (!state.map || !state.mapProvider) return;
|
||||
|
||||
if (shouldSuppressMapPopup()) {
|
||||
clearInfoOverlay();
|
||||
return;
|
||||
|
||||
@@ -166,6 +166,23 @@
|
||||
<strong id="routeFocusTitle">차량 선택</strong>
|
||||
<p id="routeFocusMeta">선택하면 경로와 도착 예정 시간이 표시됩니다.</p>
|
||||
</section>
|
||||
<section id="markerActionPanel" class="marker-action-panel is-hidden" aria-label="차량 빠른 선택">
|
||||
<div class="marker-action-head">
|
||||
<div>
|
||||
<span id="markerActionEyebrow" class="section-eyebrow">차량 선택</span>
|
||||
<strong id="markerActionTitle">-</strong>
|
||||
<p id="markerActionMeta">-</p>
|
||||
</div>
|
||||
<button id="markerActionCloseButton" type="button" aria-label="선택 닫기">닫기</button>
|
||||
</div>
|
||||
<div id="markerActionSummary" class="marker-action-summary"></div>
|
||||
<div class="marker-action-buttons">
|
||||
<button type="button" data-marker-action="route">전체 경로</button>
|
||||
<button type="button" data-marker-action="cargo">화물 정보</button>
|
||||
<button type="button" data-marker-action="history">위치 이력</button>
|
||||
<button type="button" data-marker-action="center">현재 위치</button>
|
||||
</div>
|
||||
</section>
|
||||
<div class="map-legend" aria-label="지도 범례">
|
||||
<span><i class="legend-dot active"></i>수신 중</span>
|
||||
<span><i class="legend-dot stale"></i>주의</span>
|
||||
|
||||
@@ -819,6 +819,127 @@ button:hover {
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.marker-action-panel {
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
bottom: 104px;
|
||||
z-index: 6;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
width: min(380px, calc(100% - 36px));
|
||||
padding: 14px;
|
||||
border: 1px solid rgba(22, 34, 49, 0.14);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.97);
|
||||
box-shadow: var(--shadow-strong);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.marker-action-panel.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.marker-action-head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.marker-action-head strong {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--navy);
|
||||
font-size: 17px;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.marker-action-head p {
|
||||
margin: 5px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.marker-action-head button {
|
||||
min-width: 52px;
|
||||
min-height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: #f6f8fb;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.marker-action-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.marker-action-summary span {
|
||||
min-width: 0;
|
||||
padding: 8px;
|
||||
border-radius: 7px;
|
||||
background: #f4f7fa;
|
||||
}
|
||||
|
||||
.marker-action-summary b,
|
||||
.marker-action-summary em {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.marker-action-summary b {
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.marker-action-summary em {
|
||||
margin-top: 3px;
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.marker-action-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.marker-action-buttons button {
|
||||
min-width: 0;
|
||||
min-height: 38px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid rgba(31, 111, 235, 0.18);
|
||||
border-radius: 7px;
|
||||
background: #eef5ff;
|
||||
color: #174ea6;
|
||||
font-size: 13px;
|
||||
font-weight: 950;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.marker-action-buttons button:hover,
|
||||
.marker-action-head button:hover {
|
||||
filter: brightness(0.98);
|
||||
}
|
||||
|
||||
.map-legend {
|
||||
top: 18px;
|
||||
right: 86px;
|
||||
@@ -1289,6 +1410,12 @@ button:hover {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.marker-action-panel {
|
||||
right: 10px;
|
||||
bottom: 118px;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.driver-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -1342,6 +1469,22 @@ button:hover {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.marker-action-panel {
|
||||
top: 10px;
|
||||
bottom: auto;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.marker-action-head strong {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.marker-action-summary,
|
||||
.marker-action-buttons {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.history-drawer {
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
|
||||
Reference in New Issue
Block a user