import { SMA_MA_COUNT, smaPlotId, formatSmaMaDisplayLabel } from './smaConfig'; /** Custom / Custom1 — 동일 기능, 설정만 분리 (세션만, DB 미저장) */ export type ChartCustomOverlaySlotId = 'custom' | 'custom1'; export const CHART_CUSTOM_OVERLAY_SLOTS: ChartCustomOverlaySlotId[] = ['custom', 'custom1']; /** Custom 적용 시 표시할 항목 (세션만, DB 미저장) */ export type ChartCustomOverlaySelection = { smaPlots: Record; bollinger: { basis: boolean; upper: boolean; lower: boolean; background: boolean; }; ichimoku: { conversion: boolean; base: boolean; lagging: boolean; spanA: boolean; spanB: boolean; bullishCloud: boolean; bearishCloud: boolean; }; candle: boolean; }; export const BB_CUSTOM_PLOT_IDS = ['plot0', 'plot1', 'plot2'] as const; export const BB_CUSTOM_LABELS: Record<(typeof BB_CUSTOM_PLOT_IDS)[number], string> = { plot0: '중앙값', plot1: '어퍼', plot2: '로우어', }; export function createDefaultChartCustomOverlaySelection(): ChartCustomOverlaySelection { const smaPlots: Record = {}; for (let i = 0; i < SMA_MA_COUNT; i++) { smaPlots[smaPlotId(i)] = true; } return { smaPlots, bollinger: { basis: true, upper: true, lower: true, background: true, }, ichimoku: { conversion: true, base: true, lagging: true, spanA: true, spanB: true, bullishCloud: true, bearishCloud: true, }, candle: true, }; } export function cloneChartCustomOverlaySelection( sel: ChartCustomOverlaySelection, ): ChartCustomOverlaySelection { return { smaPlots: { ...sel.smaPlots }, bollinger: { ...sel.bollinger }, ichimoku: { ...sel.ichimoku }, candle: sel.candle, }; } export function smaMaLabel( plotIndex: number, params?: Record, ): string { return formatSmaMaDisplayLabel(params, plotIndex + 1); } export const ICHIMOKU_CUSTOM_ROWS: Array<{ key: keyof ChartCustomOverlaySelection['ichimoku']; plotId?: string; label: string; }> = [ { key: 'conversion', plotId: 'plot0', label: '전환선' }, { key: 'base', plotId: 'plot1', label: '기준선' }, { key: 'lagging', plotId: 'plot2', label: '후행스팬' }, { key: 'spanA', plotId: 'plot3', label: '선행스팬1' }, { key: 'spanB', plotId: 'plot4', label: '선행스팬2' }, { key: 'bullishCloud', label: '상승 구름' }, { key: 'bearishCloud', label: '하락 구름' }, ]; /** Custom 적용 시 plot 단위로 표시를 제어하는 지표 type */ export const CUSTOM_OVERLAY_PLOT_CONTROLLED_TYPES = [ 'SMA', 'BollingerBands', 'IchimokuCloud', ] as const; export function isCustomOverlayPlotControlledType(type: string): boolean { return (CUSTOM_OVERLAY_PLOT_CONTROLLED_TYPES as readonly string[]).includes(type); } /** 지표 설정 plotVisibility 와 무관 — custom 적용 시 이 맵만 사용 */ export function isCustomPlotSelected( sel: ChartCustomOverlaySelection, type: string, plotId: string, ): boolean { if (type === 'SMA') { return sel.smaPlots[plotId] === true; } if (type === 'BollingerBands') { if (plotId === 'plot0') return sel.bollinger.basis; if (plotId === 'plot1') return sel.bollinger.upper; if (plotId === 'plot2') return sel.bollinger.lower; return false; } if (type === 'IchimokuCloud') { const row = ICHIMOKU_CUSTOM_ROWS.find(r => r.plotId === plotId); if (!row) return false; return sel.ichimoku[row.key] === true; } return true; } export function isCustomIchimokuCloudVisible( sel: ChartCustomOverlaySelection, which: 'bullish' | 'bearish', ): boolean { return which === 'bullish' ? sel.ichimoku.bullishCloud : sel.ichimoku.bearishCloud; } export type CustomOverlaySectionId = 'sma' | 'bollinger' | 'ichimoku' | 'candle'; export function isCustomSectionAllSelected( sel: ChartCustomOverlaySelection, section: CustomOverlaySectionId, ): boolean { switch (section) { case 'sma': return Array.from({ length: SMA_MA_COUNT }, (_, i) => sel.smaPlots[smaPlotId(i)] !== false, ).every(Boolean); case 'bollinger': return sel.bollinger.basis && sel.bollinger.upper && sel.bollinger.lower && sel.bollinger.background; case 'ichimoku': return ICHIMOKU_CUSTOM_ROWS.every(r => sel.ichimoku[r.key]); case 'candle': return sel.candle; default: return false; } } export function setCustomSectionAllSelected( sel: ChartCustomOverlaySelection, section: CustomOverlaySectionId, on: boolean, ): void { switch (section) { case 'sma': for (let i = 0; i < SMA_MA_COUNT; i++) { sel.smaPlots[smaPlotId(i)] = on; } break; case 'bollinger': sel.bollinger.basis = on; sel.bollinger.upper = on; sel.bollinger.lower = on; sel.bollinger.background = on; break; case 'ichimoku': for (const row of ICHIMOKU_CUSTOM_ROWS) { sel.ichimoku[row.key] = on; } break; case 'candle': sel.candle = on; break; default: break; } } export type ChartCustomOverlaySlotState = { active: boolean; selection: ChartCustomOverlaySelection; }; /** UI·Manager 공통: 한 번에 하나의 슬롯만 적용 (다른 슬롯 적용 시 기존 슬롯 자동 해제) */ export function isCustomOverlaySlotActive( activeSlot: ChartCustomOverlaySlotId | null, slot: ChartCustomOverlaySlotId, ): boolean { return activeSlot === slot; } export function resolveActiveCustomOverlaySlot( slot: ChartCustomOverlaySlotId, turnOn: boolean, current: ChartCustomOverlaySlotId | null, ): ChartCustomOverlaySlotId | null { if (turnOn) return slot; return current === slot ? null : current; } type CustomOverlayManagerApi = { setChartCustomOverlaySelection( selection: ChartCustomOverlaySelection, slot: ChartCustomOverlaySlotId, ): void; setChartCustomOverlayActive(active: boolean, slot: ChartCustomOverlaySlotId): void; }; /** React → ChartManager: 두 슬롯 선택값 동기화 후 적용 중인 슬롯만 활성화 */ export function syncChartManagerCustomOverlays( mgr: CustomOverlayManagerApi, custom: ChartCustomOverlaySlotState, custom1: ChartCustomOverlaySlotState, ): void { mgr.setChartCustomOverlaySelection(custom.selection, 'custom'); mgr.setChartCustomOverlaySelection(custom1.selection, 'custom1'); mgr.setChartCustomOverlayActive(false, 'custom'); mgr.setChartCustomOverlayActive(false, 'custom1'); if (custom1.active) { mgr.setChartCustomOverlayActive(true, 'custom1'); } else if (custom.active) { mgr.setChartCustomOverlayActive(true, 'custom'); } } /** 적용 슬롯 하나만 — Custom 적용 중 Custom1 적용 시 Custom 자동 미적용 */ export function syncChartManagerCustomOverlaysByActiveSlot( mgr: CustomOverlayManagerApi, activeSlot: ChartCustomOverlaySlotId | null, customSelection: ChartCustomOverlaySelection, custom1Selection: ChartCustomOverlaySelection, ): void { syncChartManagerCustomOverlays( mgr, { active: activeSlot === 'custom', selection: customSelection }, { active: activeSlot === 'custom1', selection: custom1Selection }, ); }