마커 클릭 선택 패널 추가

This commit is contained in:
2026-06-06 22:06:16 +09:00
parent 5278b8cf06
commit 1bf346a898
3 changed files with 399 additions and 10 deletions

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;