Skip to content

Commit 56d1dc2

Browse files
committed
feat: 컨테이너 노드 기능 추가
1 parent f734740 commit 56d1dc2

File tree

8 files changed

+291
-79
lines changed

8 files changed

+291
-79
lines changed

apps/backend/src/yjs/yjs.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ export class YjsService
134134
for (const [key, change] of event.changes.keys) {
135135
if (change.action === 'update') {
136136
const node: any = nodesMap.get(key);
137+
if (node.type !== 'note') {
138+
continue;
139+
}
137140
const { title, id } = node.data; // TODO: 이모지 추가
138141
const { x, y } = node.position;
139142
const isHolding = node.isHolding;

apps/frontend/src/features/canvas/model/useCanvas.ts

Lines changed: 121 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { useWorkspace } from "@/shared/lib/useWorkspace";
2525

2626
export interface YNode extends Node {
2727
isHolding: boolean;
28+
parentId?: string;
2829
}
2930

3031
export const useCanvas = () => {
@@ -33,6 +34,7 @@ export const useCanvas = () => {
3334
const { pages } = usePages();
3435
const queryClient = useQueryClient();
3536
const { ydoc } = useYDocStore();
37+
const { getIntersectingNodes } = useReactFlow();
3638

3739
const workspace = useWorkspace();
3840

@@ -70,23 +72,17 @@ export const useCanvas = () => {
7072
useEffect(() => {
7173
const yTitleMap = ydoc.getMap("title");
7274
const yEmojiMap = ydoc.getMap("emoji");
73-
7475
const nodesMap = ydoc.getMap("nodes");
7576

7677
yTitleMap.observeDeep((event) => {
7778
if (!event[0].path.length) return;
78-
7979
const pageId = event[0].path[0].toString().split("_")[1];
8080
const value = event[0].target.toString();
81-
8281
const existingNode = nodesMap.get(pageId) as YNode;
8382

8483
const newNode: YNode = {
85-
id: pageId,
86-
type: "note",
87-
data: { title: value, id: pageId, emoji: existingNode.data.emoji },
88-
position: existingNode.position,
89-
selected: false,
84+
...existingNode,
85+
data: { ...existingNode.data, title: value },
9086
isHolding: false,
9187
};
9288

@@ -95,18 +91,13 @@ export const useCanvas = () => {
9591

9692
yEmojiMap.observeDeep((event) => {
9793
if (!event[0].path.length) return;
98-
9994
const pageId = event[0].path[0].toString().split("_")[1];
10095
const value = event[0].target.toString();
101-
10296
const existingNode = nodesMap.get(pageId) as YNode;
10397

10498
const newNode: YNode = {
105-
id: pageId,
106-
type: "note",
107-
data: { title: existingNode.data.title, id: pageId, emoji: value },
108-
position: existingNode.position,
109-
selected: false,
99+
...existingNode,
100+
data: { ...existingNode.data, emoji: value },
110101
isHolding: false,
111102
};
112103

@@ -118,7 +109,6 @@ export const useCanvas = () => {
118109
if (!ydoc) return;
119110

120111
const wsProvider = createSocketIOProvider("flow-room", ydoc);
121-
122112
provider.current = wsProvider;
123113

124114
const nodesMap = ydoc.getMap("nodes");
@@ -240,13 +230,17 @@ export const useCanvas = () => {
240230
if (change.type === "position" && change.position) {
241231
const node = nodes.find((n) => n.id === change.id);
242232
if (node) {
243-
const updatedYNode: YNode = {
244-
...node,
245-
position: change.position,
246-
selected: false,
247-
isHolding: holdingNodeRef.current === change.id,
248-
};
249-
nodesMap.set(change.id, updatedYNode);
233+
onNodesChange([change]);
234+
235+
const currentNode = nodes.find((n) => n.id === change.id);
236+
if (currentNode) {
237+
nodesMap.set(change.id, {
238+
...currentNode,
239+
position: change.position,
240+
selected: false,
241+
isHolding: holdingNodeRef.current === change.id,
242+
});
243+
}
250244

251245
const affectedEdges = edges.filter(
252246
(edge) => edge.source === change.id || edge.target === change.id,
@@ -270,10 +264,10 @@ export const useCanvas = () => {
270264
}
271265
});
272266
}
267+
} else {
268+
onNodesChange([change]);
273269
}
274270
});
275-
276-
onNodesChange(changes);
277271
},
278272
[nodes, edges, onNodesChange],
279273
);
@@ -341,12 +335,112 @@ export const useCanvas = () => {
341335
if (ydoc) {
342336
const nodesMap = ydoc.getMap("nodes");
343337
const yNode = nodesMap.get(node.id) as YNode | undefined;
338+
344339
if (yNode) {
345-
nodesMap.set(node.id, { ...yNode, isHolding: false });
340+
const currentNode = nodes.find((n) => n.id === node.id);
341+
if (!currentNode) return;
342+
343+
if (node.type === "group") {
344+
const intersectingNotes = getIntersectingNodes(currentNode).filter(
345+
(n) => n.type === "note" && !n.parentId,
346+
);
347+
348+
intersectingNotes.forEach((noteNode) => {
349+
const relativePosition = {
350+
x: noteNode.position.x - currentNode.position.x,
351+
y: noteNode.position.y - currentNode.position.y,
352+
};
353+
354+
nodesMap.set(noteNode.id, {
355+
...noteNode,
356+
parentId: node.id,
357+
position: relativePosition,
358+
isHolding: false,
359+
});
360+
});
361+
362+
nodesMap.set(node.id, {
363+
...currentNode,
364+
isHolding: false,
365+
});
366+
} else {
367+
const intersectingGroups = getIntersectingNodes(currentNode).filter(
368+
(n) => n.type === "group",
369+
);
370+
371+
if (intersectingGroups.length > 0) {
372+
const parentNode =
373+
intersectingGroups[intersectingGroups.length - 1];
374+
375+
if (yNode.parentId === parentNode.id) {
376+
nodesMap.set(node.id, {
377+
...yNode,
378+
position: currentNode.position,
379+
isHolding: false,
380+
});
381+
} else {
382+
let absolutePosition = currentNode.position;
383+
if (yNode.parentId) {
384+
const oldParentNode = nodes.find(
385+
(n) => n.id === yNode.parentId,
386+
);
387+
if (oldParentNode) {
388+
absolutePosition = {
389+
x: oldParentNode.position.x + currentNode.position.x,
390+
y: oldParentNode.position.y + currentNode.position.y,
391+
};
392+
}
393+
}
394+
395+
const relativePosition = {
396+
x: absolutePosition.x - parentNode.position.x,
397+
y: absolutePosition.y - parentNode.position.y,
398+
};
399+
400+
nodesMap.set(node.id, {
401+
...currentNode,
402+
parentId: parentNode.id,
403+
position: relativePosition,
404+
isHolding: false,
405+
});
406+
}
407+
} else {
408+
if (yNode.parentId) {
409+
const oldParentNode = nodes.find(
410+
(n) => n.id === yNode.parentId,
411+
);
412+
if (oldParentNode) {
413+
const absolutePosition = {
414+
x: oldParentNode.position.x + currentNode.position.x,
415+
y: oldParentNode.position.y + currentNode.position.y,
416+
};
417+
418+
nodesMap.set(node.id, {
419+
...currentNode,
420+
parentId: undefined,
421+
position: absolutePosition,
422+
isHolding: false,
423+
});
424+
}
425+
} else {
426+
nodesMap.set(node.id, {
427+
...currentNode,
428+
parentId: undefined,
429+
isHolding: false,
430+
});
431+
}
432+
}
433+
}
434+
435+
setNodes((ns) => {
436+
const groups = ns.filter((n) => n.type === "group");
437+
const notes = ns.filter((n) => n.type !== "group");
438+
return [...groups, ...notes];
439+
});
346440
}
347441
}
348442
},
349-
[ydoc],
443+
[ydoc, getIntersectingNodes, nodes, setNodes],
350444
);
351445

352446
return {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { useCreatePage } from "@/features/pageSidebar/api/usePages";
2+
import { usePageStore } from "@/features/pageSidebar/model";
3+
import { initializeYText } from "@/shared/model";
4+
import { usePopover } from "@/shared/model/usePopover";
5+
import useYDocStore from "@/shared/model/ydocStore";
6+
7+
export function NewNodePanel() {
8+
const { close } = usePopover();
9+
10+
const { setCurrentPage } = usePageStore();
11+
const createMutation = useCreatePage();
12+
const { ydoc } = useYDocStore();
13+
14+
const handleNewPageButtonClick = () => {
15+
createMutation
16+
.mutateAsync({
17+
title: "제목 없음",
18+
content: {
19+
type: "doc",
20+
content: [
21+
{
22+
type: "paragraph",
23+
content: [{ type: "text", text: "" }],
24+
},
25+
],
26+
},
27+
x: 0,
28+
y: 0,
29+
emoji: null,
30+
})
31+
.then((res) => {
32+
setCurrentPage(res.pageId);
33+
34+
const yTitleMap = ydoc.getMap("title");
35+
const yEmojiMap = ydoc.getMap("emoji");
36+
37+
initializeYText(yTitleMap, `title_${res.pageId}`, "제목 없음");
38+
initializeYText(yEmojiMap, `emoji_${res.pageId}`, "");
39+
close();
40+
});
41+
};
42+
43+
const nodeMap = ydoc.getMap("nodes");
44+
45+
const handleNewContainerButtonClick = () => {
46+
const newNode = {
47+
id: Math.random().toString(36).substr(2, 9),
48+
type: "group",
49+
data: { label: "Group A" },
50+
position: { x: 100, y: 100 },
51+
style: { width: 200, height: 200 },
52+
};
53+
54+
nodeMap.set(newNode.id, newNode);
55+
};
56+
57+
return (
58+
<div className="flex flex-col gap-0.5 p-0.5 text-sm text-[#171717]">
59+
<button
60+
onClick={handleNewPageButtonClick}
61+
className="w-full rounded-md p-1.5 text-start hover:bg-[#f2f2f2]"
62+
>
63+
새 노드 추가
64+
</button>
65+
<button
66+
onClick={handleNewContainerButtonClick}
67+
className="w-full rounded-md p-1.5 text-start hover:bg-[#f2f2f2]"
68+
>
69+
컨테이너 추가
70+
</button>
71+
</div>
72+
);
73+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { usePopover } from "@/shared/model/usePopover";
2+
import { CursorPreview, ProfileForm } from "@/features/canvasTools/ui";
3+
4+
interface ProfilePanelProps {
5+
color: string;
6+
clientId: string;
7+
onColorChange: (color: string) => void;
8+
onClientIdChange: (clientId: string) => void;
9+
}
10+
11+
export function ProfilePanel({
12+
color,
13+
clientId,
14+
onColorChange,
15+
onClientIdChange,
16+
}: ProfilePanelProps) {
17+
const { close } = usePopover();
18+
19+
const handleSave = () => {
20+
close();
21+
};
22+
23+
return (
24+
<div className="flex flex-row gap-4 p-4">
25+
<CursorPreview
26+
defaultCoors={{ x: 90, y: 80 }}
27+
clientId={clientId}
28+
color={color}
29+
/>
30+
<ProfileForm
31+
color={color}
32+
clientId={clientId}
33+
onColorChange={onColorChange}
34+
onClientIdChange={onClientIdChange}
35+
onSave={handleSave}
36+
/>
37+
</div>
38+
);
39+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from "./CursorButton";
22
export * from "./CursorPreview";
33
export * from "./ProfileForm";
4+
export * from "./ProfilePanel";
5+
export * from "./NewNodePanel";

apps/frontend/src/features/pageSidebar/model/useNoteList.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ export const useNoteList = () => {
1717
useEffect(() => {
1818
nodesMap.observe(() => {
1919
const yNodes = Array.from(nodesMap.values()) as YNode[];
20-
const data = yNodes.map((yNode) => yNode.data) as NoteNodeData[];
20+
const data = yNodes
21+
.filter((yNode) => yNode.data.type === "note")
22+
.map((yNode) => yNode.data) as NoteNodeData[];
2123
setPages(data);
2224
});
2325
}, []);

0 commit comments

Comments
 (0)