228 lines
7.9 KiB
TypeScript
228 lines
7.9 KiB
TypeScript
/**
|
|
* 보조지표 일괄 설정 — Main 16종 + 전체 보조지표, 검색 필터
|
|
*/
|
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
|
import type { IndicatorConfig } from '../types';
|
|
import type { PlotDef, HLineDef } from '../utils/indicatorRegistry';
|
|
import {
|
|
buildAllIndicatorDraftMap,
|
|
applyAllDraftToChart,
|
|
allDraftListFromMap,
|
|
indicatorTypesOnChart,
|
|
type MainIndicatorDraftMap,
|
|
} from '../utils/indicatorMainConfig';
|
|
import { resetConfigToDefaults } from '../utils/indicatorSettingsEditor';
|
|
import {
|
|
getSettingsIndicatorTypes,
|
|
filterSettingsIndicatorTypes,
|
|
partitionFilteredTypes,
|
|
} from '../utils/indicatorSettingsList';
|
|
import IndicatorSettingsListSearch from './IndicatorSettingsListSearch';
|
|
import IndicatorSettingsListRow from './IndicatorSettingsListRow';
|
|
import type { IchimokuCloudColors } from '../utils/ichimokuConfig';
|
|
import { MAIN_INDICATOR_TYPES } from '../utils/indicatorMainTab';
|
|
|
|
export interface IndicatorBulkSettingsModalProps {
|
|
open: boolean;
|
|
indicators: IndicatorConfig[];
|
|
chartMarket?: string;
|
|
onApply: (result: {
|
|
chartIndicators: IndicatorConfig[];
|
|
allConfigs: IndicatorConfig[];
|
|
}) => void;
|
|
onLiveChartChange: (chartIndicators: IndicatorConfig[]) => void;
|
|
onClose: () => void;
|
|
getParams: (type: string, defaults: Record<string, number | string | boolean>) => Record<string, number | string | boolean>;
|
|
getVisualConfig: (
|
|
type: string,
|
|
defaultPlots?: PlotDef[],
|
|
defaultHlines?: HLineDef[],
|
|
) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors };
|
|
}
|
|
|
|
const ALL_TYPES = getSettingsIndicatorTypes();
|
|
|
|
const IndicatorBulkSettingsModal: React.FC<IndicatorBulkSettingsModalProps> = ({
|
|
open,
|
|
indicators,
|
|
chartMarket = '',
|
|
onApply,
|
|
onLiveChartChange,
|
|
onClose,
|
|
getParams,
|
|
getVisualConfig,
|
|
}) => {
|
|
const [draftMap, setDraftMap] = useState<MainIndicatorDraftMap>({});
|
|
const [expandedTypes, setExpandedTypes] = useState<Set<string>>(new Set());
|
|
const [search, setSearch] = useState('');
|
|
const {
|
|
panelRef: dialogRef,
|
|
dragging,
|
|
onHeaderPointerDown, headerTouchStyle,
|
|
panelStyle,
|
|
headerCursor,
|
|
} = useDraggablePanel({ centerOnMount: true });
|
|
|
|
const onChartTypes = useMemo(() => indicatorTypesOnChart(indicators), [indicators]);
|
|
|
|
const filtered = useMemo(
|
|
() => filterSettingsIndicatorTypes(ALL_TYPES, search),
|
|
[search],
|
|
);
|
|
const { main: filteredMain, other: filteredOther } = useMemo(
|
|
() => partitionFilteredTypes(filtered),
|
|
[filtered],
|
|
);
|
|
|
|
const prevOpenRef = useRef(false);
|
|
useEffect(() => {
|
|
if (open && !prevOpenRef.current) {
|
|
setDraftMap(buildAllIndicatorDraftMap(indicators, getParams, getVisualConfig));
|
|
setExpandedTypes(new Set());
|
|
setSearch('');
|
|
}
|
|
prevOpenRef.current = open;
|
|
}, [open, indicators, getParams, getVisualConfig]);
|
|
|
|
const setTypeEnabled = useCallback((type: string, enabled: boolean) => {
|
|
const next = new Set(onChartTypes);
|
|
if (enabled) next.add(type);
|
|
else next.delete(type);
|
|
onLiveChartChange(applyAllDraftToChart(indicators, draftMap, next));
|
|
}, [onChartTypes, indicators, draftMap, onLiveChartChange]);
|
|
|
|
const updateType = useCallback((type: string, updated: IndicatorConfig) => {
|
|
setDraftMap(prev => ({ ...prev, [type]: updated }));
|
|
}, []);
|
|
|
|
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 handleResetAll = useCallback(() => {
|
|
if (!window.confirm('목록에 있는 모든 보조지표 설정을 기본값으로 되돌릴까요?')) return;
|
|
setDraftMap(prev => {
|
|
const next = { ...prev };
|
|
for (const type of ALL_TYPES) {
|
|
const cfg = next[type];
|
|
if (!cfg) continue;
|
|
const reset = resetConfigToDefaults(cfg);
|
|
const active = indicators.find(i => i.type === type);
|
|
next[type] = {
|
|
...reset,
|
|
id: onChartTypes.has(type) && active ? active.id : `template_${type}`,
|
|
};
|
|
}
|
|
return next;
|
|
});
|
|
}, [indicators, onChartTypes]);
|
|
|
|
const handleApply = useCallback(() => {
|
|
const allConfigs = allDraftListFromMap(draftMap);
|
|
const chartIndicators = applyAllDraftToChart(indicators, draftMap, onChartTypes);
|
|
onApply({ chartIndicators, allConfigs });
|
|
onClose();
|
|
}, [draftMap, onChartTypes, indicators, onApply, onClose]);
|
|
|
|
const renderRows = (types: string[]) => types.map(type => {
|
|
const cfg = draftMap[type];
|
|
if (!cfg) return null;
|
|
return (
|
|
<IndicatorSettingsListRow
|
|
key={type}
|
|
type={type}
|
|
config={cfg}
|
|
enabled={onChartTypes.has(type)}
|
|
chartMarket={chartMarket}
|
|
expanded={expandedTypes.has(type)}
|
|
variant="bulk"
|
|
onToggleExpand={() => toggleExpand(type)}
|
|
onToggleEnabled={on => setTypeEnabled(type, on)}
|
|
onChange={updated => updateType(type, updated)}
|
|
/>
|
|
);
|
|
});
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div className="ibsm-overlay" onMouseDown={e => { if (e.target === e.currentTarget) onClose(); }}>
|
|
<div
|
|
ref={dialogRef}
|
|
className="ibsm-dialog ibsm-dialog-wide"
|
|
onMouseDown={e => e.stopPropagation()}
|
|
style={{
|
|
...panelStyle,
|
|
zIndex: 5001,
|
|
cursor: dragging ? 'grabbing' : undefined,
|
|
}}
|
|
>
|
|
<div
|
|
className="gc-popup-header ibsm-header"
|
|
onPointerDown={onHeaderPointerDown}
|
|
style={{ cursor: headerCursor, ...headerTouchStyle }}
|
|
>
|
|
<span className="gc-popup-title ibsm-header-title">📊 보조지표 설정</span>
|
|
<div className="ibsm-header-actions">
|
|
<button type="button" className="ibsm-icon-btn" onClick={handleResetAll} title="전체 기본값">↻</button>
|
|
<button type="button" className="gc-popup-close ibsm-icon-btn" onClick={onClose} title="닫기">✕</button>
|
|
</div>
|
|
</div>
|
|
|
|
<IndicatorSettingsListSearch
|
|
value={search}
|
|
onChange={setSearch}
|
|
totalCount={ALL_TYPES.length}
|
|
filteredCount={filtered.length}
|
|
className="ibsm-search-block"
|
|
/>
|
|
|
|
<p className="ibsm-hint">
|
|
<strong>Main</strong> 탭 {MAIN_INDICATOR_TYPES.length}종을 상단에 두었습니다. 우측 스위치로 차트에 즉시 추가·제거하고,
|
|
▼에서 파라미터·색상을 편집할 수 있습니다. <strong>적용</strong> 시 DB 기본값에 저장됩니다.
|
|
</p>
|
|
|
|
<div className="ibsm-body">
|
|
{filtered.length === 0 ? (
|
|
<div className="no-results ind-settings-no-results">
|
|
'{search}' 검색 결과 없음
|
|
</div>
|
|
) : (
|
|
<>
|
|
{filteredMain.length > 0 && (
|
|
<section className="ind-settings-section">
|
|
<h3 className="ind-settings-section-title">
|
|
Main 보조지표 <span className="ind-settings-section-count">{filteredMain.length}</span>
|
|
</h3>
|
|
{renderRows(filteredMain)}
|
|
</section>
|
|
)}
|
|
{filteredOther.length > 0 && (
|
|
<section className="ind-settings-section">
|
|
<h3 className="ind-settings-section-title">
|
|
기타 보조지표 <span className="ind-settings-section-count">{filteredOther.length}</span>
|
|
</h3>
|
|
{renderRows(filteredOther)}
|
|
</section>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="ibsm-footer">
|
|
<button type="button" className="ism-btn-cancel" onClick={onClose}>취소</button>
|
|
<button type="button" className="ism-btn-ok" onClick={handleApply}>적용</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default IndicatorBulkSettingsModal;
|