custom 툴발 적용
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import {
|
||||
type ChartCustomOverlaySelection,
|
||||
type CustomOverlaySectionId,
|
||||
cloneChartCustomOverlaySelection,
|
||||
smaMaLabel,
|
||||
BB_CUSTOM_LABELS,
|
||||
ICHIMOKU_CUSTOM_ROWS,
|
||||
isCustomSectionAllSelected,
|
||||
setCustomSectionAllSelected,
|
||||
} from '../utils/chartCustomOverlay';
|
||||
import { SMA_MA_COUNT, smaPlotId } from '../utils/smaConfig';
|
||||
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
||||
|
||||
interface ChartCustomOverlaySettingsModalProps {
|
||||
title?: string;
|
||||
selection: ChartCustomOverlaySelection;
|
||||
onSave: (selection: ChartCustomOverlaySelection) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const SECTION_META: Array<{
|
||||
id: CustomOverlaySectionId;
|
||||
ko: string;
|
||||
en: string;
|
||||
cols: 2 | 3;
|
||||
}> = [
|
||||
{ id: 'sma', ko: '이동평균선', en: 'Simple Moving Average (SMA)', cols: 3 },
|
||||
{ id: 'bollinger', ko: '볼린저 밴드', en: 'Bollinger Bands', cols: 2 },
|
||||
{ id: 'ichimoku', ko: '일목균형표', en: 'Ichimoku Cloud', cols: 3 },
|
||||
{ id: 'candle', ko: '캔들차트', en: 'Candlestick', cols: 2 },
|
||||
];
|
||||
|
||||
const ALL_SECTIONS = new Set<CustomOverlaySectionId>(SECTION_META.map(s => s.id));
|
||||
|
||||
const GridToggle: React.FC<{
|
||||
label: string;
|
||||
checked: boolean;
|
||||
onChange: (v: boolean) => void;
|
||||
}> = ({ label, checked, onChange }) => (
|
||||
<div className="ccos-grid-item">
|
||||
<label className="ibsm-toggle ccos-grid-toggle" onClick={e => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={e => onChange(e.target.checked)}
|
||||
/>
|
||||
<span className="ism-toggle-slider" />
|
||||
</label>
|
||||
<span className="ccos-grid-label">{label}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ChartCustomOverlaySettingsModal: React.FC<ChartCustomOverlaySettingsModalProps> = ({
|
||||
title = 'Custom 표시 설정',
|
||||
selection,
|
||||
onSave,
|
||||
onCancel,
|
||||
}) => {
|
||||
const draftRef = useRef(cloneChartCustomOverlaySelection(selection));
|
||||
const [, bump] = useState(0);
|
||||
const refresh = useCallback(() => bump(n => n + 1), []);
|
||||
const [expanded, setExpanded] = useState<Set<CustomOverlaySectionId>>(
|
||||
() => new Set(ALL_SECTIONS),
|
||||
);
|
||||
|
||||
const {
|
||||
panelRef,
|
||||
dragging,
|
||||
onHeaderPointerDown,
|
||||
headerTouchStyle,
|
||||
panelStyle,
|
||||
headerCursor,
|
||||
} = useDraggablePanel({ centerOnMount: true });
|
||||
|
||||
useEffect(() => {
|
||||
draftRef.current = cloneChartCustomOverlaySelection(selection);
|
||||
setExpanded(new Set(ALL_SECTIONS));
|
||||
refresh();
|
||||
}, [selection, refresh]);
|
||||
|
||||
const draft = draftRef.current;
|
||||
|
||||
const patch = useCallback((patchFn: (d: ChartCustomOverlaySelection) => void) => {
|
||||
patchFn(draftRef.current);
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const toggleSectionExpand = useCallback((id: CustomOverlaySectionId) => {
|
||||
setExpanded(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleSectionAll = useCallback((id: CustomOverlaySectionId) => {
|
||||
patch(d => {
|
||||
const allOn = isCustomSectionAllSelected(d, id);
|
||||
setCustomSectionAllSelected(d, id, !allOn);
|
||||
});
|
||||
}, [patch]);
|
||||
|
||||
const renderSectionItems = (sectionId: CustomOverlaySectionId) => {
|
||||
switch (sectionId) {
|
||||
case 'sma':
|
||||
return Array.from({ length: SMA_MA_COUNT }, (_, i) => {
|
||||
const pid = smaPlotId(i);
|
||||
return (
|
||||
<GridToggle
|
||||
key={pid}
|
||||
label={smaMaLabel(i)}
|
||||
checked={draft.smaPlots[pid] !== false}
|
||||
onChange={v => patch(d => { d.smaPlots[pid] = v; })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
case 'bollinger':
|
||||
return (
|
||||
<>
|
||||
<GridToggle
|
||||
label={BB_CUSTOM_LABELS.plot0}
|
||||
checked={draft.bollinger.basis}
|
||||
onChange={v => patch(d => { d.bollinger.basis = v; })}
|
||||
/>
|
||||
<GridToggle
|
||||
label={BB_CUSTOM_LABELS.plot1}
|
||||
checked={draft.bollinger.upper}
|
||||
onChange={v => patch(d => { d.bollinger.upper = v; })}
|
||||
/>
|
||||
<GridToggle
|
||||
label={BB_CUSTOM_LABELS.plot2}
|
||||
checked={draft.bollinger.lower}
|
||||
onChange={v => patch(d => { d.bollinger.lower = v; })}
|
||||
/>
|
||||
<GridToggle
|
||||
label="백그라운드 그리기"
|
||||
checked={draft.bollinger.background}
|
||||
onChange={v => patch(d => { d.bollinger.background = v; })}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case 'ichimoku':
|
||||
return ICHIMOKU_CUSTOM_ROWS.map(row => (
|
||||
<GridToggle
|
||||
key={row.key}
|
||||
label={row.label}
|
||||
checked={draft.ichimoku[row.key]}
|
||||
onChange={v => patch(d => { d.ichimoku[row.key] = v; })}
|
||||
/>
|
||||
));
|
||||
case 'candle':
|
||||
return (
|
||||
<GridToggle
|
||||
label="캔들차트"
|
||||
checked={draft.candle}
|
||||
onChange={v => patch(d => { d.candle = v; })}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ibsm-overlay"
|
||||
onMouseDown={e => { if (e.target === e.currentTarget) onCancel(); }}
|
||||
>
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="ibsm-dialog ibsm-dialog-wide ccos-dialog"
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
style={{
|
||||
...panelStyle,
|
||||
zIndex: 5002,
|
||||
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">{title}</span>
|
||||
<div className="ibsm-header-actions">
|
||||
<button type="button" className="gc-popup-close ibsm-icon-btn" onClick={onCancel} title="닫기">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="ibsm-hint ccos-hint">
|
||||
Custom <strong>적용</strong> 시 아래에서 켠 항목만 차트에 표시됩니다. 각 지표 설정의 on/off와는 별개이며,
|
||||
지표 우측 스위치는 해당 지표의 모든 항목을 한 번에 선택·해제합니다.
|
||||
</p>
|
||||
|
||||
<div className="ibsm-body ccos-body">
|
||||
{SECTION_META.map(meta => {
|
||||
const isExpanded = expanded.has(meta.id);
|
||||
const allOn = isCustomSectionAllSelected(draft, meta.id);
|
||||
return (
|
||||
<div
|
||||
key={meta.id}
|
||||
className={`ibsm-card ibsm-main-row${allOn ? ' ibsm-main-row--active' : ''}${isExpanded ? ' ibsm-card-expanded' : ''}`}
|
||||
>
|
||||
<div className="ibsm-card-main ibsm-main-row-head">
|
||||
<div className="ibsm-main-row-info">
|
||||
<span className="ibsm-main-row-ko">{meta.ko}</span>
|
||||
<span className="ibsm-main-row-en">{meta.en}</span>
|
||||
</div>
|
||||
<div className="ibsm-main-row-badges">
|
||||
<span className={allOn ? 'ibsm-main-badge ibsm-main-badge--on' : 'ibsm-main-badge'}>
|
||||
{allOn ? '전체 선택' : '일부 선택'}
|
||||
</span>
|
||||
</div>
|
||||
<label
|
||||
className="ibsm-toggle"
|
||||
title={allOn ? '전체 해제' : '전체 선택'}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allOn}
|
||||
onChange={() => toggleSectionAll(meta.id)}
|
||||
/>
|
||||
<span className="ism-toggle-slider" />
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="ibsm-expand"
|
||||
onClick={() => toggleSectionExpand(meta.id)}
|
||||
aria-expanded={isExpanded}
|
||||
title={isExpanded ? '접기' : '펼치기'}
|
||||
>
|
||||
{isExpanded ? '▲' : '▼'}
|
||||
</button>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div className={`ibsm-card-expand ccos-item-grid ccos-item-grid--${meta.cols}`}>
|
||||
{renderSectionItems(meta.id)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="ibsm-footer">
|
||||
<button type="button" className="ism-btn-cancel" onClick={onCancel}>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ism-btn-ok"
|
||||
onClick={() => onSave(cloneChartCustomOverlaySelection(draftRef.current))}
|
||||
>
|
||||
적용
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChartCustomOverlaySettingsModal;
|
||||
Reference in New Issue
Block a user