Files
goldenChart/frontend/src/utils/timezone.ts
T
2026-05-23 15:11:48 +09:00

214 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 앱 전역 표시 시간대 (IANA).
* DB app-settings · 설정 화면 · 하단 바에서 동일 값 사용.
*/
import { useSyncExternalStore } from 'react';
import { TickMarkType, type Time } from 'lightweight-charts';
import type { Timeframe } from '../types';
export const DEFAULT_DISPLAY_TIMEZONE = 'Asia/Seoul';
export interface TimezoneOption {
id: string;
label: string;
abbr: string;
}
export const TIMEZONE_OPTIONS: TimezoneOption[] = [
{ id: 'Asia/Seoul', label: 'Asia/Seoul (KST, UTC+9)', abbr: 'KST' },
{ id: 'UTC', label: 'UTC', abbr: 'UTC' },
{ id: 'Asia/Tokyo', label: 'Asia/Tokyo (JST, UTC+9)', abbr: 'JST' },
{ id: 'Asia/Shanghai', label: 'Asia/Shanghai (CST, UTC+8)', abbr: 'CST' },
{ id: 'Asia/Hong_Kong', label: 'Asia/Hong_Kong (HKT, UTC+8)', abbr: 'HKT' },
{ id: 'Asia/Singapore', label: 'Asia/Singapore (SGT, UTC+8)', abbr: 'SGT' },
{ id: 'Europe/London', label: 'Europe/London (GMT/BST)', abbr: 'GMT' },
{ id: 'Europe/Berlin', label: 'Europe/Berlin (CET)', abbr: 'CET' },
{ id: 'America/New_York', label: 'America/New_York (ET)', abbr: 'ET' },
{ id: 'America/Chicago', label: 'America/Chicago (CT)', abbr: 'CT' },
{ id: 'America/Los_Angeles',label: 'America/Los_Angeles (PT)', abbr: 'PT' },
];
let _tz = DEFAULT_DISPLAY_TIMEZONE;
const _listeners = new Set<() => void>();
export function getDisplayTimezone(): string {
return _tz;
}
export function setDisplayTimezone(tz: string): void {
const next = normalizeTimezone(tz);
if (_tz === next) return;
_tz = next;
_listeners.forEach(l => l());
}
export function subscribeDisplayTimezone(cb: () => void): () => void {
_listeners.add(cb);
return () => _listeners.delete(cb);
}
export function useDisplayTimezone(): string {
return useSyncExternalStore(subscribeDisplayTimezone, getDisplayTimezone, () => DEFAULT_DISPLAY_TIMEZONE);
}
export function normalizeTimezone(tz: string): string {
const t = (tz || '').trim();
if (!t) return DEFAULT_DISPLAY_TIMEZONE;
if (TIMEZONE_OPTIONS.some(o => o.id === t)) return t;
try {
Intl.DateTimeFormat(undefined, { timeZone: t });
return t;
} catch {
return DEFAULT_DISPLAY_TIMEZONE;
}
}
export function getTimezoneAbbr(tz: string): string {
const id = normalizeTimezone(tz);
return TIMEZONE_OPTIONS.find(o => o.id === id)?.abbr ?? id.split('/').pop() ?? 'TZ';
}
function isDailyTimeframe(tf?: string): boolean {
return tf === '1D' || tf === '1W' || tf === '1M';
}
/** 하단 바·시계 등 HH:mm:ss */
export function formatUnixClock(unixSec: number, tz?: string, withSeconds = true): string {
if (!unixSec) return '';
const zone = normalizeTimezone(tz ?? _tz);
const d = new Date(unixSec * 1000);
const opts: Intl.DateTimeFormatOptions = {
timeZone: zone,
hour: '2-digit',
minute: '2-digit',
hour12: false,
};
if (withSeconds) opts.second = '2-digit';
return new Intl.DateTimeFormat('en-GB', opts).format(d);
}
/** 차트 축·범례용 */
export function formatUnixForChart(unixSec: number, timeframe?: Timeframe, tz?: string): string {
if (!unixSec) return '';
const zone = normalizeTimezone(tz ?? _tz);
const d = new Date(unixSec * 1000);
if (isDailyTimeframe(timeframe)) {
return new Intl.DateTimeFormat('en-GB', {
timeZone: zone,
year: '2-digit',
month: '2-digit',
day: '2-digit',
}).format(d);
}
return new Intl.DateTimeFormat('en-GB', {
timeZone: zone,
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
}).format(d);
}
/** LWC Time → unix 초 (없으면 null) */
export function lwcTimeToUnix(time: Time): number | null {
if (typeof time === 'number') return time;
if (time && typeof time === 'object' && 'year' in time) {
const t = time as { year: number; month: number; day: number };
return Math.floor(Date.UTC(t.year, t.month - 1, t.day) / 1000);
}
return null;
}
/** LWC Time → 문자열 (크로스헤어 등) */
export function formatLwcTime(
time: number | { year: number; month: number; day: number },
timeframe?: Timeframe,
tz?: string,
): string {
if (typeof time === 'number') {
return formatUnixForChart(time, timeframe, tz);
}
const zone = normalizeTimezone(tz ?? _tz);
const utc = Date.UTC(time.year, time.month - 1, time.day);
return new Intl.DateTimeFormat('en-GB', {
timeZone: zone,
year: '2-digit',
month: '2-digit',
day: '2-digit',
}).format(new Date(utc));
}
/** LWC X축 눈금 — 선택한 표시 시간대 기준 (기본 포맷터는 브라우저 로컬 TZ 사용) */
export function formatLwcTickMark(
time: Time,
tickMarkType: TickMarkType,
timeframe?: Timeframe,
tz?: string,
): string {
const zone = normalizeTimezone(tz ?? _tz);
const unixSec = lwcTimeToUnix(time);
if (unixSec == null) return '';
const d = new Date(unixSec * 1000);
switch (tickMarkType) {
case TickMarkType.Year:
return new Intl.DateTimeFormat('en-GB', { timeZone: zone, year: 'numeric' }).format(d);
case TickMarkType.Month:
return new Intl.DateTimeFormat('en-GB', { timeZone: zone, month: 'short' }).format(d);
case TickMarkType.DayOfMonth:
if (isDailyTimeframe(timeframe)) {
return new Intl.DateTimeFormat('en-GB', {
timeZone: zone,
month: 'short',
day: 'numeric',
}).format(d);
}
return new Intl.DateTimeFormat('en-GB', { timeZone: zone, day: 'numeric' }).format(d);
case TickMarkType.Time:
return new Intl.DateTimeFormat('en-GB', {
timeZone: zone,
hour: '2-digit',
minute: '2-digit',
hour12: false,
}).format(d);
case TickMarkType.TimeWithSeconds:
return new Intl.DateTimeFormat('en-GB', {
timeZone: zone,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}).format(d);
default:
return formatUnixForChart(unixSec, timeframe, zone);
}
}
/** 하단 바 표시: `06:36:00 KST` */
export function formatBottomBarTime(unixSec: number, tz?: string): string {
if (!unixSec) return ` ${getTimezoneAbbr(tz ?? _tz)}`;
const zone = normalizeTimezone(tz ?? _tz);
return `${formatUnixClock(unixSec, zone, true)} ${getTimezoneAbbr(zone)}`;
}
/** 알림·드로잉 등 날짜+시간 */
export function formatUnixDateTime(unixSec: number, tz?: string): string {
if (!unixSec) return '—';
const zone = normalizeTimezone(tz ?? _tz);
const d = new Date(unixSec * 1000);
const date = new Intl.DateTimeFormat('en-GB', {
timeZone: zone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(d);
return `${date} ${formatUnixClock(unixSec, zone, true)}`;
}
/** 현재 시각 (호가창 시계 등) */
export function formatNowClock(tz?: string, withSeconds = true): string {
const zone = normalizeTimezone(tz ?? _tz);
return formatUnixClock(Math.floor(Date.now() / 1000), zone, withSeconds);
}