import type { IndicatorConfig } from '../types'; import { enrichIndicatorConfig, getIndicatorDef } from './indicatorRegistry'; import { normalizeSmaConfig, createDefaultSmaPlotVisibility } from './smaConfig'; import { normalizeIchimokuConfig } from './ichimokuConfig'; import { buildMainIndicatorConfig } from './indicatorMainConfig'; import type { PlotDef, HLineDef } from './indicatorRegistry'; import type { IchimokuCloudColors } from './ichimokuConfig'; /** 보조지표 인스턴스 복제 (새 id, 별도 pane) */ export function cloneIndicatorConfig(src: IndicatorConfig, newId: string): IndicatorConfig { return enrichIndicatorConfig({ ...src, id: newId, mergedWith: undefined, params: { ...src.params }, plots: src.plots?.map(p => ({ ...p })), hlines: src.hlines?.map(h => ({ ...h })), plotVisibility: src.plotVisibility ? { ...src.plotVisibility } : undefined, timeframeVisibility: src.timeframeVisibility ? { ...src.timeframeVisibility } : undefined, cloudColors: src.cloudColors ? { ...src.cloudColors } : undefined, hlinesBackground: src.hlinesBackground ? { ...src.hlinesBackground } : undefined, bandBackground: src.bandBackground ? { ...src.bandBackground } : undefined, }); } /** sourceId 바로 아래에 동일 설정의 보조지표 복사본 추가 */ export function duplicateIndicatorInList( sourceId: string, prev: IndicatorConfig[], newId: () => string, ): IndicatorConfig[] | null { const idx = prev.findIndex(i => i.id === sourceId); if (idx === -1) return null; const src = prev[idx]; const def = getIndicatorDef(src.type); if (!def || def.overlay || def.returnsMarkers) return null; const clone = cloneIndicatorConfig(src, newId()); const next = [...prev]; next.splice(idx + 1, 0, clone); return next; } /** mergedWith 체인의 루트(공유 pane 소유) 지표 id */ export function getPaneHostId(id: string, indicators: IndicatorConfig[]): string { const seen = new Set(); let cur = id; for (;;) { if (seen.has(cur)) return id; seen.add(cur); const ind = indicators.find(i => i.id === cur); if (!ind?.mergedWith) return cur; cur = ind.mergedWith; } } /** 같은 pane 에 묶인 지표 id 목록 (배열 순서 유지) */ export function getMergedGroupIds(hostId: string, indicators: IndicatorConfig[]): string[] { const host = getPaneHostId(hostId, indicators); return indicators .filter(i => getPaneHostId(i.id, indicators) === host) .map(i => i.id); } /** 드롭 대상 → 실제 pane 호스트 id */ export function resolveMergeTargetId(targetId: string, indicators: IndicatorConfig[]): string { return getPaneHostId(targetId, indicators); } /** reload 시 호스트 지표를 먼저 addIndicator 하도록 정렬 */ export function sortIndicatorsForPaneLoad(inds: IndicatorConfig[]): IndicatorConfig[] { const byId = new Map(inds.map(i => [i.id, i])); const out: IndicatorConfig[] = []; const added = new Set(); const addOne = (ind: IndicatorConfig) => { if (added.has(ind.id)) return; if (ind.mergedWith) { const host = byId.get(ind.mergedWith); if (host) addOne(host); } out.push(ind); added.add(ind.id); }; for (const ind of inds) addOne(ind); return out; } /** 보조지표 그룹(병합 pane) 통째로 순서 변경 */ export function reorderIndicatorGroup( fromId: string, insertBeforeId: string | null, prev: IndicatorConfig[], ): IndicatorConfig[] { const hostId = getPaneHostId(fromId, prev); const groupSet = new Set(getMergedGroupIds(hostId, prev)); const groupItems = prev.filter(i => groupSet.has(i.id)); const without = prev.filter(i => !groupSet.has(i.id)); if (insertBeforeId === null) { return [...without, ...groupItems]; } const beforeHost = getPaneHostId(insertBeforeId, prev); const toIdx = without.findIndex(i => i.id === beforeHost); without.splice(toIdx === -1 ? without.length : toIdx, 0, ...groupItems); return without; } /** fromId 를 intoId pane 에 병합 */ export function mergeIndicators( fromId: string, intoId: string, prev: IndicatorConfig[], ): IndicatorConfig[] { const host = resolveMergeTargetId(intoId, prev); if (fromId === host) return prev; if (getPaneHostId(fromId, prev) === host) return prev; return prev.map(i => (i.id === fromId ? { ...i, mergedWith: host } : i)); } /** 병합 pane 분리 — 호스트 그룹의 mergedWith 제거 */ export function splitIndicatorPane( hostOrMemberId: string, prev: IndicatorConfig[], ): IndicatorConfig[] { const host = getPaneHostId(hostOrMemberId, prev); return prev.map(i => { if (getPaneHostId(i.id, prev) !== host || !i.mergedWith) return i; const { mergedWith: _m, ...rest } = i; return rest; }); } export function layoutKey(inds: IndicatorConfig[]): string { return inds.map(i => `${i.id}>${i.mergedWith ?? ''}`).join(';'); } type ParamRecord = Record; type GetParams = (type: string, defaults?: ParamRecord) => ParamRecord; type GetVisual = ( type: string, defaultPlots?: PlotDef[], defaultHlines?: HLineDef[], ) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors; bandBackground?: import('../types').HlinesBackground; }; /** 탭 전체 추가 — 기존 차트 지표를 제거하고 types 목록만 새로 구성 */ export function replaceIndicatorConfigsFromTypes( types: string[], getParams: GetParams, getVisualConfig: GetVisual, newId: () => string, ): IndicatorConfig[] { return buildIndicatorConfigsFromTypes(types, [], getParams, getVisualConfig, newId); } /** DB 보조지표 설정(파라미터·색상·선굵기·유형)을 반영한 단일 IndicatorConfig */ export function buildChartIndicatorConfig( type: string, id: string, getParams: GetParams, getVisualConfig: GetVisual, extras: Partial = {}, ): IndicatorConfig | null { const def = getIndicatorDef(type); if (!def) return null; const params = getParams(type, def.defaultParams as ParamRecord); const { plots, hlines, cloudColors, bandBackground } = getVisualConfig( type, def.plots, def.hlines ?? [], ); let cfg: IndicatorConfig = { id, type: def.type, params, plots, hlines, ...(cloudColors ? { cloudColors } : {}), ...(bandBackground ? { bandBackground } : {}), ...extras, }; if (type === 'SMA') { const mainSma = buildMainIndicatorConfig(type, [], getParams, getVisualConfig); if (mainSma?.plotVisibility) { cfg.plotVisibility = mainSma.plotVisibility; } if (mainSma?.plots?.length) { cfg.plots = mainSma.plots; } cfg = normalizeSmaConfig({ ...cfg, plotVisibility: cfg.plotVisibility ?? createDefaultSmaPlotVisibility(), }); } else if (type === 'IchimokuCloud') { cfg = normalizeIchimokuConfig(cfg); } return enrichIndicatorConfig(cfg); } /** 지표 타입 목록 → 차트에 추가할 IndicatorConfig 배열 (이미 활성인 type 제외) */ export function buildIndicatorConfigsFromTypes( types: string[], existing: IndicatorConfig[], getParams: GetParams, getVisualConfig: GetVisual, newId: () => string, ): IndicatorConfig[] { const seen = new Set(existing.map(i => i.type)); const out: IndicatorConfig[] = []; for (const type of types) { if (seen.has(type)) continue; seen.add(type); const cfg = buildChartIndicatorConfig(type, newId(), getParams, getVisualConfig); if (cfg) out.push(cfg); } return out; } /** id 제거 — 병합 호스트 제거 시 해당 pane 멤버도 함께 제거 */ export function removeIndicatorFromList( prev: IndicatorConfig[], id: string, ): IndicatorConfig[] { return prev.filter(i => i.id !== id && i.mergedWith !== id); } /** type 제거 — 해당 type 인스턴스 및 병합 멤버 함께 제거 */ export function removeIndicatorTypeFromList( prev: IndicatorConfig[], type: string, ): IndicatorConfig[] { const removedIds = new Set(prev.filter(i => i.type === type).map(i => i.id)); return prev.filter( i => i.type !== type && (!i.mergedWith || !removedIds.has(i.mergedWith)), ); }