From edbd0a842e58f8c76a2047345d41d11953c2a082 Mon Sep 17 00:00:00 2001 From: KANG-YEONWOOK Date: Thu, 21 Aug 2025 11:22:38 +0900 Subject: [PATCH 1/8] fix: style --- .../buttons/ide/ExportCodeButton.tsx | 2 +- .../components/buttons/ide/RunCodeButton.tsx | 2 +- .../src/components/buttons/modal/x.tsx | 36 +++++++++ .../frontend/src/components/modal/Ide.tsx | 79 +++++-------------- .../frontend/src/components/modal/Modal.tsx | 30 +------ .../modal/{NodeMaker.tsx => ProjectMaker.tsx} | 77 +++++------------- packages/frontend/src/pages/Home.tsx | 13 ++- .../frontend/src/pages/Project/Project.tsx | 63 +++------------ packages/frontend/src/utils/api.ts | 2 +- 9 files changed, 96 insertions(+), 208 deletions(-) create mode 100644 packages/frontend/src/components/buttons/modal/x.tsx rename packages/frontend/src/components/modal/{NodeMaker.tsx => ProjectMaker.tsx} (61%) diff --git a/packages/frontend/src/components/buttons/ide/ExportCodeButton.tsx b/packages/frontend/src/components/buttons/ide/ExportCodeButton.tsx index 6a961b1..e99ada7 100644 --- a/packages/frontend/src/components/buttons/ide/ExportCodeButton.tsx +++ b/packages/frontend/src/components/buttons/ide/ExportCodeButton.tsx @@ -23,7 +23,7 @@ export default function ExportCodeButton({ return ( @@ -173,35 +160,7 @@ const IdeModal: React.FC = ({ {nodeTitle} - {/* Right side - Close button */} - + {/* Editor Container */} diff --git a/packages/frontend/src/components/modal/Modal.tsx b/packages/frontend/src/components/modal/Modal.tsx index 4616406..ca0675b 100644 --- a/packages/frontend/src/components/modal/Modal.tsx +++ b/packages/frontend/src/components/modal/Modal.tsx @@ -1,4 +1,5 @@ import React, { useEffect } from "react"; +import X from "../buttons/modal/x"; interface ModalProps { isOpen: boolean; @@ -47,34 +48,7 @@ const Modal: React.FC = ({ isOpen, onClose, children }) => { className="relative bg-[#0a0a0a] rounded-xl max-w-[75vw] max-h-[75vh] overflow-auto shadow-[0_20px_60px_rgba(0,0,0,0.8)] animate-slideUp min-w-[400px] min-h-[200px] sm:min-w-[90vw] sm:mx-4" onClick={(e) => e.stopPropagation()} > - +
{children}
diff --git a/packages/frontend/src/components/modal/NodeMaker.tsx b/packages/frontend/src/components/modal/ProjectMaker.tsx similarity index 61% rename from packages/frontend/src/components/modal/NodeMaker.tsx rename to packages/frontend/src/components/modal/ProjectMaker.tsx index 0359574..8e09477 100644 --- a/packages/frontend/src/components/modal/NodeMaker.tsx +++ b/packages/frontend/src/components/modal/ProjectMaker.tsx @@ -1,12 +1,14 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; +import { projectApi } from "../../utils/api"; +import X from "../buttons/modal/x"; -interface NodeMakerProps { +interface ProjectMakerProps { isOpen: boolean; - onClose?: () => void; + onClose: () => void; } -export default function NodeMaker({ isOpen, onClose }: NodeMakerProps) { +export default function ProjectMaker({ isOpen, onClose }: ProjectMakerProps) { const navigate = useNavigate(); const [projectName, setProjectName] = useState(""); const [projectDescription, setProjectDescription] = useState(""); @@ -28,25 +30,12 @@ export default function NodeMaker({ isOpen, onClose }: NodeMakerProps) { const projectId = crypto.randomUUID(); try { - const response = await fetch("/api/project/make", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - project_name: projectName.trim(), - project_description: projectDescription.trim() || "", - project_id: projectId, - }), + const data = await projectApi.createProject({ + project_name: projectName.trim(), + project_description: projectDescription.trim() || "", + project_id: projectId, }); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || "Failed to create project"); - } - - const data = await response.json(); - if (data.success) { // Navigate to the newly created project navigate(`/project/${projectId}`); @@ -83,42 +72,18 @@ export default function NodeMaker({ isOpen, onClose }: NodeMakerProps) { return (
e.stopPropagation()} > - -
+
+ +
+ +
)} -
+
diff --git a/packages/frontend/src/pages/Home.tsx b/packages/frontend/src/pages/Home.tsx index c6cb2a1..7746465 100644 --- a/packages/frontend/src/pages/Home.tsx +++ b/packages/frontend/src/pages/Home.tsx @@ -1,8 +1,9 @@ import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; -import NodeMaker from "../components/modal/NodeMaker"; +import ProjectMaker from "../components/modal/ProjectMaker"; import Loading from "../components/loading/Loading"; -import type { ProjectInfo, Projects } from "../types"; +import type { ProjectInfo } from "../types"; +import { projectApi } from "../utils/api"; export default function Home() { const navigate = useNavigate(); @@ -12,11 +13,7 @@ export default function Home() { const getProjects = async () => { try { - const response = await fetch("/api/project/"); - if (!response.ok) { - throw new Error(`Failed to fetch projects: ${response.statusText}`); - } - const data: Projects = await response.json(); + const data = await projectApi.getAllProjects(); if (data.success && data.projects) { setProjects(data.projects); } @@ -83,7 +80,7 @@ export default function Home() {
- { setMakingProject(false); diff --git a/packages/frontend/src/pages/Project/Project.tsx b/packages/frontend/src/pages/Project/Project.tsx index d31f26b..409d45e 100644 --- a/packages/frontend/src/pages/Project/Project.tsx +++ b/packages/frontend/src/pages/Project/Project.tsx @@ -24,43 +24,9 @@ import IdeModal from "../../components/modal/Ide"; import { removeStyle } from "./removeStyle"; import Loading from "../../components/loading/Loading"; import WrongPath from "../WrongPath/WrongPath"; +import { projectApi } from "../../utils/api"; +import type { ProjectNode, ProjectEdge, ProjectStructure } from "../../types"; -// Backend API response types -interface BackendNodeData { - title: string; - description?: string; - file?: string; -} - -interface BackendNode { - id: string; - type?: string; - position: { x: number; y: number }; - data: BackendNodeData; -} - -interface BackendEdge { - id: string; - type?: string; - source: string; - target: string; - sourceHandle?: string | null; - targetHandle?: string | null; - markerEnd?: { type: MarkerType | string }; -} - -interface BackendProject { - project_name: string; - project_description?: string; - project_id: string; - nodes: BackendNode[]; - edges: BackendEdge[]; -} - -interface ProjectApiResponse { - success: boolean; - project: BackendProject; -} export default function Project() { const { projectId } = useParams<{ projectId: string }>(); @@ -111,28 +77,17 @@ export default function Project() { return; } - const response = await fetch(`/api/project/${projectId}`); - - if (!response.ok) { - if (response.status === 404) { - setIsInvalidProject(true); - setIsLoading(false); - return; - } - throw new Error(`Failed to fetch project: ${response.statusText}`); - } - - const data: ProjectApiResponse = await response.json(); + const data = await projectApi.getProject(projectId); if (data.success && data.project) { - const project: BackendProject = data.project; + const project: ProjectStructure = data.project; // Set project title setProjectTitle(project.project_name || ""); // Transform backend nodes to ReactFlow format const transformedNodes: DefaultNodeType[] = project.nodes.map( - (node: BackendNode) => ({ + (node: ProjectNode) => ({ id: node.id, type: node.type || "default", position: node.position, @@ -152,7 +107,7 @@ export default function Project() { // Transform backend edges to ReactFlow format const transformedEdges: Edge[] = project.edges.map( - (edge: BackendEdge) => ({ + (edge: ProjectEdge) => ({ id: edge.id, type: edge.type || "custom", source: edge.source, @@ -176,16 +131,18 @@ export default function Project() { // Update node counter based on existing nodes if (project.nodes.length > 0) { const maxId = Math.max( - ...project.nodes.map((n: BackendNode) => parseInt(n.id, 10) || 0) + ...project.nodes.map((n: ProjectNode) => parseInt(n.id, 10) || 0) ); setNodeIdCounter(maxId + 1); } } } catch (err) { console.error("Error fetching project:", err); - // Check if it's a network or malformed URI error + // Check if it's a network or malformed URI error or 404 if (err instanceof TypeError && err.message.includes("URI")) { setIsInvalidProject(true); + } else if (err instanceof Error && err.message.includes("404")) { + setIsInvalidProject(true); } else { setError( err instanceof Error ? err.message : "Failed to load project" diff --git a/packages/frontend/src/utils/api.ts b/packages/frontend/src/utils/api.ts index 99a95d8..b527f99 100644 --- a/packages/frontend/src/utils/api.ts +++ b/packages/frontend/src/utils/api.ts @@ -24,7 +24,7 @@ import type { ErrorResponse, } from "../types"; -const API_BASE_URL = process.env.VITE_API_URL; +const API_BASE_URL = "/api"; // Helper function for API calls async function apiCall(endpoint: string, options?: RequestInit): Promise { From b8064a77435f49134100ffad3d740dd77f2acc95 Mon Sep 17 00:00:00 2001 From: KANG-YEONWOOK Date: Thu, 21 Aug 2025 12:04:02 +0900 Subject: [PATCH 2/8] feat: node edge CR api Connection --- packages/backend/app/core/node_operations.py | 4 +- .../src/components/layouts/ProjectPanel.tsx | 0 .../src/components/nodes/DefaultNode.tsx | 1 + .../frontend/src/pages/Project/Project.tsx | 237 +++++++++++++----- packages/frontend/src/types/interface.ts | 2 - 5 files changed, 180 insertions(+), 64 deletions(-) create mode 100644 packages/frontend/src/components/layouts/ProjectPanel.tsx diff --git a/packages/backend/app/core/node_operations.py b/packages/backend/app/core/node_operations.py index ff54b0a..799cf87 100644 --- a/packages/backend/app/core/node_operations.py +++ b/packages/backend/app/core/node_operations.py @@ -13,11 +13,11 @@ def create_node(project_id: str, node_id: str, node_type: str, position: Dict[st node_title = data.get('title', f'node_{node_id}') # Create python file for the node - py_filename = f"{node_id}_{node_title}.py".replace(" ", "_").replace("/", "_") + py_filename = f"{node_id}_{node_title}.py".replace(" ", "_").replace("/", "__") py_filepath = project_path / py_filename # Create empty python file with basic template - initial_code = data.get('code', f"# Node: {node_title}\n# ID: {node_id}\n\n# Write your Python code here\nprint('Hello, World!')") + initial_code = f"# Node: {node_title}\n# ID: {node_id}\n\n# Write your Python code here\nprint('Hello, World!')" with open(py_filepath, 'w') as f: f.write(initial_code) diff --git a/packages/frontend/src/components/layouts/ProjectPanel.tsx b/packages/frontend/src/components/layouts/ProjectPanel.tsx new file mode 100644 index 0000000..e69de29 diff --git a/packages/frontend/src/components/nodes/DefaultNode.tsx b/packages/frontend/src/components/nodes/DefaultNode.tsx index 0cbc6c3..0ab840e 100644 --- a/packages/frontend/src/components/nodes/DefaultNode.tsx +++ b/packages/frontend/src/components/nodes/DefaultNode.tsx @@ -5,6 +5,7 @@ import clsx from "clsx"; export type DefaultNodeType = Node<{ title: string; description: string; + file?: string; // File path reference for the node's Python code viewCode: () => void; }>; diff --git a/packages/frontend/src/pages/Project/Project.tsx b/packages/frontend/src/pages/Project/Project.tsx index 409d45e..e82c78c 100644 --- a/packages/frontend/src/pages/Project/Project.tsx +++ b/packages/frontend/src/pages/Project/Project.tsx @@ -3,7 +3,6 @@ import { useParams } from "react-router-dom"; import { ReactFlow, MiniMap, - Controls, Background, BackgroundVariant, useNodesState, @@ -27,7 +26,6 @@ import WrongPath from "../WrongPath/WrongPath"; import { projectApi } from "../../utils/api"; import type { ProjectNode, ProjectEdge, ProjectStructure } from "../../types"; - export default function Project() { const { projectId } = useParams<{ projectId: string }>(); @@ -94,6 +92,7 @@ export default function Project() { data: { title: node.data.title || `Node ${node.id}`, description: node.data.description || "", + file: node.data.file, // Include file reference from backend viewCode: () => { setSelectedNodeData({ nodeId: node.id, @@ -195,7 +194,9 @@ export default function Project() { // 연결 생성 핸들러 const onConnect = useCallback( - (connection: Connection) => { + async (connection: Connection) => { + if (!projectId) return; + // 중복 연결 검사 const isDuplicate = edges.some( (edge) => @@ -210,77 +211,197 @@ export default function Project() { return; } - // addEdge 함수 사용 - type: "custom" 추가 - setEdges((eds) => - addEdge( - { - ...connection, - id: `e${connection.source}-${connection.target}-${Date.now()}`, - type: "custom", - style: { stroke: "#64748b", strokeWidth: 2 }, - markerEnd: { type: MarkerType.ArrowClosed }, - }, - eds - ) - ); + const edgeId = `e${connection.source}-${connection.target}-${Date.now()}`; + + try { + // Call API to create edge on backend + const response = await projectApi.createEdge({ + project_id: projectId, + edge_id: edgeId, + edge_type: "custom", + source: connection.source!, + target: connection.target!, + marker_end: { type: MarkerType.ArrowClosed }, + }); + + if (response.success) { + // Add edge to frontend after successful backend creation + setEdges((eds) => + addEdge( + { + ...connection, + id: edgeId, + type: "custom", + style: { stroke: "#64748b", strokeWidth: 2 }, + markerEnd: { type: MarkerType.ArrowClosed }, + }, + eds + ) + ); + } + } catch (error) { + console.error("Failed to create edge:", error); + alert( + `Failed to create edge: ${ + error instanceof Error ? error.message : "Unknown error" + }` + ); + } }, - [edges, setEdges] + [edges, setEdges, projectId] ); // 새 노드 추가 - const addNewNode = useCallback(() => { + const addNewNode = useCallback(async () => { + if (!projectId) return; + const nodeId = nodeIdCounter.toString(); - const newNode: DefaultNodeType = { - id: nodeId, - type: "default", - position: { - x: Math.random() * 500 + 100, - y: Math.random() * 300 + 100, - }, - data: { - title: `Node ${nodeIdCounter}`, - description: "New node description", - viewCode: () => handleNodeClick(nodeId, `Node ${nodeIdCounter}`), - }, + const nodeTitle = `Node ${nodeIdCounter}`; + const position = { + x: Math.random() * 500 + 100, + y: Math.random() * 300 + 100, }; - setNodes((nds) => [...nds, newNode]); - setNodeIdCounter((id) => id + 1); - }, [nodeIdCounter, setNodes]); + + try { + // Call API to create node on backend + const response = await projectApi.createNode({ + project_id: projectId, + node_id: nodeId, + node_type: "default", + position: position, + data: { + title: nodeTitle, + description: "New node description", + }, + }); + + if (response.success) { + // Add node to frontend after successful backend creation + const newNode: DefaultNodeType = { + id: nodeId, + type: "default", + position: position, + data: { + title: nodeTitle, + description: "New node description", + file: response.node.data.file, // Include file reference from backend + viewCode: () => handleNodeClick(nodeId, nodeTitle), + }, + }; + setNodes((nds) => [...nds, newNode]); + setNodeIdCounter((id) => id + 1); + } + } catch (error) { + console.error("Failed to create node:", error); + alert( + `Failed to create node: ${ + error instanceof Error ? error.message : "Unknown error" + }` + ); + } + }, [nodeIdCounter, setNodes, projectId]); // 노드 삭제 핸들러 useEffect(() => { - const handleDeleteNode = (event: CustomEvent) => { - const nodeId = event.detail.id; + const deleteNodeAsync = async (nodeId: string) => { + if (!projectId) return; + + // Store current state for rollback + const previousNodes = nodes; + const previousEdges = edges; + + // Optimistic update - immediately remove from UI setNodes((nds) => nds.filter((node) => node.id !== nodeId)); setEdges((eds) => - eds.filter((edge) => edge.source !== nodeId && edge.target !== nodeId) + eds.filter( + (edge) => edge.source !== nodeId && edge.target !== nodeId + ) ); + + try { + // Call API to delete node on backend + const response = await projectApi.deleteNode({ + project_id: projectId, + node_id: nodeId, + }); + + if (!response.success) { + // Rollback on failure + setNodes(previousNodes); + setEdges(previousEdges); + alert("Failed to delete node"); + } + } catch (error) { + // Rollback on error + setNodes(previousNodes); + setEdges(previousEdges); + console.error("Failed to delete node:", error); + alert( + `Failed to delete node: ${ + error instanceof Error ? error.message : "Unknown error" + }` + ); + } + }; + + const handleDeleteNode = (event: Event) => { + const customEvent = event as CustomEvent<{ id: string }>; + const nodeId = customEvent.detail.id; + deleteNodeAsync(nodeId); }; - document.addEventListener("deleteNode", handleDeleteNode as EventListener); + document.addEventListener("deleteNode", handleDeleteNode); return () => { - document.removeEventListener( - "deleteNode", - handleDeleteNode as EventListener - ); + document.removeEventListener("deleteNode", handleDeleteNode); }; - }, [setNodes, setEdges]); + }, [setNodes, setEdges, projectId, nodes, edges]); // Edge 삭제 핸들러 - 추가 useEffect(() => { - const handleDeleteEdge = (event: CustomEvent) => { - const edgeId = event.detail.id; + const deleteEdgeAsync = async (edgeId: string) => { + if (!projectId) return; + + // Store current state for rollback + const previousEdges = edges; + + // Optimistic update - immediately remove from UI setEdges((eds) => eds.filter((edge) => edge.id !== edgeId)); + + try { + // Call API to delete edge on backend + const response = await projectApi.deleteEdge({ + project_id: projectId, + edge_id: edgeId, + }); + + if (!response.success) { + // Rollback on failure + setEdges(previousEdges); + alert("Failed to delete edge"); + } + } catch (error) { + // Rollback on error + setEdges(previousEdges); + console.error("Failed to delete edge:", error); + alert( + `Failed to delete edge: ${ + error instanceof Error ? error.message : "Unknown error" + }` + ); + } }; - document.addEventListener("deleteEdge", handleDeleteEdge as EventListener); + const handleDeleteEdge = (event: Event) => { + const customEvent = event as CustomEvent<{ id: string }>; + const edgeId = customEvent.detail.id; + deleteEdgeAsync(edgeId); + }; + + document.addEventListener("deleteEdge", handleDeleteEdge); return () => { - document.removeEventListener( - "deleteEdge", - handleDeleteEdge as EventListener - ); + document.removeEventListener("deleteEdge", handleDeleteEdge); }; - }, [setEdges]); + }, [setEdges, projectId, edges]); // MiniMap 노드 색상 함수 const nodeColor = () => { @@ -356,22 +477,18 @@ export default function Project() { deleteKeyCode={null} // Delete 키로 삭제 비활성화 > - - -
+

{projectTitle}

diff --git a/packages/frontend/src/types/interface.ts b/packages/frontend/src/types/interface.ts index d32e861..c32e076 100644 --- a/packages/frontend/src/types/interface.ts +++ b/packages/frontend/src/types/interface.ts @@ -13,8 +13,6 @@ export interface NodeData { title: string; description?: string; file?: string; - // Additional fields that might be present in the data - code?: string; // Initial code for new nodes } export interface EdgeMarkerEnd { From 05c7ee864d72459b442652a86f7261d6792bfec5 Mon Sep 17 00:00:00 2001 From: KANG-YEONWOOK Date: Thu, 21 Aug 2025 13:32:17 +0900 Subject: [PATCH 3/8] refactor: Project --- .../src/components/buttons/modal/x.tsx | 2 +- .../src/components/layouts/ProjectPanel.tsx | 0 .../frontend/src/hooks/useEdgeOperations.ts | 144 +++++ .../frontend/src/hooks/useNodeOperations.ts | 159 ++++++ packages/frontend/src/hooks/useProjectData.ts | 131 +++++ .../frontend/src/hooks/useProjectStyles.ts | 13 + .../frontend/src/pages/Project/Project.tsx | 521 ++---------------- .../src/pages/Project/errors/ProjectError.tsx | 38 ++ .../src/pages/Project/flow/ProjectFlow.tsx | 111 ++++ .../pages/Project/layouts/ProjectPanel.tsx | 28 + packages/frontend/src/utils/transformers.ts | 59 ++ packages/frontend/src/utils/validators.ts | 23 + 12 files changed, 767 insertions(+), 462 deletions(-) delete mode 100644 packages/frontend/src/components/layouts/ProjectPanel.tsx create mode 100644 packages/frontend/src/hooks/useEdgeOperations.ts create mode 100644 packages/frontend/src/hooks/useNodeOperations.ts create mode 100644 packages/frontend/src/hooks/useProjectData.ts create mode 100644 packages/frontend/src/hooks/useProjectStyles.ts create mode 100644 packages/frontend/src/pages/Project/errors/ProjectError.tsx create mode 100644 packages/frontend/src/pages/Project/flow/ProjectFlow.tsx create mode 100644 packages/frontend/src/pages/Project/layouts/ProjectPanel.tsx create mode 100644 packages/frontend/src/utils/transformers.ts create mode 100644 packages/frontend/src/utils/validators.ts diff --git a/packages/frontend/src/components/buttons/modal/x.tsx b/packages/frontend/src/components/buttons/modal/x.tsx index f2242d3..84cba22 100644 --- a/packages/frontend/src/components/buttons/modal/x.tsx +++ b/packages/frontend/src/components/buttons/modal/x.tsx @@ -5,7 +5,7 @@ interface Xprops { export default function X({ onClose }: Xprops) { return ( -
-
- ); + return ; } return ( -
- + - - - - - -
-

{projectTitle}

- -
- Nodes: {nodes.length} | Edges: {edges.length} -
-
-
-
+ {/* IDE Modal */} -
+ ); } diff --git a/packages/frontend/src/pages/Project/errors/ProjectError.tsx b/packages/frontend/src/pages/Project/errors/ProjectError.tsx new file mode 100644 index 0000000..9c81404 --- /dev/null +++ b/packages/frontend/src/pages/Project/errors/ProjectError.tsx @@ -0,0 +1,38 @@ +interface ProjectErrorProps { + error: string; + onRetry: () => void; +} + +export default function ProjectError({ error, onRetry }: ProjectErrorProps) { + return ( +
+
+
+ + + +
+

+ Failed to load project +

+

{error}

+ +
+
+ ); +} diff --git a/packages/frontend/src/pages/Project/flow/ProjectFlow.tsx b/packages/frontend/src/pages/Project/flow/ProjectFlow.tsx new file mode 100644 index 0000000..227ad96 --- /dev/null +++ b/packages/frontend/src/pages/Project/flow/ProjectFlow.tsx @@ -0,0 +1,111 @@ +import { useMemo, type ReactNode } from "react"; +import { + ReactFlow, + MiniMap, + Background, + BackgroundVariant, + Panel, + type NodeTypes, + type EdgeTypes, + type Edge, + type Connection, + type OnNodesChange, + type OnEdgesChange, +} from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; +import type { DefaultNodeType } from "../../../components/nodes/DefaultNode"; +import DefaultNode from "../../../components/nodes/DefaultNode"; +import DefaultEdge from "../../../components/edges/DefaultEdge"; + +interface ProjectFlowProps { + nodes: DefaultNodeType[]; + edges: Edge[]; + onNodesChange: OnNodesChange; + onEdgesChange: OnEdgesChange; + onConnect: (connection: Connection) => void; + isValidConnection: (connection: Edge | Connection) => boolean; + children?: ReactNode; +} + +export default function ProjectFlow({ + nodes, + edges, + onNodesChange, + onEdgesChange, + onConnect, + isValidConnection, + children, +}: ProjectFlowProps) { + // Define node types + const nodeTypes = useMemo( + () => ({ + default: DefaultNode, + }), + [] + ); + + // Define edge types + const edgeTypes = useMemo( + () => ({ + custom: DefaultEdge, + }), + [] + ); + + // MiniMap node color function + const nodeColor = () => { + return "#1e293b"; + }; + + return ( +
+ + + + + + {children && ( + + {children} + + )} + +
+ ); +} diff --git a/packages/frontend/src/pages/Project/layouts/ProjectPanel.tsx b/packages/frontend/src/pages/Project/layouts/ProjectPanel.tsx new file mode 100644 index 0000000..593b19f --- /dev/null +++ b/packages/frontend/src/pages/Project/layouts/ProjectPanel.tsx @@ -0,0 +1,28 @@ +interface ProjectPanelProps { + projectTitle: string; + addNewNode: () => void; + nodeCount: number; + edgeCount: number; +} + +export default function ProjectPanel({ + projectTitle, + addNewNode, + nodeCount, + edgeCount, +}: ProjectPanelProps) { + return ( +
+

{projectTitle}

+ +
+ Nodes: {nodeCount} | Edges: {edgeCount} +
+
+ ); +} diff --git a/packages/frontend/src/utils/transformers.ts b/packages/frontend/src/utils/transformers.ts new file mode 100644 index 0000000..ea32751 --- /dev/null +++ b/packages/frontend/src/utils/transformers.ts @@ -0,0 +1,59 @@ +import type { ProjectNode, ProjectEdge } from "../types"; +import type { DefaultNodeType } from "../components/nodes/DefaultNode"; +import type { Edge, MarkerType } from "@xyflow/react"; + +/** + * Transform backend node to ReactFlow node format + */ +export function transformNodeToReactFlow( + node: ProjectNode, + onNodeClick: (nodeId: string, title: string) => void +): DefaultNodeType { + return { + id: node.id, + type: node.type || "default", + position: node.position, + data: { + title: node.data.title || `Node ${node.id}`, + description: node.data.description || "", + file: node.data.file, + viewCode: () => { + onNodeClick(node.id, node.data.title || `Node ${node.id}`); + }, + }, + }; +} + +/** + * Transform backend edge to ReactFlow edge format + */ +export function transformEdgeToReactFlow(edge: ProjectEdge): Edge { + return { + id: edge.id, + type: edge.type || "custom", + source: edge.source, + target: edge.target, + sourceHandle: edge.sourceHandle || undefined, + targetHandle: edge.targetHandle || undefined, + style: { stroke: "#64748b", strokeWidth: 2 }, + markerEnd: edge.markerEnd + ? { + type: edge.markerEnd.type as MarkerType, + } + : { + type: "arrowclosed" as MarkerType, + }, + }; +} + +/** + * Calculate max node ID from existing nodes + */ +export function calculateMaxNodeId(nodes: ProjectNode[]): number { + if (nodes.length === 0) return 4; + + const maxId = Math.max( + ...nodes.map((n) => parseInt(n.id, 10) || 0) + ); + return maxId + 1; +} \ No newline at end of file diff --git a/packages/frontend/src/utils/validators.ts b/packages/frontend/src/utils/validators.ts new file mode 100644 index 0000000..a7b81f8 --- /dev/null +++ b/packages/frontend/src/utils/validators.ts @@ -0,0 +1,23 @@ +/** + * Validate project ID format + * @param projectId - The project ID to validate + * @returns boolean indicating if the project ID is valid + */ +export function isValidProjectId(projectId: string): boolean { + return /^[a-zA-Z0-9_-]+$/.test(projectId); +} + +/** + * Check if error is a 404 or network error + * @param error - The error to check + * @returns boolean indicating if it's a not found error + */ +export function isNotFoundError(error: unknown): boolean { + if (error instanceof TypeError && error.message.includes("URI")) { + return true; + } + if (error instanceof Error && error.message.includes("404")) { + return true; + } + return false; +} \ No newline at end of file From d05478b5f817705cef3d8840c25cb74718214d22 Mon Sep 17 00:00:00 2001 From: KANG-YEONWOOK Date: Thu, 21 Aug 2025 17:47:29 +0900 Subject: [PATCH 4/8] feat: node make modal, start node, result node --- .../src/components/modal/SetupModal.tsx | 147 ++++++++++++++++++ .../src/components/nodes/DefaultNode.tsx | 6 +- .../src/components/nodes/ResultNode.tsx | 88 +++++++++++ .../src/components/nodes/StartNode.tsx | 75 +++++++++ .../frontend/src/hooks/useNodeOperations.ts | 45 ++++-- .../frontend/src/pages/Project/Project.tsx | 35 ++++- .../src/pages/Project/flow/ProjectFlow.tsx | 4 +- .../pages/Project/layouts/ProjectPanel.tsx | 6 +- 8 files changed, 375 insertions(+), 31 deletions(-) create mode 100644 packages/frontend/src/components/modal/SetupModal.tsx create mode 100644 packages/frontend/src/components/nodes/ResultNode.tsx create mode 100644 packages/frontend/src/components/nodes/StartNode.tsx diff --git a/packages/frontend/src/components/modal/SetupModal.tsx b/packages/frontend/src/components/modal/SetupModal.tsx new file mode 100644 index 0000000..d973064 --- /dev/null +++ b/packages/frontend/src/components/modal/SetupModal.tsx @@ -0,0 +1,147 @@ +import { useState } from "react"; +import Modal from "./Modal"; + +interface SetupModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: (data: { + title: string; + description: string; + nodeType: "default" | "start" | "result"; + }) => void; + hasStartNode: boolean; +} + +export default function SetupModal({ + isOpen, + onClose, + onConfirm, + hasStartNode, +}: SetupModalProps) { + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [nodeType, setNodeType] = useState<"default" | "start" | "result">("default"); + const [error, setError] = useState(""); + + const handleConfirm = () => { + // Validation + if (!title.trim()) { + setError("Node title is required"); + return; + } + + // Check if trying to create another start node + if (nodeType === "start" && hasStartNode) { + setError("Only one Start node is allowed per project"); + return; + } + + onConfirm({ + title: title.trim(), + description: description.trim(), + nodeType, + }); + + // Reset form + setTitle(""); + setDescription(""); + setNodeType("default"); + setError(""); + onClose(); + }; + + const handleClose = () => { + // Reset form on close + setTitle(""); + setDescription(""); + setNodeType("default"); + setError(""); + onClose(); + }; + + return ( + +
+

Create New Node

+ + {error && ( +
+ {error} +
+ )} + +
+
+ + { + setTitle(e.target.value); + setError(""); + }} + className="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded text-white focus:outline-none focus:border-red-500" + placeholder="Enter node title" + autoFocus + /> +
+ +
+ +