From d741d3fec63cce6ca46df78a3835370dd82fe1e1 Mon Sep 17 00:00:00 2001 From: Macbook Date: Fri, 12 Jun 2026 09:08:44 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EB=9E=B5=ED=8E=B8=EC=A7=91=EA=B8=B0?= =?UTF-8?q?=20=ED=9A=A1=EB=B3=B4=EA=B5=AC=EA=B0=84=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/StrategyEditorPage.tsx | 109 +++- .../strategyEditor/SePanelCollapseHandle.tsx | 43 ++ .../SidewaysFilterPaletteTab.tsx | 209 +++++++ .../strategyEditor/StrategyEditorCanvas.tsx | 20 +- .../strategyEditor/usePanelResize.ts | 43 ++ frontend/src/styles/strategyEditor.css | 322 ++++++++++- frontend/src/utils/sidewaysFilterPalette.ts | 334 +++++++++++ 매매전략조건.md | 540 ++++++++++++++++++ 8 files changed, 1601 insertions(+), 19 deletions(-) create mode 100644 frontend/src/components/strategyEditor/SePanelCollapseHandle.tsx create mode 100644 frontend/src/components/strategyEditor/SidewaysFilterPaletteTab.tsx create mode 100644 frontend/src/utils/sidewaysFilterPalette.ts create mode 100644 매매전략조건.md diff --git a/frontend/src/components/StrategyEditorPage.tsx b/frontend/src/components/StrategyEditorPage.tsx index 7eb9926..4b0dd0e 100644 --- a/frontend/src/components/StrategyEditorPage.tsx +++ b/frontend/src/components/StrategyEditorPage.tsx @@ -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(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(() => loadPaletteItems('auxiliary')); const [compositePalette, setCompositePalette] = useState(() => loadPaletteItems('composite')); const [paletteSearch, setPaletteSearch] = useState(''); @@ -224,8 +246,11 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop const qbMarketBtnRef = useRef(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 -