전략편집기 수정

This commit is contained in:
Macbook
2026-06-14 22:17:53 +09:00
parent dec812089d
commit 76ffa5ac52
6 changed files with 343 additions and 52 deletions
@@ -35,6 +35,10 @@ import {
import ConditionNodeSettings from './ConditionNodeSettings';
import StochPairNodeSettings from './StochPairNodeSettings';
import PriceExtremePairNodeSettings from './PriceExtremePairNodeSettings';
import {
isPaletteHtmlDrag,
readPaletteHtmlDragData,
} from '../../utils/paletteDragSession';
const LOGIC_COLORS: Record<string, string> = {
AND: '#00aaff',
@@ -126,7 +130,7 @@ function usePaletteDropHandlers(id: string, d: StrategyFlowNodeData) {
const { screenToFlowPosition } = useReactFlow();
const onDragOver = useCallback((e: React.DragEvent) => {
if (!e.dataTransfer.types.includes('application/json')) return;
if (!isPaletteHtmlDrag(e)) return;
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
@@ -144,11 +148,10 @@ function usePaletteDropHandlers(id: string, d: StrategyFlowNodeData) {
const onDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
try {
const payload = JSON.parse(e.dataTransfer.getData('application/json'));
const flowPos = screenToFlowPosition({ x: e.clientX, y: e.clientY });
d.onDropTarget?.(id, payload, flowPos);
} catch { /* ignore */ }
const payload = readPaletteHtmlDragData(e);
if (!payload) return;
const flowPos = screenToFlowPosition({ x: e.clientX, y: e.clientY });
d.onDropTarget?.(id, payload, flowPos);
}, [d, id, screenToFlowPosition]);
return { onDragOver, onDragLeave, onDrop };
@@ -1,19 +1,13 @@
import React from 'react';
import {
handlePalettePointerDown,
needsPointerPaletteDrag,
shouldSuppressPaletteClick,
writePaletteHtmlDragData,
type PaletteDragPayload,
} from '../../utils/paletteDragSession';
export interface PaletteDragPayload {
type: 'operator' | 'indicator' | 'start';
value: string;
label: string;
composite?: boolean;
stochPair?: boolean;
priceExtremeBreakout?: boolean;
inflection33?: boolean;
stableStrategy?: boolean;
secondaryIndicator?: string;
period?: number;
shortPeriod?: number;
longPeriod?: number;
}
export type { PaletteDragPayload };
interface Props {
type: 'operator' | 'indicator' | 'start';
@@ -40,24 +34,29 @@ export default function PaletteChip({
type, value, label, desc, color, period, periodValue, shortPeriod, longPeriod,
composite = false, stochPair = false, priceExtremeBreakout = false, inflection33 = false, stableStrategy = false, secondaryIndicator, selected = false, onSelect, onAdd,
}: Props) {
const buildPayload = (): PaletteDragPayload => ({
type, value, label,
composite: composite || undefined,
stochPair: stochPair || undefined,
priceExtremeBreakout: priceExtremeBreakout || undefined,
inflection33: inflection33 || undefined,
stableStrategy: stableStrategy || undefined,
secondaryIndicator,
period: periodValue,
shortPeriod,
longPeriod,
});
const onDragStart = (e: React.DragEvent) => {
const payload: PaletteDragPayload = {
type, value, label,
composite: composite || undefined,
stochPair: stochPair || undefined,
priceExtremeBreakout: priceExtremeBreakout || undefined,
inflection33: inflection33 || undefined,
stableStrategy: stableStrategy || undefined,
secondaryIndicator,
period: periodValue,
shortPeriod,
longPeriod,
};
e.dataTransfer.setData('application/json', JSON.stringify(payload));
e.dataTransfer.effectAllowed = 'copy';
writePaletteHtmlDragData(e, buildPayload());
};
const onPointerDown = (e: React.PointerEvent) => {
handlePalettePointerDown(e, buildPayload(), composite ? `${label} + ${label}` : label);
};
const handleClick = () => {
if (shouldSuppressPaletteClick()) return;
if (onSelect) onSelect();
else onAdd();
};
@@ -80,9 +79,10 @@ export default function PaletteChip({
return (
<div
draggable
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' : ''}`}
onDragStart={onDragStart}
onPointerDown={onPointerDown}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
role="button"
@@ -1,6 +1,12 @@
import React, { useMemo, useState, useCallback } from 'react';
import { createPortal } from 'react-dom';
import type { DefType } from '../../utils/strategyEditorShared';
import {
handlePalettePointerDown,
needsPointerPaletteDrag,
writePaletteHtmlDragData,
type PaletteDragPayload,
} from '../../utils/paletteDragSession';
import {
filterSidewaysItems,
type SidewaysFilterMode,
@@ -75,8 +81,17 @@ function SidewaysFilterChip({
value: item.id,
label: item.label,
};
e.dataTransfer.setData('application/json', JSON.stringify(payload));
e.dataTransfer.effectAllowed = 'copy';
writePaletteHtmlDragData(e, payload as PaletteDragPayload);
};
const onPointerDown = (e: React.PointerEvent) => {
hideHint();
const payload: PaletteDragPayload = {
type: 'sidewaysFilter',
value: item.id,
label: item.label,
};
handlePalettePointerDown(e, payload, item.label);
};
const handleClick = () => onSelect();
@@ -94,9 +109,10 @@ function SidewaysFilterChip({
return (
<div
draggable
draggable={!needsPointerPaletteDrag()}
className={`se-palette-card se-palette-card--range se-palette-card--range-${item.mode}${selected ? ' se-palette-card--selected' : ''}`}
onDragStart={onDragStart}
onPointerDown={onPointerDown}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
onMouseEnter={e => showHint(e.currentTarget)}
@@ -29,6 +29,13 @@ import {
type DefType,
} from '../../utils/strategyEditorShared';
import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage';
import {
isPaletteHtmlDrag,
needsPointerPaletteDrag,
readPaletteHtmlDragData,
subscribePaletteDrag,
type PaletteDragEvent,
} from '../../utils/paletteDragSession';
import { setStochPairSecondary } from '../../utils/stochOverboughtPair';
import {
setPriceExtremePairFilterLevel,
@@ -1190,8 +1197,6 @@ function StrategyEditorCanvasInner({
));
}, [root, orphans, extraRoots, applyResolvedConnection, setEdges]);
const isPaletteDrag = (e: React.DragEvent) => e.dataTransfer.types.includes('application/json');
const resolvePaletteDrop = useCallback((flowPos: { x: number; y: number }) => {
const hit = findNodeAtFlowPosition(flowPos, nodesRef.current);
if (hit && isPaletteConnectTarget(hit.id, root, orphans)) {
@@ -1200,24 +1205,63 @@ function StrategyEditorCanvasInner({
return { mode: 'orphan' as const };
}, [root, orphans]);
const isPaletteDrag = (e: React.DragEvent) => isPaletteHtmlDrag(e);
const applyPaletteDropAt = useCallback((
data: { type: string; value: string; label: string; composite?: boolean },
clientX: number,
clientY: number,
) => {
const flowPos = screenToFlowPosition({ x: clientX, y: clientY });
if (data.type === 'start') {
addStartAt(flowPos);
return;
}
const drop = resolvePaletteDrop(flowPos);
if (drop.mode === 'connect') {
applyDropWithAttach(drop.anchorId, data, flowPos);
} else {
addOrphanAt(data, flowPos);
}
}, [screenToFlowPosition, applyDropWithAttach, addOrphanAt, addStartAt, resolvePaletteDrop]);
const onPaneDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
clearDropPreview();
try {
const data = JSON.parse(e.dataTransfer.getData('application/json'));
const flowPos = screenToFlowPosition({ x: e.clientX, y: e.clientY });
if (data.type === 'start') {
addStartAt(flowPos);
const data = readPaletteHtmlDragData(e);
if (!data) return;
applyPaletteDropAt(data, e.clientX, e.clientY);
}, [clearDropPreview, applyPaletteDropAt]);
useEffect(() => {
if (!needsPointerPaletteDrag()) return;
return subscribePaletteDrag((ev: PaletteDragEvent) => {
if (ev.phase === 'move') {
const flowPos = screenToFlowPosition({ x: ev.x, y: ev.y });
const drop = resolvePaletteDrop(flowPos);
if (drop.mode === 'connect') {
updateDropPreview(drop.anchorId, flowPos);
} else {
clearDropPreview();
}
return;
}
const drop = resolvePaletteDrop(flowPos);
if (drop.mode === 'connect') {
applyDropWithAttach(drop.anchorId, data, flowPos);
} else {
addOrphanAt(data, flowPos);
if (ev.phase === 'end') {
clearDropPreview();
applyPaletteDropAt(ev.payload, ev.x, ev.y);
return;
}
} catch { /* ignore */ }
}, [clearDropPreview, screenToFlowPosition, applyDropWithAttach, addOrphanAt, addStartAt, resolvePaletteDrop]);
if (ev.phase === 'cancel') {
clearDropPreview();
}
});
}, [
screenToFlowPosition,
resolvePaletteDrop,
updateDropPreview,
clearDropPreview,
applyPaletteDropAt,
]);
const onPaneDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();