Skip to content

Commit a0e9764

Browse files
authored
Merge pull request #152 from boostcampwm-2024/feature-fe-#146
에디터 업데이트 시 노드에 반영하는 기능 추가
2 parents 095cafe + f5a6292 commit a0e9764

File tree

7 files changed

+133
-44
lines changed

7 files changed

+133
-44
lines changed

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"axios": "^1.7.7",
3030
"class-variance-authority": "^0.7.0",
3131
"clsx": "^2.1.1",
32+
"fast-diff": "^1.3.0",
3233
"framer-motion": "^11.11.11",
3334
"highlight.js": "^11.10.0",
3435
"lowlight": "^3.1.0",

frontend/src/components/EditorView.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@ import * as Y from "yjs";
66

77
import Editor from "./editor";
88
import usePageStore from "@/store/usePageStore";
9-
import {
10-
usePage,
11-
useUpdatePage,
12-
useOptimisticUpdatePage,
13-
} from "@/hooks/usePages";
9+
import { usePage, useUpdatePage } from "@/hooks/usePages";
1410
import EditorLayout from "./layout/EditorLayout";
1511
import EditorTitle from "./editor/EditorTitle";
1612
import SaveStatus from "./editor/ui/SaveStatus";
@@ -36,7 +32,7 @@ export default function EditorView() {
3632
const pageContent = page?.content ?? {};
3733

3834
const updatePageMutation = useUpdatePage();
39-
const optimisticUpdatePageMutation = useOptimisticUpdatePage({
35+
/* const optimisticUpdatePageMutation = useOptimisticUpdatePage({
4036
id: currentPage ?? 0,
4137
});
4238
@@ -54,7 +50,7 @@ export default function EditorView() {
5450
onError: () => setSaveStatus("unsaved"),
5551
},
5652
);
57-
};
53+
}; */
5854

5955
const handleEditorUpdate = useDebouncedCallback(
6056
async ({ editor }: { editor: EditorInstance }) => {
@@ -91,7 +87,11 @@ export default function EditorView() {
9187
return (
9288
<EditorLayout>
9389
<SaveStatus saveStatus={saveStatus} />
94-
<EditorTitle title={pageTitle} onTitleChange={handleTitleChange} />
90+
<EditorTitle
91+
key={currentPage}
92+
currentPage={currentPage}
93+
pageContent={pageContent}
94+
/>
9595
<Editor
9696
key={ydoc.guid}
9797
initialContent={pageContent}

frontend/src/components/canvas/index.tsx

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import * as Y from "yjs";
2222
import { WebsocketProvider } from "y-websocket";
2323
import { cn } from "@/lib/utils";
2424
import { useQueryClient } from "@tanstack/react-query";
25+
import useYDocStore from "@/store/useYDocStore";
2526

2627
const proOptions = { hideAttribution: true };
2728

@@ -35,23 +36,39 @@ export default function Canvas({ className }: CanvasProps) {
3536
const { pages } = usePages();
3637
const queryClient = useQueryClient();
3738

38-
const ydoc = useRef<Y.Doc>();
39+
const { ydoc } = useYDocStore();
40+
3941
const provider = useRef<WebsocketProvider>();
4042
const existingPageIds = useRef(new Set<string>());
4143

4244
useEffect(() => {
43-
const doc = new Y.Doc();
45+
if (!pages) return;
46+
47+
const yMap = ydoc.getMap("title");
48+
49+
pages.forEach((page) => {
50+
if (yMap.get(`title_${page.id}`)) return;
51+
52+
const yText = new Y.Text();
53+
yText.insert(0, page.title);
54+
55+
yMap.set(`title_${page.id}`, yText);
56+
});
57+
}, [pages]);
58+
59+
useEffect(() => {
60+
if (!ydoc) return;
61+
4462
const wsProvider = new WebsocketProvider(
4563
import.meta.env.VITE_WS_URL,
4664
"flow-room",
47-
doc,
65+
ydoc,
4866
);
4967

50-
ydoc.current = doc;
5168
provider.current = wsProvider;
5269

53-
const nodesMap = doc.getMap("nodes");
54-
const edgesMap = doc.getMap("edges");
70+
const nodesMap = ydoc.getMap("nodes");
71+
const edgesMap = ydoc.getMap("edges");
5572

5673
nodesMap.observe((event) => {
5774
event.changes.keys.forEach((change, key) => {
@@ -86,14 +103,14 @@ export default function Canvas({ className }: CanvasProps) {
86103

87104
return () => {
88105
wsProvider.destroy();
89-
doc.destroy();
106+
ydoc.destroy();
90107
};
91-
}, [queryClient]);
108+
}, [ydoc, queryClient]);
92109

93110
useEffect(() => {
94-
if (!pages || !ydoc.current) return;
111+
if (!pages || !ydoc) return;
95112

96-
const nodesMap = ydoc.current.getMap("nodes");
113+
const nodesMap = ydoc.getMap("nodes");
97114
const currentPageIds = new Set(pages.map((page) => page.id.toString()));
98115

99116
existingPageIds.current.forEach((pageId) => {
@@ -105,27 +122,27 @@ export default function Canvas({ className }: CanvasProps) {
105122

106123
pages.forEach((page) => {
107124
const pageId = page.id.toString();
108-
if (!existingPageIds.current.has(pageId)) {
109-
const newNode = {
110-
id: pageId,
111-
position: {
112-
x: Math.random() * 500,
113-
y: Math.random() * 500,
114-
},
115-
data: { title: page.title, id: page.id },
116-
type: "note",
117-
};
118-
119-
nodesMap.set(pageId, newNode);
120-
existingPageIds.current.add(pageId);
121-
}
125+
//if (!existingPageIds.current.has(pageId)) {
126+
const newNode = {
127+
id: pageId,
128+
position: {
129+
x: Math.random() * 500,
130+
y: Math.random() * 500,
131+
},
132+
data: { title: page.title, id: page.id },
133+
type: "note",
134+
};
135+
136+
nodesMap.set(pageId, newNode);
137+
existingPageIds.current.add(pageId);
138+
//}
122139
});
123140
}, [pages]);
124141

125142
const handleNodesChange = useCallback(
126143
(changes: NodeChange[]) => {
127-
if (!ydoc.current) return;
128-
const nodesMap = ydoc.current.getMap("nodes");
144+
if (!ydoc) return;
145+
const nodesMap = ydoc.getMap("nodes");
129146

130147
onNodesChange(changes);
131148

@@ -147,8 +164,8 @@ export default function Canvas({ className }: CanvasProps) {
147164

148165
const handleEdgesChange = useCallback(
149166
(changes: EdgeChange[]) => {
150-
if (!ydoc.current) return;
151-
const edgesMap = ydoc.current.getMap("edges");
167+
if (!ydoc) return;
168+
const edgesMap = ydoc.getMap("edges");
152169

153170
changes.forEach((change) => {
154171
if (change.type === "remove") {
@@ -163,7 +180,7 @@ export default function Canvas({ className }: CanvasProps) {
163180

164181
const onConnect = useCallback(
165182
(connection: Connection) => {
166-
if (!connection.source || !connection.target || !ydoc.current) return;
183+
if (!connection.source || !connection.target || !ydoc) return;
167184

168185
const newEdge: Edge = {
169186
id: `e${connection.source}-${connection.target}`,
@@ -173,7 +190,7 @@ export default function Canvas({ className }: CanvasProps) {
173190
targetHandle: connection.targetHandle || undefined,
174191
};
175192

176-
ydoc.current.getMap("edges").set(newEdge.id, newEdge);
193+
ydoc.getMap("edges").set(newEdge.id, newEdge);
177194
setEdges((eds) => addEdge(connection, eds));
178195
},
179196
[setEdges],

frontend/src/components/editor/EditorTitle.tsx

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
1+
import useYDocStore from "@/store/useYDocStore";
2+
import { useYText } from "@/hooks/useYText";
3+
import { useOptimisticUpdatePage } from "@/hooks/usePages";
4+
import { JSONContent } from "novel";
5+
16
interface EditorTitleProps {
2-
title?: string;
3-
onTitleChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
7+
currentPage: number;
8+
pageContent: JSONContent;
49
}
510

611
export default function EditorTitle({
7-
title,
8-
onTitleChange,
12+
currentPage,
13+
pageContent,
914
}: EditorTitleProps) {
15+
const { ydoc } = useYDocStore();
16+
const { input, setYText } = useYText(ydoc, currentPage);
17+
18+
const optimisticUpdatePageMutation = useOptimisticUpdatePage({
19+
id: currentPage ?? 0,
20+
});
21+
22+
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
23+
setYText(e.target.value);
24+
25+
optimisticUpdatePageMutation.mutate({
26+
pageData: { title: e.target.value, content: pageContent },
27+
});
28+
};
29+
1030
return (
1131
<div className="p-12 pb-0">
1232
<input
1333
type="text"
34+
value={input as string}
1435
className="w-full text-xl font-bold outline-none"
15-
value={title}
16-
onChange={onTitleChange}
36+
onChange={handleTitleChange}
1737
/>
1838
</div>
1939
);

frontend/src/hooks/useYText.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useState } from "react";
2+
import * as Y from "yjs";
3+
import diff from "fast-diff";
4+
5+
function diffToDelta(diffResult) {
6+
return diffResult.map(([op, value]) =>
7+
op === diff.INSERT
8+
? { insert: value }
9+
: op === diff.EQUAL
10+
? { retain: value.length }
11+
: op === diff.DELETE
12+
? { delete: value.length }
13+
: null,
14+
);
15+
}
16+
17+
export const useYText = (ydoc: Y.Doc, currentPage: number) => {
18+
const yText = ydoc.getMap("title").get(`title_${currentPage}`) as Y.Text;
19+
20+
const [input, setInput] = useState(yText.toString());
21+
22+
const setYText = (textNew: string) => {
23+
const delta = diffToDelta(diff(input, textNew));
24+
yText.applyDelta(delta);
25+
};
26+
27+
yText.observe(() => {
28+
setInput(yText.toString());
29+
});
30+
31+
return { input, setYText };
32+
};

frontend/src/store/useYDocStore.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { create } from "zustand";
2+
import * as Y from "yjs";
3+
4+
interface YDocStore {
5+
ydoc: Y.Doc;
6+
setYDoc: (ydoc: Y.Doc) => void;
7+
}
8+
9+
const useYDocStore = create<YDocStore>((set) => ({
10+
ydoc: new Y.Doc(),
11+
setYDoc: (ydoc: Y.Doc) => set({ ydoc }),
12+
}));
13+
14+
export default useYDocStore;

frontend/yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2218,6 +2218,11 @@ fast-deep-equal@^3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
22182218
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
22192219
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
22202220

2221+
fast-diff@^1.3.0:
2222+
version "1.3.0"
2223+
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
2224+
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
2225+
22212226
fast-glob@^3.3.0, fast-glob@^3.3.2:
22222227
version "3.3.2"
22232228
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"

0 commit comments

Comments
 (0)