Files
goldenChart/frontend/src/hooks/useIndicatorSettings.ts
T
2026-06-11 00:14:14 +09:00

377 lines
13 KiB
TypeScript

/**
* useIndicatorSettings
*
* 전역 지표 파라미터 및 시각 설정(색상·선굵기·수평선)을 DB와 동기화하는 훅.
*
* - 앱 시작 시 DB에서 로드 → indicatorRegistry.ts 하드코딩 기본값을 완전 대체
* - 지표 파라미터 또는 시각 설정이 변경되면 DB에 저장
* - 백엔드 Ta4j 계산 시에도 동일한 파라미터 값 사용
*
* 사용 예:
* ```tsx
* const { getParams, saveParams, getVisualConfig, saveVisual } = useIndicatorSettings();
*
* // 지표 추가 시: DB 저장값 우선 적용 (파라미터 + 색상)
* const params = getParams(type, def.defaultParams);
* const { plots, hlines } = getVisualConfig(type, def.plots, def.hlines);
*
* // 지표 설정 저장 시: 파라미터 + 시각 설정 모두 DB에 반영
* saveParams(updated.type, updated.params);
* saveVisual(updated.type, updated.plots, updated.hlines);
* ```
*/
import { useEffect, useRef, useCallback, useState } from 'react';
import { getIndicatorDef, mergePlotDefs, mergePlotDefsFromSavedVisual, normalizeHLines, normalizeObvParams, enrichIndicatorConfig, PlotDef, HLineDef } from '../utils/indicatorRegistry';
import { createDefaultSmaParams, createDefaultSmaPlots, normalizeSmaConfig } from '../utils/smaConfig';
import { normalizeIchimokuConfig, mergeIchimokuPlots } from '../utils/ichimokuConfig';
import {
DEFAULT_ICHIMOKU_CLOUD_COLORS,
type IchimokuCloudColors,
resolveIchimokuCloudColors,
} from '../utils/ichimokuConfig';
import type { HlinesBackground, IndicatorConfig } from '../types';
import { resolveBbBandBackground } from '../utils/bollingerConfig';
import {
loadIndicatorSettings,
saveIndicatorSettings,
saveAllIndicatorSettings,
loadIndicatorVisualSettings,
saveIndicatorVisualSettings,
} from '../utils/backendApi';
import { invalidateWorkspaceChartIndicatorsCache } from '../utils/workspaceChartIndicatorsCache';
type ParamMap = Record<string, unknown>;
type AllSettings = Record<string, ParamMap>;
export interface IndicatorVisual {
plots?: PlotDef[];
hlines?: HLineDef[];
cloudColors?: IchimokuCloudColors;
/** 볼린저밴드 어퍼~로우어 배경 (업비트 백그라운드 그리기) */
bandBackground?: HlinesBackground;
}
/** getVisualConfig 반환 타입 */
export interface IndicatorVisualConfig {
plots: PlotDef[];
hlines: HLineDef[];
cloudColors?: IchimokuCloudColors;
bandBackground?: HlinesBackground;
}
type VisualCache = Record<string, IndicatorVisual>;
// ── 전역 싱글톤 캐시 — 여러 슬롯에서 공유하여 중복 네트워크 요청 방지 ──
let _cache: AllSettings | null = null;
let _loadPromise: Promise<AllSettings> | null = null;
let _visualCache: VisualCache | null = null;
let _visualLoadPromise: Promise<VisualCache> | null = null;
/** 로그인 세션별 1회만 DB 로드 — 페이지 전환 시 캐시 유지 */
let _loadedSessionKey: number | null = null;
function ensureLoaded(): Promise<AllSettings> {
if (_cache !== null) return Promise.resolve(_cache);
if (_loadPromise) return _loadPromise;
_loadPromise = loadIndicatorSettings().then(data => {
_cache = (data as AllSettings) ?? {};
_loadPromise = null;
notifyIndicatorSettingsChanged();
return _cache;
});
return _loadPromise;
}
function ensureVisualLoaded(): Promise<VisualCache> {
if (_visualCache !== null) return Promise.resolve(_visualCache);
if (_visualLoadPromise) return _visualLoadPromise;
_visualLoadPromise = loadIndicatorVisualSettings().then(data => {
_visualCache = (data as VisualCache) ?? {};
_visualLoadPromise = null;
notifyIndicatorSettingsChanged();
return _visualCache;
});
return _visualLoadPromise;
}
type SettingsListener = () => void;
const _settingsListeners = new Set<SettingsListener>();
function notifyIndicatorSettingsChanged() {
_settingsListeners.forEach(fn => fn());
}
/** 보조지표 설정 변경 구독 (전략편집기 DEF 동기화 등) */
export function subscribeIndicatorSettings(listener: SettingsListener): () => void {
_settingsListeners.add(listener);
return () => { _settingsListeners.delete(listener); };
}
/** 캐시를 강제로 무효화 (테스트 또는 로그아웃 시) */
export function invalidateIndicatorSettingsCache() {
_cache = null;
_loadPromise = null;
_visualCache = null;
_visualLoadPromise = null;
_loadedSessionKey = null;
invalidateWorkspaceChartIndicatorsCache();
}
// ─────────────────────────────────────────────────────────────────────────────
/**
* @param sessionKey 로그인 세션 키 (변경 시 캐시 무효화 및 재로드)
* @param enabled false 이면 DB 로드를 건너뜀 — 차트·전략편집·알림 페이지 진입 시에만 true
*/
export function useIndicatorSettings(sessionKey = 0, enabled = true) {
const [settingsRevision, setSettingsRevision] = useState(0);
const [settingsLoaded, setSettingsLoaded] = useState(
() => _cache !== null && _visualCache !== null,
);
// 로그인/로그아웃 시 지표 설정 재로드 (페이지 전환만으로는 캐시 유지)
useEffect(() => {
const unsub = subscribeIndicatorSettings(() => setSettingsRevision(r => r + 1));
if (!enabled) return unsub;
const cacheWarm = _loadedSessionKey === sessionKey
&& _cache !== null
&& _visualCache !== null;
if (cacheWarm) {
setSettingsLoaded(true);
return unsub;
}
if (_loadedSessionKey !== sessionKey) {
invalidateIndicatorSettingsCache();
}
setSettingsLoaded(false);
Promise.all([
ensureLoaded().catch(err =>
console.error('[useIndicatorSettings] params load failed', err),
),
ensureVisualLoaded().catch(err =>
console.error('[useIndicatorSettings] visual load failed', err),
),
]).then(() => {
_loadedSessionKey = sessionKey;
setSettingsLoaded(true);
setSettingsRevision(r => r + 1);
});
return unsub;
}, [sessionKey, enabled]);
// ── 파라미터 ────────────────────────────────────────────────────────────
/**
* 지표 타입에 대한 파라미터를 반환.
* DB 저장값 → defaultParams 순으로 우선순위 적용.
*/
const getParams = useCallback(
(
type: string,
defaults?: Record<string, number | string | boolean>
): Record<string, number | string | boolean> => {
const dbParams = _cache?.[type] ?? {};
const fallback = defaults ?? getIndicatorDef(type)?.defaultParams ?? {};
const merged = { ...fallback, ...dbParams } as Record<string, number | string | boolean>;
if (type === 'SMA') {
return normalizeSmaConfig({ id: '', type: 'SMA', params: merged }).params;
}
if (type === 'IchimokuCloud') {
return normalizeIchimokuConfig({ id: '', type: 'IchimokuCloud', params: merged }).params;
}
if (type === 'OBV') {
return normalizeObvParams(merged);
}
return merged;
},
[settingsRevision],
);
/** 특정 지표 파라미터를 DB에 저장하고 캐시를 업데이트 */
const saveParams = useCallback(
(type: string, params: Record<string, number | string | boolean>) => {
if (_cache) {
_cache = { ..._cache, [type]: params as ParamMap };
} else {
_cache = { [type]: params as ParamMap };
}
saveIndicatorSettings(type, params as ParamMap).catch(err =>
console.error(`[useIndicatorSettings] saveParams failed for ${type}`, err)
);
notifyIndicatorSettingsChanged();
},
[]
);
/** 현재 활성 지표 목록 전체를 DB에 저장 */
const saveAll = useCallback(
(allParams: AllSettings) => {
_cache = { ...allParams };
saveAllIndicatorSettings(allParams).catch(err =>
console.error('[useIndicatorSettings] saveAll failed', err)
);
notifyIndicatorSettingsChanged();
},
[]
);
/**
* 설정 화면 — 보조지표 기본값 일괄 저장 (파라미터 PUT + 시각 PATCH).
* 저장 완료 후 캐시 갱신 및 구독자(settingsRevision) 알림.
*/
const saveAllIndicatorDefaults = useCallback(
async (configs: Record<string, IndicatorConfig>) => {
const entries = Object.values(configs);
if (entries.length === 0) return;
const allParams: AllSettings = { ...(_cache ?? {}) };
for (const cfg of entries) {
allParams[cfg.type] = cfg.params as ParamMap;
}
await saveAllIndicatorSettings(allParams);
_cache = { ...allParams };
const visualEntries = entries.map(cfg => {
const visual: IndicatorVisual = {
plots: cfg.plots,
hlines: cfg.hlines,
};
if (cfg.cloudColors) visual.cloudColors = cfg.cloudColors;
if (cfg.type === 'BollingerBands' && cfg.bandBackground) {
visual.bandBackground = cfg.bandBackground;
}
return { type: cfg.type, visual };
});
await Promise.all(
visualEntries.map(({ type, visual }) =>
saveIndicatorVisualSettings(type, visual),
),
);
const nextVisual: VisualCache = { ...(_visualCache ?? {}) };
for (const { type, visual } of visualEntries) {
nextVisual[type] = visual;
}
_visualCache = nextVisual;
notifyIndicatorSettingsChanged();
},
[],
);
// ── 시각 설정 (색상·선굵기·수평선) ──────────────────────────────────────
/**
* 지표의 시각 설정을 반환.
* DB 저장값 → registry defaults 순으로 우선순위 적용.
*
* @param type 지표 타입 (e.g. "RSI")
* @param defaultPlots 없을 경우 사용할 기본 플롯 (indicatorRegistry.plots)
* @param defaultHlines 없을 경우 사용할 기본 수평선 (indicatorRegistry.hlines)
*/
const getVisualConfig = useCallback(
(
type: string,
defaultPlots?: PlotDef[],
defaultHlines?: HLineDef[],
): IndicatorVisualConfig => {
const saved = _visualCache?.[type];
const def = getIndicatorDef(type);
const registryPlots = defaultPlots ?? def?.plots ?? [];
let plots = mergePlotDefsFromSavedVisual(saved?.plots as PlotDef[] | undefined, registryPlots, type);
const registryHlines = defaultHlines ?? def?.hlines ?? [];
const rawHlines = (saved?.hlines as HLineDef[] | undefined) ?? registryHlines;
const hlines = normalizeHLines(rawHlines, type, registryHlines);
if (type === 'SMA') {
plots = normalizeSmaConfig({
id: '',
type: 'SMA',
params: createDefaultSmaParams(),
plots,
}).plots ?? createDefaultSmaPlots();
}
if (type === 'IchimokuCloud') {
plots = mergeIchimokuPlots(plots, registryPlots);
}
if (type === 'OBV') {
plots = enrichIndicatorConfig({
id: '',
type: 'OBV',
params: (def?.defaultParams ?? {}) as Record<string, number | string | boolean>,
plots,
}).plots ?? plots;
}
const cloudColors = type === 'IchimokuCloud'
? resolveIchimokuCloudColors(saved?.cloudColors ?? DEFAULT_ICHIMOKU_CLOUD_COLORS)
: undefined;
const bandBackground = type === 'BollingerBands'
? resolveBbBandBackground(saved?.bandBackground)
: undefined;
// 깊은 복사: 여러 슬롯이 같은 참조를 공유하지 않도록
return {
plots: plots.map(p => ({ ...p })),
hlines: hlines.map(h => ({ ...h })),
cloudColors: cloudColors ? { ...cloudColors } : undefined,
bandBackground: bandBackground ? { ...bandBackground } : undefined,
};
},
[settingsRevision],
);
/**
* 지표의 시각 설정을 DB에 저장하고 캐시를 업데이트.
* IndicatorSettingsModal 저장 콜백에서 호출.
*/
const saveVisual = useCallback(
(
type: string,
plots?: PlotDef[],
hlines?: HLineDef[],
cloudColors?: IchimokuCloudColors,
bandBackground?: HlinesBackground,
) => {
const visual: IndicatorVisual = { plots, hlines };
if (cloudColors) visual.cloudColors = cloudColors;
if (type === 'BollingerBands') {
visual.bandBackground = bandBackground ?? resolveBbBandBackground(_visualCache?.[type]?.bandBackground);
}
if (_visualCache) {
_visualCache = { ..._visualCache, [type]: visual };
} else {
_visualCache = { [type]: visual };
}
saveIndicatorVisualSettings(type, visual).catch(err =>
console.error(`[useIndicatorSettings] saveVisual failed for ${type}`, err)
);
notifyIndicatorSettingsChanged();
},
[]
);
return {
getParams,
saveParams,
saveAll,
saveAllIndicatorDefaults,
getVisualConfig,
saveVisual,
settingsRevision,
/** DB 파라미터·시각 설정 로드 완료 여부 (전략편집기 DEF 구성 전 대기용) */
settingsLoaded,
};
}