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}
`, };