/** * 설정 화면 — 보조지표 기본값 (Main 16종 상단 + 전체 지표, 검색 필터) */ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import type { IndicatorConfig } from '../types'; import type { PlotDef, HLineDef } from '../utils/indicatorRegistry'; import { buildMainIndicatorConfig } from '../utils/indicatorMainConfig'; import { indicatorDefaultsFingerprint } from '../utils/indicatorDefaultsFingerprint'; import IndicatorSettingsForm from './IndicatorSettingsForm'; import { resetConfigToDefaults } from '../utils/indicatorSettingsEditor'; import type { IchimokuCloudColors } from '../utils/ichimokuConfig'; import { getSettingsIndicatorTypes, filterSettingsIndicatorTypes, partitionFilteredTypes, } from '../utils/indicatorSettingsList'; import IndicatorSettingsListSearch from './IndicatorSettingsListSearch'; import IndicatorSettingsListRow from './IndicatorSettingsListRow'; import { MAIN_INDICATOR_TYPES } from '../utils/indicatorMainTab'; import { useIndicatorListOrder } from '../hooks/useIndicatorListOrder'; export interface IndicatorSaveUiState { save: () => Promise; dirty: boolean; saving: boolean; saveMessage: string | null; } export interface IndicatorMainDefaultsPanelProps { chartIndicators: IndicatorConfig[]; onAddToChart: (type: string, config?: IndicatorConfig) => void; onRemoveFromChart: (type: string) => void; getParams: (type: string, defaults: Record) => Record; saveParams: (type: string, params: Record) => void; getVisualConfig: ( type: string, defaultPlots?: PlotDef[], defaultHlines?: HLineDef[], ) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors }; saveVisual: ( type: string, plots?: PlotDef[], hlines?: HLineDef[], cloudColors?: IchimokuCloudColors, ) => void; /** DB에 보조지표 기본값 일괄 저장 */ saveAllDefaults?: (configs: Record) => Promise; /** 상단 저장 버튼 상태 (SettingsPage 헤더 연동) */ onSaveUiState?: (state: IndicatorSaveUiState | null) => void; /** DB 로드 완료·저장 후 재로드 트리거 */ settingsRevision?: number; /** 목록 순서 변경 시 차트 보조지표 순서 동기화 */ onListOrderChange?: (orderedTypes: string[]) => void; } const ALL_TYPES = getSettingsIndicatorTypes(); function buildDefaultsMap( getParams: IndicatorMainDefaultsPanelProps['getParams'], getVisualConfig: IndicatorMainDefaultsPanelProps['getVisualConfig'], ): Record { const next: Record = {}; for (const type of ALL_TYPES) { const cfg = buildMainIndicatorConfig(type, [], getParams, getVisualConfig); if (cfg) next[type] = cfg; } return next; } function fingerprintMap(map: Record): string { return ALL_TYPES.map(t => map[t] ? indicatorDefaultsFingerprint(map[t]) : '').join('|'); } const IndicatorMainDefaultsPanel: React.FC = ({ chartIndicators, onAddToChart, onRemoveFromChart, getParams, getVisualConfig, saveAllDefaults, onSaveUiState, settingsRevision = 0, onListOrderChange, }) => { const onChartTypes = useMemo(() => new Set(chartIndicators.map(i => i.type)), [chartIndicators]); const { listOrder, moveType } = useIndicatorListOrder(onListOrderChange); const [draggingType, setDraggingType] = useState(null); const [dragOverType, setDragOverType] = useState(null); const [expandedTypes, setExpandedTypes] = useState>(() => new Set()); const [configs, setConfigs] = useState>({}); const [baselineFp, setBaselineFp] = useState(''); const [search, setSearch] = useState(''); const [saving, setSaving] = useState(false); const [saveMessage, setSaveMessage] = useState(null); const reloadFromDb = useCallback(() => { const next = buildDefaultsMap(getParams, getVisualConfig); setConfigs(next); setBaselineFp(fingerprintMap(next)); setSaveMessage(null); }, [getParams, getVisualConfig]); useEffect(() => { reloadFromDb(); }, [reloadFromDb, settingsRevision]); useEffect(() => { const clearDrag = () => { setDraggingType(null); setDragOverType(null); }; document.addEventListener('dragend', clearDrag); return () => document.removeEventListener('dragend', clearDrag); }, []); const dirty = useMemo( () => baselineFp !== '' && fingerprintMap(configs) !== baselineFp, [configs, baselineFp], ); const filtered = useMemo( () => filterSettingsIndicatorTypes(listOrder, search), [listOrder, search], ); const { main: filteredMain, other: filteredOther } = useMemo( () => partitionFilteredTypes(filtered), [filtered], ); const toggleExpand = useCallback((type: string) => { setExpandedTypes(prev => { const next = new Set(prev); if (next.has(type)) next.delete(type); else next.add(type); return next; }); }, []); const handleConfigChange = useCallback((type: string, updated: IndicatorConfig) => { setConfigs(prev => ({ ...prev, [type]: updated })); setSaveMessage(null); }, []); const handleToggleChart = useCallback((type: string, enabled: boolean) => { if (enabled) { onAddToChart(type, configs[type]); } else { onRemoveFromChart(type); } }, [configs, onAddToChart, onRemoveFromChart]); const handleRowDefaults = useCallback((type: string) => { const base = configs[type]; if (!base) return; const reset = resetConfigToDefaults(base); handleConfigChange(type, reset); }, [configs, handleConfigChange]); const handleResetAll = useCallback(() => { if (!window.confirm('목록에 있는 모든 보조지표 기본값을 초기화할까요? (저장 버튼을 눌러야 DB에 반영됩니다)')) return; const next: Record = {}; for (const type of ALL_TYPES) { const base = configs[type] ?? buildMainIndicatorConfig(type, [], getParams, getVisualConfig); if (!base) continue; next[type] = resetConfigToDefaults(base); } setConfigs(next); setSaveMessage(null); }, [configs, getParams, getVisualConfig]); const handleSaveAll = useCallback(async () => { if (!saveAllDefaults) { setSaveMessage('저장 API를 사용할 수 없습니다.'); return; } if (!dirty) return; setSaving(true); setSaveMessage(null); try { await saveAllDefaults(configs); const fp = fingerprintMap(configs); setBaselineFp(fp); setSaveMessage('저장되었습니다.'); } catch (err) { console.error('[IndicatorMainDefaultsPanel] save failed', err); setSaveMessage('저장에 실패했습니다. 다시 시도해 주세요.'); } finally { setSaving(false); } }, [saveAllDefaults, configs, dirty]); useEffect(() => { if (!onSaveUiState) return; onSaveUiState({ save: handleSaveAll, dirty, saving, saveMessage, }); return () => onSaveUiState(null); }, [onSaveUiState, handleSaveAll, dirty, saving, saveMessage]); const canReorder = !search.trim(); const renderRows = (types: string[]) => types.map(type => { const cfg = configs[type]; if (!cfg) return null; return ( { setDraggingType(type); e.dataTransfer.setData('text/indicator-type', type); e.dataTransfer.effectAllowed = 'move'; }} onDragOver={e => { if (!draggingType || draggingType === type) return; e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDragOverType(type); }} onDragLeave={() => { if (dragOverType === type) setDragOverType(null); }} onDrop={e => { e.preventDefault(); const from = e.dataTransfer.getData('text/indicator-type') || draggingType; if (from && from !== type) moveType(from, type, 'before'); setDraggingType(null); setDragOverType(null); }} onToggleExpand={() => toggleExpand(type)} onToggleEnabled={on => handleToggleChart(type, on)} onChange={updated => handleConfigChange(type, updated)} onRowDefaults={() => handleRowDefaults(type)} /> ); }); return ( <>

지표 추가 팝업 Main 탭 {MAIN_INDICATOR_TYPES.length}종을 상단에 두었고, 그 아래에 등록된 모든 보조지표가 있습니다. 우측 스위치로 차트에 바로 추가·제거할 수 있습니다. 핸들로 목록 순서를 바꿀 수 있으며, 차트에 표시되는 보조지표 pane 순서도 이 목록을 따릅니다. 파라미터·색상을 변경한 뒤 상단 저장 버튼을 눌러 DB에 반영하세요.

{filtered.length === 0 ? (
'{search}' 검색 결과 없음
) : ( <> {filteredMain.length > 0 && (

Main 보조지표 {filteredMain.length}

{renderRows(filteredMain)}
)} {filteredOther.length > 0 && (

기타 보조지표 {filteredOther.length}

{renderRows(filteredOther)}
)} )}
); }; export default IndicatorMainDefaultsPanel;