import React, { memo, useCallback, useState } from 'react'; import { Handle, Position, useConnection, useReactFlow, type NodeProps } from '@xyflow/react'; import { isStartNodeId, type HandleSide, type StrategyFlowNodeData, } from '../../utils/strategyFlowLayout'; import { getStartCandleTypes } from '../../utils/strategyStartNodes'; import StartTimeframeCheckboxes from './StartTimeframeCheckboxes'; import LogicGateOpToggle from './LogicGateOpToggle'; import { hasNodeSettings } from '../../utils/conditionPeriods'; import { getStrategyIndicatorDisplayName } from '../../utils/strategyPaletteStorage'; import { isPriceExtremeBreakoutPairRoot, priceExtremePairDisplayName, inferPriceExtremeFilterLevel, } from '../../utils/priceExtremeBreakoutPair'; import { isInflection33PairRoot, inflection33DisplayName, inferInflection33FilterLevel, } from '../../utils/inflection33Strategy'; import Inflection33PairNodeSettings from './Inflection33PairNodeSettings'; import { isStableStrategyPairRoot, stableStrategyDisplayName, inferStableStrategyFilterLevel, type StableStrategyId, } from '../../utils/stableStrategyPairs'; import StableStrategyPairNodeSettings from './StableStrategyPairNodeSettings'; import { isStochOverboughtPairRoot, stochPairDisplayName, } from '../../utils/stochOverboughtPair'; import ConditionNodeSettings from './ConditionNodeSettings'; import StochPairNodeSettings from './StochPairNodeSettings'; import PriceExtremePairNodeSettings from './PriceExtremePairNodeSettings'; import { isPaletteHtmlDrag, readPaletteHtmlDragData, } from '../../utils/paletteDragSession'; 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 onDragEnter = useCallback((e: React.DragEvent) => { if (!isPaletteHtmlDrag(e)) return; e.preventDefault(); e.stopPropagation(); }, []); const onDragOver = useCallback((e: React.DragEvent) => { if (!isPaletteHtmlDrag(e)) 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(); 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 { onDragEnter, onDragOver, onDragLeave, onDrop }; } export const StartNode = memo(function StartNode({ id, data }: NodeProps) { const d = data as StrategyFlowNodeData; const isSell = d.signalTab === 'sell'; const { onDragEnter, onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d); const candleTypes = d.candleTypes?.length ? d.candleTypes : getStartCandleTypes({ candleType: d.candleType ?? '1m' }); const canDelete = id !== '__strategy_start__' && !!d.onDeleteStart; const stop = (e: React.SyntheticEvent) => e.stopPropagation(); return (
START
{canDelete && ( )}
d.onStartCandleTypesChange?.(id, types)} />
); }); 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 isStochPair = isStochOverboughtPairRoot(node); const isPriceExtremePair = isPriceExtremeBreakoutPairRoot(node); const isInflection33Pair = isInflection33PairRoot(node); const isStableStrategyPair = isStableStrategyPairRoot(node); const { onDragEnter, onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d); const [settingsOpen, setSettingsOpen] = useState(false); return (
{node.type === 'AND' || node.type === 'OR' ? ( d.onChangeLogicGateType?.(id, op)} compact /> ) : ( [{node.type}] )}
{isStableStrategyPair && node.stableStrategyPair && (
{stableStrategyDisplayName( node.stableStrategyPair.strategyId as StableStrategyId, node.stableStrategyPair.mode, )}
)} {isInflection33Pair && node.inflection33Pair && (
{inflection33DisplayName(node.inflection33Pair.mode, node.inflection33Pair.period)}
)} {isPriceExtremePair && node.priceExtremePair && (
{priceExtremePairDisplayName( node.priceExtremePair.mode, node.priceExtremePair.shortPeriod, node.priceExtremePair.longPeriod, )}
)} {isStochPair && node.stochPair && (
{stochPairDisplayName(node.stochPair.secondaryIndicatorType)}
)} {(node.children ?? []).length === 0 && ( 조건을 드롭하세요 )} {settingsOpen && isStochPair && node.stochPair && ( d.onUpdateStochPairSecondary?.(id, sec)} onClose={() => setSettingsOpen(false)} /> )} {settingsOpen && isStableStrategyPair && node.stableStrategyPair && ( d.onUpdateStableStrategyFilterLevel?.(id, level)} onClose={() => setSettingsOpen(false)} /> )} {settingsOpen && isInflection33Pair && node.inflection33Pair && ( d.onUpdateInflection33FilterLevel?.(id, level)} onClose={() => setSettingsOpen(false)} /> )} {settingsOpen && isPriceExtremePair && node.priceExtremePair && ( d.onUpdatePriceExtremeFilterLevel?.(id, level)} onClose={() => setSettingsOpen(false)} /> )}
); }); 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 indLabel = getStrategyIndicatorDisplayName(ind); const condType = cond?.conditionType ?? ''; const isCross = ['CROSS_UP', 'CROSS_DOWN'].includes(condType); const tone = isCross ? 'cross' : 'value'; const isSell = d.signalTab === 'sell'; const { onDragEnter, 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 (
{indLabel}
{showSettings && ( )}
{d.label ?? indLabel}
{settingsOpen && cond && d.def && ( setSettingsOpen(false)} /> )}
); });