/** * 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, normalizeHLines, 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 { IndicatorConfig } from '../types'; import { loadIndicatorSettings, saveIndicatorSettings, saveAllIndicatorSettings, loadIndicatorVisualSettings, saveIndicatorVisualSettings, } from '../utils/backendApi'; type ParamMap = Record; type AllSettings = Record; export interface IndicatorVisual { plots?: PlotDef[]; hlines?: HLineDef[]; cloudColors?: IchimokuCloudColors; } type VisualCache = Record; // ── 전역 싱글톤 캐시 — 여러 슬롯에서 공유하여 중복 네트워크 요청 방지 ── let _cache: AllSettings | null = null; let _loadPromise: Promise | null = null; let _visualCache: VisualCache | null = null; let _visualLoadPromise: Promise | null = null; function ensureLoaded(): Promise { 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 { 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(); 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; } // ───────────────────────────────────────────────────────────────────────────── export function useIndicatorSettings(sessionKey = 0) { const [settingsRevision, setSettingsRevision] = useState(0); // 로그인/로그아웃 시 지표 설정 재로드 useEffect(() => { invalidateIndicatorSettingsCache(); const unsub = subscribeIndicatorSettings(() => setSettingsRevision(r => r + 1)); Promise.all([ ensureLoaded().catch(err => console.error('[useIndicatorSettings] params load failed', err), ), ensureVisualLoaded().catch(err => console.error('[useIndicatorSettings] visual load failed', err), ), ]).then(() => setSettingsRevision(r => r + 1)); return unsub; }, [sessionKey]); // ── 파라미터 ──────────────────────────────────────────────────────────── /** * 지표 타입에 대한 파라미터를 반환. * DB 저장값 → defaultParams 순으로 우선순위 적용. */ const getParams = useCallback( ( type: string, defaults?: Record ): Record => { const dbParams = _cache?.[type] ?? {}; const fallback = defaults ?? getIndicatorDef(type)?.defaultParams ?? {}; const merged = { ...fallback, ...dbParams } as Record; if (type === 'SMA') { return normalizeSmaConfig({ id: '', type: 'SMA', params: merged }).params; } if (type === 'IchimokuCloud') { return normalizeIchimokuConfig({ id: '', type: 'IchimokuCloud', params: merged }).params; } return merged; }, [] ); /** 특정 지표 파라미터를 DB에 저장하고 캐시를 업데이트 */ const saveParams = useCallback( (type: string, params: Record) => { 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) => { 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; return { type: cfg.type, visual }; }); await Promise.all( visualEntries.map(({ type, visual }) => saveIndicatorVisualSettings(type, visual as { plots?: unknown[]; hlines?: unknown[]; cloudColors?: IchimokuCloudColors }), ), ); 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[] ): { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors } => { const saved = _visualCache?.[type]; const def = getIndicatorDef(type); const registryPlots = defaultPlots ?? def?.plots ?? []; let plots = mergePlotDefs(saved?.plots as PlotDef[] | undefined, registryPlots); 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); } const cloudColors = type === 'IchimokuCloud' ? resolveIchimokuCloudColors(saved?.cloudColors ?? DEFAULT_ICHIMOKU_CLOUD_COLORS) : undefined; // 깊은 복사: 여러 슬롯이 같은 참조를 공유하지 않도록 return { plots: plots.map(p => ({ ...p })), hlines: hlines.map(h => ({ ...h })), cloudColors: cloudColors ? { ...cloudColors } : undefined, }; }, [] ); /** * 지표의 시각 설정을 DB에 저장하고 캐시를 업데이트. * IndicatorSettingsModal 저장 콜백에서 호출. */ const saveVisual = useCallback( ( type: string, plots?: PlotDef[], hlines?: HLineDef[], cloudColors?: IchimokuCloudColors, ) => { const visual: IndicatorVisual = { plots, hlines }; if (cloudColors) visual.cloudColors = cloudColors; if (_visualCache) { _visualCache = { ..._visualCache, [type]: visual }; } else { _visualCache = { [type]: visual }; } saveIndicatorVisualSettings(type, visual as { plots?: unknown[]; hlines?: unknown[]; cloudColors?: IchimokuCloudColors }).catch(err => console.error(`[useIndicatorSettings] saveVisual failed for ${type}`, err) ); notifyIndicatorSettingsChanged(); }, [] ); return { getParams, saveParams, saveAll, saveAllIndicatorDefaults, getVisualConfig, saveVisual, settingsRevision, }; }