위젯 기능 추가
This commit is contained in:
@@ -1,38 +1,6 @@
|
||||
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { INDICATOR_REGISTRY, type IndicatorDef } from '../utils/indicatorRegistry';
|
||||
import {
|
||||
MAIN_INDICATOR_TYPES,
|
||||
MAIN_INDICATOR_LABELS,
|
||||
customTabKey,
|
||||
isCustomTabKey,
|
||||
parseCustomTabKey,
|
||||
type IndicatorTabKey,
|
||||
} from '../utils/indicatorMainTab';
|
||||
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,
|
||||
updateCustomTab,
|
||||
deleteCustomTab,
|
||||
type IndicatorCustomTab,
|
||||
} from '../utils/indicatorCustomTabsStorage';
|
||||
import IndicatorCustomTabEditor from './IndicatorCustomTabEditor';
|
||||
import { getIndicatorListLabels } from '../utils/indicatorSettingsList';
|
||||
// IndicatorCategory는 IndDropdown 상태 타입에 사용됨
|
||||
import IndicatorDropdownPanel from './IndicatorDropdownPanel';
|
||||
import type { ChartType, Theme, Timeframe, ChartMode, IndicatorConfig } from '../types';
|
||||
import { MarketSearchPanel } from './MarketSearchPanel';
|
||||
import { getKoreanName } from '../utils/marketNameCache';
|
||||
@@ -165,12 +133,6 @@ const IcPlus = () => (
|
||||
<line x1="7" y1="2" x2="7" y2="12"/><line x1="2" y1="7" x2="12" y2="7"/>
|
||||
</svg>
|
||||
);
|
||||
const IcGear = () => (
|
||||
<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"/>
|
||||
<path d="M7 1 L7.7 2.8 L9.8 2 L9.8 4.2 L11.8 5 L11 7 L11.8 9 L9.8 9.8 L9.8 12 L7.7 11.2 L7 13 L6.3 11.2 L4.2 12 L4.2 9.8 L2.2 9 L3 7 L2.2 5 L4.2 4.2 L4.2 2 L6.3 2.8 Z"/>
|
||||
</svg>
|
||||
);
|
||||
const IcCandlestick = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<line x1="4" y1="2" x2="4" y2="5"/><rect x="2" y="5" width="4" height="5" rx="0.5" fill="currentColor" opacity="0.3"/><line x1="4" y1="10" x2="4" y2="14"/>
|
||||
@@ -298,12 +260,6 @@ const IcReplay = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
// ─── 기본 탭 (사용자 정의 탭은 localStorage) ───────────────────────────────
|
||||
const BUILTIN_TABS: Array<{ key: IndicatorTabKey; label: string }> = [
|
||||
{ key: 'All', label: '전체' },
|
||||
{ key: 'Main', label: '주요지표' },
|
||||
];
|
||||
|
||||
// ─── 차트 타입 아이콘 매핑 ─────────────────────────────────────────────────
|
||||
const ChartIcon: Record<ChartType, React.ReactNode> = {
|
||||
candlestick: <IcCandlestick />,
|
||||
@@ -322,487 +278,6 @@ const CHART_TYPES: { value: ChartType; label: string }[] = [
|
||||
{ value: 'baseline', label: '기준선' },
|
||||
];
|
||||
|
||||
// ─── 지표 선택 오버레이 패널 ──────────────────────────────────────────────
|
||||
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, 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 [customTabsRevision, setCustomTabsRevision] = 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);
|
||||
const searchFocusedRef = useRef(false);
|
||||
|
||||
const refreshCustomTabs = useCallback(() => {
|
||||
setCustomTabs(loadCustomTabs());
|
||||
setCustomTabsRevision(t => t + 1);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onTabsChanged = () => refreshCustomTabs();
|
||||
window.addEventListener('gc-indicator-custom-tabs-changed', onTabsChanged);
|
||||
return () => window.removeEventListener('gc-indicator-custom-tabs-changed', onTabsChanged);
|
||||
}, [refreshCustomTabs]);
|
||||
|
||||
const selectedCustomTab = React.useMemo(() => {
|
||||
const id = parseCustomTabKey(category);
|
||||
return id ? customTabs.find(t => t.id === id) ?? null : null;
|
||||
}, [category, customTabs]);
|
||||
|
||||
const openCreateEditor = () => {
|
||||
setEditorMode('create');
|
||||
setEditorOpen(true);
|
||||
};
|
||||
|
||||
const openEditEditor = () => {
|
||||
if (!selectedCustomTab) return;
|
||||
setEditorMode('edit');
|
||||
setEditorOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteTab = () => {
|
||||
if (!selectedCustomTab) return;
|
||||
if (!window.confirm(`"${selectedCustomTab.name}" 탭을 삭제할까요?`)) return;
|
||||
deleteCustomTab(selectedCustomTab.id);
|
||||
refreshCustomTabs();
|
||||
setCategory('All');
|
||||
};
|
||||
|
||||
const handleEditorSave = (name: string, indicatorTypes: string[]) => {
|
||||
if (editorMode === 'create') {
|
||||
const tab = createCustomTab(name, indicatorTypes);
|
||||
refreshCustomTabs();
|
||||
setCategory(customTabKey(tab.id));
|
||||
} else if (selectedCustomTab) {
|
||||
updateCustomTab(selectedCustomTab.id, { name, indicatorTypes });
|
||||
refreshCustomTabs();
|
||||
}
|
||||
setEditorOpen(false);
|
||||
setSearch('');
|
||||
};
|
||||
|
||||
const tabTypeOrder = React.useMemo((): string[] | null => {
|
||||
void mainOrderTick;
|
||||
void customTabsRevision;
|
||||
if (search.trim()) return null;
|
||||
if (category === 'Main') return getOrderedMainIndicatorTypes();
|
||||
if (isCustomTabKey(category)) {
|
||||
const id = parseCustomTabKey(category);
|
||||
if (!id) return null;
|
||||
const tab = customTabs.find(t => t.id === id);
|
||||
return tab ? [...tab.indicatorTypes] : null;
|
||||
}
|
||||
return null;
|
||||
}, [category, customTabs, search, mainOrderTick, customTabsRevision]);
|
||||
|
||||
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)) {
|
||||
const id = parseCustomTabKey(category);
|
||||
if (!id) return;
|
||||
const updated = updateCustomTab(id, { indicatorTypes: next });
|
||||
if (updated) {
|
||||
setCustomTabs(prev => prev.map(t => (t.id === id ? updated : t)));
|
||||
setCustomTabsRevision(t => t + 1);
|
||||
} else {
|
||||
refreshCustomTabs();
|
||||
}
|
||||
onIndicatorOrderChange?.(next);
|
||||
}
|
||||
}, [tabTypeOrder, category, 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: getOrderedMainIndicatorTypes() };
|
||||
}
|
||||
if (isCustomTabKey(category) && selectedCustomTab) {
|
||||
const types = selectedCustomTab.indicatorTypes.filter(
|
||||
t => INDICATOR_REGISTRY.some(d => d.type === t),
|
||||
);
|
||||
return { label: selectedCustomTab.name, types };
|
||||
}
|
||||
return null;
|
||||
}, [category, selectedCustomTab]);
|
||||
|
||||
const addAllDisabled = category === 'All' || !addAllContext || addAllContext.types.length === 0;
|
||||
|
||||
const handleAddAllInTab = () => {
|
||||
if (!addAllContext || addAllContext.types.length === 0) return;
|
||||
if (category === 'Main') {
|
||||
confirmAndApplyIndicatorTab({ kind: 'main' }, onAddMany, onAdd);
|
||||
return;
|
||||
}
|
||||
if (isCustomTabKey(category) && selectedCustomTab) {
|
||||
confirmAndApplyIndicatorTab({ kind: 'custom', tab: selectedCustomTab }, onAddMany, onAdd);
|
||||
}
|
||||
};
|
||||
|
||||
const stopHeaderBtn = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (editorOpen || searchFocusedRef.current) return;
|
||||
searchRef.current?.focus();
|
||||
searchFocusedRef.current = true;
|
||||
}, [editorOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (editorOpen) {
|
||||
setEditorOpen(false);
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [onClose, editorOpen]);
|
||||
|
||||
const isActive = (type: string) => activeIndicators.some(a => a.type === type);
|
||||
|
||||
const filtered = React.useMemo(() => {
|
||||
const q = search.toLowerCase();
|
||||
const matchesQuery = (d: IndicatorDef) => {
|
||||
if (!q) return true;
|
||||
const main = MAIN_INDICATOR_LABELS[d.type];
|
||||
return (
|
||||
d.name.toLowerCase().includes(q)
|
||||
|| d.shortName.toLowerCase().includes(q)
|
||||
|| d.description.includes(q)
|
||||
|| (main?.ko.includes(q) ?? false)
|
||||
|| (main?.en.toLowerCase().includes(q) ?? false)
|
||||
);
|
||||
};
|
||||
|
||||
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) && 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, tabTypeOrder]);
|
||||
|
||||
const {
|
||||
panelRef,
|
||||
dragging,
|
||||
onHeaderPointerDown, headerTouchStyle,
|
||||
panelStyle,
|
||||
headerCursor,
|
||||
} = useDraggablePanel({ centerOnMount: true });
|
||||
|
||||
return (
|
||||
// 배경 클릭 시 닫힘
|
||||
<div className="ind-overlay" onMouseDown={e => { if (e.target === e.currentTarget) onClose(); }}>
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="ind-panel"
|
||||
style={{
|
||||
...panelStyle,
|
||||
zIndex: 2001,
|
||||
cursor: dragging ? 'grabbing' : undefined,
|
||||
}}
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
className="gc-popup-header"
|
||||
onPointerDown={onHeaderPointerDown}
|
||||
style={{ cursor: headerCursor, ...headerTouchStyle }}
|
||||
>
|
||||
<span className="gc-popup-title">지표 추가</span>
|
||||
<div className="ind-panel-header-actions">
|
||||
<button type="button" className="gc-popup-close" onClick={onClose} aria-label="닫기">✕</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ind-panel-header-btn"
|
||||
onPointerDown={stopHeaderBtn}
|
||||
onClick={openCreateEditor}
|
||||
>
|
||||
탭 추가
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ind-panel-header-btn"
|
||||
disabled={!selectedCustomTab}
|
||||
onPointerDown={stopHeaderBtn}
|
||||
onClick={openEditEditor}
|
||||
>
|
||||
수정
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ind-panel-header-btn danger"
|
||||
disabled={!selectedCustomTab}
|
||||
onPointerDown={stopHeaderBtn}
|
||||
onClick={handleDeleteTab}
|
||||
>
|
||||
삭제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 검색창 */}
|
||||
<div className="ind-panel-search-row">
|
||||
<div className="ind-panel-search-wrap">
|
||||
<IcSearch />
|
||||
<input
|
||||
ref={searchRef}
|
||||
className="ind-panel-search"
|
||||
placeholder={`지표 검색... (전체 ${INDICATOR_REGISTRY.length}개)`}
|
||||
value={search}
|
||||
onChange={e => { setSearch(e.target.value); setCategory('All'); }}
|
||||
/>
|
||||
{search && (
|
||||
<button className="ind-panel-clear" onClick={() => setSearch('')}>✕</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카테고리 탭 */}
|
||||
<div className="ind-panel-cats-row">
|
||||
<div className="ind-panel-cats" role="tablist" aria-label="지표 탭">
|
||||
{BUILTIN_TABS.map(c => {
|
||||
const cnt = c.key === 'All'
|
||||
? INDICATOR_REGISTRY.length
|
||||
: MAIN_INDICATOR_TYPES.length;
|
||||
return (
|
||||
<button
|
||||
key={c.key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={category === c.key}
|
||||
className={`ind-panel-cat${category === c.key ? ' active' : ''}`}
|
||||
onClick={() => { setCategory(c.key); setSearch(''); }}
|
||||
>
|
||||
{c.label}
|
||||
<span className="ind-panel-cat-cnt"> {cnt}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{customTabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={category === customTabKey(tab.id)}
|
||||
className={`ind-panel-cat${category === customTabKey(tab.id) ? ' active' : ''}`}
|
||||
onClick={() => { setCategory(customTabKey(tab.id)); setSearch(''); }}
|
||||
>
|
||||
{tab.name}
|
||||
<span className="ind-panel-cat-cnt"> {tab.indicatorTypes.length}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="ind-panel-add-all"
|
||||
disabled={addAllDisabled}
|
||||
title={addAllDisabled ? '전체 탭에서는 사용할 수 없습니다' : '기존 보조지표를 제거하고 현재 탭 지표로 교체'}
|
||||
aria-label="현재 탭 지표로 보조지표 교체"
|
||||
onClick={handleAddAllInTab}
|
||||
>
|
||||
<IcPlus />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 지표 목록 */}
|
||||
<div className="ind-panel-list">
|
||||
{/* 활성 지표가 있을 때 상단에 표시 */}
|
||||
{activeIndicators.length > 0 && !search && category === 'All' && (
|
||||
<div className="ind-panel-section">
|
||||
<div className="ind-panel-section-title">활성 지표 ({activeIndicators.length})</div>
|
||||
{activeIndicators.map(a => {
|
||||
const lbl = getIndicatorListLabels(a.type);
|
||||
return (
|
||||
<div key={a.id} className="ind-panel-item active">
|
||||
<div className="ind-panel-item-info">
|
||||
<span className="ind-panel-name">{lbl.ko}</span>
|
||||
<span className="ind-panel-desc">{lbl.en}</span>
|
||||
</div>
|
||||
<div className="ind-panel-item-right">
|
||||
<button className="ind-panel-rm" onClick={() => onRemove(a.type)}>제거</button>
|
||||
{onOpenSettings && (
|
||||
<button
|
||||
type="button"
|
||||
className="ind-panel-settings"
|
||||
title="설정"
|
||||
aria-label={`${lbl.ko} 설정`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onOpenSettings(a.type);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<IcGear />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="ind-panel-divider" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<div className="ind-panel-empty">
|
||||
{search
|
||||
? `"${search}" 검색 결과 없음`
|
||||
: isCustomTabKey(category)
|
||||
? '이 탭에 표시할 지표가 없습니다. 수정 버튼으로 지표를 추가해 주세요.'
|
||||
: '표시할 지표가 없습니다'}
|
||||
</div>
|
||||
) : (
|
||||
filtered.map(def => {
|
||||
const active = isActive(def.type);
|
||||
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' : ''}${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>
|
||||
</div>
|
||||
<div className="ind-panel-item-right">
|
||||
{def.overlay && <span className="ind-panel-badge">오버레이</span>}
|
||||
{active ? (
|
||||
<button className="ind-panel-rm" onClick={() => onRemove(def.type)}>제거</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="ind-panel-add"
|
||||
title="차트에 추가"
|
||||
onClick={() => {
|
||||
onAdd(def.type);
|
||||
syncChartToTabOrder();
|
||||
}}
|
||||
>
|
||||
<IcPlus />
|
||||
</button>
|
||||
)}
|
||||
{onOpenSettings && (
|
||||
<button
|
||||
type="button"
|
||||
className="ind-panel-settings"
|
||||
title="설정"
|
||||
aria-label={`${mainLbl?.ko ?? def.koreanName} 설정`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onOpenSettings(def.type);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<IcGear />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editorOpen && (
|
||||
<IndicatorCustomTabEditor
|
||||
mode={editorMode}
|
||||
initialName={editorMode === 'edit' && selectedCustomTab ? selectedCustomTab.name : ''}
|
||||
initialTypes={editorMode === 'edit' && selectedCustomTab ? selectedCustomTab.indicatorTypes : []}
|
||||
onSave={handleEditorSave}
|
||||
onClose={() => setEditorOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Toolbar ──────────────────────────────────────────────────────────────
|
||||
const Toolbar: React.FC<ToolbarProps> = ({
|
||||
@@ -1038,7 +513,7 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
||||
</button>
|
||||
{/* 지표 패널은 fixed overlay로 렌더링 — 부모 overflow 영향 없음 */}
|
||||
{showIndMenu && (
|
||||
<IndDropdown
|
||||
<IndicatorDropdownPanel
|
||||
activeIndicators={activeIndicators}
|
||||
onAdd={type => onAddIndicator(type)}
|
||||
onAddMany={onAddIndicators}
|
||||
|
||||
Reference in New Issue
Block a user