diff --git a/src/app/(canvas)/assets/Node.module.scss b/src/app/(canvas)/assets/Node.module.scss
index 0b3e4132..9251a795 100644
--- a/src/app/(canvas)/assets/Node.module.scss
+++ b/src/app/(canvas)/assets/Node.module.scss
@@ -176,6 +176,12 @@
.paramKey {
color: #6b7280;
margin-right: 8px;
+
+ &.required::after {
+ content: " *";
+ color: #ef4444;
+ font-weight: bold;
+ }
}
.paramValue {
diff --git a/src/app/(canvas)/components/Canvas.jsx b/src/app/(canvas)/components/Canvas.jsx
index 236ee20a..22cd076b 100644
--- a/src/app/(canvas)/components/Canvas.jsx
+++ b/src/app/(canvas)/components/Canvas.jsx
@@ -56,6 +56,7 @@ const Canvas = forwardRef(({ onStateChange }, ref) => {
const [portPositions, setPortPositions] = useState({});
const [snappedPortKey, setSnappedPortKey] = useState(null);
const [isSnapTargetValid, setIsSnapTargetValid] = useState(true);
+ const [copiedNode, setCopiedNode] = useState(null); // 복사된 노드 저장
const nodesRef = useRef(nodes);
const edgePreviewRef = useRef(edgePreview);
@@ -81,13 +82,17 @@ const Canvas = forwardRef(({ onStateChange }, ref) => {
setPortPositions(newPortPositions);
}, [nodes, view.scale]);
- // 상태 변경 감지 및 콜백 호출
+
+
useEffect(() => {
- if (onStateChange && (nodes.length > 0 || edges.length > 0)) {
+ if (onStateChange) {
const currentState = { view, nodes, edges };
onStateChange(currentState);
}
- }, [nodes, edges, onStateChange]);
+ }, [nodes, edges, view, onStateChange]);
+
+ // 키보드 이벤트 리스너 등록
+
const registerPortRef = useCallback((nodeId, portId, portType, el) => {
const key = `${nodeId}__PORTKEYDELIM__${portId}__PORTKEYDELIM__${portType}`;
@@ -147,6 +152,36 @@ const Canvas = forwardRef(({ onStateChange }, ref) => {
return Math.sqrt(Math.pow(pos1.x - pos2.x, 2) + Math.pow(pos1.y - pos2.y, 2));
};
+ const copySelectedNode = () => {
+ if (selectedNodeId) {
+ const nodeToCopy = nodes.find(node => node.id === selectedNodeId);
+ if (nodeToCopy) {
+ setCopiedNode(nodeToCopy);
+ console.log('Node copied:', nodeToCopy.data.nodeName);
+ }
+ }
+ };
+
+ const pasteNode = () => {
+ if (copiedNode) {
+ const newNode = {
+ ...copiedNode,
+ id: `${copiedNode.data.id}-${Date.now()}`, // 새로운 고유 ID 생성
+ position: {
+ x: copiedNode.position.x + 50, // 약간 오프셋을 주어 겹치지 않게
+ y: copiedNode.position.y + 50
+ }
+ };
+
+ setNodes(prev => [...prev, newNode]);
+ setSelectedNodeId(newNode.id); // 새로 생성된 노드를 선택
+ console.log('Node pasted:', newNode.data.nodeName);
+ }
+ };
+
+ // 키보드 이벤트 핸들러
+
+
const handleParameterChange = useCallback((nodeId, paramId, value) => {
setNodes(prevNodes =>
prevNodes.map(node => {
@@ -239,6 +274,22 @@ const Canvas = forwardRef(({ onStateChange }, ref) => {
}
};
+ const handleKeyDown = useCallback((e) => {
+ if (e.ctrlKey && e.key === 'c') {
+ e.preventDefault();
+ copySelectedNode();
+ }
+ else if (e.ctrlKey && e.key === 'v') {
+ e.preventDefault();
+ pasteNode();
+ }
+ else if (e.key === 'Delete' && selectedNodeId) {
+ e.preventDefault();
+ setNodes(prev => prev.filter(node => node.id !== selectedNodeId));
+ setSelectedNodeId(null);
+ }
+ }, [selectedNodeId, copiedNode, nodes]);
+
const handleNodeMouseDown = useCallback((e, nodeId) => {
if (e.button !== 0) return;
setSelectedNodeId(nodeId);
@@ -413,6 +464,19 @@ const Canvas = forwardRef(({ onStateChange }, ref) => {
return () => container.removeEventListener('wheel', handleWheel);
}, []);
+ useEffect(() => {
+ const container = containerRef.current;
+ if (container) {
+ container.addEventListener('keydown', handleKeyDown);
+ // 포커스를 받을 수 있도록 tabindex 설정
+ container.setAttribute('tabindex', '0');
+
+ return () => {
+ container.removeEventListener('keydown', handleKeyDown);
+ };
+ }
+ }, [handleKeyDown]);
+
useEffect(() => {
const container = containerRef.current;
const content = contentRef.current;
@@ -450,6 +514,9 @@ const Canvas = forwardRef(({ onStateChange }, ref) => {
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
+ onClick={() => containerRef.current?.focus()} // 클릭 시 포커스
+ tabIndex={0} // 키보드 포커스 가능하도록
+ style={{ outline: 'none' }} // 포커스 아웃라인 제거
>
{
+const Header = ({ onMenuClick, onSave, onLoad, onExport, workflowName: externalWorkflowName, onWorkflowNameChange }) => {
const [workflowName, setWorkflowName] = useState('Workflow');
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState('');
const inputRef = useRef(null);
useEffect(() => {
- const savedName = getWorkflowName();
- setWorkflowName(savedName);
- }, []);
+ if (externalWorkflowName) {
+ setWorkflowName(externalWorkflowName);
+ } else {
+ const savedName = getWorkflowName();
+ setWorkflowName(savedName);
+ }
+ }, [externalWorkflowName]);
useEffect(() => {
if (isEditing && inputRef.current) {
@@ -31,6 +35,12 @@ const Header = ({ onMenuClick, onSave, onLoad, onExport }) => {
const finalValue = trimmedValue || 'Workflow';
setWorkflowName(finalValue);
saveWorkflowName(finalValue);
+
+ // 부모 컴포넌트에 변경사항 알림
+ if (onWorkflowNameChange) {
+ onWorkflowNameChange(finalValue);
+ }
+
setIsEditing(false);
};
diff --git a/src/app/(canvas)/components/Node.jsx b/src/app/(canvas)/components/Node.jsx
index 3c6c3fdd..2ad20d1c 100644
--- a/src/app/(canvas)/components/Node.jsx
+++ b/src/app/(canvas)/components/Node.jsx
@@ -92,7 +92,9 @@ const Node = ({ id, data, position, onNodeMouseDown, isSelected, onPortMouseDown
PARAMETER
{parameters.map(param => (
-
{param.name}
+
+ {param.name}
+
{param.options && param.options.length > 0 ? (