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 { DepthLegend } from "../../widgets/legend/DepthLegend";
|
||||||
import { DEFAULT_MAP_STYLE_SETTINGS } from "../../features/mapSettings/types";
|
import { DEFAULT_MAP_STYLE_SETTINGS } from "../../features/mapSettings/types";
|
||||||
import type { MapStyleSettings } from "../../features/mapSettings/types";
|
import type { MapStyleSettings } from "../../features/mapSettings/types";
|
||||||
|
import { fmtDateTimeFull, fmtIsoFull } from "../../shared/lib/datetime";
|
||||||
import {
|
import {
|
||||||
buildLegacyHitMap,
|
buildLegacyHitMap,
|
||||||
computeCountsByType,
|
computeCountsByType,
|
||||||
@ -47,13 +48,6 @@ const AIS_CENTER = {
|
|||||||
radiusMeters: 2_000_000,
|
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 Bbox = [number, number, number, number]; // [lonMin, latMin, lonMax, latMax]
|
||||||
type FleetRelationSortMode = "count" | "range";
|
type FleetRelationSortMode = "count" | "range";
|
||||||
|
|
||||||
@ -148,9 +142,9 @@ export function DashboardPage() {
|
|||||||
|
|
||||||
const [isProjectionLoading, setIsProjectionLoading] = useState(false);
|
const [isProjectionLoading, setIsProjectionLoading] = useState(false);
|
||||||
|
|
||||||
const [clock, setClock] = useState(() => new Date().toLocaleString("ko-KR", { hour12: false }));
|
const [clock, setClock] = useState(() => fmtDateTimeFull(new Date()));
|
||||||
useEffect(() => {
|
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);
|
return () => window.clearInterval(id);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -543,7 +537,7 @@ export function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 6, color: "var(--muted)", fontSize: 10 }}>최근 fetch</div>
|
<div style={{ marginTop: 6, color: "var(--muted)", fontSize: 10 }}>최근 fetch</div>
|
||||||
<div>
|
<div>
|
||||||
{fmtLocal(snapshot.lastFetchAt)}{" "}
|
{fmtIsoFull(snapshot.lastFetchAt)}{" "}
|
||||||
<span style={{ color: "var(--muted)", fontSize: 10 }}>
|
<span style={{ color: "var(--muted)", fontSize: 10 }}>
|
||||||
({snapshot.lastFetchMinutes ?? "-"}m, inserted {snapshot.lastInserted}, upserted {snapshot.lastUpserted})
|
({snapshot.lastFetchMinutes ?? "-"}m, inserted {snapshot.lastInserted}, upserted {snapshot.lastUpserted})
|
||||||
</span>
|
</span>
|
||||||
@ -567,7 +561,7 @@ export function DashboardPage() {
|
|||||||
<span style={{ color: "var(--muted)", fontSize: 10 }}>/ {targetsInScope.length}</span>
|
<span style={{ color: "var(--muted)", fontSize: 10 }}>/ {targetsInScope.length}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 6, color: "var(--muted)", fontSize: 10 }}>생성시각</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>
|
||||||
)}
|
)}
|
||||||
</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 { AisTarget } from "../../entities/aisTarget/model/types";
|
||||||
import type { LegacyVesselInfo } from "../../entities/legacyVessel/model/types";
|
import type { LegacyVesselInfo } from "../../entities/legacyVessel/model/types";
|
||||||
|
import { fmtIsoFull } from "../../shared/lib/datetime";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
target: AisTarget;
|
target: AisTarget;
|
||||||
@ -85,11 +86,11 @@ export function AisInfoPanel({ target: t, legacy, onClose }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="ir">
|
<div className="ir">
|
||||||
<span className="il">Msg TS</span>
|
<span className="il">Msg TS</span>
|
||||||
<span className="iv">{t.messageTimestamp || "-"}</span>
|
<span className="iv">{fmtIsoFull(t.messageTimestamp)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ir">
|
<div className="ir">
|
||||||
<span className="il">Received</span>
|
<span className="il">Received</span>
|
||||||
<span className="iv">{t.receivedDate || "-"}</span>
|
<span className="iv">{fmtIsoFull(t.receivedDate)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useMemo, useState } from "react";
|
|||||||
import type { AisTarget } from "../../entities/aisTarget/model/types";
|
import type { AisTarget } from "../../entities/aisTarget/model/types";
|
||||||
import type { LegacyVesselIndex } from "../../entities/legacyVessel/lib";
|
import type { LegacyVesselIndex } from "../../entities/legacyVessel/lib";
|
||||||
import { matchLegacyVessel } from "../../entities/legacyVessel/lib";
|
import { matchLegacyVessel } from "../../entities/legacyVessel/lib";
|
||||||
|
import { fmtIsoTime } from "../../shared/lib/datetime";
|
||||||
|
|
||||||
type SortMode = "recent" | "speed";
|
type SortMode = "recent" | "speed";
|
||||||
|
|
||||||
@ -23,13 +24,6 @@ function getSpeedColor(sog: unknown) {
|
|||||||
return "#64748B";
|
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) {
|
export function AisTargetList({ targets, selectedMmsi, onSelectMmsi, legacyIndex }: Props) {
|
||||||
const [q, setQ] = useState("");
|
const [q, setQ] = useState("");
|
||||||
const [mode, setMode] = useState<SortMode>("recent");
|
const [mode, setMode] = useState<SortMode>("recent");
|
||||||
@ -96,7 +90,7 @@ export function AisTargetList({ targets, selectedMmsi, onSelectMmsi, legacyIndex
|
|||||||
const sel = selectedMmsi && t.mmsi === selectedMmsi;
|
const sel = selectedMmsi && t.mmsi === selectedMmsi;
|
||||||
const sp = isFiniteNumber(t.sog) ? t.sog.toFixed(1) : "?";
|
const sp = isFiniteNumber(t.sog) ? t.sog.toFixed(1) : "?";
|
||||||
const sc = getSpeedColor(t.sog);
|
const sc = getSpeedColor(t.sog);
|
||||||
const ts = fmtLocalTime(t.messageTimestamp);
|
const ts = fmtIsoTime(t.messageTimestamp);
|
||||||
const legacy = legacyIndex ? matchLegacyVessel(t, legacyIndex) : null;
|
const legacy = legacyIndex ? matchLegacyVessel(t, legacyIndex) : null;
|
||||||
const legacyCode = legacy?.shipCode || "";
|
const legacyCode = legacy?.shipCode || "";
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { ZONE_META } from "../../entities/zone/model/meta";
|
import { ZONE_META } from "../../entities/zone/model/meta";
|
||||||
import { SPEED_MAX, VESSEL_TYPES } from "../../entities/vessel/model/meta";
|
import { SPEED_MAX, VESSEL_TYPES } from "../../entities/vessel/model/meta";
|
||||||
import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types";
|
import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types";
|
||||||
|
import { fmtIsoFull } from "../../shared/lib/datetime";
|
||||||
import { haversineNm } from "../../shared/lib/geo/haversineNm";
|
import { haversineNm } from "../../shared/lib/geo/haversineNm";
|
||||||
import { OVERLAY_RGB, rgbToHex } from "../../shared/lib/map/palette";
|
import { OVERLAY_RGB, rgbToHex } from "../../shared/lib/map/palette";
|
||||||
|
|
||||||
@ -75,7 +76,7 @@ export function VesselInfoPanel({ vessel: v, allVessels, onClose, onSelectMmsi }
|
|||||||
</div>
|
</div>
|
||||||
<div className="ir">
|
<div className="ir">
|
||||||
<span className="il">Msg TS</span>
|
<span className="il">Msg TS</span>
|
||||||
<span className="iv">{v.messageTimestamp || "-"}</span>
|
<span className="iv">{fmtIsoFull(v.messageTimestamp)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ir">
|
<div className="ir">
|
||||||
<span className="il">소유주</span>
|
<span className="il">소유주</span>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { AisTarget } from '../../../entities/aisTarget/model/types';
|
import type { AisTarget } from '../../../entities/aisTarget/model/types';
|
||||||
import type { LegacyVesselInfo } from '../../../entities/legacyVessel/model/types';
|
import type { LegacyVesselInfo } from '../../../entities/legacyVessel/model/types';
|
||||||
|
import { fmtIsoFull } from '../../../shared/lib/datetime';
|
||||||
import { isFiniteNumber, toSafeNumber } from './setUtils';
|
import { isFiniteNumber, toSafeNumber } from './setUtils';
|
||||||
|
|
||||||
export function formatNm(value: number | null | undefined) {
|
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 style="font-weight: 700; margin-bottom: 4px;">${name}</div>
|
||||||
<div>MMSI: <b>${mmsi}</b>${vesselType ? ` · ${vesselType}` : ''}</div>
|
<div>MMSI: <b>${mmsi}</b>${vesselType ? ` · ${vesselType}` : ''}</div>
|
||||||
<div>SOG: <b>${sog ?? '?'}</b> kt · COG: <b>${cog ?? '?'}</b>°</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}
|
${legacyHtml}
|
||||||
</div>`,
|
</div>`,
|
||||||
};
|
};
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user