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