실시간 차트 보조지표 탭 문제 수정
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
||||
splitIndicatorPane,
|
||||
replaceIndicatorConfigsFromTypes,
|
||||
} from '../utils/indicatorPaneMerge';
|
||||
import { sortIndicatorsByTypeOrder } from '../utils/indicatorListOrder';
|
||||
import { saveSlot } from '../utils/backendApi';
|
||||
import type {
|
||||
OHLCVBar, ChartType, Theme, Timeframe,
|
||||
@@ -279,13 +280,16 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
} else if (type === 'IchimokuCloud') {
|
||||
cfg = normalizeIchimokuConfig(cfg);
|
||||
}
|
||||
return [...prev, cfg];
|
||||
return sortIndicatorsByTypeOrder([...prev, cfg]);
|
||||
});
|
||||
},
|
||||
addIndicators: (types: string[]) => {
|
||||
if (!types.length) return;
|
||||
setIndicators(
|
||||
replaceIndicatorConfigsFromTypes(types, getParams, getVisualConfig, newIndId),
|
||||
sortIndicatorsByTypeOrder(
|
||||
replaceIndicatorConfigsFromTypes(types, getParams, getVisualConfig, newIndId),
|
||||
types,
|
||||
),
|
||||
);
|
||||
},
|
||||
removeIndicatorByType: (type: string) => {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
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<void>;
|
||||
@@ -48,6 +49,8 @@ export interface IndicatorMainDefaultsPanelProps {
|
||||
onSaveUiState?: (state: IndicatorSaveUiState | null) => void;
|
||||
/** DB 로드 완료·저장 후 재로드 트리거 */
|
||||
settingsRevision?: number;
|
||||
/** 목록 순서 변경 시 차트 보조지표 순서 동기화 */
|
||||
onListOrderChange?: (orderedTypes: string[]) => void;
|
||||
}
|
||||
|
||||
const ALL_TYPES = getSettingsIndicatorTypes();
|
||||
@@ -77,8 +80,12 @@ const IndicatorMainDefaultsPanel: React.FC<IndicatorMainDefaultsPanelProps> = ({
|
||||
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<string | null>(null);
|
||||
const [dragOverType, setDragOverType] = useState<string | null>(null);
|
||||
const [expandedTypes, setExpandedTypes] = useState<Set<string>>(() => new Set());
|
||||
const [configs, setConfigs] = useState<Record<string, IndicatorConfig>>({});
|
||||
const [baselineFp, setBaselineFp] = useState('');
|
||||
@@ -97,14 +104,23 @@ const IndicatorMainDefaultsPanel: React.FC<IndicatorMainDefaultsPanelProps> = ({
|
||||
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(ALL_TYPES, search),
|
||||
[search],
|
||||
() => filterSettingsIndicatorTypes(listOrder, search),
|
||||
[listOrder, search],
|
||||
);
|
||||
const { main: filteredMain, other: filteredOther } = useMemo(
|
||||
() => partitionFilteredTypes(filtered),
|
||||
@@ -184,6 +200,8 @@ const IndicatorMainDefaultsPanel: React.FC<IndicatorMainDefaultsPanelProps> = ({
|
||||
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;
|
||||
@@ -195,6 +213,29 @@ const IndicatorMainDefaultsPanel: React.FC<IndicatorMainDefaultsPanelProps> = ({
|
||||
enabled={onChartTypes.has(type)}
|
||||
expanded={expandedTypes.has(type)}
|
||||
variant="settings"
|
||||
draggable={canReorder}
|
||||
isDragOver={canReorder && dragOverType === type && draggingType !== type}
|
||||
onDragHandleStart={e => {
|
||||
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)}
|
||||
@@ -208,7 +249,8 @@ const IndicatorMainDefaultsPanel: React.FC<IndicatorMainDefaultsPanelProps> = ({
|
||||
<div className="stg-ind-intro">
|
||||
<p>
|
||||
지표 추가 팝업 <strong>Main</strong> 탭 {MAIN_INDICATOR_TYPES.length}종을 상단에 두었고, 그 아래에 등록된 모든 보조지표가 있습니다.
|
||||
우측 스위치로 차트에 바로 추가·제거할 수 있습니다. 파라미터·색상을 변경한 뒤 <strong>상단 저장</strong> 버튼을 눌러 DB에 반영하세요.
|
||||
우측 스위치로 차트에 바로 추가·제거할 수 있습니다. <strong>⠿</strong> 핸들로 목록 순서를 바꿀 수 있으며, 차트에 표시되는 보조지표 pane 순서도 이 목록을 따릅니다.
|
||||
파라미터·색상을 변경한 뒤 <strong>상단 저장</strong> 버튼을 눌러 DB에 반영하세요.
|
||||
</p>
|
||||
<button type="button" className="stg-ind-reset-all" onClick={handleResetAll}>
|
||||
전체 기본값 복원
|
||||
@@ -218,7 +260,7 @@ const IndicatorMainDefaultsPanel: React.FC<IndicatorMainDefaultsPanelProps> = ({
|
||||
<IndicatorSettingsListSearch
|
||||
value={search}
|
||||
onChange={setSearch}
|
||||
totalCount={ALL_TYPES.length}
|
||||
totalCount={listOrder.length}
|
||||
filteredCount={filtered.length}
|
||||
className="stg-ind-search-block"
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,13 @@ export interface IndicatorSettingsListRowProps {
|
||||
onToggleEnabled: (enabled: boolean) => void;
|
||||
onChange: (updated: IndicatorConfig) => void;
|
||||
onRowDefaults?: () => void;
|
||||
/** 목록 순서 드래그 */
|
||||
draggable?: boolean;
|
||||
isDragOver?: boolean;
|
||||
onDragHandleStart?: (e: React.DragEvent) => void;
|
||||
onDragOver?: (e: React.DragEvent) => void;
|
||||
onDragLeave?: () => void;
|
||||
onDrop?: (e: React.DragEvent) => void;
|
||||
}
|
||||
|
||||
const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
||||
@@ -28,14 +35,20 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
||||
onToggleEnabled,
|
||||
onChange,
|
||||
onRowDefaults,
|
||||
draggable = false,
|
||||
isDragOver = false,
|
||||
onDragHandleStart,
|
||||
onDragOver,
|
||||
onDragLeave,
|
||||
onDrop,
|
||||
}) => {
|
||||
const labels = getIndicatorListLabels(type);
|
||||
const def = getIndicatorDef(type);
|
||||
|
||||
const cardClass =
|
||||
variant === 'bulk'
|
||||
? `ibsm-card ibsm-main-row${enabled ? ' ibsm-main-row--active' : ''}${expanded ? ' ibsm-card-expanded' : ''}`
|
||||
: `stg-ind-card${enabled ? ' stg-ind-card--on-chart' : ''}${expanded ? ' stg-ind-card--expanded' : ''}`;
|
||||
? `ibsm-card ibsm-main-row${enabled ? ' ibsm-main-row--active' : ''}${expanded ? ' ibsm-card-expanded' : ''}${isDragOver ? ' ind-settings-row--drag-over' : ''}`
|
||||
: `stg-ind-card${enabled ? ' stg-ind-card--on-chart' : ''}${expanded ? ' stg-ind-card--expanded' : ''}${isDragOver ? ' ind-settings-row--drag-over' : ''}`;
|
||||
|
||||
const headClass = variant === 'bulk' ? 'ibsm-card-main ibsm-main-row-head' : 'stg-ind-card-head';
|
||||
const infoClass = variant === 'bulk' ? 'ibsm-main-row-info' : 'stg-ind-card-titles';
|
||||
@@ -48,8 +61,26 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
||||
const expandWrapClass = variant === 'bulk' ? 'ibsm-card-expand' : 'stg-ind-card-body';
|
||||
|
||||
return (
|
||||
<div className={cardClass}>
|
||||
<div
|
||||
className={cardClass}
|
||||
onDragOver={draggable ? onDragOver : undefined}
|
||||
onDragLeave={draggable ? onDragLeave : undefined}
|
||||
onDrop={draggable ? onDrop : undefined}
|
||||
>
|
||||
<div className={headClass}>
|
||||
{draggable && (
|
||||
<button
|
||||
type="button"
|
||||
className="ind-settings-drag-handle"
|
||||
draggable
|
||||
title="드래그하여 순서 변경"
|
||||
aria-label="순서 변경"
|
||||
onDragStart={onDragHandleStart}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
⠿
|
||||
</button>
|
||||
)}
|
||||
<div className={infoClass}>
|
||||
<span className={koClass}>{labels.ko}</span>
|
||||
<span className={enClass}>{labels.en}</span>
|
||||
|
||||
@@ -33,42 +33,6 @@ interface PaneItem {
|
||||
|
||||
const MIN_SUB_PANE_HEIGHT = 12;
|
||||
|
||||
interface DomPaneLayout {
|
||||
topY: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* LWC 테이블 DOM 에서 보조지표 pane 행만 읽음 (pane index 무관).
|
||||
* orphan 빈 pane 행이 있으면 제외하고 하단 expectCount 개만 사용.
|
||||
*/
|
||||
function getDomSubIndicatorLayouts(
|
||||
containerEl: HTMLElement,
|
||||
expectCount: number,
|
||||
): DomPaneLayout[] {
|
||||
const containerRect = containerEl.getBoundingClientRect();
|
||||
const rows = Array.from(containerEl.querySelectorAll('tr'))
|
||||
.map(tr => tr.getBoundingClientRect())
|
||||
.filter(rect => rect.height > MIN_SUB_PANE_HEIGHT)
|
||||
.sort((a, b) => a.top - b.top);
|
||||
|
||||
// pane 0=캔들(첫 행). 둘째 행이 낮으면 거래량 pane → 보조지표는 그 다음부터
|
||||
let startIdx = 1;
|
||||
if (rows.length > 1 && rows[1].height < Math.min(100, rows[0].height * 0.35)) {
|
||||
startIdx = 2;
|
||||
}
|
||||
|
||||
let subs = rows.slice(startIdx).map(rect => ({
|
||||
topY: Math.round(rect.top - containerRect.top),
|
||||
height: Math.round(rect.height),
|
||||
}));
|
||||
|
||||
if (expectCount > 0 && subs.length > expectCount) {
|
||||
subs = subs.slice(subs.length - expectCount);
|
||||
}
|
||||
return subs;
|
||||
}
|
||||
|
||||
interface PaneCapture {
|
||||
dataUrl: string;
|
||||
cssWidth: number;
|
||||
@@ -247,88 +211,80 @@ function fillMissingLayoutsByPaneIndex(
|
||||
}
|
||||
}
|
||||
|
||||
/** DOM 행 중 paneIndex 레이아웃 topY 와 가장 가까운 행 선택 */
|
||||
function fillMissingLayoutsFromDom(
|
||||
/**
|
||||
* 실제 시리즈가 붙은 sub-pane 만으로 좌표 보강 (orphan 빈 pane 제외).
|
||||
* paneIndex 미등록(-1)·탭 일괄 교체 직후에도 레이블 위치를 맞춘다.
|
||||
*/
|
||||
function fillMissingLayoutsFromActiveSubPanes(
|
||||
items: PaneItem[],
|
||||
containerEl: HTMLElement,
|
||||
paneLayouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
||||
subLayouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
||||
): void {
|
||||
const missing = items.filter(i => i.layoutTopY == null && i.paneIndex >= 2);
|
||||
if (subLayouts.length === 0) return;
|
||||
|
||||
const missing = items.filter(i => i.layoutTopY == null);
|
||||
if (missing.length === 0) return;
|
||||
|
||||
const domSubs = getDomSubIndicatorLayouts(containerEl, missing.length);
|
||||
if (domSubs.length === 0) return;
|
||||
|
||||
const usedDom = new Set<number>();
|
||||
const usedPane = new Set<number>();
|
||||
const sorted = [...missing].sort((a, b) => a.paneIndex - b.paneIndex);
|
||||
|
||||
for (const item of sorted) {
|
||||
const ref = layoutForPaneIndex(item.paneIndex, paneLayouts);
|
||||
let bestIdx = -1;
|
||||
let bestDist = Infinity;
|
||||
for (let i = 0; i < domSubs.length; i++) {
|
||||
if (usedDom.has(i)) continue;
|
||||
const dist = ref
|
||||
? Math.abs(domSubs[i].topY - ref.topY)
|
||||
: domSubs[i].topY;
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
if (bestIdx < 0) continue;
|
||||
if (ref && bestDist > 96) continue;
|
||||
usedDom.add(bestIdx);
|
||||
applyPaneLayoutToItem(item, domSubs[bestIdx]);
|
||||
if (item.paneIndex < 2) continue;
|
||||
const lay = subLayouts.find(l => l.paneIndex === item.paneIndex && !usedPane.has(l.paneIndex));
|
||||
if (!lay) continue;
|
||||
applyPaneLayoutToItem(item, lay);
|
||||
usedPane.add(lay.paneIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function buildPaneItems(
|
||||
manager: ChartManager,
|
||||
containerEl: HTMLElement | null,
|
||||
indicators: IndicatorConfig[],
|
||||
optPaneMap?: Map<string, number>,
|
||||
): PaneItem[] {
|
||||
const paneMap = new Map(optPaneMap ?? []);
|
||||
for (const info of manager.getIndicatorPaneInfo()) {
|
||||
paneMap.set(info.id, info.paneIndex);
|
||||
}
|
||||
const wantedIds = new Set(
|
||||
indicators
|
||||
.filter(ind => {
|
||||
const def = getIndicatorDef(ind.type);
|
||||
return def && !def.overlay && !def.returnsMarkers;
|
||||
})
|
||||
.map(ind => ind.id),
|
||||
);
|
||||
|
||||
// ChartManager 에 실제 로드된 지표만 — React state 만 있고 pane 미부착인 항목 제외
|
||||
const items: PaneItem[] = [];
|
||||
for (const ind of indicators) {
|
||||
for (const info of manager.getIndicatorPaneInfo()) {
|
||||
if (!wantedIds.has(info.id)) continue;
|
||||
const ind = info.config;
|
||||
const def = getIndicatorDef(ind.type);
|
||||
if (!def || def.overlay || def.returnsMarkers) continue;
|
||||
const plotColors = (ind.plots ?? def.plots ?? []).map(p => (p.color ?? '#aaa').slice(0, 7));
|
||||
|
||||
let chartPane = paneMap.get(ind.id) ?? -1;
|
||||
let chartPane = info.paneIndex;
|
||||
if (ind.mergedWith) {
|
||||
const hostPane = paneMap.get(ind.mergedWith);
|
||||
if (hostPane != null && hostPane >= 2) chartPane = hostPane;
|
||||
const host = manager.getIndicatorPaneIndex(ind.mergedWith);
|
||||
if (host >= 2) chartPane = host;
|
||||
}
|
||||
|
||||
items.push({
|
||||
id: ind.id,
|
||||
id: info.id,
|
||||
paneIndex: chartPane,
|
||||
label: makeLabel(ind),
|
||||
plotColors,
|
||||
plotColors: info.plotColors.map(c => c.slice(0, 7)),
|
||||
hidden: ind.hidden === true,
|
||||
});
|
||||
}
|
||||
|
||||
if (items.length === 0) return [];
|
||||
|
||||
items.sort((a, b) => a.paneIndex - b.paneIndex);
|
||||
|
||||
const subLayouts = manager.getIndicatorSubPaneLayouts();
|
||||
|
||||
for (const item of items) {
|
||||
const lay = manager.getIndicatorPaneScreenLayout(item.id);
|
||||
if (lay) applyPaneLayoutToItem(item, lay, true);
|
||||
}
|
||||
|
||||
const paneLayouts = manager.getPaneLayouts();
|
||||
fillMissingLayoutsByPaneIndex(items, paneLayouts);
|
||||
fillMissingLayoutsByPaneIndex(items, manager.getIndicatorSubPaneLayouts());
|
||||
|
||||
if (containerEl?.isConnected) {
|
||||
fillMissingLayoutsFromDom(items, containerEl, paneLayouts);
|
||||
}
|
||||
fillMissingLayoutsByPaneIndex(items, subLayouts);
|
||||
fillMissingLayoutsFromActiveSubPanes(items, subLayouts);
|
||||
|
||||
items.sort((a, b) => {
|
||||
const ay = a.layoutTopY ?? 1e9;
|
||||
@@ -353,6 +309,17 @@ function paneItemLayout(
|
||||
return l ? { topY: l.topY, height: l.height } : null;
|
||||
}
|
||||
|
||||
/** 렌더 시점 pane 좌표 — resetPaneHeights 후 캐시 topY 가 어긋나지 않도록 시리즈 DOM 우선 */
|
||||
function resolvePaneItemLayout(
|
||||
manager: ChartManager,
|
||||
item: PaneItem,
|
||||
layouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
||||
): { topY: number; height: number } | null {
|
||||
const live = manager.getIndicatorPaneScreenLayout(item.id);
|
||||
if (live) return live;
|
||||
return paneItemLayout(item, layouts);
|
||||
}
|
||||
|
||||
// ── 아이콘 SVG ────────────────────────────────────────────────────────────────
|
||||
/** ⤡ 전체화면 확장 — 네 꼭지점 바깥 방향 화살표 */
|
||||
const IconExpand = () => (
|
||||
@@ -422,6 +389,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
}) => {
|
||||
const [paneItems, setPaneItems] = useState<PaneItem[]>([]);
|
||||
const [paneLayouts, setPaneLayouts] = useState<Array<{ paneIndex: number; topY: number; height: number }>>([]);
|
||||
const [layoutEpoch, setLayoutEpoch] = useState(0);
|
||||
const [values, setValues] = useState<Record<string, number[]>>({});
|
||||
const [dragState, setDragState] = useState<DragState | null>(null);
|
||||
const retryRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||
@@ -451,11 +419,10 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
useEffect(() => { indicatorsRef.current = indicators; }, [indicators]);
|
||||
|
||||
const syncPaneItems = useCallback(() => {
|
||||
const el = containerElRef.current;
|
||||
if (!el?.isConnected) return;
|
||||
const map = manager.getIndicatorPaneMapping();
|
||||
if (!containerElRef.current?.isConnected) return;
|
||||
setPaneLayouts(manager.getPaneLayouts());
|
||||
setPaneItems(buildPaneItems(manager, el, indicators, map));
|
||||
setPaneItems(buildPaneItems(manager, indicators));
|
||||
setLayoutEpoch(e => e + 1);
|
||||
}, [indicators, manager]);
|
||||
|
||||
const refreshLayouts = useCallback(() => {
|
||||
@@ -486,7 +453,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
setPaneItems([]);
|
||||
scheduleRetry();
|
||||
return () => retryRef.current.forEach(clearTimeout);
|
||||
}, [scheduleRetry]);
|
||||
}, [scheduleRetry, indicators]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = manager.subscribePaneLayout(() => scheduleRetry());
|
||||
@@ -505,13 +472,6 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = manager.subscribeCrosshair((p: MouseEventParams<Time>) => {
|
||||
const map = manager.getIndicatorPaneMapping();
|
||||
const layouts = manager.getPaneLayouts();
|
||||
setPaneLayouts(layouts);
|
||||
const el = containerElRef.current;
|
||||
if (el?.isConnected) {
|
||||
setPaneItems(buildPaneItems(manager, el, indicatorsRef.current, map));
|
||||
}
|
||||
if (!p.time) { setValues({}); return; }
|
||||
setValues(manager.getIndicatorValuesByIdFromParams(p));
|
||||
});
|
||||
@@ -525,16 +485,16 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
if (!items.length) return null;
|
||||
const rect = containerElRef.current.getBoundingClientRect();
|
||||
if (idx <= 0) {
|
||||
const pl = paneItemLayout(items[0], layouts);
|
||||
const pl = resolvePaneItemLayout(manager, items[0], layouts);
|
||||
return pl ? rect.top + pl.topY : null;
|
||||
}
|
||||
if (idx >= items.length) {
|
||||
const pl = paneItemLayout(items[items.length - 1], layouts);
|
||||
const pl = resolvePaneItemLayout(manager, items[items.length - 1], layouts);
|
||||
return pl ? rect.top + pl.topY + pl.height : null;
|
||||
}
|
||||
const pl = paneItemLayout(items[idx - 1], layouts);
|
||||
const pl = resolvePaneItemLayout(manager, items[idx - 1], layouts);
|
||||
return pl ? rect.top + pl.topY + pl.height : null;
|
||||
}, []);
|
||||
}, [manager]);
|
||||
|
||||
const unbindPaneDragRef = useRef<(() => void) | null>(null);
|
||||
|
||||
@@ -553,7 +513,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
const fromIndex = paneItemsRef.current.findIndex(i => i.id === id);
|
||||
if (!item || fromIndex === -1) return;
|
||||
|
||||
const layout = paneItemLayout(item, paneLayoutsRef.current);
|
||||
const layout = resolvePaneItemLayout(manager, item, paneLayoutsRef.current);
|
||||
const paneCapture = layout
|
||||
? capturePaneRegion(containerElRef.current, layout.topY, layout.height)
|
||||
: null;
|
||||
@@ -628,7 +588,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
|
||||
for (const item of items) {
|
||||
if (getPaneHostId(item.id, inds) === dragHost) continue;
|
||||
const l = paneItemLayout(item, layouts);
|
||||
const l = resolvePaneItemLayout(manager, item, layouts);
|
||||
if (!l || mouseY < l.topY || mouseY >= l.topY + l.height) continue;
|
||||
const rel = (mouseY - l.topY) / Math.max(l.height, 1);
|
||||
if (rel > 0.28 && rel < 0.72) {
|
||||
@@ -640,7 +600,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
|
||||
if (dropMode === 'reorder') {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const l = paneItemLayout(items[i], layouts);
|
||||
const l = resolvePaneItemLayout(manager, items[i], layouts);
|
||||
if (l && mouseY < l.topY + l.height / 2) {
|
||||
drop = i;
|
||||
break;
|
||||
@@ -674,6 +634,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
? getDropLineY(dragState!.dropBeforeIndex)
|
||||
: null;
|
||||
const containerRect = containerEl.getBoundingClientRect();
|
||||
void layoutEpoch;
|
||||
|
||||
const draggingHostId = isDragging
|
||||
? getPaneHostId(dragState!.draggingId, indicators)
|
||||
@@ -684,20 +645,20 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
: -1;
|
||||
|
||||
const draggingLayout = draggingItemIdx >= 0
|
||||
? paneItemLayout(paneItems[draggingItemIdx], paneLayouts)
|
||||
? resolvePaneItemLayout(manager, paneItems[draggingItemIdx], paneLayouts)
|
||||
: null;
|
||||
|
||||
const mergeTargetLayout = isDragging && dragState?.mergeTargetId
|
||||
? (() => {
|
||||
const t = paneItems.find(i => i.id === dragState.mergeTargetId);
|
||||
return t ? paneItemLayout(t, paneLayouts) : null;
|
||||
return t ? resolvePaneItemLayout(manager, t, paneLayouts) : null;
|
||||
})()
|
||||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{paneItems.map((item, itemIdx) => {
|
||||
const pl = paneItemLayout(item, paneLayouts);
|
||||
const pl = resolvePaneItemLayout(manager, item, paneLayouts);
|
||||
if (!pl) return null;
|
||||
|
||||
const screenX = containerRect.left;
|
||||
@@ -918,8 +879,8 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
<>
|
||||
{/* ① 전체 지표 영역 dim — 고스트·shift 가 더 선명하게 보이도록 */}
|
||||
{paneItems.length > 0 && (() => {
|
||||
const firstL = paneItemLayout(paneItems[0], paneLayouts);
|
||||
const lastL = paneItemLayout(paneItems[paneItems.length - 1], paneLayouts);
|
||||
const firstL = resolvePaneItemLayout(manager, paneItems[0], paneLayouts);
|
||||
const lastL = resolvePaneItemLayout(manager, paneItems[paneItems.length - 1], paneLayouts);
|
||||
if (!firstL || !lastL) return null;
|
||||
return (
|
||||
<div style={{
|
||||
|
||||
@@ -106,6 +106,8 @@ interface SettingsPageProps {
|
||||
chartIndicators?: IndicatorConfig[];
|
||||
onAddIndicatorToChart?: (type: string, config?: IndicatorConfig) => void;
|
||||
onRemoveIndicatorFromChart?: (type: string) => void;
|
||||
/** 보조지표 설정 목록 순서 변경 → 차트 pane 순서 */
|
||||
onIndicatorListOrderChange?: (orderedTypes: string[]) => void;
|
||||
chartSeriesPriceLabels?: boolean;
|
||||
onChartSeriesPriceLabels?: (v: boolean) => void;
|
||||
chartVolumeVisible?: boolean;
|
||||
@@ -1516,6 +1518,7 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
chartIndicators = [],
|
||||
onAddIndicatorToChart,
|
||||
onRemoveIndicatorFromChart,
|
||||
onIndicatorListOrderChange,
|
||||
chartSeriesPriceLabels = true,
|
||||
onChartSeriesPriceLabels,
|
||||
chartVolumeVisible = true,
|
||||
@@ -1642,6 +1645,7 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
saveAllDefaults={saveAllIndicatorDefaults}
|
||||
onSaveUiState={setIndicatorSaveUi}
|
||||
settingsRevision={indicatorSettingsRevision}
|
||||
onListOrderChange={onIndicatorListOrderChange}
|
||||
/>
|
||||
);
|
||||
case 'backtest': return (
|
||||
|
||||
@@ -13,6 +13,16 @@ import {
|
||||
import {
|
||||
confirmAndApplyIndicatorTab,
|
||||
} from '../utils/indicatorTabApply';
|
||||
import {
|
||||
getOrderedMainIndicatorTypes,
|
||||
saveMainTabOrder,
|
||||
} from '../utils/indicatorMainTabOrder';
|
||||
import {
|
||||
mergeIndicatorListOrder,
|
||||
reorderIndicatorListType,
|
||||
saveIndicatorListOrder,
|
||||
} from '../utils/indicatorListOrder';
|
||||
import { getSettingsIndicatorTypes } from '../utils/indicatorSettingsList';
|
||||
import {
|
||||
loadCustomTabs,
|
||||
createCustomTab,
|
||||
@@ -76,6 +86,8 @@ export interface ToolbarProps {
|
||||
onScrollToNow?: () => void;
|
||||
onAddIndicator: (type: string) => void;
|
||||
onAddIndicators?: (types: string[]) => void;
|
||||
/** 탭 목록 순서 변경 → 차트 pane 순서 동기화 */
|
||||
onIndicatorOrderChange?: (orderedTypes: string[]) => void;
|
||||
onRemoveByType: (type: string) => void;
|
||||
onAutoFib: () => void;
|
||||
onClearFib: () => void;
|
||||
@@ -307,17 +319,21 @@ interface IndDropdownProps {
|
||||
activeIndicators: IndicatorConfig[];
|
||||
onAdd: (type: string) => void;
|
||||
onAddMany?: (types: string[]) => void;
|
||||
onIndicatorOrderChange?: (orderedTypes: string[]) => void;
|
||||
onRemove: (type: string) => void;
|
||||
onOpenSettings?: (type: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const IndDropdown: React.FC<IndDropdownProps> = ({
|
||||
activeIndicators, onAdd, onAddMany, onRemove, onOpenSettings, onClose,
|
||||
activeIndicators, onAdd, onAddMany, onIndicatorOrderChange, onRemove, onOpenSettings, onClose,
|
||||
}) => {
|
||||
const [search, setSearch] = React.useState('');
|
||||
const [category, setCategory] = React.useState<IndicatorTabKey>('All');
|
||||
const [customTabs, setCustomTabs] = React.useState<IndicatorCustomTab[]>(() => loadCustomTabs());
|
||||
const [mainOrderTick, setMainOrderTick] = React.useState(0);
|
||||
const [draggingType, setDraggingType] = React.useState<string | null>(null);
|
||||
const [dragOverType, setDragOverType] = React.useState<string | null>(null);
|
||||
const [editorOpen, setEditorOpen] = React.useState(false);
|
||||
const [editorMode, setEditorMode] = React.useState<'create' | 'edit'>('create');
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
@@ -364,10 +380,52 @@ const IndDropdown: React.FC<IndDropdownProps> = ({
|
||||
setSearch('');
|
||||
};
|
||||
|
||||
const tabTypeOrder = React.useMemo((): string[] | null => {
|
||||
void mainOrderTick;
|
||||
if (search.trim()) return null;
|
||||
if (category === 'Main') return getOrderedMainIndicatorTypes();
|
||||
if (isCustomTabKey(category) && selectedCustomTab) {
|
||||
return [...selectedCustomTab.indicatorTypes];
|
||||
}
|
||||
return null;
|
||||
}, [category, selectedCustomTab, customTabs, search, mainOrderTick]);
|
||||
|
||||
const canReorderList = tabTypeOrder != null && tabTypeOrder.length > 1;
|
||||
|
||||
const syncChartToTabOrder = useCallback(() => {
|
||||
if (tabTypeOrder?.length && onIndicatorOrderChange) {
|
||||
onIndicatorOrderChange(tabTypeOrder);
|
||||
}
|
||||
}, [tabTypeOrder, onIndicatorOrderChange]);
|
||||
|
||||
const moveTypeInTabList = useCallback((fromType: string, toType: string) => {
|
||||
if (!tabTypeOrder) return;
|
||||
const next = reorderIndicatorListType(tabTypeOrder, fromType, toType, 'before');
|
||||
if (category === 'Main') {
|
||||
saveMainTabOrder(next);
|
||||
setMainOrderTick(t => t + 1);
|
||||
saveIndicatorListOrder(mergeIndicatorListOrder(next, getSettingsIndicatorTypes()));
|
||||
onIndicatorOrderChange?.(next);
|
||||
} else if (isCustomTabKey(category) && selectedCustomTab) {
|
||||
updateCustomTab(selectedCustomTab.id, { indicatorTypes: next });
|
||||
refreshCustomTabs();
|
||||
onIndicatorOrderChange?.(next);
|
||||
}
|
||||
}, [tabTypeOrder, category, selectedCustomTab, refreshCustomTabs, onIndicatorOrderChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const clearDrag = () => {
|
||||
setDraggingType(null);
|
||||
setDragOverType(null);
|
||||
};
|
||||
document.addEventListener('dragend', clearDrag);
|
||||
return () => document.removeEventListener('dragend', clearDrag);
|
||||
}, []);
|
||||
|
||||
const addAllContext = React.useMemo(() => {
|
||||
if (category === 'All') return null;
|
||||
if (category === 'Main') {
|
||||
return { label: '주요지표', types: [...MAIN_INDICATOR_TYPES] };
|
||||
return { label: '주요지표', types: getOrderedMainIndicatorTypes() };
|
||||
}
|
||||
if (isCustomTabKey(category) && selectedCustomTab) {
|
||||
const types = selectedCustomTab.indicatorTypes.filter(
|
||||
@@ -430,25 +488,22 @@ const IndDropdown: React.FC<IndDropdownProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
if (category === 'Main') {
|
||||
return MAIN_INDICATOR_TYPES
|
||||
if (category === 'Main' && tabTypeOrder) {
|
||||
return tabTypeOrder
|
||||
.map(type => INDICATOR_REGISTRY.find(d => d.type === type))
|
||||
.filter((d): d is IndicatorDef => d != null)
|
||||
.filter(matchesQuery);
|
||||
}
|
||||
|
||||
if (isCustomTabKey(category)) {
|
||||
const tabId = parseCustomTabKey(category);
|
||||
const tab = tabId ? customTabs.find(t => t.id === tabId) : null;
|
||||
if (!tab) return [];
|
||||
return tab.indicatorTypes
|
||||
if (isCustomTabKey(category) && tabTypeOrder) {
|
||||
return tabTypeOrder
|
||||
.map(type => INDICATOR_REGISTRY.find(d => d.type === type))
|
||||
.filter((d): d is IndicatorDef => d != null)
|
||||
.filter(matchesQuery);
|
||||
}
|
||||
|
||||
return INDICATOR_REGISTRY.filter(matchesQuery);
|
||||
}, [search, category, customTabs]);
|
||||
}, [search, category, customTabs, tabTypeOrder]);
|
||||
|
||||
const {
|
||||
panelRef,
|
||||
@@ -625,8 +680,45 @@ const IndDropdown: React.FC<IndDropdownProps> = ({
|
||||
const mainLbl = (category === 'Main' || isCustomTabKey(category))
|
||||
? MAIN_INDICATOR_LABELS[def.type]
|
||||
: undefined;
|
||||
const isDragOver = canReorderList && dragOverType === def.type && draggingType !== def.type;
|
||||
return (
|
||||
<div key={def.type} className={`ind-panel-item${active ? ' active' : ''}`}>
|
||||
<div
|
||||
key={def.type}
|
||||
className={`ind-panel-item${active ? ' active' : ''}${isDragOver ? ' ind-panel-item--drag-over' : ''}`}
|
||||
onDragOver={canReorderList ? e => {
|
||||
if (!draggingType || draggingType === def.type) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
setDragOverType(def.type);
|
||||
} : undefined}
|
||||
onDragLeave={canReorderList ? () => {
|
||||
if (dragOverType === def.type) setDragOverType(null);
|
||||
} : undefined}
|
||||
onDrop={canReorderList ? e => {
|
||||
e.preventDefault();
|
||||
const from = e.dataTransfer.getData('text/indicator-type') || draggingType;
|
||||
if (from && from !== def.type) moveTypeInTabList(from, def.type);
|
||||
setDraggingType(null);
|
||||
setDragOverType(null);
|
||||
} : undefined}
|
||||
>
|
||||
{canReorderList && (
|
||||
<button
|
||||
type="button"
|
||||
className="ind-settings-drag-handle ind-panel-drag-handle"
|
||||
draggable
|
||||
title="드래그하여 순서 변경"
|
||||
aria-label="순서 변경"
|
||||
onDragStart={e => {
|
||||
setDraggingType(def.type);
|
||||
e.dataTransfer.setData('text/indicator-type', def.type);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
⠿
|
||||
</button>
|
||||
)}
|
||||
<div className="ind-panel-item-info">
|
||||
<span className="ind-panel-name">{mainLbl?.ko ?? def.description}</span>
|
||||
<span className="ind-panel-desc">{mainLbl?.en ?? def.name}</span>
|
||||
@@ -640,7 +732,10 @@ const IndDropdown: React.FC<IndDropdownProps> = ({
|
||||
type="button"
|
||||
className="ind-panel-add"
|
||||
title="차트에 추가"
|
||||
onClick={() => onAdd(def.type)}
|
||||
onClick={() => {
|
||||
onAdd(def.type);
|
||||
syncChartToTabOrder();
|
||||
}}
|
||||
>
|
||||
<IcPlus />
|
||||
</button>
|
||||
@@ -687,7 +782,7 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
||||
symbol, timeframe, chartType, theme, mode, logScale, showGrid,
|
||||
activeIndicators,
|
||||
onSymbol, onTimeframe, onChartType, onTheme, onMode,
|
||||
onFitContent, onScrollToNow, onAddIndicator, onAddIndicators, onRemoveByType,
|
||||
onFitContent, onScrollToNow, onAddIndicator, onAddIndicators, onIndicatorOrderChange, onRemoveByType,
|
||||
onAutoFib, onClearFib, onScreenshot,
|
||||
onToggleStats, onToggleWatch, onToggleAlert,
|
||||
tradeNotifyUnread = 0, onOpenTradeNotifications,
|
||||
@@ -865,6 +960,7 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
||||
activeIndicators={activeIndicators}
|
||||
onAdd={type => onAddIndicator(type)}
|
||||
onAddMany={onAddIndicators}
|
||||
onIndicatorOrderChange={onIndicatorOrderChange}
|
||||
onRemove={type => onRemoveByType(type)}
|
||||
onOpenSettings={onOpenIndicatorSettings}
|
||||
onClose={() => setShowIndMenu(false)}
|
||||
|
||||
@@ -50,7 +50,7 @@ import ChartContextMenu from './ChartContextMenu';
|
||||
import CandlePaneControls from './CandlePaneControls';
|
||||
import { getKoreanName } from '../utils/marketNameCache';
|
||||
import { DEFAULT_DISPLAY_TIMEZONE } from '../utils/timezone';
|
||||
import { classifyIndicatorChartChange, singleIndParamKey } from '../utils/indicatorChartSync';
|
||||
import { chartHasStaleIndicators, classifyIndicatorChartChange, singleIndParamKey } from '../utils/indicatorChartSync';
|
||||
import type { ChartPaneSeparatorOptions } from '../types/chartPaneSeparator';
|
||||
import PaneSeparatorOverlay from './PaneSeparatorOverlay';
|
||||
|
||||
@@ -429,6 +429,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
return;
|
||||
}
|
||||
afterIndicatorPaneMutation(mgr);
|
||||
// resetPaneHeights 직후 레이블 좌표 재동기화 (탭 교체 후 어긋남 방지)
|
||||
mgr.notifyPaneLayoutChanged();
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
if (reloadGen !== indicatorReloadGenRef.current) {
|
||||
fadeOutIndicatorReloadCover(cover);
|
||||
@@ -437,6 +439,14 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
restoreLogicalRange(mgr, savedLR);
|
||||
fadeOutIndicatorReloadCover(cover);
|
||||
indicatorSyncInFlightRef.current = false;
|
||||
mgr.notifyPaneLayoutChanged();
|
||||
[50, 150, 350, 700, 1200].forEach(ms => {
|
||||
setTimeout(() => {
|
||||
if (reloadGen === indicatorReloadGenRef.current) {
|
||||
mgr.notifyPaneLayoutChanged();
|
||||
}
|
||||
}, ms);
|
||||
});
|
||||
onComplete?.();
|
||||
}));
|
||||
});
|
||||
@@ -475,7 +485,17 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
|
||||
const expected = countExpectedVisibleIndicators(inds);
|
||||
if (expected === 0) return;
|
||||
if (m.getLoadedIndicatorCount() >= expected) return;
|
||||
|
||||
const loadedIds = m.getLoadedIndicatorIds();
|
||||
if (chartHasStaleIndicators(loadedIds, inds)) {
|
||||
reloadIndicatorsWithCover(m, inds, () => {
|
||||
prevBarsKey.current = barsKey(barData, market);
|
||||
syncIndicatorTrackingRefs(inds);
|
||||
applyPaneLayout(m);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (loadedIds.size >= expected) return;
|
||||
|
||||
prevBarsKey.current = '';
|
||||
const ct = prevChartType.current;
|
||||
@@ -484,7 +504,10 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
|
||||
if (m.hasMainSeries()) {
|
||||
void repairMissingIndicators(m, inds).then(() => {
|
||||
if (m.getLoadedIndicatorCount() >= expected) {
|
||||
if (
|
||||
m.getLoadedIndicatorCount() >= expected
|
||||
&& !chartHasStaleIndicators(m.getLoadedIndicatorIds(), inds)
|
||||
) {
|
||||
prevBarsKey.current = barsKey(barData, market);
|
||||
syncIndicatorTrackingRefs(inds);
|
||||
applyPaneLayout(m);
|
||||
@@ -496,7 +519,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
}
|
||||
reloadAllRef.current?.(m, barData, ct, th, ls, inds);
|
||||
}, 120);
|
||||
}, [market, barsMarket, applyPaneLayout, repairMissingIndicators, syncIndicatorTrackingRefs]);
|
||||
}, [market, barsMarket, applyPaneLayout, repairMissingIndicators, reloadIndicatorsWithCover, syncIndicatorTrackingRefs]);
|
||||
|
||||
/** setData 직후 캔들·볼륨 pane 레이아웃 확정 (지표 로드 중단 시에도 빈 화면 방지) */
|
||||
const commitCandleLayout = useCallback((mgr: ChartManager) => {
|
||||
@@ -1037,12 +1060,21 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
|
||||
if (barsChanged || ctChanged) {
|
||||
void reloadAll(mgr, bars, chartType, theme, logScale, indicators);
|
||||
} else if (indicatorsIncomplete) {
|
||||
// 누락 지표만 추가 (전체 제거·재추가로 화면이 비는 문제 방지)
|
||||
void repairMissingIndicators(mgr, indicators).then(() => {
|
||||
syncIndicatorTrackingRefs(indicators);
|
||||
prevBarsKey.current = barsKey(bars, market);
|
||||
});
|
||||
} else if (indicatorsIncomplete && !indChanged) {
|
||||
// 지표 목록은 같지만 일부만 로드된 경우 — 누락분만 추가.
|
||||
// 차트에 이전 탭 id가 남아 있으면(설정 화면에서 탭 교체 등) 전체 재로드.
|
||||
const loadedIds = mgr.getLoadedIndicatorIds();
|
||||
if (chartHasStaleIndicators(loadedIds, indicators)) {
|
||||
reloadIndicatorsWithCover(mgr, indicators, () => {
|
||||
syncIndicatorTrackingRefs(indicators);
|
||||
prevBarsKey.current = barsKey(bars, market);
|
||||
});
|
||||
} else {
|
||||
void repairMissingIndicators(mgr, indicators).then(() => {
|
||||
syncIndicatorTrackingRefs(indicators);
|
||||
prevBarsKey.current = barsKey(bars, market);
|
||||
});
|
||||
}
|
||||
} else if (lsChanged) {
|
||||
prevLogScale.current = logScale;
|
||||
mgr.setLogScale(logScale);
|
||||
@@ -1168,7 +1200,11 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
|
||||
const expected = countExpectedVisibleIndicators(indicators);
|
||||
if (expected === 0) return;
|
||||
if (mgr.getLoadedIndicatorCount() >= expected) return;
|
||||
const loadedIds = mgr.getLoadedIndicatorIds();
|
||||
if (
|
||||
loadedIds.size >= expected
|
||||
&& !chartHasStaleIndicators(loadedIds, indicators)
|
||||
) return;
|
||||
|
||||
const timers = [120, 400, 900].map(delay =>
|
||||
window.setTimeout(() => {
|
||||
@@ -1176,10 +1212,23 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
const m = managerRef.current;
|
||||
if (!m || !m.hasMainSeries()) return;
|
||||
if (!canApplyChartBars(bars, barsMarket, market)) return;
|
||||
const exp = countExpectedVisibleIndicators(indicatorsRef.current);
|
||||
if (exp === 0 || m.getLoadedIndicatorCount() >= exp) return;
|
||||
void repairMissingIndicators(m, indicatorsRef.current).then(() => {
|
||||
if (m.getLoadedIndicatorCount() >= exp) return;
|
||||
const inds = indicatorsRef.current;
|
||||
const exp = countExpectedVisibleIndicators(inds);
|
||||
if (exp === 0) return;
|
||||
const ids = m.getLoadedIndicatorIds();
|
||||
if (chartHasStaleIndicators(ids, inds)) {
|
||||
reloadIndicatorsWithCover(m, inds, () => {
|
||||
syncIndicatorTrackingRefs(inds);
|
||||
prevBarsKey.current = barsKey(bars, market);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (ids.size >= exp) return;
|
||||
void repairMissingIndicators(m, inds).then(() => {
|
||||
if (
|
||||
m.getLoadedIndicatorCount() >= exp
|
||||
&& !chartHasStaleIndicators(m.getLoadedIndicatorIds(), inds)
|
||||
) return;
|
||||
prevBarsKey.current = '';
|
||||
queueIndicatorRecovery();
|
||||
});
|
||||
@@ -1187,7 +1236,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
);
|
||||
|
||||
return () => { timers.forEach(clearTimeout); };
|
||||
}, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators, chartVisible, queueIndicatorRecovery, repairMissingIndicators]);
|
||||
}, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators, chartVisible, queueIndicatorRecovery, repairMissingIndicators, reloadIndicatorsWithCover, syncIndicatorTrackingRefs]);
|
||||
|
||||
// cursor 모드에서 드로잉 위에 있으면 pointer 커서로 변경
|
||||
const handleWrapperMouseMove = useCallback((e: React.PointerEvent) => {
|
||||
|
||||
Reference in New Issue
Block a user