/** * 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 } from 'react'; import { getIndicatorDef, mergePlotDefs, 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 { 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; 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; return _visualCache; }); return _visualLoadPromise; } /** 캐시를 강제로 무효화 (테스트 또는 로그아웃 시) */ export function invalidateIndicatorSettingsCache() { _cache = null; _loadPromise = null; _visualCache = null; _visualLoadPromise = null; } // ───────────────────────────────────────────────────────────────────────────── export function useIndicatorSettings(sessionKey = 0) { // 로그인/로그아웃 시 지표 설정 재로드 useEffect(() => { invalidateIndicatorSettingsCache(); ensureLoaded().catch(err => console.error('[useIndicatorSettings] params load failed', err) ); ensureVisualLoaded().catch(err => console.error('[useIndicatorSettings] visual load failed', err) ); }, [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) ); }, [] ); /** 현재 활성 지표 목록 전체를 DB에 저장 */ const saveAll = useCallback( (allParams: AllSettings) => { _cache = { ...allParams }; saveAllIndicatorSettings(allParams).catch(err => console.error('[useIndicatorSettings] saveAll failed', err) ); }, [] ); // ── 시각 설정 (색상·선굵기·수평선) ────────────────────────────────────── /** * 지표의 시각 설정을 반환. * 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 hlines = (saved?.hlines as HLineDef[] | undefined) ?? defaultHlines ?? def?.hlines ?? []; 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) ); }, [] ); return { getParams, saveParams, saveAll, getVisualConfig, saveVisual }; }