전국 화물차 시뮬레이터 추가
This commit is contained in:
@@ -30,6 +30,7 @@ const elements = {
|
||||
driverForm: document.getElementById("driverForm"),
|
||||
driverNameInput: document.getElementById("driverNameInput"),
|
||||
vehicleNoInput: document.getElementById("vehicleNoInput"),
|
||||
vehicleTypeInput: document.getElementById("vehicleTypeInput"),
|
||||
phoneInput: document.getElementById("phoneInput"),
|
||||
driverIdInput: document.getElementById("driverIdInput"),
|
||||
driverCount: document.getElementById("driverCount"),
|
||||
@@ -41,6 +42,44 @@ const elements = {
|
||||
historyList: document.getElementById("historyList")
|
||||
};
|
||||
|
||||
const VEHICLE_TYPES = {
|
||||
"cargo-truck": {
|
||||
label: "카고트럭",
|
||||
shortLabel: "카고",
|
||||
className: "type-cargo"
|
||||
},
|
||||
"tractor-trailer": {
|
||||
label: "트레일러",
|
||||
shortLabel: "트레",
|
||||
className: "type-trailer"
|
||||
},
|
||||
"box-truck": {
|
||||
label: "탑차",
|
||||
shortLabel: "탑차",
|
||||
className: "type-box"
|
||||
},
|
||||
"van": {
|
||||
label: "용달차",
|
||||
shortLabel: "용달",
|
||||
className: "type-van"
|
||||
},
|
||||
"refrigerated": {
|
||||
label: "냉동탑차",
|
||||
shortLabel: "냉동",
|
||||
className: "type-cold"
|
||||
},
|
||||
"container": {
|
||||
label: "컨테이너",
|
||||
shortLabel: "컨",
|
||||
className: "type-container"
|
||||
},
|
||||
"wingbody": {
|
||||
label: "윙바디",
|
||||
shortLabel: "윙",
|
||||
className: "type-wing"
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initializeMap();
|
||||
bindActions();
|
||||
@@ -232,7 +271,8 @@ function connectEvents() {
|
||||
replaceLocations(payload.locations || []);
|
||||
});
|
||||
state.eventSource.addEventListener("location.updated", (event) => {
|
||||
upsertLocation(JSON.parse(event.data), { focus: true });
|
||||
const location = JSON.parse(event.data);
|
||||
upsertLocation(location, { focus: state.selectedDriverId === location.driverId });
|
||||
});
|
||||
state.eventSource.addEventListener("drivers.updated", (event) => {
|
||||
const payload = JSON.parse(event.data);
|
||||
@@ -257,6 +297,7 @@ async function postDemoLocation() {
|
||||
body: JSON.stringify({
|
||||
driverId: driver.driverId,
|
||||
vehicleNo: driver.vehicleNo,
|
||||
vehicleType: driver.vehicleType || "cargo-truck",
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy: 12,
|
||||
@@ -375,6 +416,7 @@ function renderDrivers() {
|
||||
|
||||
function createVehicleItem(location) {
|
||||
const status = getLocationStatus(location);
|
||||
const vehicleType = getVehicleType(location);
|
||||
const item = document.createElement("article");
|
||||
item.className = "vehicle-item";
|
||||
item.tabIndex = 0;
|
||||
@@ -389,14 +431,18 @@ function createVehicleItem(location) {
|
||||
badge.className = `badge ${status.key}`;
|
||||
badge.textContent = status.label;
|
||||
|
||||
const typeBadge = document.createElement("span");
|
||||
typeBadge.className = `vehicle-type-chip ${vehicleType.className}`;
|
||||
typeBadge.textContent = vehicleType.label;
|
||||
|
||||
const vehicleNo = document.createElement("strong");
|
||||
vehicleNo.textContent = location.vehicleNo || location.driverId;
|
||||
|
||||
title.append(badge, vehicleNo);
|
||||
title.append(badge, typeBadge, vehicleNo);
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "vehicle-meta";
|
||||
meta.textContent = `${location.driverName || location.driverId} · ${formatDateTime(location.receivedAt)} · ${formatCoordinate(location.latitude)}, ${formatCoordinate(location.longitude)}`;
|
||||
meta.textContent = `${location.driverName || location.driverId} · ${getCargoSummary(location)} · ${getRouteSummary(location)} · ${formatDateTime(location.receivedAt)}`;
|
||||
|
||||
const speed = document.createElement("div");
|
||||
speed.className = "speed";
|
||||
@@ -417,6 +463,7 @@ function createVehicleItem(location) {
|
||||
}
|
||||
|
||||
function createDriverItem(driver) {
|
||||
const vehicleType = getVehicleType(driver);
|
||||
const item = document.createElement("article");
|
||||
item.className = "driver-item";
|
||||
|
||||
@@ -431,7 +478,7 @@ function createDriverItem(driver) {
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "vehicle-meta";
|
||||
meta.textContent = [driver.driverId, driver.phone].filter(Boolean).join(" · ");
|
||||
meta.textContent = [vehicleType.label, driver.driverId, driver.phone].filter(Boolean).join(" · ");
|
||||
|
||||
title.append(name, meta);
|
||||
|
||||
@@ -483,6 +530,7 @@ async function submitDriverForm(event) {
|
||||
const payload = {
|
||||
name: elements.driverNameInput.value.trim(),
|
||||
vehicleNo: elements.vehicleNoInput.value.trim(),
|
||||
vehicleType: elements.vehicleTypeInput.value,
|
||||
phone: elements.phoneInput.value.trim(),
|
||||
driverId: elements.driverIdInput.value.trim()
|
||||
};
|
||||
@@ -608,12 +656,13 @@ function updateKakaoMarker(location) {
|
||||
|
||||
function updateLeafletMarker(location) {
|
||||
const status = getLocationStatus(location);
|
||||
const vehicleType = getVehicleType(location);
|
||||
const position = [location.latitude, location.longitude];
|
||||
const icon = L.divIcon({
|
||||
className: `leaflet-truck-marker truck-marker ${status.key}`,
|
||||
html: createTruckMarkerHtml(location, status),
|
||||
iconSize: [58, 34],
|
||||
iconAnchor: [29, 34],
|
||||
className: `leaflet-truck-marker truck-marker ${status.key} ${vehicleType.className}`,
|
||||
html: createTruckMarkerHtml(location, vehicleType),
|
||||
iconSize: [74, 38],
|
||||
iconAnchor: [37, 38],
|
||||
popupAnchor: [0, -30]
|
||||
});
|
||||
|
||||
@@ -636,20 +685,24 @@ function updateLeafletMarker(location) {
|
||||
}
|
||||
|
||||
function createTruckMarkerElement(location, status) {
|
||||
const vehicleType = getVehicleType(location);
|
||||
const markerElement = document.createElement("button");
|
||||
markerElement.className = `truck-marker ${status.key}`;
|
||||
markerElement.className = `truck-marker ${status.key} ${vehicleType.className}`;
|
||||
markerElement.type = "button";
|
||||
markerElement.title = location.vehicleNo || location.driverId;
|
||||
markerElement.setAttribute("aria-label", `${location.vehicleNo || location.driverId} 위치 보기`);
|
||||
markerElement.innerHTML = createTruckMarkerHtml(location, status);
|
||||
markerElement.title = `${vehicleType.label} ${location.vehicleNo || location.driverId}`;
|
||||
markerElement.setAttribute("aria-label", `${vehicleType.label} ${location.vehicleNo || location.driverId} 위치 보기`);
|
||||
markerElement.innerHTML = createTruckMarkerHtml(location, vehicleType);
|
||||
markerElement.addEventListener("click", () => focusLocation(location));
|
||||
return markerElement;
|
||||
}
|
||||
|
||||
function createTruckMarkerHtml(location) {
|
||||
function createTruckMarkerHtml(location, vehicleType = getVehicleType(location)) {
|
||||
const label = location.vehicleNo ? location.vehicleNo.slice(-4) : location.driverId.slice(0, 4);
|
||||
return `
|
||||
<span class="truck-marker__trailer">${escapeHtml(label)}</span>
|
||||
<span class="truck-marker__body">
|
||||
<span class="truck-marker__type">${escapeHtml(vehicleType.shortLabel)}</span>
|
||||
<span class="truck-marker__plate">${escapeHtml(label)}</span>
|
||||
</span>
|
||||
<span class="truck-marker__cab"></span>
|
||||
`;
|
||||
}
|
||||
@@ -793,6 +846,7 @@ function showInfoOverlay(location) {
|
||||
}
|
||||
|
||||
function createPopupElement(location, status) {
|
||||
const vehicleType = getVehicleType(location);
|
||||
const container = document.createElement("div");
|
||||
container.className = "kakao-popup";
|
||||
|
||||
@@ -801,7 +855,7 @@ function createPopupElement(location, status) {
|
||||
title.textContent = location.vehicleNo || location.driverId;
|
||||
|
||||
const driver = document.createElement("div");
|
||||
driver.textContent = `${location.driverName || location.driverId} · ${status.label}`;
|
||||
driver.textContent = `${vehicleType.label} · ${location.driverName || location.driverId} · ${status.label}`;
|
||||
|
||||
const cargo = document.createElement("div");
|
||||
cargo.className = "popup-cargo";
|
||||
@@ -872,6 +926,11 @@ function hideMapMessage() {
|
||||
elements.mapMessage.classList.add("is-hidden");
|
||||
}
|
||||
|
||||
function getVehicleType(source) {
|
||||
const key = String(source.vehicleType || "cargo-truck").trim();
|
||||
return VEHICLE_TYPES[key] || VEHICLE_TYPES["cargo-truck"];
|
||||
}
|
||||
|
||||
function getLocationStatus(location) {
|
||||
const receivedAt = new Date(location.receivedAt).getTime();
|
||||
const ageMs = Date.now() - receivedAt;
|
||||
|
||||
@@ -65,6 +65,18 @@
|
||||
<span>차량번호</span>
|
||||
<input id="vehicleNoInput" name="vehicleNo" required autocomplete="off" />
|
||||
</label>
|
||||
<label>
|
||||
<span>차량 유형</span>
|
||||
<select id="vehicleTypeInput" name="vehicleType">
|
||||
<option value="cargo-truck">카고트럭</option>
|
||||
<option value="tractor-trailer">트레일러</option>
|
||||
<option value="box-truck">탑차</option>
|
||||
<option value="van">용달차</option>
|
||||
<option value="refrigerated">냉동탑차</option>
|
||||
<option value="container">컨테이너</option>
|
||||
<option value="wingbody">윙바디</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>전화번호</span>
|
||||
<input id="phoneInput" name="phone" autocomplete="off" />
|
||||
|
||||
@@ -236,8 +236,7 @@ button:hover {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.driver-form label:nth-child(3),
|
||||
.driver-form label:nth-child(4),
|
||||
.driver-form label:nth-child(n + 3),
|
||||
.driver-form button {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
@@ -248,7 +247,8 @@ button:hover {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.driver-form input {
|
||||
.driver-form input,
|
||||
.driver-form select {
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
border: 1px solid var(--border);
|
||||
@@ -259,7 +259,8 @@ button:hover {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.driver-form input:focus {
|
||||
.driver-form input:focus,
|
||||
.driver-form select:focus {
|
||||
border-color: var(--blue);
|
||||
outline: 2px solid rgba(31, 111, 235, 0.14);
|
||||
}
|
||||
@@ -388,6 +389,34 @@ button:hover {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.vehicle-type-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
padding: 0 7px;
|
||||
border-radius: 5px;
|
||||
background: #e8eef6;
|
||||
color: #2c3b4f;
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.vehicle-type-chip.type-trailer,
|
||||
.vehicle-type-chip.type-container {
|
||||
background: rgba(44, 62, 80, 0.1);
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.vehicle-type-chip.type-van {
|
||||
background: rgba(2, 132, 199, 0.12);
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
.vehicle-type-chip.type-cold {
|
||||
background: rgba(14, 116, 144, 0.12);
|
||||
color: #0e7490;
|
||||
}
|
||||
|
||||
.speed {
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
@@ -501,19 +530,80 @@ button:hover {
|
||||
.truck-marker {
|
||||
--truck-color: var(--blue);
|
||||
--truck-cab: #1556b8;
|
||||
--truck-accent: rgba(255, 255, 255, 0.2);
|
||||
--body-width: 42px;
|
||||
--body-height: 24px;
|
||||
--cab-width: 19px;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
width: max-content;
|
||||
min-width: 56px;
|
||||
min-height: 34px;
|
||||
min-width: calc(var(--body-width) + var(--cab-width));
|
||||
min-height: 38px;
|
||||
border: 0;
|
||||
padding: 0 0 7px;
|
||||
padding: 0 0 8px;
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
filter: drop-shadow(0 8px 13px rgba(16, 32, 48, 0.28));
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: filter 0.18s ease, transform 0.18s ease;
|
||||
}
|
||||
|
||||
.truck-marker:hover {
|
||||
filter: drop-shadow(0 10px 16px rgba(16, 32, 48, 0.34));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.truck-marker.stale {
|
||||
--truck-color: var(--amber);
|
||||
--truck-cab: #8f4307;
|
||||
}
|
||||
|
||||
.truck-marker.offline {
|
||||
--truck-color: var(--red);
|
||||
--truck-cab: #8f1d15;
|
||||
}
|
||||
|
||||
.truck-marker.type-trailer {
|
||||
--truck-color: #334155;
|
||||
--truck-cab: #0f172a;
|
||||
--body-width: 62px;
|
||||
--body-height: 24px;
|
||||
}
|
||||
|
||||
.truck-marker.type-container {
|
||||
--truck-color: #475569;
|
||||
--truck-cab: #1e293b;
|
||||
--body-width: 58px;
|
||||
--body-height: 24px;
|
||||
--truck-accent: repeating-linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0.18) 0 2px,
|
||||
transparent 2px 8px
|
||||
);
|
||||
}
|
||||
|
||||
.truck-marker.type-box,
|
||||
.truck-marker.type-wing {
|
||||
--body-width: 48px;
|
||||
--body-height: 25px;
|
||||
}
|
||||
|
||||
.truck-marker.type-van {
|
||||
--truck-color: #0284c7;
|
||||
--truck-cab: #0369a1;
|
||||
--body-width: 34px;
|
||||
--body-height: 21px;
|
||||
--cab-width: 22px;
|
||||
}
|
||||
|
||||
.truck-marker.type-cold {
|
||||
--truck-color: #0e7490;
|
||||
--truck-cab: #155e75;
|
||||
--body-width: 47px;
|
||||
--body-height: 25px;
|
||||
--truck-accent: linear-gradient(135deg, rgba(255, 255, 255, 0.28), transparent 45%);
|
||||
}
|
||||
|
||||
.truck-marker.stale {
|
||||
@@ -539,34 +629,57 @@ button:hover {
|
||||
}
|
||||
|
||||
.truck-marker::before {
|
||||
left: 10px;
|
||||
left: 11px;
|
||||
}
|
||||
|
||||
.truck-marker::after {
|
||||
right: 11px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.truck-marker__trailer {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 38px;
|
||||
height: 22px;
|
||||
padding: 0 7px;
|
||||
.truck-marker__body {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
place-items: center;
|
||||
width: var(--body-width);
|
||||
height: var(--body-height);
|
||||
border: 2px solid #ffffff;
|
||||
border-right: 0;
|
||||
border-radius: 5px 0 0 5px;
|
||||
background: var(--truck-color);
|
||||
font-size: 11px;
|
||||
background:
|
||||
var(--truck-accent),
|
||||
var(--truck-color);
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.truck-marker.type-van .truck-marker__body {
|
||||
grid-template-rows: 1fr;
|
||||
border-radius: 9px 0 0 7px;
|
||||
}
|
||||
|
||||
.truck-marker.type-van .truck-marker__type {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.truck-marker__type {
|
||||
align-self: end;
|
||||
font-size: 9px;
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.truck-marker__plate {
|
||||
align-self: start;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.truck-marker__cab {
|
||||
position: relative;
|
||||
width: 18px;
|
||||
height: 19px;
|
||||
width: var(--cab-width);
|
||||
height: calc(var(--body-height) - 3px);
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 0 6px 5px 0;
|
||||
background: var(--truck-cab);
|
||||
|
||||
Reference in New Issue
Block a user