실시간 차트 보조지표 탭 문제 수정

This commit is contained in:
Macbook
2026-05-28 01:26:53 +09:00
parent 4f6694b206
commit 98dfb3613c
19 changed files with 801 additions and 182 deletions
+109 -13
View File
@@ -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)}