관제 화면 문구와 반응형 표시 개선
This commit is contained in:
@@ -436,7 +436,7 @@ function render() {
|
|||||||
elements.totalCount.textContent = String(counts.total);
|
elements.totalCount.textContent = String(counts.total);
|
||||||
elements.activeCount.textContent = String(counts.active);
|
elements.activeCount.textContent = String(counts.active);
|
||||||
elements.staleCount.textContent = String(counts.stale);
|
elements.staleCount.textContent = String(counts.stale);
|
||||||
elements.lastUpdated.textContent = locations[0] ? `${locations.length}대 표시 · ${formatDateTime(locations[0].receivedAt)}` : "수신 대기";
|
elements.lastUpdated.textContent = locations[0] ? `${locations.length}대 · ${formatTime(locations[0].receivedAt)}` : "수신 대기";
|
||||||
renderOperationsSummary(locations, counts);
|
renderOperationsSummary(locations, counts);
|
||||||
renderMapStatus(locations, counts);
|
renderMapStatus(locations, counts);
|
||||||
syncStatusFilterButtons();
|
syncStatusFilterButtons();
|
||||||
@@ -448,7 +448,7 @@ function render() {
|
|||||||
if (!visibleLocations.length) {
|
if (!visibleLocations.length) {
|
||||||
const empty = document.createElement("div");
|
const empty = document.createElement("div");
|
||||||
empty.className = "empty-state";
|
empty.className = "empty-state";
|
||||||
empty.textContent = locations.length ? "조건에 맞는 차량이 없습니다." : "수신된 차량 위치가 없습니다.";
|
empty.textContent = locations.length ? "조건에 맞는 차량이 없습니다." : "수신 위치가 없습니다.";
|
||||||
elements.vehicleList.append(empty);
|
elements.vehicleList.append(empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -459,16 +459,16 @@ function render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderOperationsSummary(locations, counts) {
|
function renderOperationsSummary(locations, counts) {
|
||||||
const activeText = counts.active > 0 ? `${counts.active}대 운행중` : "운행 대기";
|
const activeText = counts.active > 0 ? `${counts.active}대 운행 중` : "운행 대기";
|
||||||
const staleText = counts.stale > 0 ? `주의 ${counts.stale}대 확인 필요` : "모든 차량 정상 수신";
|
const staleText = counts.stale > 0 ? `주의 ${counts.stale}대` : "정상 수신";
|
||||||
const averageEtaMinutes = calculateAverageEtaMinutes(locations);
|
const averageEtaMinutes = calculateAverageEtaMinutes(locations);
|
||||||
const cargoTypes = new Set(locations.map(getCargoName).filter(Boolean));
|
const cargoTypes = new Set(locations.map(getCargoName).filter(Boolean));
|
||||||
const routeCount = locations.filter((location) => getRoutePath(location).length >= 2).length;
|
const routeCount = locations.filter((location) => getRoutePath(location).length >= 2).length;
|
||||||
|
|
||||||
elements.opsHeadline.textContent = `전국 ${activeText}`;
|
elements.opsHeadline.textContent = `전국 ${activeText}`;
|
||||||
elements.opsSubline.textContent = locations.length
|
elements.opsSubline.textContent = locations.length
|
||||||
? `${staleText} · ${locations.length}대 실시간 관제`
|
? `${staleText} · ${locations.length}대 관제 중`
|
||||||
: "시뮬레이터 또는 기사 앱의 첫 위치를 기다리고 있습니다.";
|
: "첫 위치를 기다리는 중입니다.";
|
||||||
elements.averageEta.textContent = averageEtaMinutes === null ? "-" : formatDuration(averageEtaMinutes).replace("약 ", "");
|
elements.averageEta.textContent = averageEtaMinutes === null ? "-" : formatDuration(averageEtaMinutes).replace("약 ", "");
|
||||||
elements.cargoTypeCount.textContent = cargoTypes.size ? `${cargoTypes.size}종` : "-";
|
elements.cargoTypeCount.textContent = cargoTypes.size ? `${cargoTypes.size}종` : "-";
|
||||||
elements.routePreviewCount.textContent = routeCount ? `${routeCount}건` : "-";
|
elements.routePreviewCount.textContent = routeCount ? `${routeCount}건` : "-";
|
||||||
@@ -595,7 +595,7 @@ function createVehicleItem(location) {
|
|||||||
const speedUnit = document.createElement("span");
|
const speedUnit = document.createElement("span");
|
||||||
speedUnit.textContent = "km/h";
|
speedUnit.textContent = "km/h";
|
||||||
const etaValue = document.createElement("small");
|
const etaValue = document.createElement("small");
|
||||||
etaValue.textContent = getRouteProgressSummary(location) || "ETA 대기";
|
etaValue.textContent = getRouteProgressSummary(location) || "도착 대기";
|
||||||
speed.append(speedValue, speedUnit, etaValue);
|
speed.append(speedValue, speedUnit, etaValue);
|
||||||
|
|
||||||
main.append(title, meta, routeLine, cargoLine, progress);
|
main.append(title, meta, routeLine, cargoLine, progress);
|
||||||
@@ -1110,18 +1110,18 @@ function createDestinationMarkerHtml(location) {
|
|||||||
function renderRouteFocusPanel(location) {
|
function renderRouteFocusPanel(location) {
|
||||||
if (!location) {
|
if (!location) {
|
||||||
elements.routeFocusPanel.classList.remove("is-active");
|
elements.routeFocusPanel.classList.remove("is-active");
|
||||||
elements.routeFocusTitle.textContent = "차량을 선택하세요";
|
elements.routeFocusTitle.textContent = "차량 선택";
|
||||||
elements.routeFocusMeta.textContent = "목적지 경로와 예상 도착 시간이 지도 위에 표시됩니다.";
|
elements.routeFocusMeta.textContent = "선택하면 경로와 도착 예정 시간이 표시됩니다.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const metrics = getRouteMetrics(location);
|
const metrics = getRouteMetrics(location);
|
||||||
const vehicleType = getVehicleType(location);
|
const vehicleType = getVehicleType(location);
|
||||||
const cargo = getCargoSummary(location);
|
const cargo = getCargoSummary(location);
|
||||||
const destination = getDestinationName(location) || "목적지 미등록";
|
|
||||||
const progress = metrics
|
const progress = metrics
|
||||||
? `${formatDistance(metrics.remainingDistanceKm)} 남음 · ${formatDuration(metrics.etaMinutes)} · ${formatEtaTime(metrics.estimatedArrivalAt)} 도착`
|
? `${formatDistance(metrics.remainingDistanceKm)} 남음 · ${formatClockTime(metrics.estimatedArrivalAt)} 도착`
|
||||||
: "경로 데이터 대기";
|
: "경로 데이터 대기";
|
||||||
|
const destination = getDestinationName(location) || "목적지 미등록";
|
||||||
|
|
||||||
elements.routeFocusPanel.classList.add("is-active");
|
elements.routeFocusPanel.classList.add("is-active");
|
||||||
elements.routeFocusTitle.textContent = `${location.vehicleNo || location.driverId} · ${vehicleType.label}`;
|
elements.routeFocusTitle.textContent = `${location.vehicleNo || location.driverId} · ${vehicleType.label}`;
|
||||||
@@ -1351,8 +1351,8 @@ function getLocationStatus(location) {
|
|||||||
const receivedAt = new Date(location.receivedAt).getTime();
|
const receivedAt = new Date(location.receivedAt).getTime();
|
||||||
const ageMs = Date.now() - receivedAt;
|
const ageMs = Date.now() - receivedAt;
|
||||||
|
|
||||||
if (ageMs <= 2 * 60 * 1000) return { key: "active", label: "수신중" };
|
if (ageMs <= 2 * 60 * 1000) return { key: "active", label: "수신 중" };
|
||||||
if (ageMs <= 10 * 60 * 1000) return { key: "stale", label: "지연" };
|
if (ageMs <= 10 * 60 * 1000) return { key: "stale", label: "주의" };
|
||||||
return { key: "offline", label: "미수신" };
|
return { key: "offline", label: "미수신" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1363,8 +1363,7 @@ function formatDateTime(value) {
|
|||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit"
|
||||||
second: "2-digit"
|
|
||||||
}).format(date);
|
}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1411,16 +1410,16 @@ function getRouteSummary(location) {
|
|||||||
const origin = firstString(location.origin, location.departure);
|
const origin = firstString(location.origin, location.departure);
|
||||||
const destination = firstString(location.destination, location.arrival);
|
const destination = firstString(location.destination, location.arrival);
|
||||||
|
|
||||||
if (origin && destination) return `구간: ${origin} -> ${destination}`;
|
if (origin && destination) return `${origin} → ${destination}`;
|
||||||
if (destination) return `도착지: ${destination}`;
|
if (destination) return `도착: ${destination}`;
|
||||||
if (origin) return `상차지: ${origin}`;
|
if (origin) return `상차: ${origin}`;
|
||||||
return "도착지: 미등록";
|
return "도착 미등록";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRouteProgressSummary(location) {
|
function getRouteProgressSummary(location) {
|
||||||
const metrics = getRouteMetrics(location);
|
const metrics = getRouteMetrics(location);
|
||||||
if (!metrics) return "";
|
if (!metrics) return "";
|
||||||
return `남은 ${formatDistance(metrics.remainingDistanceKm)} · ETA ${formatEtaTime(metrics.estimatedArrivalAt)}`;
|
return `${formatDistance(metrics.remainingDistanceKm)} · ${formatClockTime(metrics.estimatedArrivalAt)} 도착`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRouteProgressPercent(location) {
|
function getRouteProgressPercent(location) {
|
||||||
@@ -1614,6 +1613,16 @@ function formatEtaTime(value) {
|
|||||||
}).format(date);
|
}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatClockTime(value) {
|
||||||
|
const date = value instanceof Date ? value : new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return "-";
|
||||||
|
return new Intl.DateTimeFormat("ko-KR", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
function firstString(...values) {
|
function firstString(...values) {
|
||||||
for (const value of values) {
|
for (const value of values) {
|
||||||
if (value === undefined || value === null) continue;
|
if (value === undefined || value === null) continue;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>CargoRadar Control</title>
|
<title>CargoRadar 관제</title>
|
||||||
<link rel="stylesheet" href="/styles.css" />
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -13,11 +13,11 @@
|
|||||||
<span class="brand-mark" aria-hidden="true">CR</span>
|
<span class="brand-mark" aria-hidden="true">CR</span>
|
||||||
<div>
|
<div>
|
||||||
<strong class="brand">CargoRadar</strong>
|
<strong class="brand">CargoRadar</strong>
|
||||||
<span class="subtitle">Live freight control</span>
|
<span class="subtitle">실시간 화물 관제</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-context">
|
<div class="topbar-context">
|
||||||
<span class="topbar-chip">Demo control tower</span>
|
<span class="topbar-chip">시범 관제</span>
|
||||||
<time id="systemClock" datetime="">--:--</time>
|
<time id="systemClock" datetime="">--:--</time>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<path d="M15 18v-2" />
|
<path d="M15 18v-2" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
샘플 위치
|
샘플 전송
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -74,11 +74,11 @@
|
|||||||
<strong id="totalCount">0</strong>
|
<strong id="totalCount">0</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<span class="metric-label">수신중</span>
|
<span class="metric-label">수신 중</span>
|
||||||
<strong id="activeCount">0</strong>
|
<strong id="activeCount">0</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<span class="metric-label">지연</span>
|
<span class="metric-label">주의</span>
|
||||||
<strong id="staleCount">0</strong>
|
<strong id="staleCount">0</strong>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div class="status-filters" aria-label="차량 상태 필터">
|
<div class="status-filters" aria-label="차량 상태 필터">
|
||||||
<button class="filter-button is-active" type="button" data-status-filter="all">전체</button>
|
<button class="filter-button is-active" type="button" data-status-filter="all">전체</button>
|
||||||
<button class="filter-button" type="button" data-status-filter="active">수신중</button>
|
<button class="filter-button" type="button" data-status-filter="active">수신 중</button>
|
||||||
<button class="filter-button" type="button" data-status-filter="stale">주의</button>
|
<button class="filter-button" type="button" data-status-filter="stale">주의</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,21 +153,21 @@
|
|||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
<div id="mapMessage" class="map-message">지도 로딩 중</div>
|
<div id="mapMessage" class="map-message">지도 로딩 중</div>
|
||||||
<section class="map-status-panel" aria-label="지도 운영 상태">
|
<section class="map-status-panel" aria-label="지도 운영 상태">
|
||||||
<span class="section-eyebrow">Realtime map</span>
|
<span class="section-eyebrow">실시간 지도</span>
|
||||||
<strong id="mapStatusTitle">전국 화물 이동 상황</strong>
|
<strong id="mapStatusTitle">화물 이동 현황</strong>
|
||||||
<div class="map-status-grid">
|
<div class="map-status-grid">
|
||||||
<span><b id="mapActiveCount">0</b> 수신중</span>
|
<span><b id="mapActiveCount">0</b> 수신 중</span>
|
||||||
<span><b id="mapEtaSummary">-</b> 평균 ETA</span>
|
<span><b id="mapEtaSummary">-</b> 평균 ETA</span>
|
||||||
<span><b id="mapUpdatedAt">-</b> 최근 수신</span>
|
<span><b id="mapUpdatedAt">-</b> 최근 수신</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section id="routeFocusPanel" class="route-focus-panel" aria-live="polite">
|
<section id="routeFocusPanel" class="route-focus-panel" aria-live="polite">
|
||||||
<span class="section-eyebrow">Selected route</span>
|
<span class="section-eyebrow">선택 경로</span>
|
||||||
<strong id="routeFocusTitle">차량을 선택하세요</strong>
|
<strong id="routeFocusTitle">차량 선택</strong>
|
||||||
<p id="routeFocusMeta">목적지 경로와 예상 도착 시간이 지도 위에 표시됩니다.</p>
|
<p id="routeFocusMeta">선택하면 경로와 도착 예정 시간이 표시됩니다.</p>
|
||||||
</section>
|
</section>
|
||||||
<div class="map-legend" aria-label="지도 범례">
|
<div class="map-legend" aria-label="지도 범례">
|
||||||
<span><i class="legend-dot active"></i>수신중</span>
|
<span><i class="legend-dot active"></i>수신 중</span>
|
||||||
<span><i class="legend-dot stale"></i>주의</span>
|
<span><i class="legend-dot stale"></i>주의</span>
|
||||||
<span><i class="legend-line"></i>목적지 경로</span>
|
<span><i class="legend-line"></i>목적지 경로</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ button:hover {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--navy);
|
color: var(--navy);
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
line-height: 1.15;
|
line-height: 1.25;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -236,6 +236,7 @@ button:hover {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 750;
|
font-weight: 750;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
|
word-break: keep-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-brief-grid {
|
.ops-brief-grid {
|
||||||
@@ -686,7 +687,7 @@ button:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.speed small {
|
.speed small {
|
||||||
max-width: 92px;
|
max-width: 112px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: #174ea6;
|
color: #174ea6;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -815,11 +816,12 @@ button:hover {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
|
word-break: keep-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-legend {
|
.map-legend {
|
||||||
top: 18px;
|
top: 18px;
|
||||||
right: 18px;
|
right: 86px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -1316,4 +1318,27 @@ button:hover {
|
|||||||
max-width: calc(100% - 20px);
|
max-width: calc(100% - 20px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vehicle-item {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-title {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-title strong {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed {
|
||||||
|
grid-template-columns: auto auto minmax(0, 1fr);
|
||||||
|
align-items: baseline;
|
||||||
|
justify-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed small {
|
||||||
|
max-width: none;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user