추세검색 설정 추가
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user