추세검색 설정 추가

This commit is contained in:
Macbook
2026-05-27 01:36:06 +09:00
parent c1bcf88c6c
commit 2e08c6b16f
42 changed files with 1507 additions and 226 deletions
+18
View File
@@ -37,6 +37,14 @@ import type { ChartLegendVisibility } from '../types/chartLegend';
import { DEFAULT_MAIN_CHART_STYLE } from '../utils/storage';
import { DEFAULT_SYNC, type SyncOptions } from '../utils/layoutTypes';
import { DEFAULT_DISPLAY_TIMEZONE, normalizeTimezone, setDisplayTimezone } from '../utils/timezone';
import {
clampVirtualTargetMax,
DEFAULT_VIRTUAL_TARGET_MAX,
} from '../utils/virtualTargetLimits';
import {
resolveTrendSearchAppSettings,
type TrendSearchAppSettings,
} from '../utils/trendSearchAppSettings';
// 전역 캐시 — 여러 컴포넌트에서 공유하여 중복 요청 방지
let _cache: AppSettingsDto | null = null;
@@ -96,6 +104,7 @@ export function resolveAppDefaults(s: AppSettingsDto) {
paperMinOrderKrw: s.paperMinOrderKrw ?? 5000,
paperAutoTradeEnabled: s.paperAutoTradeEnabled ?? false,
paperAutoTradeBudgetPct: s.paperAutoTradeBudgetPct ?? 95,
virtualTargetMaxCount: clampVirtualTargetMax(s.virtualTargetMaxCount ?? DEFAULT_VIRTUAL_TARGET_MAX),
tradingMode: s.tradingMode ?? 'PAPER',
liveAutoTradeEnabled: s.liveAutoTradeEnabled ?? false,
hasUpbitKeys: s.hasUpbitKeys ?? false,
@@ -104,6 +113,9 @@ export function resolveAppDefaults(s: AppSettingsDto) {
liveAutoTradeBudgetPct: s.liveAutoTradeBudgetPct ?? 95,
fcmPushEnabled: s.fcmPushEnabled ?? false,
displayTimezone: normalizeTimezone(s.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE),
trendSearchSettings: resolveTrendSearchAppSettings(
s.trendSearchSettings as Partial<TrendSearchAppSettings> | null | undefined,
),
};
}
@@ -158,3 +170,9 @@ export function useAppSettings(sessionKey = 0) {
return { settings, defaults, isLoaded, save };
}
/** 앱 설정 캐시 기준 투자대상 최대 개수 (훅 외부·추가 API 등) */
export function getVirtualTargetMaxCount(): number {
const n = _cache?.virtualTargetMaxCount;
return clampVirtualTargetMax(n ?? DEFAULT_VIRTUAL_TARGET_MAX);
}
@@ -0,0 +1,90 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
loadVirtualTargets,
VIRTUAL_SESSION_CHANGED_EVENT,
} from '../utils/virtualTradingStorage';
import {
addVirtualTarget,
removeVirtualTarget,
type VirtualTargetMeta,
} from '../utils/virtualTargetMutations';
import { getVirtualTargetMaxCount } from './useAppSettings';
import { virtualTargetLimitMessage } from '../utils/virtualTargetLimits';
/** 가상투자 투자대상 마켓 집합 (추세검색 카드 등) */
export function useVirtualTargetRegistry() {
const [targets, setTargets] = useState(() => loadVirtualTargets());
const [busyMarket, setBusyMarket] = useState<string | null>(null);
const refresh = useCallback(() => {
setTargets(loadVirtualTargets());
}, []);
useEffect(() => {
refresh();
const onChange = () => refresh();
window.addEventListener(VIRTUAL_SESSION_CHANGED_EVENT, onChange);
return () => window.removeEventListener(VIRTUAL_SESSION_CHANGED_EVENT, onChange);
}, [refresh]);
const addedSet = useMemo(
() => new Set(targets.map(t => t.market)),
[targets],
);
const isInTargets = useCallback(
(market: string) => addedSet.has(market),
[addedSet],
);
const isPinned = useCallback(
(market: string) => targets.find(t => t.market === market)?.pinned === true,
[targets],
);
const add = useCallback(async (market: string, meta?: VirtualTargetMeta) => {
if (addedSet.has(market) || busyMarket === market) return;
const maxCount = getVirtualTargetMaxCount();
if (targets.length >= maxCount) {
window.alert(virtualTargetLimitMessage(maxCount));
return;
}
setBusyMarket(market);
try {
const next = await addVirtualTarget(market, meta);
setTargets(next);
} catch (e) {
console.warn('[VirtualTarget] add failed', market, e);
window.alert('투자대상 추가에 실패했습니다.');
} finally {
setBusyMarket(null);
}
}, [addedSet, busyMarket, targets]);
const remove = useCallback(async (market: string) => {
if (!addedSet.has(market) || busyMarket === market) return;
if (targets.find(t => t.market === market)?.pinned) {
window.alert('고정된 종목은 투자대상에서 삭제할 수 없습니다.');
return;
}
setBusyMarket(market);
try {
const next = await removeVirtualTarget(market);
setTargets(next);
} catch (e) {
console.warn('[VirtualTarget] remove failed', market, e);
window.alert('투자대상 제거에 실패했습니다.');
} finally {
setBusyMarket(null);
}
}, [addedSet, busyMarket, targets]);
return {
addedSet,
isInTargets,
isPinned,
add,
remove,
busyMarket,
};
}