화물차 지도 마커와 카카오 키 전환
This commit is contained in:
@@ -9,7 +9,7 @@ const PORT = Number(process.env.PORT || 8080);
|
||||
const DATA_DIR = process.env.DATA_DIR || path.resolve(__dirname, "..", "data");
|
||||
const WEB_ROOT = process.env.WEB_ROOT || path.resolve(__dirname, "..", "..", "web", "public");
|
||||
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || "";
|
||||
const DEFAULT_KAKAO_JAVASCRIPT_KEY = "5e0f889314e6d4f26fb18ee63d09c1ec";
|
||||
const DEFAULT_KAKAO_JAVASCRIPT_KEY = "07f4728599ceaf4299f77c772a135423";
|
||||
|
||||
const DRIVERS_FILE = path.join(DATA_DIR, "drivers.json");
|
||||
const LATEST_FILE = path.join(DATA_DIR, "latest-locations.json");
|
||||
@@ -400,6 +400,10 @@ function createLocationRecord(body, driver) {
|
||||
speed: optionalNumber(body.speed),
|
||||
heading: optionalNumber(body.heading),
|
||||
batteryLevel: optionalNumber(body.batteryLevel),
|
||||
cargoName: optionalString(body.cargoName || body.cargoItem || body.cargo || body.itemName, 80),
|
||||
cargoWeight: optionalString(body.cargoWeight || body.weight, 40),
|
||||
origin: optionalString(body.origin || body.departure, 80),
|
||||
destination: optionalString(body.destination || body.arrival, 80),
|
||||
provider: body.provider ? String(body.provider) : "",
|
||||
recordedAt: normalizeDate(body.recordedAt),
|
||||
receivedAt: new Date().toISOString()
|
||||
@@ -426,6 +430,11 @@ function optionalNumber(value) {
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function optionalString(value, maxLength) {
|
||||
if (value === undefined || value === null) return "";
|
||||
return String(value).trim().slice(0, maxLength);
|
||||
}
|
||||
|
||||
function normalizeDate(value) {
|
||||
if (!value) return new Date().toISOString();
|
||||
const parsed = new Date(value);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const state = {
|
||||
map: null,
|
||||
mapProvider: "",
|
||||
markers: new Map(),
|
||||
infoOverlay: null,
|
||||
drivers: new Map(),
|
||||
@@ -51,26 +52,31 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
async function initializeMap() {
|
||||
const kakaoJavaScriptKey = window.CARGORADAR_CONFIG?.kakaoJavaScriptKey || "";
|
||||
|
||||
if (!kakaoJavaScriptKey) {
|
||||
showMapMessage("Kakao JavaScript 키가 설정되지 않았습니다.");
|
||||
return;
|
||||
if (kakaoJavaScriptKey) {
|
||||
try {
|
||||
await loadKakaoMapsSdk(kakaoJavaScriptKey);
|
||||
const center = new kakao.maps.LatLng(36.45, 127.85);
|
||||
state.map = new kakao.maps.Map(document.getElementById("map"), {
|
||||
center,
|
||||
level: 13
|
||||
});
|
||||
state.mapProvider = "kakao";
|
||||
|
||||
state.map.addControl(new kakao.maps.ZoomControl(), kakao.maps.ControlPosition.RIGHT);
|
||||
state.map.addControl(new kakao.maps.MapTypeControl(), kakao.maps.ControlPosition.TOPRIGHT);
|
||||
hideMapMessage();
|
||||
updateMarkers({ fitBounds: state.locations.size > 0 });
|
||||
return;
|
||||
} catch (error) {
|
||||
console.warn("Kakao map unavailable. Falling back to OpenStreetMap.", error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await loadKakaoMapsSdk(kakaoJavaScriptKey);
|
||||
const center = new kakao.maps.LatLng(36.45, 127.85);
|
||||
state.map = new kakao.maps.Map(document.getElementById("map"), {
|
||||
center,
|
||||
level: 13
|
||||
});
|
||||
|
||||
state.map.addControl(new kakao.maps.ZoomControl(), kakao.maps.ControlPosition.RIGHT);
|
||||
state.map.addControl(new kakao.maps.MapTypeControl(), kakao.maps.ControlPosition.TOPRIGHT);
|
||||
hideMapMessage();
|
||||
updateMarkers({ fitBounds: state.locations.size > 0 });
|
||||
await initializeLeafletMap();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showMapMessage("Kakao 지도 키 또는 허용 도메인을 확인하세요.");
|
||||
showMapMessage("지도 로딩에 실패했습니다. 네트워크 상태를 확인하세요.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,12 +90,88 @@ function loadKakaoMapsSdk(appKey) {
|
||||
const script = document.createElement("script");
|
||||
script.src = `https://dapi.kakao.com/v2/maps/sdk.js?appkey=${encodeURIComponent(appKey)}&autoload=false`;
|
||||
script.async = true;
|
||||
script.onload = () => kakao.maps.load(resolve);
|
||||
script.onload = () => {
|
||||
if (!window.kakao?.maps) {
|
||||
reject(new Error("kakao_maps_not_authorized"));
|
||||
return;
|
||||
}
|
||||
kakao.maps.load(resolve);
|
||||
};
|
||||
script.onerror = () => reject(new Error("kakao_maps_load_failed"));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
async function initializeLeafletMap() {
|
||||
await loadLeafletAssets();
|
||||
state.mapProvider = "leaflet";
|
||||
state.map = L.map("map", {
|
||||
attributionControl: true,
|
||||
zoomControl: true
|
||||
}).setView([36.45, 127.85], 7);
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(state.map);
|
||||
|
||||
showMapMessage("카카오맵 권한 전까지 OpenStreetMap으로 표시 중");
|
||||
window.setTimeout(hideMapMessage, 4500);
|
||||
updateMarkers({ fitBounds: state.locations.size > 0 });
|
||||
}
|
||||
|
||||
function loadLeafletAssets() {
|
||||
if (window.L?.map) return Promise.resolve();
|
||||
|
||||
const version = "1.9.4";
|
||||
const stylesheet = loadStylesheet(
|
||||
"leaflet-stylesheet",
|
||||
`https://unpkg.com/leaflet@${version}/dist/leaflet.css`
|
||||
);
|
||||
const script = loadScript(
|
||||
"leaflet-script",
|
||||
`https://unpkg.com/leaflet@${version}/dist/leaflet.js`
|
||||
);
|
||||
|
||||
return Promise.all([stylesheet, script]).then(() => {
|
||||
if (!window.L?.map) throw new Error("leaflet_load_failed");
|
||||
});
|
||||
}
|
||||
|
||||
function loadStylesheet(id, href) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (document.getElementById(id)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const link = document.createElement("link");
|
||||
link.id = id;
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
link.onload = resolve;
|
||||
link.onerror = () => reject(new Error(`${id}_load_failed`));
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
function loadScript(id, src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (document.getElementById(id)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.id = id;
|
||||
script.src = src;
|
||||
script.async = true;
|
||||
script.onload = resolve;
|
||||
script.onerror = () => reject(new Error(`${id}_load_failed`));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
function bindActions() {
|
||||
elements.refreshButton.addEventListener("click", refreshLatestLocations);
|
||||
elements.demoButton.addEventListener("click", postDemoLocation);
|
||||
@@ -180,6 +262,10 @@ async function postDemoLocation() {
|
||||
accuracy: 12,
|
||||
speed: 42,
|
||||
heading: Math.round((now / 1000) % 360),
|
||||
cargoName: "전자부품",
|
||||
cargoWeight: "1.2t",
|
||||
origin: "서울 상차지",
|
||||
destination: "평택 물류센터",
|
||||
provider: "demo",
|
||||
recordedAt: new Date().toISOString()
|
||||
})
|
||||
@@ -225,8 +311,8 @@ function upsertLocation(location, options = {}) {
|
||||
loadLocationHistory(location);
|
||||
}
|
||||
|
||||
if (options.focus && state.map && window.kakao?.maps) {
|
||||
state.map.setCenter(new kakao.maps.LatLng(location.latitude, location.longitude));
|
||||
if (options.focus) {
|
||||
centerMapOnLocation(location);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,13 +551,13 @@ async function copyToken(token) {
|
||||
}
|
||||
|
||||
function updateMarkers({ fitBounds = false } = {}) {
|
||||
if (!state.map || !window.kakao?.maps) return;
|
||||
if (!state.map || !state.mapProvider) return;
|
||||
|
||||
const activeIds = new Set(state.locations.keys());
|
||||
|
||||
for (const [driverId, markerOverlay] of state.markers) {
|
||||
for (const [driverId, marker] of state.markers) {
|
||||
if (!activeIds.has(driverId)) {
|
||||
markerOverlay.setMap(null);
|
||||
removeMarker(marker);
|
||||
state.markers.delete(driverId);
|
||||
}
|
||||
}
|
||||
@@ -481,54 +567,149 @@ function updateMarkers({ fitBounds = false } = {}) {
|
||||
}
|
||||
|
||||
if (fitBounds && state.markers.size > 0) {
|
||||
fitMapToLocations();
|
||||
}
|
||||
}
|
||||
|
||||
function updateMarker(location) {
|
||||
if (!state.map || !state.mapProvider) return;
|
||||
|
||||
if (state.mapProvider === "leaflet") {
|
||||
updateLeafletMarker(location);
|
||||
return;
|
||||
}
|
||||
|
||||
updateKakaoMarker(location);
|
||||
}
|
||||
|
||||
function updateKakaoMarker(location) {
|
||||
const position = new kakao.maps.LatLng(location.latitude, location.longitude);
|
||||
const status = getLocationStatus(location);
|
||||
const markerElement = createTruckMarkerElement(location, status);
|
||||
|
||||
let marker = state.markers.get(location.driverId);
|
||||
|
||||
if (!marker) {
|
||||
marker = {
|
||||
provider: "kakao",
|
||||
overlay: new kakao.maps.CustomOverlay({
|
||||
position,
|
||||
content: markerElement,
|
||||
yAnchor: 1
|
||||
})
|
||||
};
|
||||
marker.overlay.setMap(state.map);
|
||||
state.markers.set(location.driverId, marker);
|
||||
} else {
|
||||
marker.overlay.setPosition(position);
|
||||
marker.overlay.setContent(markerElement);
|
||||
}
|
||||
}
|
||||
|
||||
function updateLeafletMarker(location) {
|
||||
const status = getLocationStatus(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],
|
||||
popupAnchor: [0, -30]
|
||||
});
|
||||
|
||||
let marker = state.markers.get(location.driverId);
|
||||
|
||||
if (!marker) {
|
||||
marker = {
|
||||
provider: "leaflet",
|
||||
marker: L.marker(position, { icon, title: location.vehicleNo || location.driverId })
|
||||
};
|
||||
marker.marker.on("click", () => focusLocation(location));
|
||||
marker.marker.addTo(state.map);
|
||||
state.markers.set(location.driverId, marker);
|
||||
} else {
|
||||
marker.marker.setLatLng(position);
|
||||
marker.marker.setIcon(icon);
|
||||
marker.marker.off("click");
|
||||
marker.marker.on("click", () => focusLocation(location));
|
||||
}
|
||||
}
|
||||
|
||||
function createTruckMarkerElement(location, status) {
|
||||
const markerElement = document.createElement("button");
|
||||
markerElement.className = `truck-marker ${status.key}`;
|
||||
markerElement.type = "button";
|
||||
markerElement.title = location.vehicleNo || location.driverId;
|
||||
markerElement.setAttribute("aria-label", `${location.vehicleNo || location.driverId} 위치 보기`);
|
||||
markerElement.innerHTML = createTruckMarkerHtml(location, status);
|
||||
markerElement.addEventListener("click", () => focusLocation(location));
|
||||
return markerElement;
|
||||
}
|
||||
|
||||
function createTruckMarkerHtml(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__cab"></span>
|
||||
`;
|
||||
}
|
||||
|
||||
function removeMarker(marker) {
|
||||
if (marker.provider === "kakao") {
|
||||
marker.overlay.setMap(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (marker.provider === "leaflet") {
|
||||
marker.marker.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function fitMapToLocations() {
|
||||
if (state.mapProvider === "kakao") {
|
||||
const bounds = new kakao.maps.LatLngBounds();
|
||||
for (const location of state.locations.values()) {
|
||||
bounds.extend(new kakao.maps.LatLng(location.latitude, location.longitude));
|
||||
}
|
||||
state.map.setBounds(bounds);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.mapProvider === "leaflet") {
|
||||
const bounds = L.latLngBounds([...state.locations.values()].map((location) => {
|
||||
return [location.latitude, location.longitude];
|
||||
}));
|
||||
state.map.fitBounds(bounds, {
|
||||
maxZoom: 14,
|
||||
padding: [40, 40]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateMarker(location) {
|
||||
if (!state.map || !window.kakao?.maps) return;
|
||||
function centerMapOnLocation(location, options = {}) {
|
||||
if (!state.map || !state.mapProvider) return;
|
||||
|
||||
const position = new kakao.maps.LatLng(location.latitude, location.longitude);
|
||||
const status = getLocationStatus(location);
|
||||
const label = location.vehicleNo ? location.vehicleNo.slice(-4) : location.driverId.slice(0, 4);
|
||||
if (state.mapProvider === "kakao") {
|
||||
const position = new kakao.maps.LatLng(location.latitude, location.longitude);
|
||||
state.map.setCenter(position);
|
||||
if (options.zoom && state.map.getLevel() > 4) {
|
||||
state.map.setLevel(4);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const markerElement = document.createElement("button");
|
||||
markerElement.className = `kakao-marker ${status.key}`;
|
||||
markerElement.type = "button";
|
||||
markerElement.textContent = label;
|
||||
markerElement.title = location.vehicleNo || location.driverId;
|
||||
markerElement.addEventListener("click", () => focusLocation(location));
|
||||
|
||||
let markerOverlay = state.markers.get(location.driverId);
|
||||
|
||||
if (!markerOverlay) {
|
||||
markerOverlay = new kakao.maps.CustomOverlay({
|
||||
position,
|
||||
content: markerElement,
|
||||
yAnchor: 1
|
||||
});
|
||||
markerOverlay.setMap(state.map);
|
||||
state.markers.set(location.driverId, markerOverlay);
|
||||
} else {
|
||||
markerOverlay.setPosition(position);
|
||||
markerOverlay.setContent(markerElement);
|
||||
if (state.mapProvider === "leaflet") {
|
||||
const nextZoom = options.zoom ? Math.max(state.map.getZoom(), 14) : state.map.getZoom();
|
||||
state.map.setView([location.latitude, location.longitude], nextZoom, { animate: true });
|
||||
}
|
||||
}
|
||||
|
||||
function focusLocation(location) {
|
||||
loadLocationHistory(location);
|
||||
|
||||
if (!state.map || !window.kakao?.maps) return;
|
||||
if (!state.map || !state.mapProvider) return;
|
||||
|
||||
const position = new kakao.maps.LatLng(location.latitude, location.longitude);
|
||||
state.map.setCenter(position);
|
||||
if (state.map.getLevel() > 4) {
|
||||
state.map.setLevel(4);
|
||||
}
|
||||
centerMapOnLocation(location, { zoom: true });
|
||||
showInfoOverlay(location);
|
||||
}
|
||||
|
||||
@@ -588,15 +769,24 @@ function closeHistoryDrawer() {
|
||||
|
||||
function showInfoOverlay(location) {
|
||||
const status = getLocationStatus(location);
|
||||
const position = new kakao.maps.LatLng(location.latitude, location.longitude);
|
||||
const content = createPopupElement(location, status);
|
||||
|
||||
if (!state.infoOverlay) {
|
||||
state.infoOverlay = new kakao.maps.CustomOverlay({
|
||||
yAnchor: 1.25
|
||||
});
|
||||
if (state.mapProvider === "leaflet") {
|
||||
state.infoOverlay = L.popup({
|
||||
closeButton: true,
|
||||
offset: [0, -24]
|
||||
})
|
||||
.setLatLng([location.latitude, location.longitude])
|
||||
.setContent(content)
|
||||
.openOn(state.map);
|
||||
return;
|
||||
}
|
||||
|
||||
const position = new kakao.maps.LatLng(location.latitude, location.longitude);
|
||||
|
||||
if (!state.infoOverlay) {
|
||||
state.infoOverlay = new kakao.maps.CustomOverlay({ yAnchor: 1.35 });
|
||||
}
|
||||
state.infoOverlay.setContent(content);
|
||||
state.infoOverlay.setPosition(position);
|
||||
state.infoOverlay.setMap(state.map);
|
||||
@@ -613,13 +803,20 @@ function createPopupElement(location, status) {
|
||||
const driver = document.createElement("div");
|
||||
driver.textContent = `${location.driverName || location.driverId} · ${status.label}`;
|
||||
|
||||
const cargo = document.createElement("div");
|
||||
cargo.className = "popup-cargo";
|
||||
cargo.textContent = `화물: ${getCargoSummary(location)}`;
|
||||
|
||||
const destination = document.createElement("div");
|
||||
destination.textContent = getRouteSummary(location);
|
||||
|
||||
const receivedAt = document.createElement("div");
|
||||
receivedAt.textContent = formatDateTime(location.receivedAt);
|
||||
receivedAt.textContent = `수신: ${formatDateTime(location.receivedAt)}`;
|
||||
|
||||
const coordinate = document.createElement("div");
|
||||
coordinate.textContent = `${formatCoordinate(location.latitude)}, ${formatCoordinate(location.longitude)}`;
|
||||
const movement = document.createElement("div");
|
||||
movement.textContent = `속도: ${formatSpeed(location.speed)}`;
|
||||
|
||||
container.append(title, driver, receivedAt, coordinate);
|
||||
container.append(title, driver, cargo, destination, movement, receivedAt);
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -706,10 +903,42 @@ function formatTime(value) {
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function formatSpeed(value) {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
const speed = Number(value);
|
||||
if (!Number.isFinite(speed)) return "-";
|
||||
return `${Math.round(speed)} km/h`;
|
||||
}
|
||||
|
||||
function formatCoordinate(value) {
|
||||
return Number(value).toFixed(5);
|
||||
}
|
||||
|
||||
function getCargoSummary(location) {
|
||||
const cargoName = firstString(location.cargoName, location.cargoItem, location.cargo, location.itemName);
|
||||
const cargoWeight = firstString(location.cargoWeight, location.weight);
|
||||
return [cargoName, cargoWeight].filter(Boolean).join(" · ") || "미등록";
|
||||
}
|
||||
|
||||
function getRouteSummary(location) {
|
||||
const origin = firstString(location.origin, location.departure);
|
||||
const destination = firstString(location.destination, location.arrival);
|
||||
|
||||
if (origin && destination) return `구간: ${origin} -> ${destination}`;
|
||||
if (destination) return `도착지: ${destination}`;
|
||||
if (origin) return `상차지: ${origin}`;
|
||||
return "도착지: 미등록";
|
||||
}
|
||||
|
||||
function firstString(...values) {
|
||||
for (const value of values) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const text = String(value).trim();
|
||||
if (text) return text;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
|
||||
@@ -498,31 +498,94 @@ button:hover {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.kakao-marker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.truck-marker {
|
||||
--truck-color: var(--blue);
|
||||
--truck-cab: #1556b8;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
width: max-content;
|
||||
min-width: 42px;
|
||||
min-height: 30px;
|
||||
padding: 0 9px;
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 999px;
|
||||
background: var(--blue);
|
||||
min-width: 56px;
|
||||
min-height: 34px;
|
||||
border: 0;
|
||||
padding: 0 0 7px;
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 8px 18px rgba(16, 32, 48, 0.25);
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
filter: drop-shadow(0 8px 13px rgba(16, 32, 48, 0.28));
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.kakao-marker.stale {
|
||||
background: var(--amber);
|
||||
.truck-marker.stale {
|
||||
--truck-color: var(--amber);
|
||||
--truck-cab: #8f4307;
|
||||
}
|
||||
|
||||
.kakao-marker.offline {
|
||||
background: var(--red);
|
||||
.truck-marker.offline {
|
||||
--truck-color: var(--red);
|
||||
--truck-cab: #8f1d15;
|
||||
}
|
||||
|
||||
.truck-marker::before,
|
||||
.truck-marker::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
background: #17212b;
|
||||
}
|
||||
|
||||
.truck-marker::before {
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.truck-marker::after {
|
||||
right: 11px;
|
||||
}
|
||||
|
||||
.truck-marker__trailer {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 38px;
|
||||
height: 22px;
|
||||
padding: 0 7px;
|
||||
border: 2px solid #ffffff;
|
||||
border-right: 0;
|
||||
border-radius: 5px 0 0 5px;
|
||||
background: var(--truck-color);
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.truck-marker__cab {
|
||||
position: relative;
|
||||
width: 18px;
|
||||
height: 19px;
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 0 6px 5px 0;
|
||||
background: var(--truck-cab);
|
||||
}
|
||||
|
||||
.truck-marker__cab::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
width: 7px;
|
||||
height: 5px;
|
||||
border-radius: 1px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.leaflet-truck-marker {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.kakao-popup {
|
||||
@@ -543,6 +606,11 @@ button:hover {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.popup-cargo {
|
||||
margin-top: 6px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.app-shell {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
|
||||
Reference in New Issue
Block a user