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;
|
||||
@@ -3,6 +3,7 @@
|
||||
* pane 하단: 드래그 핸들 + 펼침메뉴(분리·복사·전체보기·종료)
|
||||
*/
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { IndicatorConfig } from '../types';
|
||||
import { ChartManager } from '../utils/ChartManager';
|
||||
import { getPaneHostId } from '../utils/indicatorPaneMerge';
|
||||
@@ -35,6 +36,13 @@ export interface ChartRightToolbarProps {
|
||||
onToggleHidden?: (id: string) => void;
|
||||
/** 지표명 툴바 — 설정 모달 */
|
||||
onSettings?: (id: string) => void;
|
||||
/** Custom / Custom1 오버레이 — 적용 여부 (동시 적용 불가, 설정은 슬롯별 분리) */
|
||||
customOverlayActive?: boolean;
|
||||
onCustomOverlayActiveChange?: (active: boolean) => void;
|
||||
onOpenCustomOverlaySettings?: () => void;
|
||||
custom1OverlayActive?: boolean;
|
||||
onCustom1OverlayActiveChange?: (active: boolean) => void;
|
||||
onOpenCustom1OverlaySettings?: () => void;
|
||||
}
|
||||
|
||||
const BTN_SIZE = 32;
|
||||
@@ -159,6 +167,24 @@ const IconEye = ({ hidden }: { hidden?: boolean }) => hidden ? (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CUSTOM_MENU_ID = '__custom_overlay__';
|
||||
const CUSTOM1_MENU_ID = '__custom1_overlay__';
|
||||
|
||||
const IconCustom = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
|
||||
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
||||
<path d="M2 4h10M2 7h10M2 10h6"/>
|
||||
<circle cx="10.5" cy="10" r="1.2" fill="currentColor" stroke="none"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconCheck = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="2,7.5 5.5,11 12,3"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconGear = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.4">
|
||||
<circle cx="7" cy="7" r="2.3"/>
|
||||
@@ -166,6 +192,62 @@ const IconGear = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CustomOverlayFlyoutMenu: React.FC<{
|
||||
title: string;
|
||||
flyoutPos: { top: number; left: number };
|
||||
overlayActive: boolean;
|
||||
onToggleActive: () => void;
|
||||
onOpenSettings: () => void;
|
||||
onCancelClose: () => void;
|
||||
onScheduleClose: () => void;
|
||||
onCloseFlyout: () => void;
|
||||
}> = ({
|
||||
title,
|
||||
flyoutPos,
|
||||
overlayActive,
|
||||
onToggleActive,
|
||||
onOpenSettings,
|
||||
onCancelClose,
|
||||
onScheduleClose,
|
||||
onCloseFlyout,
|
||||
}) => createPortal(
|
||||
<div
|
||||
className="tv-side-flyout tv-side-flyout--right-anchor tv-side-flyout--custom-overlay"
|
||||
style={{ top: flyoutPos.top, left: flyoutPos.left }}
|
||||
onMouseEnter={onCancelClose}
|
||||
onMouseLeave={onScheduleClose}
|
||||
>
|
||||
<div className="tv-side-flyout-header">{title}</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`tv-side-flyout-item${overlayActive ? ' active' : ''}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onToggleActive();
|
||||
onCloseFlyout();
|
||||
}}
|
||||
>
|
||||
<span className="tv-side-flyout-icon"><IconCheck /></span>
|
||||
<span className="tv-side-flyout-title">
|
||||
{overlayActive ? '미적용' : '적용'}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="tv-side-flyout-item"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onOpenSettings();
|
||||
onCloseFlyout();
|
||||
}}
|
||||
>
|
||||
<span className="tv-side-flyout-icon"><IconGear /></span>
|
||||
<span className="tv-side-flyout-title">설정</span>
|
||||
</button>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
|
||||
interface MenuItemDef {
|
||||
key: string;
|
||||
title: string;
|
||||
@@ -266,6 +348,12 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||
manager, indicators, chartHeight, paused = false,
|
||||
candleOverlayToggles,
|
||||
onToggleCandleOverlay,
|
||||
customOverlayActive = false,
|
||||
onCustomOverlayActiveChange,
|
||||
onOpenCustomOverlaySettings,
|
||||
custom1OverlayActive = false,
|
||||
onCustom1OverlayActiveChange,
|
||||
onOpenCustom1OverlaySettings,
|
||||
onSplit, onRemove, onDuplicate,
|
||||
onExpand, onRestore, focusedId,
|
||||
onDragStart, onToggleHidden, onSettings,
|
||||
@@ -341,6 +429,10 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||
|
||||
const candlePane = paneLayouts.find(l => l.paneIndex === 0);
|
||||
const showOverlayIcons = !!(candleOverlayToggles?.length && candlePane && onToggleCandleOverlay);
|
||||
const showCustomBtn = !!(showOverlayIcons && onCustomOverlayActiveChange && onOpenCustomOverlaySettings);
|
||||
const showCustom1Btn = !!(showOverlayIcons && onCustom1OverlayActiveChange && onOpenCustom1OverlaySettings);
|
||||
const customMenuOpen = openMenuId === CUSTOM_MENU_ID;
|
||||
const custom1MenuOpen = openMenuId === CUSTOM1_MENU_ID;
|
||||
|
||||
const focusedLayoutId = focusedId
|
||||
? resolveFocusedPaneLayoutId(focusedId, indicators)
|
||||
@@ -395,6 +487,33 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||
{OVERLAY_ICONS[item.key]}
|
||||
</button>
|
||||
))}
|
||||
{showCustomBtn && (
|
||||
<button
|
||||
type="button"
|
||||
className={`tv-side-btn tv-side-group-btn chart-right-toolbar-btn chart-right-toolbar-btn--custom${customOverlayActive ? ' active' : ''}${customMenuOpen ? ' active' : ''}`}
|
||||
title="Custom 표시"
|
||||
style={{ marginTop: BTN_GAP }}
|
||||
onMouseEnter={e => openFlyout(CUSTOM_MENU_ID, e.currentTarget)}
|
||||
onMouseLeave={scheduleClose}
|
||||
>
|
||||
<IconCustom />
|
||||
<span className="tv-side-expand-arrow" />
|
||||
</button>
|
||||
)}
|
||||
{showCustom1Btn && (
|
||||
<button
|
||||
type="button"
|
||||
className={`tv-side-btn tv-side-group-btn chart-right-toolbar-btn chart-right-toolbar-btn--custom1${custom1OverlayActive ? ' active' : ''}${custom1MenuOpen ? ' active' : ''}`}
|
||||
title="Custom1 표시"
|
||||
style={{ marginTop: BTN_GAP }}
|
||||
onMouseEnter={e => openFlyout(CUSTOM1_MENU_ID, e.currentTarget)}
|
||||
onMouseLeave={scheduleClose}
|
||||
>
|
||||
<IconCustom />
|
||||
<span className="chart-right-toolbar-custom1-badge">1</span>
|
||||
<span className="tv-side-expand-arrow" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -457,7 +576,33 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||
})}
|
||||
</div>
|
||||
|
||||
{openItem && openMenuId && (() => {
|
||||
{customMenuOpen && showCustomBtn && (
|
||||
<CustomOverlayFlyoutMenu
|
||||
title="Custom"
|
||||
flyoutPos={flyoutPos}
|
||||
overlayActive={customOverlayActive}
|
||||
onToggleActive={() => onCustomOverlayActiveChange?.(!customOverlayActive)}
|
||||
onOpenSettings={() => onOpenCustomOverlaySettings?.()}
|
||||
onCancelClose={cancelClose}
|
||||
onScheduleClose={scheduleClose}
|
||||
onCloseFlyout={closeFlyout}
|
||||
/>
|
||||
)}
|
||||
|
||||
{custom1MenuOpen && showCustom1Btn && (
|
||||
<CustomOverlayFlyoutMenu
|
||||
title="Custom1"
|
||||
flyoutPos={flyoutPos}
|
||||
overlayActive={custom1OverlayActive}
|
||||
onToggleActive={() => onCustom1OverlayActiveChange?.(!custom1OverlayActive)}
|
||||
onOpenSettings={() => onOpenCustom1OverlaySettings?.()}
|
||||
onCancelClose={cancelClose}
|
||||
onScheduleClose={scheduleClose}
|
||||
onCloseFlyout={closeFlyout}
|
||||
/>
|
||||
)}
|
||||
|
||||
{openItem && openMenuId && openMenuId !== CUSTOM_MENU_ID && openMenuId !== CUSTOM1_MENU_ID && (() => {
|
||||
const menuItems = buildMenuItems(openItem, indicators, paneItems, {
|
||||
focusedId, onSplit, onDuplicate, onExpand, onRestore, onRemove,
|
||||
onToggleHidden, onSettings,
|
||||
|
||||
@@ -51,6 +51,15 @@ import {
|
||||
DEFAULT_CHART_OVERLAY_VISIBILITY,
|
||||
listChartOverlayToggleItems,
|
||||
} from '../utils/chartOverlayVisibility';
|
||||
import {
|
||||
type ChartCustomOverlaySelection,
|
||||
type ChartCustomOverlaySlotId,
|
||||
createDefaultChartCustomOverlaySelection,
|
||||
isCustomOverlaySlotActive,
|
||||
resolveActiveCustomOverlaySlot,
|
||||
syncChartManagerCustomOverlaysByActiveSlot,
|
||||
} from '../utils/chartCustomOverlay';
|
||||
import ChartCustomOverlaySettingsModal from './ChartCustomOverlaySettingsModal';
|
||||
import { saveSlot } from '../utils/backendApi';
|
||||
import type {
|
||||
OHLCVBar, ChartType, Theme, Timeframe,
|
||||
@@ -672,6 +681,73 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
[overlayVisibility],
|
||||
);
|
||||
|
||||
const [customOverlaySelection, setCustomOverlaySelection] = useState<ChartCustomOverlaySelection>(
|
||||
() => createDefaultChartCustomOverlaySelection(),
|
||||
);
|
||||
const [custom1OverlaySelection, setCustom1OverlaySelection] = useState<ChartCustomOverlaySelection>(
|
||||
() => createDefaultChartCustomOverlaySelection(),
|
||||
);
|
||||
const [activeCustomOverlaySlot, setActiveCustomOverlaySlot] = useState<ChartCustomOverlaySlotId | null>(null);
|
||||
const [customOverlaySettingsSlot, setCustomOverlaySettingsSlot] = useState<ChartCustomOverlaySlotId | null>(null);
|
||||
const customOverlayActive = isCustomOverlaySlotActive(activeCustomOverlaySlot, 'custom');
|
||||
const custom1OverlayActive = isCustomOverlaySlotActive(activeCustomOverlaySlot, 'custom1');
|
||||
|
||||
const syncCustomOverlaysToManager = useCallback((mgr: ChartManager) => {
|
||||
syncChartManagerCustomOverlaysByActiveSlot(
|
||||
mgr,
|
||||
activeCustomOverlaySlot,
|
||||
customOverlaySelection,
|
||||
custom1OverlaySelection,
|
||||
);
|
||||
}, [activeCustomOverlaySlot, customOverlaySelection, custom1OverlaySelection]);
|
||||
|
||||
const handleCustomOverlayActiveChange = useCallback((active: boolean) => {
|
||||
const nextSlot = resolveActiveCustomOverlaySlot('custom', active, activeCustomOverlaySlot);
|
||||
setActiveCustomOverlaySlot(nextSlot);
|
||||
const mgr = managerRef.current;
|
||||
if (mgr) {
|
||||
syncChartManagerCustomOverlaysByActiveSlot(
|
||||
mgr,
|
||||
nextSlot,
|
||||
customOverlaySelection,
|
||||
custom1OverlaySelection,
|
||||
);
|
||||
}
|
||||
}, [activeCustomOverlaySlot, customOverlaySelection, custom1OverlaySelection]);
|
||||
|
||||
const handleCustom1OverlayActiveChange = useCallback((active: boolean) => {
|
||||
const nextSlot = resolveActiveCustomOverlaySlot('custom1', active, activeCustomOverlaySlot);
|
||||
setActiveCustomOverlaySlot(nextSlot);
|
||||
const mgr = managerRef.current;
|
||||
if (mgr) {
|
||||
syncChartManagerCustomOverlaysByActiveSlot(
|
||||
mgr,
|
||||
nextSlot,
|
||||
customOverlaySelection,
|
||||
custom1OverlaySelection,
|
||||
);
|
||||
}
|
||||
}, [activeCustomOverlaySlot, customOverlaySelection, custom1OverlaySelection]);
|
||||
|
||||
const handleSaveCustomOverlaySelection = useCallback((
|
||||
slot: ChartCustomOverlaySlotId,
|
||||
selection: ChartCustomOverlaySelection,
|
||||
) => {
|
||||
const nextCustomSel = slot === 'custom' ? selection : customOverlaySelection;
|
||||
const nextCustom1Sel = slot === 'custom1' ? selection : custom1OverlaySelection;
|
||||
if (slot === 'custom') setCustomOverlaySelection(selection);
|
||||
else setCustom1OverlaySelection(selection);
|
||||
setCustomOverlaySettingsSlot(null);
|
||||
const mgr = managerRef.current;
|
||||
if (!mgr) return;
|
||||
syncChartManagerCustomOverlaysByActiveSlot(
|
||||
mgr,
|
||||
activeCustomOverlaySlot,
|
||||
nextCustomSel,
|
||||
nextCustom1Sel,
|
||||
);
|
||||
}, [activeCustomOverlaySlot, customOverlaySelection, custom1OverlaySelection]);
|
||||
|
||||
const handleToggleCandleOverlay = useCallback((key: ChartOverlayToggleKey) => {
|
||||
setOverlayVisibility(prev => {
|
||||
const next = { ...prev, [key]: !prev[key] };
|
||||
@@ -937,6 +1013,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
mgr.setVolumeVisible(chartVolumeVisible);
|
||||
if (chartPaneSeparator) mgr.setPaneSeparatorOptions(chartPaneSeparator);
|
||||
mgr.setChartOverlayVisibility(overlayVisibilityRef.current);
|
||||
syncCustomOverlaysToManager(mgr);
|
||||
// Time sync: 가시 시간 범위 변화 구독
|
||||
mgr.subscribeVisibleTimeRange(r => {
|
||||
if (!r || isSyncingTimeRef.current) return;
|
||||
@@ -962,6 +1039,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
const mgr = managerRef.current;
|
||||
if (!mgr) return;
|
||||
mgr.setChartOverlayVisibility(overlayVisibilityRef.current);
|
||||
syncCustomOverlaysToManager(mgr);
|
||||
if (skipSyncApplyRef.current) {
|
||||
skipSyncApplyRef.current = false;
|
||||
const fb = mgr.hasIchimoku() ? 28 : 0;
|
||||
@@ -1016,8 +1094,31 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
onFullView={onFullView ? () => onFullView(symbolRef.current) : undefined}
|
||||
candleOverlayToggles={compactMode ? undefined : candleOverlayToggles}
|
||||
onToggleCandleOverlay={compactMode ? undefined : handleToggleCandleOverlay}
|
||||
customOverlayActive={compactMode ? undefined : customOverlayActive}
|
||||
onCustomOverlayActiveChange={compactMode ? undefined : handleCustomOverlayActiveChange}
|
||||
onOpenCustomOverlaySettings={compactMode ? undefined : () => setCustomOverlaySettingsSlot('custom')}
|
||||
custom1OverlayActive={compactMode ? undefined : custom1OverlayActive}
|
||||
onCustom1OverlayActiveChange={compactMode ? undefined : handleCustom1OverlayActiveChange}
|
||||
onOpenCustom1OverlaySettings={compactMode ? undefined : () => setCustomOverlaySettingsSlot('custom1')}
|
||||
/>
|
||||
|
||||
{customOverlaySettingsSlot === 'custom' && !compactMode && (
|
||||
<ChartCustomOverlaySettingsModal
|
||||
title="Custom 표시 설정"
|
||||
selection={customOverlaySelection}
|
||||
onSave={sel => handleSaveCustomOverlaySelection('custom', sel)}
|
||||
onCancel={() => setCustomOverlaySettingsSlot(null)}
|
||||
/>
|
||||
)}
|
||||
{customOverlaySettingsSlot === 'custom1' && !compactMode && (
|
||||
<ChartCustomOverlaySettingsModal
|
||||
title="Custom1 표시 설정"
|
||||
selection={custom1OverlaySelection}
|
||||
onSave={sel => handleSaveCustomOverlaySelection('custom1', sel)}
|
||||
onCancel={() => setCustomOverlaySettingsSlot(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 컨텍스트 툴바 */}
|
||||
{ctxToolbar && (() => {
|
||||
const cfg = ctxToolbar.entryId !== '__main__'
|
||||
|
||||
@@ -165,6 +165,12 @@ interface TradingChartProps {
|
||||
/** 캔들 오버레이(이동평균·일목·볼린저) 표시 토글 — 우측 툴바 상단 */
|
||||
candleOverlayToggles?: CandleOverlayToggleItem[];
|
||||
onToggleCandleOverlay?: (key: import('../utils/chartOverlayVisibility').ChartOverlayToggleKey) => void;
|
||||
customOverlayActive?: boolean;
|
||||
onCustomOverlayActiveChange?: (active: boolean) => void;
|
||||
onOpenCustomOverlaySettings?: () => void;
|
||||
custom1OverlayActive?: boolean;
|
||||
onCustom1OverlayActiveChange?: (active: boolean) => void;
|
||||
onOpenCustom1OverlaySettings?: () => void;
|
||||
}
|
||||
|
||||
const TradingChart: React.FC<TradingChartProps> = ({
|
||||
@@ -209,6 +215,12 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
showCandlePaneControls = true,
|
||||
candleOverlayToggles,
|
||||
onToggleCandleOverlay,
|
||||
customOverlayActive,
|
||||
onCustomOverlayActiveChange,
|
||||
onOpenCustomOverlaySettings,
|
||||
custom1OverlayActive,
|
||||
onCustom1OverlayActiveChange,
|
||||
onOpenCustom1OverlaySettings,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null); // 스크롤 래퍼
|
||||
@@ -1609,6 +1621,12 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
paused={paneLegendPaused}
|
||||
candleOverlayToggles={candleOverlayToggles}
|
||||
onToggleCandleOverlay={onToggleCandleOverlay}
|
||||
customOverlayActive={customOverlayActive}
|
||||
onCustomOverlayActiveChange={onCustomOverlayActiveChange}
|
||||
onOpenCustomOverlaySettings={onOpenCustomOverlaySettings}
|
||||
custom1OverlayActive={custom1OverlayActive}
|
||||
onCustom1OverlayActiveChange={onCustom1OverlayActiveChange}
|
||||
onOpenCustom1OverlaySettings={onOpenCustom1OverlaySettings}
|
||||
onSplit={onSplitIndicatorPane}
|
||||
onRemove={onRemoveIndicator}
|
||||
onDuplicate={onDuplicateIndicator}
|
||||
|
||||
Reference in New Issue
Block a user