diff --git a/apps/web/src/pages/dashboard/DashboardPage.tsx b/apps/web/src/pages/dashboard/DashboardPage.tsx
index 87e5bdb..dc4e1dc 100644
--- a/apps/web/src/pages/dashboard/DashboardPage.tsx
+++ b/apps/web/src/pages/dashboard/DashboardPage.tsx
@@ -28,6 +28,7 @@ import { MapSettingsPanel } from "../../features/mapSettings/MapSettingsPanel";
import { DepthLegend } from "../../widgets/legend/DepthLegend";
import { DEFAULT_MAP_STYLE_SETTINGS } from "../../features/mapSettings/types";
import type { MapStyleSettings } from "../../features/mapSettings/types";
+import { fmtDateTimeFull, fmtIsoFull } from "../../shared/lib/datetime";
import {
buildLegacyHitMap,
computeCountsByType,
@@ -47,13 +48,6 @@ const AIS_CENTER = {
radiusMeters: 2_000_000,
};
-function fmtLocal(iso: string | null) {
- if (!iso) return "-";
- const d = new Date(iso);
- if (Number.isNaN(d.getTime())) return iso;
- return d.toLocaleString("ko-KR", { hour12: false });
-}
-
type Bbox = [number, number, number, number]; // [lonMin, latMin, lonMax, latMax]
type FleetRelationSortMode = "count" | "range";
@@ -148,9 +142,9 @@ export function DashboardPage() {
const [isProjectionLoading, setIsProjectionLoading] = useState(false);
- const [clock, setClock] = useState(() => new Date().toLocaleString("ko-KR", { hour12: false }));
+ const [clock, setClock] = useState(() => fmtDateTimeFull(new Date()));
useEffect(() => {
- const id = window.setInterval(() => setClock(new Date().toLocaleString("ko-KR", { hour12: false })), 1000);
+ const id = window.setInterval(() => setClock(fmtDateTimeFull(new Date())), 1000);
return () => window.clearInterval(id);
}, []);
@@ -543,7 +537,7 @@ export function DashboardPage() {
최근 fetch
- {fmtLocal(snapshot.lastFetchAt)}{" "}
+ {fmtIsoFull(snapshot.lastFetchAt)}{" "}
({snapshot.lastFetchMinutes ?? "-"}m, inserted {snapshot.lastInserted}, upserted {snapshot.lastUpserted})
@@ -567,7 +561,7 @@ export function DashboardPage() {
/ {targetsInScope.length}
생성시각
- {legacyData?.generatedAt ? fmtLocal(legacyData.generatedAt) : "loading..."}
+ {legacyData?.generatedAt ? fmtIsoFull(legacyData.generatedAt) : "loading..."}
)}
diff --git a/apps/web/src/shared/lib/datetime.ts b/apps/web/src/shared/lib/datetime.ts
new file mode 100644
index 0000000..5d532c2
--- /dev/null
+++ b/apps/web/src/shared/lib/datetime.ts
@@ -0,0 +1,50 @@
+/**
+ * 타임존 & 날짜 포맷 유틸리티
+ *
+ * 현재 KST 고정. 추후 토글 필요 시 DISPLAY_TZ 상수만 변경.
+ */
+
+/** 표시용 타임존. 'UTC' | 'Asia/Seoul' 등 IANA tz 문자열. */
+export const DISPLAY_TZ = 'Asia/Seoul' as const;
+
+/** 표시 레이블 (예: "KST") */
+export const DISPLAY_TZ_LABEL = 'KST' as const;
+
+/* ── 포맷 함수 ─────────────────────────────────────────────── */
+
+const pad2 = (n: number) => String(n).padStart(2, '0');
+
+/** DISPLAY_TZ 기준으로 Date → "YYYY년 MM월 DD일 HH시 mm분 ss초" */
+export function fmtDateTimeFull(date: Date): string {
+ const parts = new Intl.DateTimeFormat('ko-KR', {
+ timeZone: DISPLAY_TZ,
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false,
+ }).formatToParts(date);
+
+ const p: Record = {};
+ for (const { type, value } of parts) p[type] = value;
+
+ return `${p.year}년 ${p.month}월 ${p.day}일 ${p.hour}시 ${pad2(Number(p.minute))}분 ${pad2(Number(p.second))}초`;
+}
+
+/** ISO 문자열 → "YYYY년 MM월 DD일 HH시 mm분 ss초" (파싱 실패 시 fallback) */
+export function fmtIsoFull(iso: string | null | undefined): string {
+ if (!iso) return '-';
+ const d = new Date(iso);
+ if (Number.isNaN(d.getTime())) return String(iso);
+ return fmtDateTimeFull(d);
+}
+
+/** ISO 문자열 → "HH:mm:ss" (시간만) */
+export function fmtIsoTime(iso: string | null | undefined): string {
+ if (!iso) return '';
+ const d = new Date(iso);
+ if (Number.isNaN(d.getTime())) return String(iso);
+ return d.toLocaleTimeString('ko-KR', { timeZone: DISPLAY_TZ, hour12: false });
+}
diff --git a/apps/web/src/widgets/aisInfo/AisInfoPanel.tsx b/apps/web/src/widgets/aisInfo/AisInfoPanel.tsx
index 1f1bc51..7df8139 100644
--- a/apps/web/src/widgets/aisInfo/AisInfoPanel.tsx
+++ b/apps/web/src/widgets/aisInfo/AisInfoPanel.tsx
@@ -1,5 +1,6 @@
import type { AisTarget } from "../../entities/aisTarget/model/types";
import type { LegacyVesselInfo } from "../../entities/legacyVessel/model/types";
+import { fmtIsoFull } from "../../shared/lib/datetime";
type Props = {
target: AisTarget;
@@ -85,11 +86,11 @@ export function AisInfoPanel({ target: t, legacy, onClose }: Props) {
Msg TS
- {t.messageTimestamp || "-"}
+ {fmtIsoFull(t.messageTimestamp)}
Received
- {t.receivedDate || "-"}
+ {fmtIsoFull(t.receivedDate)}
);
diff --git a/apps/web/src/widgets/aisTargetList/AisTargetList.tsx b/apps/web/src/widgets/aisTargetList/AisTargetList.tsx
index d9b79af..67fa2c1 100644
--- a/apps/web/src/widgets/aisTargetList/AisTargetList.tsx
+++ b/apps/web/src/widgets/aisTargetList/AisTargetList.tsx
@@ -2,6 +2,7 @@ import { useMemo, useState } from "react";
import type { AisTarget } from "../../entities/aisTarget/model/types";
import type { LegacyVesselIndex } from "../../entities/legacyVessel/lib";
import { matchLegacyVessel } from "../../entities/legacyVessel/lib";
+import { fmtIsoTime } from "../../shared/lib/datetime";
type SortMode = "recent" | "speed";
@@ -23,13 +24,6 @@ function getSpeedColor(sog: unknown) {
return "#64748B";
}
-function fmtLocalTime(iso: string | null | undefined) {
- if (!iso) return "";
- const d = new Date(iso);
- if (Number.isNaN(d.getTime())) return String(iso);
- return d.toLocaleTimeString("ko-KR", { hour12: false });
-}
-
export function AisTargetList({ targets, selectedMmsi, onSelectMmsi, legacyIndex }: Props) {
const [q, setQ] = useState("");
const [mode, setMode] = useState("recent");
@@ -96,7 +90,7 @@ export function AisTargetList({ targets, selectedMmsi, onSelectMmsi, legacyIndex
const sel = selectedMmsi && t.mmsi === selectedMmsi;
const sp = isFiniteNumber(t.sog) ? t.sog.toFixed(1) : "?";
const sc = getSpeedColor(t.sog);
- const ts = fmtLocalTime(t.messageTimestamp);
+ const ts = fmtIsoTime(t.messageTimestamp);
const legacy = legacyIndex ? matchLegacyVessel(t, legacyIndex) : null;
const legacyCode = legacy?.shipCode || "";
diff --git a/apps/web/src/widgets/info/VesselInfoPanel.tsx b/apps/web/src/widgets/info/VesselInfoPanel.tsx
index 845640e..5920a9d 100644
--- a/apps/web/src/widgets/info/VesselInfoPanel.tsx
+++ b/apps/web/src/widgets/info/VesselInfoPanel.tsx
@@ -1,6 +1,7 @@
import { ZONE_META } from "../../entities/zone/model/meta";
import { SPEED_MAX, VESSEL_TYPES } from "../../entities/vessel/model/meta";
import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types";
+import { fmtIsoFull } from "../../shared/lib/datetime";
import { haversineNm } from "../../shared/lib/geo/haversineNm";
import { OVERLAY_RGB, rgbToHex } from "../../shared/lib/map/palette";
@@ -75,7 +76,7 @@ export function VesselInfoPanel({ vessel: v, allVessels, onClose, onSelectMmsi }
Msg TS
- {v.messageTimestamp || "-"}
+ {fmtIsoFull(v.messageTimestamp)}
소유주
diff --git a/apps/web/src/widgets/map3d/lib/tooltips.ts b/apps/web/src/widgets/map3d/lib/tooltips.ts
index fb06a29..5fe4996 100644
--- a/apps/web/src/widgets/map3d/lib/tooltips.ts
+++ b/apps/web/src/widgets/map3d/lib/tooltips.ts
@@ -1,5 +1,6 @@
import type { AisTarget } from '../../../entities/aisTarget/model/types';
import type { LegacyVesselInfo } from '../../../entities/legacyVessel/model/types';
+import { fmtIsoFull } from '../../../shared/lib/datetime';
import { isFiniteNumber, toSafeNumber } from './setUtils';
export function formatNm(value: number | null | undefined) {
@@ -54,7 +55,7 @@ export function getShipTooltipHtml({
${name}
MMSI: ${mmsi}${vesselType ? ` · ${vesselType}` : ''}
SOG: ${sog ?? '?'} kt · COG: ${cog ?? '?'}°
- ${msg ? `
${msg}
` : ''}
+ ${msg ? `
${fmtIsoFull(msg)}
` : ''}
${legacyHtml}
`,
};