TCP 서버 모드 추가
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
import { Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState, Position, Handle, ReactFlowProvider } from '@xyflow/react';
|
||||
import React, { useCallback, memo, useMemo, useRef, useState, useEffect } from 'react';
|
||||
import { Play } from 'lucide-react';
|
||||
|
||||
// Optimized node components with proper prop types and memoization
|
||||
const StartNode = memo(({ data, isConnectable }) => {
|
||||
// Memoize handles configuration
|
||||
const handles = useMemo(() => ([
|
||||
{ id: 'start-right', type: 'source', position: Position.Right }
|
||||
]), []);
|
||||
|
||||
return (
|
||||
<div className="bg-green-50 rounded-lg border-2 border-green-500 shadow-lg p-4 w-[100px]">
|
||||
{handles.map(handle => (
|
||||
<Handle
|
||||
key={handle.id}
|
||||
type={handle.type}
|
||||
position={handle.position}
|
||||
isConnectable={isConnectable}
|
||||
className="w-3 h-3 bg-green-500"
|
||||
/>
|
||||
))}
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Play className="h-4 w-4 text-green-600" />
|
||||
<span className="font-semibold text-sm text-green-700">Start</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const ApiNode = memo(({ data, isConnectable }) => {
|
||||
// Memoize handles and colors
|
||||
const handles = useMemo(() => ([
|
||||
{ id: `${data.id}-left`, type: 'target', position: Position.Left },
|
||||
{ id: `${data.id}-right`, type: 'source', position: Position.Right }
|
||||
]), [data.id]);
|
||||
|
||||
const methodColor = useMemo(() => {
|
||||
const colors = {
|
||||
GET: 'bg-green-500',
|
||||
POST: 'bg-blue-500',
|
||||
PUT: 'bg-yellow-500',
|
||||
DELETE: 'bg-red-500',
|
||||
default: 'bg-gray-500'
|
||||
};
|
||||
return colors[data.method] || colors.default;
|
||||
}, [data.method]);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg border shadow-lg p-4 w-[200px]">
|
||||
{handles.map(handle => (
|
||||
<Handle
|
||||
key={handle.id}
|
||||
type={handle.type}
|
||||
position={handle.position}
|
||||
isConnectable={isConnectable}
|
||||
className="w-3 h-3 bg-gray-500"
|
||||
/>
|
||||
))}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${methodColor}`} />
|
||||
<span className="font-bold text-sm">{data.method}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{data.name}</div>
|
||||
<div className="text-sm truncate text-gray-600 mb-2">
|
||||
{data.path}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const ConditionNode = memo(({ data, isConnectable }) => {
|
||||
// Memoize handle styles
|
||||
const handleStyles = useMemo(() => ({
|
||||
true: {
|
||||
top: '30%',
|
||||
backgroundColor: '#22c55e',
|
||||
border: '2px solid #16a34a'
|
||||
},
|
||||
false: {
|
||||
top: '70%',
|
||||
backgroundColor: '#ef4444',
|
||||
border: '2px solid #dc2626'
|
||||
}
|
||||
}), []);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg border shadow-lg p-4 w-[200px]">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="in"
|
||||
isConnectable={isConnectable}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="true"
|
||||
style={handleStyles.true}
|
||||
isConnectable={isConnectable}
|
||||
className="w-3 h-3 !opacity-100"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="false"
|
||||
style={handleStyles.false}
|
||||
isConnectable={isConnectable}
|
||||
className="w-3 h-3 !opacity-100"
|
||||
/>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-purple-500" />
|
||||
<span className="font-bold text-sm">Condition</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{data.condition || 'Add condition'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const DelayNode = memo(({ data, isConnectable }) => {
|
||||
const handles = useMemo(() => ([
|
||||
{ id: `${data.id}-left`, type: 'target', position: Position.Left },
|
||||
{ id: `${data.id}-right`, type: 'source', position: Position.Right }
|
||||
]), [data.id]);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg border shadow-lg p-4 w-[200px]">
|
||||
{handles.map(handle => (
|
||||
<Handle
|
||||
key={handle.id}
|
||||
type={handle.type}
|
||||
position={handle.position}
|
||||
isConnectable={isConnectable}
|
||||
className="w-3 h-3 bg-gray-500"
|
||||
/>
|
||||
))}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-orange-500" />
|
||||
<span className="font-bold text-sm">Delay</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
Delay: {data.duration || '1000'}ms
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// Add display names for debugging
|
||||
StartNode.displayName = 'StartNode';
|
||||
ApiNode.displayName = 'ApiNode';
|
||||
ConditionNode.displayName = 'ConditionNode';
|
||||
DelayNode.displayName = 'DelayNode';
|
||||
|
||||
// Memoize initial nodes and edges
|
||||
const initialNodes = [
|
||||
{
|
||||
id: 'start',
|
||||
type: 'startNode',
|
||||
position: { x: 100, y: 50 },
|
||||
data: { id: 'start' }
|
||||
}
|
||||
];
|
||||
|
||||
const initialEdges = [];
|
||||
|
||||
// Memoize node types
|
||||
const nodeTypes = {
|
||||
startNode: StartNode,
|
||||
apiNode: ApiNode,
|
||||
conditionNode: ConditionNode,
|
||||
delayNode: DelayNode
|
||||
};
|
||||
|
||||
const DiagramArea = () => {
|
||||
const reactFlowWrapper = useRef(null);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const [selectedNodeId, setSelectedNodeId] = useState(null);
|
||||
|
||||
// Click handlers for node selection
|
||||
const onNodeClick = useCallback((event, node) => {
|
||||
event.stopPropagation();
|
||||
setSelectedNodeId(node.id);
|
||||
}, []);
|
||||
|
||||
const onPaneClick = useCallback(() => {
|
||||
setSelectedNodeId(null);
|
||||
}, []);
|
||||
|
||||
// Apply styles to nodes
|
||||
useEffect(() => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => ({
|
||||
...node,
|
||||
className: node.id === selectedNodeId ? 'border-2 border-primary shadow-md bg-primary/5' : null
|
||||
}))
|
||||
);
|
||||
}, [selectedNodeId, setNodes]);
|
||||
|
||||
// Memoize callback functions
|
||||
const onConnect = useCallback((params) => {
|
||||
setEdges(prev => [...prev, { ...params, id: `e${prev.length + 1}` }]);
|
||||
}, []);
|
||||
|
||||
const onInit = useCallback((instance) => {
|
||||
reactFlowWrapper.current = instance;
|
||||
}, []);
|
||||
|
||||
const onDragOver = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!reactFlowWrapper.current) return;
|
||||
|
||||
try {
|
||||
const apiData = JSON.parse(event.dataTransfer.getData('application/json'));
|
||||
const position = reactFlowWrapper.current.screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
|
||||
const newNode = {
|
||||
id: `api-${Date.now()}`,
|
||||
type: 'apiNode',
|
||||
position,
|
||||
data: {
|
||||
id: apiData.id,
|
||||
name: apiData.name,
|
||||
method: apiData.method,
|
||||
path: apiData.path,
|
||||
collectionId: apiData.collectionId
|
||||
}
|
||||
};
|
||||
|
||||
setNodes(prev => prev.concat(newNode));
|
||||
} catch (error) {
|
||||
console.error('Error adding new node:', error);
|
||||
}
|
||||
}, [setNodes]);
|
||||
|
||||
// Default edge options memoized
|
||||
const defaultEdgeOptions = useMemo(() => ({ type: 'smoothstep' }), []);
|
||||
|
||||
return (
|
||||
<div className="h-[50vh] border-b">
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onInit={onInit}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
onNodeClick={onNodeClick}
|
||||
onPaneClick={onPaneClick}
|
||||
nodeTypes={nodeTypes}
|
||||
defaultZoom={1.0} // 초기 배율 설정 (1이 기본값)
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
<MiniMap />
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DiagramArea);
|
||||
Reference in New Issue
Block a user