feat: KST 타임스탬프 포맷 유틸 적용

shared/lib/datetime.ts에 KST 고정 포맷 함수 추가.
AIS 정보, 선박 목록, 대시보드 등의 날짜 표시를
로컬 포맷에서 KST 명시적 포맷으로 통일.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
htlee 2026-02-16 12:12:43 +09:00
부모 22099862e6
커밋 cc807bb5f6
6개의 변경된 파일64개의 추가작업 그리고 23개의 파일을 삭제

파일 보기

@ -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() {
</div>
<div style={{ marginTop: 6, color: "var(--muted)", fontSize: 10 }}> fetch</div>
<div>
{fmtLocal(snapshot.lastFetchAt)}{" "}
{fmtIsoFull(snapshot.lastFetchAt)}{" "}
<span style={{ color: "var(--muted)", fontSize: 10 }}>
({snapshot.lastFetchMinutes ?? "-"}m, inserted {snapshot.lastInserted}, upserted {snapshot.lastUpserted})
</span>
@ -567,7 +561,7 @@ export function DashboardPage() {
<span style={{ color: "var(--muted)", fontSize: 10 }}>/ {targetsInScope.length}</span>
</div>
<div style={{ marginTop: 6, color: "var(--muted)", fontSize: 10 }}></div>
<div style={{ fontSize: 10, color: "var(--text)" }}>{legacyData?.generatedAt ? fmtLocal(legacyData.generatedAt) : "loading..."}</div>
<div style={{ fontSize: 10, color: "var(--text)" }}>{legacyData?.generatedAt ? fmtIsoFull(legacyData.generatedAt) : "loading..."}</div>
</div>
)}
</div>

파일 보기

@ -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<string, string> = {};
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 });
}

파일 보기

@ -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) {
</div>
<div className="ir">
<span className="il">Msg TS</span>
<span className="iv">{t.messageTimestamp || "-"}</span>
<span className="iv">{fmtIsoFull(t.messageTimestamp)}</span>
</div>
<div className="ir">
<span className="il">Received</span>
<span className="iv">{t.receivedDate || "-"}</span>
<span className="iv">{fmtIsoFull(t.receivedDate)}</span>
</div>
</div>
);

파일 보기

@ -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<SortMode>("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 || "";

파일 보기

@ -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 }
</div>
<div className="ir">
<span className="il">Msg TS</span>
<span className="iv">{v.messageTimestamp || "-"}</span>
<span className="iv">{fmtIsoFull(v.messageTimestamp)}</span>
</div>
<div className="ir">
<span className="il"></span>

파일 보기

@ -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({
<div style="font-weight: 700; margin-bottom: 4px;">${name}</div>
<div>MMSI: <b>${mmsi}</b>${vesselType ? ` · ${vesselType}` : ''}</div>
<div>SOG: <b>${sog ?? '?'}</b> kt · COG: <b>${cog ?? '?'}</b>°</div>
${msg ? `<div style="opacity:.7; font-size: 11px; margin-top: 4px;">${msg}</div>` : ''}
${msg ? `<div style="opacity:.7; font-size: 11px; margin-top: 4px;">${fmtIsoFull(msg)}</div>` : ''}
${legacyHtml}
</div>`,
};