From c8cbcfbdbc6498d4e720bcdd40b2b10ea5b18ca6 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Thu, 5 Dec 2024 17:09:58 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20leave/page=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=EA=B1=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #306 --- client/src/features/workSpace/hooks/usePagesManage.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/features/workSpace/hooks/usePagesManage.ts b/client/src/features/workSpace/hooks/usePagesManage.ts index 9977da5..a26984a 100644 --- a/client/src/features/workSpace/hooks/usePagesManage.ts +++ b/client/src/features/workSpace/hooks/usePagesManage.ts @@ -7,10 +7,10 @@ import { import { Page as CRDTPage } from "@noctaCrdt/Page"; import { WorkSpace } from "@noctaCrdt/WorkSpace"; import { useEffect, useState, useRef, useCallback } from "react"; +import { PAGE, SIDE_BAR } from "@src/constants/size"; import { useSocketStore } from "@src/stores/useSocketStore"; import { useToastStore } from "@src/stores/useToastStore"; import { Page } from "@src/types/page"; -import { PAGE, SIDE_BAR } from "@src/constants/size"; const PAGE_OFFSET = 60; @@ -242,6 +242,11 @@ export const usePagesManage = (workspace: WorkSpace | null, clientId: number | n page.id === pageId ? { ...page, isVisible: false, isLoaded: false } : page, ), ); + // Socket.IO를 통해 서버에 페이지 퇴장 알림 + const socketStore = useSocketStore.getState(); + if (socketStore.socket) { + socketStore.socket.emit("leave/page", { pageId }); + } }; const updatePage = ( pageId: string, From 4589340edcac1b6dc31466eb42e63bd7e199934a Mon Sep 17 00:00:00 2001 From: Ludovico7 Date: Thu, 5 Dec 2024 17:21:03 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=EB=A7=88=ED=81=AC=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=20=EB=AC=B8=EB=B2=95=20=EB=B6=99=EC=97=AC=EB=84=A3=EA=B8=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/editor/hooks/useCopyAndPaste.ts | 97 +++++++++++-------- 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/client/src/features/editor/hooks/useCopyAndPaste.ts b/client/src/features/editor/hooks/useCopyAndPaste.ts index acc8d8a..e5381e9 100644 --- a/client/src/features/editor/hooks/useCopyAndPaste.ts +++ b/client/src/features/editor/hooks/useCopyAndPaste.ts @@ -128,7 +128,7 @@ export const useCopyAndPaste = ({ editorCRDT.currentBlock!.crdt.currentCaret = caretPosition + metadata.length; } else { - const text = e.clipboardData.getData("text/plain"); + let text = e.clipboardData.getData("text/plain"); if (!block || text.length === 0) return; const caretPosition = block.crdt.currentCaret; @@ -138,52 +138,58 @@ export const useCopyAndPaste = ({ (b) => b.id === block.id, ); const textList = text.split("\n"); + let prevQuote = false; textList.forEach((line, index) => { - if (index === 0) { - line.split("").forEach((char, index) => { - const charNode = block.crdt.localInsert(index, char, block.id, pageId); - sendCharInsertOperation({ - type: "charInsert", - node: charNode.node, - blockId: block.id, - pageId, - }); - }); - const isMarkdownGrammer = checkMarkdownPattern(line); - if (isMarkdownGrammer && block.type === "p") { - block.type = isMarkdownGrammer.type; - sendBlockUpdateOperation(editorCRDT.localUpdate(block, pageId)); - for (let i = 0; i < isMarkdownGrammer.length; i++) { - sendCharDeleteOperation(block.crdt.localDelete(0, block.id, pageId)); - } + let currentLine = line; + if (currentLine.startsWith("- [ ]")) { + currentLine = currentLine.slice(2); + } + const newBlock = editorCRDT.localInsert(currentBlockIndex, ""); + sendBlockInsertOperation({ + type: "blockInsert", + node: newBlock.node, + pageId, + }); + line.split("").forEach((char, index) => { + sendCharInsertOperation( + newBlock.node.crdt.localInsert(index, char, newBlock.node.id, pageId), + ); + }); + const isMarkdownGrammer = checkMarkdownPattern(currentLine); + if (isMarkdownGrammer && newBlock.node.type === "p") { + let markdownText = isMarkdownGrammer.length; + if (isMarkdownGrammer.type === "blockquote" && prevQuote === false) { + prevQuote = true; } - } else { - const newBlock = editorCRDT.localInsert(currentBlockIndex, ""); - sendBlockInsertOperation({ - type: "blockInsert", - node: newBlock.node, - pageId, - }); - line.split("").forEach((char, index) => { - sendCharInsertOperation( - newBlock.node.crdt.localInsert(index, char, newBlock.node.id, pageId), + if ( + isMarkdownGrammer.type === "blockquote" && + prevQuote === true && + currentLine === ">" + ) { + prevQuote = false; + return; + } + if (isMarkdownGrammer.type === "checkbox" && currentLine.startsWith("[ ]")) { + markdownText += 1; + } + newBlock.node.type = isMarkdownGrammer.type; + sendBlockUpdateOperation(editorCRDT.localUpdate(newBlock.node, pageId)); + for (let i = 0; i <= markdownText; i++) { + sendCharDeleteOperation( + newBlock.node.crdt.localDelete(0, newBlock.node.id, pageId), ); - }); - const isMarkdownGrammer = checkMarkdownPattern(line); - if (isMarkdownGrammer && newBlock.node.type === "p") { - newBlock.node.type = isMarkdownGrammer.type; - sendBlockUpdateOperation(editorCRDT.localUpdate(newBlock.node, pageId)); - for (let i = 0; i < isMarkdownGrammer.length; i++) { - sendCharDeleteOperation( - newBlock.node.crdt.localDelete(0, newBlock.node.id, pageId), - ); - } } } currentBlockIndex += 1; }); editorCRDT.LinkedList.updateAllOrderedListIndices(); } else { + let grammarCount = 0; + if (text.startsWith("- [ ]")) { + console.log("checkbox"); + text = text.slice(2); + } + console.log(text); // 텍스트를 한 글자씩 순차적으로 삽입 text.split("").forEach((char, index) => { const insertPosition = caretPosition + index; @@ -195,8 +201,21 @@ export const useCopyAndPaste = ({ pageId, }); }); + const isMarkdownGrammer = checkMarkdownPattern(text); + if (isMarkdownGrammer && block.type === "p") { + block.type = isMarkdownGrammer.type; + let markdownText = isMarkdownGrammer.length; + sendBlockUpdateOperation(editorCRDT.localUpdate(block, pageId)); + if (isMarkdownGrammer.type === "checkbox" && text.startsWith("[ ]")) { + markdownText += 1; + } + for (let i = 0; i < markdownText; i++) { + sendCharDeleteOperation(block.crdt.localDelete(0, block.id, pageId)); + grammarCount += 1; + } + } // 캐럿 위치 업데이트 - editorCRDT.currentBlock!.crdt.currentCaret = caretPosition + text.length; + editorCRDT.currentBlock!.crdt.currentCaret = caretPosition + text.length - grammarCount; } } From e16e731561816224f250f01ac8be6b027d6a51a7 Mon Sep 17 00:00:00 2001 From: Ludovico7 Date: Thu, 5 Dec 2024 17:51:08 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=B8=EB=8D=B4=ED=8A=B8=20=EA=B0=99=EC=9D=B4=20=EB=B6=99?= =?UTF-8?q?=EC=97=AC=EB=84=A3=EA=B8=B0=20=EA=B0=80=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/editor/hooks/useCopyAndPaste.ts | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/client/src/features/editor/hooks/useCopyAndPaste.ts b/client/src/features/editor/hooks/useCopyAndPaste.ts index e5381e9..f7c9c71 100644 --- a/client/src/features/editor/hooks/useCopyAndPaste.ts +++ b/client/src/features/editor/hooks/useCopyAndPaste.ts @@ -73,6 +73,14 @@ export const useCopyAndPaste = ({ [], ); + const indentChecker = (text: string) => { + const indent = text.match(/^(?:( {3})|( {6})|( {9}))/); + if (indent) { + return indent[0].length / 3; + } + return 0; + }; + const handlePaste = useCallback( (e: React.ClipboardEvent, blockRef: HTMLDivElement | null, block: Block) => { e.preventDefault(); @@ -138,19 +146,23 @@ export const useCopyAndPaste = ({ (b) => b.id === block.id, ); const textList = text.split("\n"); - let prevQuote = false; - textList.forEach((line, index) => { + textList.forEach((line) => { + if (line.length === 0) return; let currentLine = line; + const newBlock = editorCRDT.localInsert(currentBlockIndex, ""); if (currentLine.startsWith("- [ ]")) { currentLine = currentLine.slice(2); } - const newBlock = editorCRDT.localInsert(currentBlockIndex, ""); + if (indentChecker(currentLine) > 0) { + newBlock.node.indent = indentChecker(currentLine); + currentLine = currentLine.slice(indentChecker(currentLine) * 3 + 1); + } sendBlockInsertOperation({ type: "blockInsert", node: newBlock.node, pageId, }); - line.split("").forEach((char, index) => { + currentLine.split("").forEach((char, index) => { sendCharInsertOperation( newBlock.node.crdt.localInsert(index, char, newBlock.node.id, pageId), ); @@ -158,17 +170,6 @@ export const useCopyAndPaste = ({ const isMarkdownGrammer = checkMarkdownPattern(currentLine); if (isMarkdownGrammer && newBlock.node.type === "p") { let markdownText = isMarkdownGrammer.length; - if (isMarkdownGrammer.type === "blockquote" && prevQuote === false) { - prevQuote = true; - } - if ( - isMarkdownGrammer.type === "blockquote" && - prevQuote === true && - currentLine === ">" - ) { - prevQuote = false; - return; - } if (isMarkdownGrammer.type === "checkbox" && currentLine.startsWith("[ ]")) { markdownText += 1; } @@ -186,10 +187,8 @@ export const useCopyAndPaste = ({ } else { let grammarCount = 0; if (text.startsWith("- [ ]")) { - console.log("checkbox"); text = text.slice(2); } - console.log(text); // 텍스트를 한 글자씩 순차적으로 삽입 text.split("").forEach((char, index) => { const insertPosition = caretPosition + index; From be88b84cd25635d9b41e90c0987df761d8b99c99 Mon Sep 17 00:00:00 2001 From: Ludovico7 Date: Thu, 5 Dec 2024 23:38:02 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80?= =?UTF-8?q?=20uncaught=20error=20=EC=95=88=EB=82=98=EC=98=A4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jang seo yun Co-authored-by: hyonun --- client/src/utils/caretUtils.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/client/src/utils/caretUtils.ts b/client/src/utils/caretUtils.ts index 42d47f4..ece233e 100644 --- a/client/src/utils/caretUtils.ts +++ b/client/src/utils/caretUtils.ts @@ -142,7 +142,19 @@ export const setCaretPosition = ({ } const range = document.createRange(); - range.setStart(targetNode, targetOffset); + try { + range.setStart(targetNode, targetOffset); + } catch (rangeError) { + // setStart에 실패한 경우, 첫 번째 텍스트 노드를 찾아서 position 위치에 캐럿 설정 + const firstTextNode = walker.firstChild(); + if (firstTextNode) { + const textLength = firstTextNode.textContent?.length || 0; + range.setStart(firstTextNode, Math.min(position, textLength)); + } else { + // 텍스트 노드가 없는 경우 편집 가능한 div의 시작점에 설정 + range.setStart(editableDiv, 0); + } + } range.collapse(true); selection.removeAllRanges();