매매 시그널 알림 화면 수정

This commit is contained in:
Macbook
2026-05-29 00:46:20 +09:00
parent cbad62a5b0
commit e43b5cbd5a
45 changed files with 2186 additions and 415 deletions
+12
View File
@@ -36,6 +36,11 @@ import { resolveChartLegendOptions } from '../types/chartLegend';
import type { ChartLegendVisibility } from '../types/chartLegend';
import { DEFAULT_MAIN_CHART_STYLE } from '../utils/storage';
import { DEFAULT_SYNC, type SyncOptions } from '../utils/layoutTypes';
import {
DEFAULT_CHART_TIME_FORMAT,
normalizeChartTimeFormat,
setChartTimeFormat,
} from '../utils/chartTimeFormat';
import { DEFAULT_DISPLAY_TIMEZONE, normalizeTimezone, setDisplayTimezone } from '../utils/timezone';
import {
clampVirtualTargetMax,
@@ -170,6 +175,7 @@ export function resolveAppDefaults(s: AppSettingsDto) {
liveAutoTradeBudgetPct: s.liveAutoTradeBudgetPct ?? 95,
fcmPushEnabled: s.fcmPushEnabled ?? false,
displayTimezone: normalizeTimezone(s.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE),
chartTimeFormat: normalizeChartTimeFormat(s.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT),
trendSearchSettings: resolveTrendSearchAppSettings(
s.trendSearchSettings as Partial<TrendSearchAppSettings> | null | undefined,
),
@@ -194,6 +200,7 @@ export function useAppSettings(sessionKey = 0) {
if (_cache !== null) {
setSettings(_cache);
setDisplayTimezone(normalizeTimezone(_cache.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE));
setChartTimeFormat(normalizeChartTimeFormat(_cache.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT));
setIsLoaded(true);
} else {
setIsLoaded(false);
@@ -202,6 +209,7 @@ export function useAppSettings(sessionKey = 0) {
} else if (_cache !== null) {
setSettings(_cache);
setDisplayTimezone(normalizeTimezone(_cache.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE));
setChartTimeFormat(normalizeChartTimeFormat(_cache.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT));
setIsLoaded(true);
} else {
setIsLoaded(false);
@@ -211,6 +219,7 @@ export function useAppSettings(sessionKey = 0) {
if (mountedRef.current) {
setSettings(data);
setDisplayTimezone(normalizeTimezone(data.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE));
setChartTimeFormat(normalizeChartTimeFormat(data.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT));
setIsLoaded(true);
}
}).catch(err => {
@@ -238,6 +247,9 @@ export function useAppSettings(sessionKey = 0) {
if (patch.displayTimezone != null) {
setDisplayTimezone(normalizeTimezone(patch.displayTimezone));
}
if (patch.chartTimeFormat != null) {
setChartTimeFormat(normalizeChartTimeFormat(patch.chartTimeFormat));
}
saveAppSettings(patch).then(updated => {
if (updated) {
_cache = { ...(_cache ?? {}), ...updated };
@@ -0,0 +1,35 @@
import { useLayoutEffect, useRef } from 'react';
/**
* 신호등+하단 %/문구(.vtd-sig-light-rail) 높이를 측정해
* 이퀄라이저 pane 눈금·막대 높이(--vtd-sig-rail-height)와 동기화
*/
export function useSignalVisualRailHeight(deps: unknown[] = []) {
const visualRef = useRef<HTMLDivElement>(null);
const lightRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const visual = visualRef.current;
const light = lightRef.current;
if (!visual || !light) return;
const sync = () => {
const h = Math.ceil(light.getBoundingClientRect().height);
if (h > 0) {
visual.style.setProperty('--vtd-sig-rail-height', `${h}px`);
}
};
sync();
const ro = new ResizeObserver(sync);
ro.observe(light);
window.addEventListener('resize', sync);
return () => {
ro.disconnect();
window.removeEventListener('resize', sync);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return { visualRef, lightRef };
}
@@ -0,0 +1,63 @@
/**
* 매매 시그널 알림 목록 행 — 종목×전략 실시간 신호 스냅샷 (가상매매 카드 요약형과 동일 소스)
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import type { StrategyDto } from '../utils/backendApi';
import {
fetchLiveSnapshot,
type VirtualIndicatorSnapshot,
} from './useVirtualIndicatorSnapshots';
export function useTradeNotificationSignalSnapshot(
market: string,
strategyId: number | null | undefined,
strategy: StrategyDto | undefined,
enabled: boolean,
pollMs = 3000,
): { snapshot: VirtualIndicatorSnapshot | undefined; loading: boolean } {
const [snapshot, setSnapshot] = useState<VirtualIndicatorSnapshot | undefined>();
const [loading, setLoading] = useState(false);
const emptyRetryRef = useRef(0);
const genRef = useRef(0);
const refresh = useCallback(async () => {
if (!enabled || strategyId == null) {
setSnapshot(undefined);
setLoading(false);
return;
}
const gen = ++genRef.current;
setLoading(true);
const pinFirst = emptyRetryRef.current === 0;
const snap = await fetchLiveSnapshot(market, strategyId, strategy, pinFirst);
if (gen !== genRef.current) return;
if (snap && snap.rows.length > 0) {
emptyRetryRef.current = 0;
} else {
emptyRetryRef.current += 1;
}
setSnapshot(snap ?? undefined);
setLoading(false);
}, [market, strategyId, strategy, enabled]);
useEffect(() => {
if (!enabled || strategyId == null) {
setSnapshot(undefined);
setLoading(false);
emptyRetryRef.current = 0;
return;
}
emptyRetryRef.current = 0;
void refresh();
const id = window.setInterval(() => void refresh(), pollMs);
return () => {
clearInterval(id);
genRef.current += 1;
};
}, [market, strategyId, enabled, pollMs, refresh]);
return { snapshot, loading };
}
@@ -108,7 +108,7 @@ function staticSnapshot(
}
/** 백엔드가 업비트 REST·WS로 캔들 warm-up 후 Ta4j 평가 */
async function fetchLiveSnapshot(
export async function fetchLiveSnapshot(
market: string,
strategyId: number,
strategy: StrategyDto | undefined,