전략편집기 수정

This commit is contained in:
Macbook
2026-06-15 01:04:12 +09:00
parent 4a7fbcd582
commit 16178cb475
5 changed files with 108 additions and 8 deletions
@@ -129,6 +129,12 @@ function MultiSideHandles({
function usePaletteDropHandlers(id: string, d: StrategyFlowNodeData) { function usePaletteDropHandlers(id: string, d: StrategyFlowNodeData) {
const { screenToFlowPosition } = useReactFlow(); const { screenToFlowPosition } = useReactFlow();
const onDragEnter = useCallback((e: React.DragEvent) => {
if (!isPaletteHtmlDrag(e)) return;
e.preventDefault();
e.stopPropagation();
}, []);
const onDragOver = useCallback((e: React.DragEvent) => { const onDragOver = useCallback((e: React.DragEvent) => {
if (!isPaletteHtmlDrag(e)) return; if (!isPaletteHtmlDrag(e)) return;
e.preventDefault(); e.preventDefault();
@@ -154,13 +160,13 @@ function usePaletteDropHandlers(id: string, d: StrategyFlowNodeData) {
d.onDropTarget?.(id, payload, flowPos); d.onDropTarget?.(id, payload, flowPos);
}, [d, id, screenToFlowPosition]); }, [d, id, screenToFlowPosition]);
return { onDragOver, onDragLeave, onDrop }; return { onDragEnter, onDragOver, onDragLeave, onDrop };
} }
export const StartNode = memo(function StartNode({ id, data }: NodeProps) { export const StartNode = memo(function StartNode({ id, data }: NodeProps) {
const d = data as StrategyFlowNodeData; const d = data as StrategyFlowNodeData;
const isSell = d.signalTab === 'sell'; const isSell = d.signalTab === 'sell';
const { onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d); const { onDragEnter, onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d);
const candleTypes = d.candleTypes?.length const candleTypes = d.candleTypes?.length
? d.candleTypes ? d.candleTypes
: getStartCandleTypes({ candleType: d.candleType ?? '1m' }); : getStartCandleTypes({ candleType: d.candleType ?? '1m' });
@@ -171,6 +177,7 @@ export const StartNode = memo(function StartNode({ id, data }: NodeProps) {
return ( return (
<div <div
className={`se-flow-node se-flow-node--start${isSell ? ' se-flow-node--sell-mode' : ''} ${d.dragOver ? 'se-flow-node--drop' : ''}`} className={`se-flow-node se-flow-node--start${isSell ? ' se-flow-node--sell-mode' : ''} ${d.dragOver ? 'se-flow-node--drop' : ''}`}
onDragEnter={onDragEnter}
onDragOver={onDragOver} onDragOver={onDragOver}
onDragLeave={onDragLeave} onDragLeave={onDragLeave}
onDrop={onDrop} onDrop={onDrop}
@@ -221,13 +228,14 @@ export const LogicGateNode = memo(function LogicGateNode({ id, data, selected }:
const isPriceExtremePair = isPriceExtremeBreakoutPairRoot(node); const isPriceExtremePair = isPriceExtremeBreakoutPairRoot(node);
const isInflection33Pair = isInflection33PairRoot(node); const isInflection33Pair = isInflection33PairRoot(node);
const isStableStrategyPair = isStableStrategyPairRoot(node); const isStableStrategyPair = isStableStrategyPairRoot(node);
const { onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d); const { onDragEnter, onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d);
const [settingsOpen, setSettingsOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false);
return ( return (
<div <div
className={`se-flow-node se-flow-node--logic${isStochPair ? ' se-flow-node--stoch-pair' : ''}${isPriceExtremePair ? ' se-flow-node--price-extreme-pair' : ''}${isInflection33Pair ? ' se-flow-node--inflection33-pair' : ''}${isStableStrategyPair ? ' se-flow-node--stable-strategy-pair' : ''}${isSell ? ' se-flow-node--sell-mode' : ''}${d.isOrphan ? ' se-flow-node--orphan' : ''} ${selected ? 'se-flow-node--selected' : ''} ${d.dragOver ? ' se-flow-node--drop' : ''}${settingsOpen ? ' se-flow-node--settings-open' : ''}`} className={`se-flow-node se-flow-node--logic${isStochPair ? ' se-flow-node--stoch-pair' : ''}${isPriceExtremePair ? ' se-flow-node--price-extreme-pair' : ''}${isInflection33Pair ? ' se-flow-node--inflection33-pair' : ''}${isStableStrategyPair ? ' se-flow-node--stable-strategy-pair' : ''}${isSell ? ' se-flow-node--sell-mode' : ''}${d.isOrphan ? ' se-flow-node--orphan' : ''} ${selected ? 'se-flow-node--selected' : ''} ${d.dragOver ? ' se-flow-node--drop' : ''}${settingsOpen ? ' se-flow-node--settings-open' : ''}`}
style={{ '--se-gate-color': color } as React.CSSProperties} style={{ '--se-gate-color': color } as React.CSSProperties}
onDragEnter={onDragEnter}
onDragOver={onDragOver} onDragOver={onDragOver}
onDragLeave={onDragLeave} onDragLeave={onDragLeave}
onDrop={onDrop} onDrop={onDrop}
@@ -398,7 +406,7 @@ export const ConditionFlowNode = memo(function ConditionFlowNode({ id, data, sel
const isCross = ['CROSS_UP', 'CROSS_DOWN'].includes(condType); const isCross = ['CROSS_UP', 'CROSS_DOWN'].includes(condType);
const tone = isCross ? 'cross' : 'value'; const tone = isCross ? 'cross' : 'value';
const isSell = d.signalTab === 'sell'; const isSell = d.signalTab === 'sell';
const { onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d); const { onDragEnter, onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d);
const [settingsOpen, setSettingsOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false);
const showSettings = cond && hasNodeSettings(cond); const showSettings = cond && hasNodeSettings(cond);
@@ -409,6 +417,7 @@ export const ConditionFlowNode = memo(function ConditionFlowNode({ id, data, sel
return ( return (
<div <div
className={`se-flow-node se-flow-node--cond se-flow-node--${tone}${isSell ? ' se-flow-node--sell-mode' : ''}${d.isOrphan ? ' se-flow-node--orphan' : ''} ${selected ? 'se-flow-node--selected' : ''} ${d.dragOver ? 'se-flow-node--drop' : ''}${settingsOpen ? ' se-flow-node--settings-open' : ''}`} className={`se-flow-node se-flow-node--cond se-flow-node--${tone}${isSell ? ' se-flow-node--sell-mode' : ''}${d.isOrphan ? ' se-flow-node--orphan' : ''} ${selected ? 'se-flow-node--selected' : ''} ${d.dragOver ? 'se-flow-node--drop' : ''}${settingsOpen ? ' se-flow-node--settings-open' : ''}`}
onDragEnter={onDragEnter}
onDragOver={onDragOver} onDragOver={onDragOver}
onDragLeave={onDragLeave} onDragLeave={onDragLeave}
onDrop={onDrop} onDrop={onDrop}
@@ -3,6 +3,7 @@ import {
handlePaletteMouseDown, handlePaletteMouseDown,
handlePalettePointerDown, handlePalettePointerDown,
handlePaletteTouchStart, handlePaletteTouchStart,
markPaletteHtmlDragEnd,
needsPointerPaletteDrag, needsPointerPaletteDrag,
shouldSuppressPaletteClick, shouldSuppressPaletteClick,
writePaletteHtmlDragData, writePaletteHtmlDragData,
@@ -53,6 +54,10 @@ export default function PaletteChip({
writePaletteHtmlDragData(e, buildPayload()); writePaletteHtmlDragData(e, buildPayload());
}; };
const onDragEnd = () => {
markPaletteHtmlDragEnd();
};
const onPointerDown = (e: React.PointerEvent) => { const onPointerDown = (e: React.PointerEvent) => {
handlePalettePointerDown(e, buildPayload(), composite ? `${label} + ${label}` : label); handlePalettePointerDown(e, buildPayload(), composite ? `${label} + ${label}` : label);
}; };
@@ -92,6 +97,7 @@ export default function PaletteChip({
draggable={!needsPointerPaletteDrag()} draggable={!needsPointerPaletteDrag()}
className={`se-palette-card ${color ? `se-palette-card--${color}` : 'se-palette-card--ind'}${composite ? ' se-palette-card--composite' : ''}${selected ? ' se-palette-card--selected' : ''}`} className={`se-palette-card ${color ? `se-palette-card--${color}` : 'se-palette-card--ind'}${composite ? ' se-palette-card--composite' : ''}${selected ? ' se-palette-card--selected' : ''}`}
onDragStart={onDragStart} onDragStart={onDragStart}
onDragEnd={onDragEnd}
onPointerDown={onPointerDown} onPointerDown={onPointerDown}
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
onTouchStart={onTouchStart} onTouchStart={onTouchStart}
@@ -4,6 +4,7 @@ import type { DefType } from '../../utils/strategyEditorShared';
import { import {
handlePalettePointerDown, handlePalettePointerDown,
handlePaletteTouchStart, handlePaletteTouchStart,
markPaletteHtmlDragEnd,
needsPointerPaletteDrag, needsPointerPaletteDrag,
writePaletteHtmlDragData, writePaletteHtmlDragData,
type PaletteDragPayload, type PaletteDragPayload,
@@ -85,6 +86,11 @@ function SidewaysFilterChip({
writePaletteHtmlDragData(e, payload as PaletteDragPayload); writePaletteHtmlDragData(e, payload as PaletteDragPayload);
}; };
const onDragEnd = () => {
hideHint();
markPaletteHtmlDragEnd();
};
const onPointerDown = (e: React.PointerEvent) => { const onPointerDown = (e: React.PointerEvent) => {
hideHint(); hideHint();
const payload: PaletteDragPayload = { const payload: PaletteDragPayload = {
@@ -123,6 +129,7 @@ function SidewaysFilterChip({
draggable={!needsPointerPaletteDrag()} draggable={!needsPointerPaletteDrag()}
className={`se-palette-card se-palette-card--range se-palette-card--range-${item.mode}${selected ? ' se-palette-card--selected' : ''}`} className={`se-palette-card se-palette-card--range se-palette-card--range-${item.mode}${selected ? ' se-palette-card--selected' : ''}`}
onDragStart={onDragStart} onDragStart={onDragStart}
onDragEnd={onDragEnd}
onPointerDown={onPointerDown} onPointerDown={onPointerDown}
onTouchStart={onTouchStart} onTouchStart={onTouchStart}
onClick={handleClick} onClick={handleClick}
@@ -32,6 +32,8 @@ import {
import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage'; import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage';
import { import {
isPaletteHtmlDrag, isPaletteHtmlDrag,
isPaletteHtmlDragActive,
markPaletteHtmlDragEnd,
needsPointerPaletteDrag, needsPointerPaletteDrag,
readPaletteHtmlDragData, readPaletteHtmlDragData,
setPaletteDragDropHandler, setPaletteDragDropHandler,
@@ -1282,6 +1284,40 @@ function StrategyEditorCanvasInner({
const lastPreviewAnchorRef = useRef<string | null>(null); const lastPreviewAnchorRef = useRef<string | null>(null);
const lastPreviewKeyRef = useRef<string | null>(null); const lastPreviewKeyRef = useRef<string | null>(null);
useEffect(() => {
if (needsPointerPaletteDrag()) return;
const onDocDragOver = (e: DragEvent) => {
if (!isPaletteHtmlDragActive()) return;
e.preventDefault();
const { flowPos, anchorId } = resolveDropFromClientRef.current(e.clientX, e.clientY);
if (anchorId) {
const sideKey = `${anchorId}:${Math.round(flowPos.x / 8)}:${Math.round(flowPos.y / 8)}`;
if (sideKey === lastPreviewKeyRef.current) return;
lastPreviewKeyRef.current = sideKey;
lastPreviewAnchorRef.current = anchorId;
updatePreviewRef.current(anchorId, flowPos);
return;
}
if (lastPreviewAnchorRef.current != null) {
lastPreviewAnchorRef.current = null;
lastPreviewKeyRef.current = null;
clearPreviewRef.current();
}
};
const onDocDragEnd = () => {
lastPreviewAnchorRef.current = null;
lastPreviewKeyRef.current = null;
clearPreviewRef.current();
markPaletteHtmlDragEnd();
};
document.addEventListener('dragover', onDocDragOver, { passive: false });
document.addEventListener('dragend', onDocDragEnd);
return () => {
document.removeEventListener('dragover', onDocDragOver);
document.removeEventListener('dragend', onDocDragEnd);
};
}, []);
useEffect(() => { useEffect(() => {
if (!needsPointerPaletteDrag()) return; if (!needsPointerPaletteDrag()) return;
return subscribePaletteDrag((ev: PaletteDragEvent) => { return subscribePaletteDrag((ev: PaletteDragEvent) => {
@@ -1320,6 +1356,11 @@ function StrategyEditorCanvasInner({
}); });
}, []); }, []);
const onPaneDragEnter = useCallback((e: React.DragEvent) => {
if (!isPaletteDrag(e)) return;
e.preventDefault();
}, [isPaletteDrag]);
const onPaneDragOver = useCallback((e: React.DragEvent) => { const onPaneDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
if (!isPaletteDrag(e)) return; if (!isPaletteDrag(e)) return;
@@ -1378,6 +1419,7 @@ function StrategyEditorCanvasInner({
isValidConnection={isValidConnection} isValidConnection={isValidConnection}
onReconnect={onReconnect} onReconnect={onReconnect}
onDrop={onPaneDrop} onDrop={onPaneDrop}
onDragEnter={onPaneDragEnter}
onDragOver={onPaneDragOver} onDragOver={onPaneDragOver}
onDragLeave={onPaneDragLeave} onDragLeave={onPaneDragLeave}
connectionMode={ConnectionMode.Loose} connectionMode={ConnectionMode.Loose}
+40 -4
View File
@@ -54,6 +54,26 @@ let moveRafId: number | null = null;
let pendingMove: { x: number; y: number } | null = null; let pendingMove: { x: number; y: number } | null = null;
let unbindGesture: (() => void) | null = null; let unbindGesture: (() => void) | null = null;
let lastDragClientXY: { x: number; y: number } | null = null; let lastDragClientXY: { x: number; y: number } | null = null;
let htmlPaletteDragActive = false;
function installHtmlPaletteDragGlobals() {
if (typeof window === 'undefined') return;
const w = window as Window & { __gcPaletteHtmlDragGlobals?: boolean };
if (w.__gcPaletteHtmlDragGlobals) return;
w.__gcPaletteHtmlDragGlobals = true;
window.addEventListener('dragover', (e) => {
if (!htmlPaletteDragActive) return;
e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
}, { passive: false });
window.addEventListener('dragend', () => {
htmlPaletteDragActive = false;
});
}
installHtmlPaletteDragGlobals();
function hasCoarsePointer(): boolean { function hasCoarsePointer(): boolean {
if (typeof window === 'undefined') return false; if (typeof window === 'undefined') return false;
@@ -306,7 +326,20 @@ export function handlePaletteTouchStart(
startPaletteDragGesture(touch.clientX, touch.clientY, touch.identifier, payload); startPaletteDragGesture(touch.clientX, touch.clientY, touch.identifier, payload);
} }
export function markPaletteHtmlDragStart(): void {
htmlPaletteDragActive = true;
}
export function markPaletteHtmlDragEnd(): void {
htmlPaletteDragActive = false;
}
export function isPaletteHtmlDragActive(): boolean {
return htmlPaletteDragActive;
}
export function writePaletteHtmlDragData(e: React.DragEvent, payload: PaletteDragPayload): void { export function writePaletteHtmlDragData(e: React.DragEvent, payload: PaletteDragPayload): void {
markPaletteHtmlDragStart();
const json = JSON.stringify(payload); const json = JSON.stringify(payload);
e.dataTransfer.setData('application/json', json); e.dataTransfer.setData('application/json', json);
e.dataTransfer.setData('text/plain', json); e.dataTransfer.setData('text/plain', json);
@@ -314,13 +347,16 @@ export function writePaletteHtmlDragData(e: React.DragEvent, payload: PaletteDra
} }
export function isPaletteHtmlDrag(e: React.DragEvent): boolean { export function isPaletteHtmlDrag(e: React.DragEvent): boolean {
return e.dataTransfer.types.includes('application/json') if (htmlPaletteDragActive) return true;
|| e.dataTransfer.types.includes('text/plain'); const types = e.dataTransfer?.types;
if (!types) return false;
return Array.from(types).includes('application/json')
|| Array.from(types).includes('text/plain');
} }
export function readPaletteHtmlDragData(e: React.DragEvent): PaletteDragPayload | null { export function readPaletteHtmlDragData(e: React.DragEvent): PaletteDragPayload | null {
const raw = e.dataTransfer.getData('application/json') const raw = e.dataTransfer.getData('text/plain')
|| e.dataTransfer.getData('text/plain'); || e.dataTransfer.getData('application/json');
if (!raw) return null; if (!raw) return null;
try { try {
return JSON.parse(raw) as PaletteDragPayload; return JSON.parse(raw) as PaletteDragPayload;