diff --git a/apps/web/public/app.js b/apps/web/public/app.js index 37917d4..bc0a71a 100644 --- a/apps/web/public/app.js +++ b/apps/web/public/app.js @@ -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; diff --git a/apps/web/public/index.html b/apps/web/public/index.html index bbff03a..54c63d9 100644 --- a/apps/web/public/index.html +++ b/apps/web/public/index.html @@ -3,7 +3,7 @@ - CargoRadar Control + CargoRadar 관제 @@ -13,11 +13,11 @@
CargoRadar - Live freight control + 실시간 화물 관제
- Demo control tower + 시범 관제
@@ -41,7 +41,7 @@ - 샘플 위치 + 샘플 전송
@@ -74,11 +74,11 @@ 0
- 수신중 + 수신 중 0
- 지연 + 주의 0
@@ -100,7 +100,7 @@
- +
@@ -153,21 +153,21 @@
지도 로딩 중
- Realtime map - 전국 화물 이동 상황 + 실시간 지도 + 화물 이동 현황
- 0 수신중 + 0 수신 중 - 평균 ETA - 최근 수신
- Selected route - 차량을 선택하세요 -

목적지 경로와 예상 도착 시간이 지도 위에 표시됩니다.

+ 선택 경로 + 차량 선택 +

선택하면 경로와 도착 예정 시간이 표시됩니다.

- 수신중 + 수신 중 주의 목적지 경로
diff --git a/apps/web/public/styles.css b/apps/web/public/styles.css index 2b678f5..cf96348 100644 --- a/apps/web/public/styles.css +++ b/apps/web/public/styles.css @@ -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; + } }