goldenChat base source add
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
||||
import { INDICATOR_REGISTRY, type IndicatorDef, type IndicatorCategory } from '../utils/indicatorRegistry';
|
||||
import type { IndicatorConfig } from '../types';
|
||||
import { getNumericParamSpec } from '../utils/indicatorParamSpec';
|
||||
import NumericParamInput from './NumericParamInput';
|
||||
|
||||
interface IndicatorModalProps {
|
||||
activeIndicators: IndicatorConfig[];
|
||||
onAdd: (def: IndicatorDef, params: Record<string, number | string | boolean>) => void;
|
||||
onRemove: (id: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const CATEGORIES: IndicatorCategory[] = [
|
||||
'Moving Averages', 'Channels & Bands', 'Oscillators', 'Momentum',
|
||||
'Trend', 'Volatility', 'Volume', 'Candlestick Patterns',
|
||||
];
|
||||
|
||||
const IndicatorModal: React.FC<IndicatorModalProps> = ({ activeIndicators, onAdd, onRemove, onClose }) => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [category, setCategory] = useState<IndicatorCategory | 'All'>('All');
|
||||
const [selected, setSelected] = useState<IndicatorDef | null>(null);
|
||||
const [params, setParams] = useState<Record<string, number | string | boolean>>({});
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return INDICATOR_REGISTRY.filter(d => {
|
||||
const matchCat = category === 'All' || d.category === category;
|
||||
const matchQ = !search || d.name.toLowerCase().includes(search.toLowerCase()) || d.shortName.toLowerCase().includes(search.toLowerCase());
|
||||
return matchCat && matchQ;
|
||||
});
|
||||
}, [search, category]);
|
||||
|
||||
const select = (def: IndicatorDef) => {
|
||||
setSelected(def);
|
||||
setParams({ ...def.defaultParams });
|
||||
};
|
||||
|
||||
const isActive = (type: string) => activeIndicators.some(a => a.type === type);
|
||||
|
||||
const handleAdd = () => {
|
||||
if (!selected) return;
|
||||
onAdd(selected, params);
|
||||
setSelected(null);
|
||||
};
|
||||
|
||||
const handleRemoveAll = (type: string) => {
|
||||
activeIndicators.filter(a => a.type === type).forEach(a => onRemove(a.id));
|
||||
};
|
||||
|
||||
const {
|
||||
panelRef,
|
||||
dragging,
|
||||
onHeaderPointerDown, headerTouchStyle,
|
||||
panelStyle,
|
||||
headerCursor,
|
||||
} = useDraggablePanel({ centerOnMount: true });
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onMouseDown={e => { if (e.target === e.currentTarget) onClose(); }}>
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="indicator-modal"
|
||||
style={{
|
||||
...panelStyle,
|
||||
zIndex: 5001,
|
||||
cursor: dragging ? 'grabbing' : undefined,
|
||||
}}
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
className="gc-popup-header modal-header"
|
||||
onPointerDown={onHeaderPointerDown}
|
||||
style={{ cursor: headerCursor, ...headerTouchStyle }}
|
||||
>
|
||||
<span className="gc-popup-title modal-title">📈 지표 추가</span>
|
||||
<span className="modal-subtitle">{INDICATOR_REGISTRY.length}개 지표 지원</span>
|
||||
<button type="button" className="gc-popup-close modal-close" onClick={onClose}>✕</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-search">
|
||||
<input
|
||||
className="modal-search-input"
|
||||
placeholder="지표 검색..."
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="modal-cats">
|
||||
{(['All', ...CATEGORIES] as Array<'All' | IndicatorCategory>).map(c => (
|
||||
<button
|
||||
key={c}
|
||||
className={`cat-btn ${category === c ? 'active' : ''}`}
|
||||
onClick={() => setCategory(c)}
|
||||
>{c === 'All' ? `전체 (${INDICATOR_REGISTRY.length})` : c} {c !== 'All' && `(${INDICATOR_REGISTRY.filter(d => d.category === c).length})`}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
<div className="indicator-list">
|
||||
{filtered.map(def => (
|
||||
<div
|
||||
key={def.type}
|
||||
className={`indicator-item ${selected?.type === def.type ? 'selected' : ''} ${isActive(def.type) ? 'in-use' : ''}`}
|
||||
onClick={() => select(def)}
|
||||
>
|
||||
<div className="indicator-item-left">
|
||||
<span className="indicator-name">{def.name}</span>
|
||||
<span className="indicator-desc">{def.description}</span>
|
||||
</div>
|
||||
<div className="indicator-item-right">
|
||||
{def.overlay && <span className="badge overlay">오버레이</span>}
|
||||
{def.returnsMarkers && <span className="badge markers">마커</span>}
|
||||
{isActive(def.type) ? (
|
||||
<button className="rm-btn" onClick={e => { e.stopPropagation(); handleRemoveAll(def.type); }}>제거</button>
|
||||
) : (
|
||||
<button className="add-btn" onClick={e => { e.stopPropagation(); select(def); onAdd(def, { ...def.defaultParams }); }}>+</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{filtered.length === 0 && (
|
||||
<div className="no-results">'{search}' 검색 결과 없음</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selected && (
|
||||
<div className="indicator-settings">
|
||||
<div className="settings-title">{selected.name} 설정</div>
|
||||
<div className="settings-desc">{selected.description}</div>
|
||||
{Object.entries(params).map(([key, val]) => (
|
||||
<div key={key} className="param-row">
|
||||
<label className="param-label">{key}</label>
|
||||
{typeof val === 'boolean' ? (
|
||||
<input type="checkbox" checked={val as boolean} onChange={e => setParams(p => ({ ...p, [key]: e.target.checked }))} />
|
||||
) : typeof val === 'number' ? (
|
||||
<NumericParamInput
|
||||
className="param-input"
|
||||
value={val as number}
|
||||
spec={getNumericParamSpec(selected.type, key, val as number)}
|
||||
onChange={v => setParams(p => ({ ...p, [key]: v }))}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
className="param-input"
|
||||
type="text"
|
||||
value={val as string}
|
||||
onChange={e => setParams(p => ({ ...p, [key]: e.target.value }))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="settings-actions">
|
||||
<button className="settings-add-btn" onClick={handleAdd}>지표 추가</button>
|
||||
<button className="settings-cancel" onClick={() => setSelected(null)}>취소</button>
|
||||
</div>
|
||||
{selected.plots.length > 0 && (
|
||||
<div className="plot-preview">
|
||||
{selected.plots.map(p => (
|
||||
<div key={p.id} className="plot-legend-row">
|
||||
<span className="plot-color" style={{ background: p.color }} />
|
||||
<span className="plot-title">{p.title}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activeIndicators.length > 0 && (
|
||||
<div className="modal-active">
|
||||
<div className="active-title">활성 지표 ({activeIndicators.length})</div>
|
||||
<div className="active-list">
|
||||
{activeIndicators.map(a => (
|
||||
<span key={a.id} className="active-tag">
|
||||
{a.type}
|
||||
<button onClick={() => onRemove(a.id)}>✕</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndicatorModal;
|
||||
Reference in New Issue
Block a user