전략편집기 횡보구간 컨트롤 추가

This commit is contained in:
Macbook
2026-06-12 09:08:44 +09:00
parent 74b0ea4ab6
commit d741d3fec6
8 changed files with 1601 additions and 19 deletions
+102 -7
View File
@@ -45,6 +45,8 @@ import {
type StartCombineOp,
} from '../utils/strategyStartNodes';
import IndicatorPaletteTab from './strategyEditor/IndicatorPaletteTab';
import SidewaysFilterPaletteTab from './strategyEditor/SidewaysFilterPaletteTab';
import { buildSidewaysFilterNode } from '../utils/sidewaysFilterPalette';
import {
loadPaletteItems,
type PaletteItem,
@@ -99,7 +101,16 @@ import {
strategyDtoToListExportItem,
} from '../utils/strategyImportExport';
import PaletteChip from './strategyEditor/PaletteChip';
import { readStoredSize, storeSize, usePanelResize } from './strategyEditor/usePanelResize';
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
import {
clampPanelSize,
readStoredBool,
readStoredSize,
storeBool,
storeSize,
usePanelResize,
useRightPanelResize,
} from './strategyEditor/usePanelResize';
import StrategyDescriptionModal from './strategyEditor/StrategyDescriptionModal';
import ReactDOM from 'react-dom';
import { MarketSearchPanel } from './MarketSearchPanel';
@@ -120,10 +131,21 @@ const BACKTEST_FOCUS_KEY = 'backtest_focus_id';
const LEFT_PANEL_MIN = 220;
const LEFT_PANEL_MAX = 520;
const LEFT_PANEL_DEFAULT = 280;
const RIGHT_PANEL_MIN = 380;
const RIGHT_PANEL_MAX = 560;
const RIGHT_PANEL_DEFAULT = 380;
const TERMINAL_MIN = 88;
const TERMINAL_MAX = 420;
const TERMINAL_DEFAULT = 140;
function readRightPanelWidth(): number {
return clampPanelSize(
readStoredSize('se-right-width', RIGHT_PANEL_DEFAULT),
RIGHT_PANEL_MIN,
RIGHT_PANEL_MAX,
);
}
interface Props {
theme: Theme;
onNavigateToBacktest?: () => void;
@@ -189,7 +211,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
const [sellCondition, setSellCondition] = useState<LogicNode | null>(null);
const [signalTab, setSignalTab] = useState<'buy' | 'sell'>('buy');
const [rightTab, setRightTab] = useState<'indicators' | 'templates'>('indicators');
const [indicatorSubTab, setIndicatorSubTab] = useState<'auxiliary' | 'composite'>('auxiliary');
const [indicatorSubTab, setIndicatorSubTab] = useState<'auxiliary' | 'composite' | 'range'>('auxiliary');
const [auxiliaryPalette, setAuxiliaryPalette] = useState<PaletteItem[]>(() => loadPaletteItems('auxiliary'));
const [compositePalette, setCompositePalette] = useState<PaletteItem[]>(() => loadPaletteItems('composite'));
const [paletteSearch, setPaletteSearch] = useState('');
@@ -224,8 +246,11 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
const qbMarketBtnRef = useRef<HTMLButtonElement>(null);
const [leftWidth, setLeftWidth] = useState(() => readStoredSize('se-left-width', LEFT_PANEL_DEFAULT));
const [rightWidth, setRightWidth] = useState(() => readRightPanelWidth());
const [rightOpen, setRightOpen] = useState(() => readStoredBool('se-right-open', true));
const [terminalHeight, setTerminalHeight] = useState(() => readStoredSize('se-terminal-height', TERMINAL_DEFAULT));
const leftWidthRef = useRef(leftWidth);
const rightWidthRef = useRef(rightWidth);
const terminalHeightRef = useRef(terminalHeight);
const [buyLayout, setBuyLayout] = useState(() => normalizeTabLayoutState(emptySignalFlowLayout()));
const [sellLayout, setSellLayout] = useState(() => normalizeTabLayoutState(emptySignalFlowLayout()));
@@ -246,6 +271,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
const layoutPersistReadyRef = useRef(false);
const STRATEGY_AUTOSAVE_MS = 350;
leftWidthRef.current = leftWidth;
rightWidthRef.current = rightWidth;
terminalHeightRef.current = terminalHeight;
const [layoutSeedKey, setLayoutSeedKey] = useState('draft:buy:0');
@@ -260,6 +286,22 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
v => storeSize('se-left-width', v),
);
const onRightSplitter = useRightPanelResize(
setRightWidth,
() => rightWidthRef.current,
RIGHT_PANEL_MIN,
RIGHT_PANEL_MAX,
v => storeSize('se-right-width', v),
);
const toggleRightPanel = useCallback(() => {
setRightOpen(prev => {
const next = !prev;
storeBool('se-right-open', next);
return next;
});
}, []);
const onTerminalSplitter = usePanelResize(
'horizontal',
setTerminalHeight,
@@ -271,8 +313,9 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
const bodyStyle = useMemo(() => ({
'--se-left-width': `${leftWidth}px`,
'--se-right-width': `${rightWidth}px`,
'--se-terminal-height': `${terminalHeight}px`,
}) as React.CSSProperties, [leftWidth, terminalHeight]);
}) as React.CSSProperties, [leftWidth, rightWidth, terminalHeight]);
const showSnack = (msg: string, ok = true) => {
setSnack({ msg, ok });
@@ -1021,6 +1064,15 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
scheduleStrategyPersist();
}, [signalTab, DEF, currentRoot, setCurrentRoot, scheduleStrategyPersist]);
const applySidewaysFilter = useCallback((filterId: string) => {
const newNode = buildSidewaysFilterNode(filterId, DEF);
if (!newNode) return;
const root = currentRoot;
if (!root) setCurrentRoot(newNode);
else setCurrentRoot(mergeAtRoot(root, newNode, false));
scheduleStrategyPersist();
}, [DEF, currentRoot, setCurrentRoot, scheduleStrategyPersist]);
const templates = useMemo(() => getStrategyTemplates(DEF), [DEF]);
const templateRows = useMemo(() => {
@@ -1645,7 +1697,24 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
</div>
</main>
<aside className="se-right">
{rightOpen && (
<div
className="se-splitter se-splitter--v"
role="separator"
aria-orientation="vertical"
aria-label="우측 패널 너비 조절"
onPointerDown={onRightSplitter}
/>
)}
<div className={`se-side-wrap se-side-wrap--right${rightOpen ? ' se-side-wrap--open' : ''}`}>
<SePanelCollapseHandle
side="right"
open={rightOpen}
onToggle={toggleRightPanel}
label="지표 패널"
/>
<aside className={`se-right se-right--collapsible${rightOpen ? ' se-right--collapsible--open' : ''}`}>
<div className="se-palette-panel">
<div className="se-right-tabs">
<button type="button" className={rightTab === 'indicators' ? 'se-right-tab se-right-tab--on' : 'se-right-tab'} onClick={() => setRightTab('indicators')}></button>
@@ -1702,7 +1771,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
<div className="se-palette-subtabs">
<button
type="button"
className={`se-palette-subtab${indicatorSubTab === 'auxiliary' ? ' se-palette-subtab--on' : ''}`}
className={`se-palette-subtab se-palette-subtab--aux${indicatorSubTab === 'auxiliary' ? ' se-palette-subtab--on' : ''}`}
onClick={() => {
setIndicatorSubTab('auxiliary');
setSelectedPaletteKey(null);
@@ -1712,7 +1781,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
</button>
<button
type="button"
className={`se-palette-subtab${indicatorSubTab === 'composite' ? ' se-palette-subtab--on' : ''}`}
className={`se-palette-subtab se-palette-subtab--composite${indicatorSubTab === 'composite' ? ' se-palette-subtab--on' : ''}`}
onClick={() => {
setIndicatorSubTab('composite');
setSelectedPaletteKey(null);
@@ -1720,6 +1789,16 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
>
</button>
<button
type="button"
className={`se-palette-subtab se-palette-subtab--range${indicatorSubTab === 'range' ? ' se-palette-subtab--on' : ''}`}
onClick={() => {
setIndicatorSubTab('range');
setSelectedPaletteKey(null);
}}
>
</button>
</div>
{indicatorSubTab === 'auxiliary' ? (
<IndicatorPaletteTab
@@ -1739,7 +1818,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
applyPaletteItem(item);
}}
/>
) : (
) : indicatorSubTab === 'composite' ? (
<IndicatorPaletteTab
kind="composite"
items={compositePalette}
@@ -1757,6 +1836,21 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
applyPaletteItem(item);
}}
/>
) : (
<SidewaysFilterPaletteTab
def={DEF}
searchQuery={paletteSearch}
selectedItemId={
selectedPaletteKey?.startsWith('range:')
? selectedPaletteKey.slice('range:'.length)
: null
}
onSelectItem={id => setSelectedPaletteKey(id ? paletteKey('range', id) : null)}
onAddToCanvas={item => {
selectPalette('range', item.id);
applySidewaysFilter(item.id);
}}
/>
)}
</>
)}
@@ -1821,6 +1915,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
</div>
</div>
</aside>
</div>
</div>
</div>
</div>