feat: KST 타임스탬프 포맷 유틸 적용
shared/lib/datetime.ts에 KST 고정 포맷 함수 추가. AIS 정보, 선박 목록, 대시보드 등의 날짜 표시를 로컬 포맷에서 KST 명시적 포맷으로 통일. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
22099862e6
커밋
cc807bb5f6
@ -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>
|
||||
|
||||
50
apps/web/src/shared/lib/datetime.ts
Normal file
50
apps/web/src/shared/lib/datetime.ts
Normal file
@ -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>`,
|
||||
};
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user