import React, { memo, useCallback, useState } from 'react'; import { Handle, Position, useConnection, useReactFlow, type NodeProps } from '@xyflow/react'; import { START_NODE_ID, type HandleSide, type StrategyFlowNodeData } from '../../utils/strategyFlowLayout'; import { hasNodeSettings } from '../../utils/conditionPeriods'; import ConditionNodeSettings from './ConditionNodeSettings'; const LOGIC_COLORS: Record = { AND: '#00aaff', OR: '#00d4ff', NOT: '#ffcc00', }; const SIDES: { side: HandleSide; position: Position }[] = [ { side: 'top', position: Position.Top }, { side: 'right', position: Position.Right }, { side: 'bottom', position: Position.Bottom }, { side: 'left', position: Position.Left }, ]; type ConnectHover = { handleId: string | null | undefined; handleType: 'source' | 'target'; nodeId: string; isValid: boolean; }; function MultiSideHandles({ nodeId, sourceOnly = false, targetOnly = false, activeSourceSide = null, canSourceConnect = true, canTargetConnect = true, }: { nodeId: string; sourceOnly?: boolean; targetOnly?: boolean; activeSourceSide?: HandleSide | null; canSourceConnect?: boolean; canTargetConnect?: boolean; }) { const connectHover = useConnection((s): ConnectHover | null => { if (!s.inProgress || s.isValid === null || !s.toHandle) return null; return { nodeId: s.toHandle.nodeId, handleId: s.toHandle.id, handleType: s.toHandle.type, isValid: s.isValid, }; }); const hoverClass = (handleId: string, handleType: 'source' | 'target') => { if (!connectHover) return ''; if (connectHover.nodeId !== nodeId || connectHover.handleType !== handleType) return ''; if (connectHover.handleId !== handleId) return ''; return connectHover.isValid ? ' se-flow-handle--valid-hover' : ' se-flow-handle--invalid'; }; return ( <> {SIDES.map(({ side, position }) => ( {!targetOnly && ( )} {!sourceOnly && ( )} ))} ); } function usePaletteDropHandlers(id: string, d: StrategyFlowNodeData) { const { screenToFlowPosition } = useReactFlow(); const onDragOver = useCallback((e: React.DragEvent) => { if (!e.dataTransfer.types.includes('application/json')) return; e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'copy'; const flowPos = screenToFlowPosition({ x: e.clientX, y: e.clientY }); d.onDragOverTarget?.(id, flowPos); }, [d, id, screenToFlowPosition]); const onDragLeave = useCallback((e: React.DragEvent) => { e.stopPropagation(); const rel = e.relatedTarget as Node | null; if (rel && e.currentTarget.contains(rel)) return; d.onDragLeaveTarget?.(id); }, [d, id]); 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 */ } }, [d, id, screenToFlowPosition]); return { onDragOver, onDragLeave, onDrop }; } export const StartNode = memo(function StartNode({ data }: NodeProps) { const d = data as StrategyFlowNodeData; const isSell = d.signalTab === 'sell'; const { onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(START_NODE_ID, d); return (
START
); }); export const LogicGateNode = memo(function LogicGateNode({ id, data, selected }: NodeProps) { const d = data as StrategyFlowNodeData; const node = d.logicNode!; const color = LOGIC_COLORS[node.type] ?? '#00aaff'; const isSell = d.signalTab === 'sell'; const { onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d); return (
[{node.type}]
{(node.children ?? []).length === 0 && ( 조건을 드롭하세요 )}
); }); export const ConditionFlowNode = memo(function ConditionFlowNode({ id, data, selected }: NodeProps) { const d = data as StrategyFlowNodeData; const node = d.logicNode!; const cond = node.condition; const ind = cond?.indicatorType ?? 'COND'; const condType = cond?.conditionType ?? ''; const isCross = ['CROSS_UP', 'CROSS_DOWN'].includes(condType); const tone = isCross ? 'cross' : 'value'; const isSell = d.signalTab === 'sell'; const { onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d); const [settingsOpen, setSettingsOpen] = useState(false); const showSettings = cond && hasNodeSettings(cond); const handleSettingsChange = useCallback((next: NonNullable) => { d.onUpdateCondition?.(id, next); }, [d, id]); return (
{ind}
{showSettings && ( )}
{d.label ?? ind}
{settingsOpen && cond && d.def && ( setSettingsOpen(false)} /> )}
); });