diff --git a/packages/frontend/src/components/Node.tsx b/packages/frontend/src/components/Node.tsx
index d74b97b1..6c93e4ef 100644
--- a/packages/frontend/src/components/Node.tsx
+++ b/packages/frontend/src/components/Node.tsx
@@ -1,5 +1,6 @@
import { ReactNode, useEffect, useRef, useState } from "react";
-import { Circle, Group, Text } from "react-konva";
+import { Circle, Group, KonvaNodeEvents, Text } from "react-konva";
+import { useNavigate } from "react-router-dom";
import Konva from "konva";
import { KonvaEventObject } from "konva/lib/Node";
@@ -10,7 +11,8 @@ type NodeProps = {
y: number;
draggable?: boolean;
children?: ReactNode;
-} & Konva.GroupConfig;
+} & Konva.GroupConfig &
+ KonvaNodeEvents;
type NodeHandlers = {
onDragStart: () => void;
@@ -117,3 +119,21 @@ export function NoteNode({ x, y, name, ...rest }: NoteNodeProps) {
);
}
+
+export type SubspaceNodeProps = {
+ x: number;
+ y: number;
+ name: string;
+ src: string;
+} & NodeHandlers;
+
+export function SubspaceNode({ x, y, name, src, ...rest }: SubspaceNodeProps) {
+ const navigate = useNavigate();
+
+ return (
+ navigate(`/space/${src}`)} {...rest}>
+
+
+
+ );
+}
diff --git a/packages/frontend/src/components/mock.ts b/packages/frontend/src/components/mock.ts
deleted file mode 100644
index a4cf81ad..00000000
--- a/packages/frontend/src/components/mock.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { Edge, Node } from "shared/types";
-
-export const nodeSample: Node[] = [
- { id: "0", x: 0, y: 0, type: "head", name: "Hello World" },
- { id: "1", x: 100, y: 100, type: "note", name: "first" },
- { id: "2", x: -100, y: 100, type: "note", name: "second" },
-];
-
-export const edgeSample: Edge[] = [
- { from: nodeSample[0], to: nodeSample[1] },
- { from: nodeSample[0], to: nodeSample[2] },
-];
diff --git a/packages/frontend/src/components/space/SpaceView.tsx b/packages/frontend/src/components/space/SpaceView.tsx
index 225571cf..afe90c37 100644
--- a/packages/frontend/src/components/space/SpaceView.tsx
+++ b/packages/frontend/src/components/space/SpaceView.tsx
@@ -1,12 +1,14 @@
-import React, { useEffect, useState } from "react";
+import React from "react";
import { Layer, Stage } from "react-konva";
import { Html } from "react-konva-utils";
import Konva from "konva";
import type { Node } from "shared/types";
+import { createSpace } from "@/api/space";
import Edge from "@/components/Edge";
-import { HeadNode, NoteNode } from "@/components/Node";
+import { HeadNode, NoteNode, SubspaceNode } from "@/components/Node";
+import useAutofit from "@/hooks/useAutofit";
import useDragNode from "@/hooks/useDragNode";
import useYjsSpace from "@/hooks/useYjsSpace";
import { useZoomSpace } from "@/hooks/useZoomSpace.ts";
@@ -17,6 +19,7 @@ import { MemoizedNearIndicator } from "./NearNodeIndicator";
import PaletteMenu from "./PaletteMenu";
interface SpaceViewProps {
+ spaceId: string;
autofitTo?: Element | React.RefObject;
}
@@ -24,8 +27,7 @@ const dragBoundFunc = function (this: Konva.Node) {
return this.absolutePosition();
};
-export default function SpaceView({ autofitTo }: SpaceViewProps) {
- const [stageSize, setStageSize] = useState({ width: 0, height: 0 });
+export default function SpaceView({ spaceId, autofitTo }: SpaceViewProps) {
const stageRef = React.useRef(null);
const { zoomSpace } = useZoomSpace({ stageRef });
@@ -35,42 +37,64 @@ export default function SpaceView({ autofitTo }: SpaceViewProps) {
const { drag, dropPosition, handlePaletteSelect } = useDragNode(nodesArray, {
createNode: (type, parentNode, position, name = "New Note") => {
- defineNode({ type, x: position.x, y: position.y, name }, parentNode.id);
- },
- createEdge: (fromNode, toNode) => {
- defineEdge(fromNode.id, toNode.id);
- },
- });
- const { startNode, handlers } = drag;
-
- useEffect(() => {
- if (!autofitTo) {
- return undefined;
- }
+ if (type === "note") {
+ let src = "";
+ // FIXME: note 생성 후 id 입력
+ defineNode(
+ {
+ type,
+ x: position.x,
+ y: position.y,
+ name,
+ src,
+ },
+ parentNode.id,
+ );
- const containerRef =
- "current" in autofitTo ? autofitTo : { current: autofitTo };
+ return;
+ }
- function resizeStage() {
- const container = containerRef.current;
+ if (type === "subspace") {
+ createSpace({
+ spaceName: name,
+ userId: "honeyflow",
+ parentContextNodeId: spaceId,
+ }).then((res) => {
+ const [urlPath] = res.urlPath;
+ defineNode(
+ {
+ type,
+ x: position.x,
+ y: position.y,
+ name,
+ src: urlPath,
+ },
+ parentNode.id,
+ );
+ });
- if (!container) {
return;
}
- const width = container.clientWidth;
- const height = container.clientHeight;
-
- setStageSize({ width, height });
- }
+ defineNode(
+ {
+ type,
+ x: position.x,
+ y: position.y,
+ name,
+ src: "",
+ },
+ parentNode.id,
+ );
+ },
- resizeStage();
+ createEdge: (fromNode, toNode) => {
+ defineEdge(fromNode.id, toNode.id);
+ },
+ });
+ const { startNode, handlers } = drag;
- window.addEventListener("resize", resizeStage);
- return () => {
- window.removeEventListener("resize", resizeStage);
- };
- }, [autofitTo]);
+ const stageSize = useAutofit(autofitTo);
const nodeComponents = {
head: (node: Node) => (
@@ -96,6 +120,19 @@ export default function SpaceView({ autofitTo }: SpaceViewProps) {
dragBoundFunc={dragBoundFunc}
/>
),
+ subspace: (node: Node) => (
+ handlers.onDragStart(node)}
+ onDragMove={handlers.onDragMove}
+ onDragEnd={handlers.onDragEnd}
+ dragBoundFunc={dragBoundFunc}
+ />
+ ),
};
return (
@@ -145,7 +182,7 @@ export default function SpaceView({ autofitTo }: SpaceViewProps) {
}}
>
diff --git a/packages/frontend/src/hooks/useAutofit.ts b/packages/frontend/src/hooks/useAutofit.ts
new file mode 100644
index 00000000..e40e7b8e
--- /dev/null
+++ b/packages/frontend/src/hooks/useAutofit.ts
@@ -0,0 +1,39 @@
+import { RefObject, useEffect, useState } from "react";
+
+export default function useAutofit(
+ container: RefObject | T | undefined,
+) {
+ const [size, setSize] = useState({ width: 0, height: 0 });
+
+ useEffect(() => {
+ if (!container) {
+ return undefined;
+ }
+
+ const containerRef =
+ "current" in container ? container : { current: container };
+
+ function resizeStage() {
+ const container = containerRef.current;
+
+ if (!container) {
+ return;
+ }
+
+ const width = container.clientWidth;
+ const height = container.clientHeight;
+
+ setSize({ width, height });
+ }
+
+ resizeStage();
+
+ // NOTE: ResizeObserver를 도입할 것을 고려할 것
+ window.addEventListener("resize", resizeStage);
+ return () => {
+ window.removeEventListener("resize", resizeStage);
+ };
+ }, [container]);
+
+ return size;
+}
diff --git a/packages/frontend/src/hooks/useYjsSpace.tsx b/packages/frontend/src/hooks/useYjsSpace.tsx
index 1519b45a..b7ce489e 100644
--- a/packages/frontend/src/hooks/useYjsSpace.tsx
+++ b/packages/frontend/src/hooks/useYjsSpace.tsx
@@ -22,28 +22,22 @@ const MOCK_DATA = {
};
export default function useYjsSpace() {
- const { yDoc, yProvider } = useYjsStore();
- const [yContext, setYContext] = useState>();
+ const { yDoc } = useYjsStore();
- useEffect(() => {
- if (!yDoc) return;
- const context = yDoc.getMap("context");
- setYContext(context);
- }, [yDoc]);
-
- // TODO 코드 개선
- const yNodes = yContext?.get("nodes") as Y.Map | undefined;
- const yEdges = yContext?.get("edges") as Y.Map | undefined;
- const nodes = useY(yNodes) as SpaceData["nodes"] | undefined;
- const edgesRaw = useY(yEdges) as
- | Record
- | undefined;
+ const yContext = yDoc?.getMap("context") as Y.Map> | undefined;
+
+ const context = useY(yContext) as SpaceData | undefined;
+ const nodes = context?.nodes;
+ const edgesRaw = context?.edges as EdgeWithId[] | undefined;
const [edges, setEdges] = useState();
// update functions
const defineNode = (node: Omit, parentNodeId?: Node["id"]) => {
+ const yNodes = yContext?.get("nodes") as Y.Map | undefined;
+ const yEdges = yContext?.get("edges") as Y.Map | undefined;
+
if (!yDoc || !yNodes || !yEdges) {
return;
}
@@ -61,6 +55,9 @@ export default function useYjsSpace() {
};
const defineEdge = (fromNodeId: string, toNodeId: string) => {
+ const yNodes = yContext?.get("nodes") as Y.Map | undefined;
+ const yEdges = yContext?.get("edges") as Y.Map | undefined;
+
if (!yDoc || !yNodes || !yEdges) {
return;
}
@@ -76,6 +73,9 @@ export default function useYjsSpace() {
};
const deleteNode = (nodeId: Node["id"]) => {
+ const yNodes = yContext?.get("nodes") as Y.Map | undefined;
+ const yEdges = yContext?.get("edges") as Y.Map | undefined;
+
if (!yDoc || !yNodes || !yEdges) {
return;
}
@@ -96,6 +96,8 @@ export default function useYjsSpace() {
};
const updateNode = (nodeId: Node["id"], patch: Partial>) => {
+ const yNodes = yContext?.get("nodes") as Y.Map | undefined;
+
const prev = yNodes?.get(nodeId);
if (!yNodes || !prev) {
@@ -124,24 +126,8 @@ export default function useYjsSpace() {
}, {});
setEdges(edgesData);
-
- // NOTE nodes는 포함되지 않아도 될 것 같긴 하다 -> 이후 이를 고려해 로직 수정
}, [edgesRaw, nodes]);
- useEffect(() => {
- if (!yDoc || !yProvider) {
- return undefined;
- }
-
- const handleOnSync = (/* isSynced: boolean */) => {
- const yContext = yDoc.getMap("context");
- setYContext(yContext);
- };
-
- yProvider.on("sync", handleOnSync);
- return () => yProvider.off("sync", handleOnSync);
- }, [yDoc, yProvider]);
-
/* NOTE - 개발 단계에서 프론트엔드 Space 개발을 위한 Mock 데이터 임의 설정 */
if (import.meta.env.VITE_MOCK) {
if (!yDoc || !nodes || Object.keys(nodes || {}).length === 0) {
diff --git a/packages/frontend/src/hooks/yjs/useY.ts b/packages/frontend/src/hooks/yjs/useY.ts
index 9eb0fed3..91ba8018 100644
--- a/packages/frontend/src/hooks/yjs/useY.ts
+++ b/packages/frontend/src/hooks/yjs/useY.ts
@@ -41,8 +41,8 @@ export default function useY | undefined>(
};
if (yData) {
- yData.observe(callback);
- return () => yData.unobserve(callback);
+ yData.observeDeep(callback);
+ return () => yData.unobserveDeep(callback);
}
return () => {};
diff --git a/packages/shared/types/index.ts b/packages/shared/types/index.ts
index 4a25f853..f81945fa 100644
--- a/packages/shared/types/index.ts
+++ b/packages/shared/types/index.ts
@@ -4,6 +4,7 @@ export type Node = {
x: number;
y: number;
type: "head" | "note" | "url" | "image" | "subspace";
+ src: string;
};
export type Edge = {