From f5a6292b6634bdcea1dd8f45fabcf2b0b7f8a814 Mon Sep 17 00:00:00 2001 From: yewonJin Date: Thu, 14 Nov 2024 15:52:56 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=97=90=EB=94=94=ED=84=B0=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=8B=9C=20=EB=85=B8=EB=93=9C?= =?UTF-8?q?=EC=97=90=20=EB=B0=98=EC=98=81=ED=95=98=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 1 + frontend/src/components/EditorView.tsx | 16 ++-- frontend/src/components/canvas/index.tsx | 77 +++++++++++-------- .../src/components/editor/EditorTitle.tsx | 32 ++++++-- frontend/src/hooks/useYText.ts | 32 ++++++++ frontend/src/store/useYDocStore.ts | 14 ++++ frontend/yarn.lock | 5 ++ 7 files changed, 133 insertions(+), 44 deletions(-) create mode 100644 frontend/src/hooks/useYText.ts create mode 100644 frontend/src/store/useYDocStore.ts diff --git a/frontend/package.json b/frontend/package.json index 5980b96b..5132d7ed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,7 @@ "axios": "^1.7.7", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "fast-diff": "^1.3.0", "framer-motion": "^11.11.11", "highlight.js": "^11.10.0", "lowlight": "^3.1.0", diff --git a/frontend/src/components/EditorView.tsx b/frontend/src/components/EditorView.tsx index cf8743b2..9d6d59f9 100644 --- a/frontend/src/components/EditorView.tsx +++ b/frontend/src/components/EditorView.tsx @@ -6,11 +6,7 @@ import * as Y from "yjs"; import Editor from "./editor"; import usePageStore from "@/store/usePageStore"; -import { - usePage, - useUpdatePage, - useOptimisticUpdatePage, -} from "@/hooks/usePages"; +import { usePage, useUpdatePage } from "@/hooks/usePages"; import EditorLayout from "./layout/EditorLayout"; import EditorTitle from "./editor/EditorTitle"; import SaveStatus from "./editor/ui/SaveStatus"; @@ -36,7 +32,7 @@ export default function EditorView() { const pageContent = page?.content ?? {}; const updatePageMutation = useUpdatePage(); - const optimisticUpdatePageMutation = useOptimisticUpdatePage({ + /* const optimisticUpdatePageMutation = useOptimisticUpdatePage({ id: currentPage ?? 0, }); @@ -54,7 +50,7 @@ export default function EditorView() { onError: () => setSaveStatus("unsaved"), }, ); - }; + }; */ const handleEditorUpdate = useDebouncedCallback( async ({ editor }: { editor: EditorInstance }) => { @@ -91,7 +87,11 @@ export default function EditorView() { return ( - + (); + const { ydoc } = useYDocStore(); + const provider = useRef(); const existingPageIds = useRef(new Set()); useEffect(() => { - const doc = new Y.Doc(); + if (!pages) return; + + const yMap = ydoc.getMap("title"); + + pages.forEach((page) => { + if (yMap.get(`title_${page.id}`)) return; + + const yText = new Y.Text(); + yText.insert(0, page.title); + + yMap.set(`title_${page.id}`, yText); + }); + }, [pages]); + + useEffect(() => { + if (!ydoc) return; + const wsProvider = new WebsocketProvider( import.meta.env.VITE_WS_URL, "flow-room", - doc, + ydoc, ); - ydoc.current = doc; provider.current = wsProvider; - const nodesMap = doc.getMap("nodes"); - const edgesMap = doc.getMap("edges"); + const nodesMap = ydoc.getMap("nodes"); + const edgesMap = ydoc.getMap("edges"); nodesMap.observe((event) => { event.changes.keys.forEach((change, key) => { @@ -86,14 +103,14 @@ export default function Canvas({ className }: CanvasProps) { return () => { wsProvider.destroy(); - doc.destroy(); + ydoc.destroy(); }; - }, [queryClient]); + }, [ydoc, queryClient]); useEffect(() => { - if (!pages || !ydoc.current) return; + if (!pages || !ydoc) return; - const nodesMap = ydoc.current.getMap("nodes"); + const nodesMap = ydoc.getMap("nodes"); const currentPageIds = new Set(pages.map((page) => page.id.toString())); existingPageIds.current.forEach((pageId) => { @@ -105,27 +122,27 @@ export default function Canvas({ className }: CanvasProps) { pages.forEach((page) => { const pageId = page.id.toString(); - if (!existingPageIds.current.has(pageId)) { - const newNode = { - id: pageId, - position: { - x: Math.random() * 500, - y: Math.random() * 500, - }, - data: { title: page.title, id: page.id }, - type: "note", - }; - - nodesMap.set(pageId, newNode); - existingPageIds.current.add(pageId); - } + //if (!existingPageIds.current.has(pageId)) { + const newNode = { + id: pageId, + position: { + x: Math.random() * 500, + y: Math.random() * 500, + }, + data: { title: page.title, id: page.id }, + type: "note", + }; + + nodesMap.set(pageId, newNode); + existingPageIds.current.add(pageId); + //} }); }, [pages]); const handleNodesChange = useCallback( (changes: NodeChange[]) => { - if (!ydoc.current) return; - const nodesMap = ydoc.current.getMap("nodes"); + if (!ydoc) return; + const nodesMap = ydoc.getMap("nodes"); onNodesChange(changes); @@ -147,8 +164,8 @@ export default function Canvas({ className }: CanvasProps) { const handleEdgesChange = useCallback( (changes: EdgeChange[]) => { - if (!ydoc.current) return; - const edgesMap = ydoc.current.getMap("edges"); + if (!ydoc) return; + const edgesMap = ydoc.getMap("edges"); changes.forEach((change) => { if (change.type === "remove") { @@ -163,7 +180,7 @@ export default function Canvas({ className }: CanvasProps) { const onConnect = useCallback( (connection: Connection) => { - if (!connection.source || !connection.target || !ydoc.current) return; + if (!connection.source || !connection.target || !ydoc) return; const newEdge: Edge = { id: `e${connection.source}-${connection.target}`, @@ -173,7 +190,7 @@ export default function Canvas({ className }: CanvasProps) { targetHandle: connection.targetHandle || undefined, }; - ydoc.current.getMap("edges").set(newEdge.id, newEdge); + ydoc.getMap("edges").set(newEdge.id, newEdge); setEdges((eds) => addEdge(connection, eds)); }, [setEdges], diff --git a/frontend/src/components/editor/EditorTitle.tsx b/frontend/src/components/editor/EditorTitle.tsx index 66e1cc50..a6687325 100644 --- a/frontend/src/components/editor/EditorTitle.tsx +++ b/frontend/src/components/editor/EditorTitle.tsx @@ -1,19 +1,39 @@ +import useYDocStore from "@/store/useYDocStore"; +import { useYText } from "@/hooks/useYText"; +import { useOptimisticUpdatePage } from "@/hooks/usePages"; +import { JSONContent } from "novel"; + interface EditorTitleProps { - title?: string; - onTitleChange?: (e: React.ChangeEvent) => void; + currentPage: number; + pageContent: JSONContent; } export default function EditorTitle({ - title, - onTitleChange, + currentPage, + pageContent, }: EditorTitleProps) { + const { ydoc } = useYDocStore(); + const { input, setYText } = useYText(ydoc, currentPage); + + const optimisticUpdatePageMutation = useOptimisticUpdatePage({ + id: currentPage ?? 0, + }); + + const handleTitleChange = (e: React.ChangeEvent) => { + setYText(e.target.value); + + optimisticUpdatePageMutation.mutate({ + pageData: { title: e.target.value, content: pageContent }, + }); + }; + return (
); diff --git a/frontend/src/hooks/useYText.ts b/frontend/src/hooks/useYText.ts new file mode 100644 index 00000000..410734e3 --- /dev/null +++ b/frontend/src/hooks/useYText.ts @@ -0,0 +1,32 @@ +import { useState } from "react"; +import * as Y from "yjs"; +import diff from "fast-diff"; + +function diffToDelta(diffResult) { + return diffResult.map(([op, value]) => + op === diff.INSERT + ? { insert: value } + : op === diff.EQUAL + ? { retain: value.length } + : op === diff.DELETE + ? { delete: value.length } + : null, + ); +} + +export const useYText = (ydoc: Y.Doc, currentPage: number) => { + const yText = ydoc.getMap("title").get(`title_${currentPage}`) as Y.Text; + + const [input, setInput] = useState(yText.toString()); + + const setYText = (textNew: string) => { + const delta = diffToDelta(diff(input, textNew)); + yText.applyDelta(delta); + }; + + yText.observe(() => { + setInput(yText.toString()); + }); + + return { input, setYText }; +}; diff --git a/frontend/src/store/useYDocStore.ts b/frontend/src/store/useYDocStore.ts new file mode 100644 index 00000000..f103d4b5 --- /dev/null +++ b/frontend/src/store/useYDocStore.ts @@ -0,0 +1,14 @@ +import { create } from "zustand"; +import * as Y from "yjs"; + +interface YDocStore { + ydoc: Y.Doc; + setYDoc: (ydoc: Y.Doc) => void; +} + +const useYDocStore = create((set) => ({ + ydoc: new Y.Doc(), + setYDoc: (ydoc: Y.Doc) => set({ ydoc }), +})); + +export default useYDocStore; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 9bd2b916..121675bc 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2218,6 +2218,11 @@ fast-deep-equal@^3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-diff@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + fast-glob@^3.3.0, fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"