diff --git a/client/src/features/editor/Editor.tsx b/client/src/features/editor/Editor.tsx index 56bec69a..7f9edbdb 100644 --- a/client/src/features/editor/Editor.tsx +++ b/client/src/features/editor/Editor.tsx @@ -21,6 +21,7 @@ import { Block } from "./components/block/Block.tsx"; import { useBlockDragAndDrop } from "./hooks/useBlockDragAndDrop"; import { useBlockOptionSelect } from "./hooks/useBlockOption.ts"; import { useMarkdownGrammer } from "./hooks/useMarkdownGrammer"; +import { useTextOptionSelect } from "./hooks/useTextOptions.ts"; interface EditorProps { onTitleChange: (title: string) => void; @@ -84,6 +85,13 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr sendCharInsertOperation, }); + const { onTextStyleUpdate } = useTextOptionSelect({ + editorCRDT: editorCRDT.current, + setEditorState, + pageId, + sendBlockUpdateOperation, + }); + const handleTitleChange = (e: React.ChangeEvent) => { onTitleChange(e.target.value); }; @@ -109,35 +117,14 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr const [addedChar] = newContent; charNode = block.crdt.localInsert(0, addedChar, block.id, pageId); editorCRDT.current.currentBlock!.crdt.currentCaret = caretPosition; - requestAnimationFrame(() => { - setCaretPosition({ - blockId: block.id, - linkedList: editorCRDT.current.LinkedList, - position: caretPosition, - }); - }); } else if (caretPosition > currentContent.length) { const addedChar = newContent[newContent.length - 1]; charNode = block.crdt.localInsert(currentContent.length, addedChar, block.id, pageId); editorCRDT.current.currentBlock!.crdt.currentCaret = caretPosition; - requestAnimationFrame(() => { - setCaretPosition({ - blockId: block.id, - linkedList: editorCRDT.current.LinkedList, - position: caretPosition, - }); - }); } else { const addedChar = newContent[caretPosition - 1]; charNode = block.crdt.localInsert(caretPosition - 1, addedChar, block.id, pageId); editorCRDT.current.currentBlock!.crdt.currentCaret = caretPosition; - requestAnimationFrame(() => { - setCaretPosition({ - blockId: block.id, - linkedList: editorCRDT.current.LinkedList, - position: caretPosition, - }); - }); } sendCharInsertOperation({ node: charNode.node, blockId: block.id, pageId }); } else if (newContent.length < currentContent.length) { @@ -145,13 +132,6 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr operationNode = block.crdt.localDelete(caretPosition, block.id, pageId); sendCharDeleteOperation(operationNode); editorCRDT.current.currentBlock!.crdt.currentCaret = caretPosition; - requestAnimationFrame(() => { - setCaretPosition({ - blockId: block.id, - linkedList: editorCRDT.current.LinkedList, - position: caretPosition, - }); - }); } setEditorState({ clock: editorCRDT.current.clock, @@ -170,7 +150,8 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr linkedList: editorCRDT.current.LinkedList, position: editorCRDT.current.currentBlock?.crdt.currentCaret, }); - }, [editorCRDT.current.currentBlock?.crdt.read().length]); + // 서윤님 피드백 반영 + }, [editorCRDT.current.currentBlock?.id.serialize()]); useEffect(() => { if (subscriptionRef.current) return; @@ -224,8 +205,6 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr onRemoteBlockUpdate: (operation) => { console.log(operation, "block : 업데이트 확인합니다이"); if (!editorCRDT.current) return; - // ?? - console.log("타입", operation.node); editorCRDT.current.remoteUpdate(operation.node, operation.pageId); setEditorState({ clock: editorCRDT.current.clock, @@ -243,6 +222,18 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr }); }, + onRemoteCharUpdate: (operation) => { + console.log(operation, "char : 업데이트 확인합니다이"); + if (!editorCRDT.current) return; + const targetBlock = + editorCRDT.current.LinkedList.nodeMap[JSON.stringify(operation.blockId)]; + targetBlock.crdt.remoteUpdate(operation); + setEditorState({ + clock: editorCRDT.current.clock, + linkedList: editorCRDT.current.LinkedList, + }); + }, + onRemoteCursor: (position) => { console.log(position, "커서위치 수신"); }, @@ -295,6 +286,7 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr onTypeSelect={handleTypeSelect} onCopySelect={handleCopySelect} onDeleteSelect={handleDeleteSelect} + onTextStyleUpdate={onTextStyleUpdate} /> ))} diff --git a/client/src/features/editor/components/block/Block.tsx b/client/src/features/editor/components/block/Block.tsx index e9fdfb9b..cc75de0c 100644 --- a/client/src/features/editor/components/block/Block.tsx +++ b/client/src/features/editor/components/block/Block.tsx @@ -1,13 +1,15 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { AnimationType, ElementType } from "@noctaCrdt/Interfaces"; -import { Block as CRDTBlock } from "@noctaCrdt/Node"; +import { AnimationType, ElementType, TextStyleType } from "@noctaCrdt/Interfaces"; +import { Block as CRDTBlock, Char } from "@noctaCrdt/Node"; import { BlockId } from "@noctaCrdt/NodeId"; import { motion } from "framer-motion"; -import { memo, useRef } from "react"; +import { memo, useRef, useState } from "react"; +import { useModal } from "@src/components/modal/useModal"; import { useBlockAnimation } from "../../hooks/useBlockAnimtaion"; import { IconBlock } from "../IconBlock/IconBlock"; import { MenuBlock } from "../MenuBlock/MenuBlock"; +import { TextOptionModal } from "../TextOptionModal/TextOptionModal"; import { blockAnimation } from "./Block.animation"; import { textContainerStyle, blockContainerStyle, contentWrapperStyle } from "./Block.style"; @@ -22,8 +24,8 @@ interface BlockProps { onTypeSelect: (blockId: BlockId, type: ElementType) => void; onCopySelect: (blockId: BlockId) => void; onDeleteSelect: (blockId: BlockId) => void; + onTextStyleUpdate: (styleType: TextStyleType, blockId: BlockId, nodes: Array) => void; } - export const Block: React.FC = memo( ({ id, @@ -36,11 +38,12 @@ export const Block: React.FC = memo( onTypeSelect, onCopySelect, onDeleteSelect, + onTextStyleUpdate, }: BlockProps) => { - console.log("블록 초기화 상태", block); const blockRef = useRef(null); const blockCRDTRef = useRef(block); - + const { isOpen, openModal, closeModal } = useModal(); + const [selectedNodes, setSelectedNodes] = useState | null>(null); const { isAnimationStart } = useBlockAnimation(blockRef); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, @@ -70,6 +73,39 @@ export const Block: React.FC = memo( onDeleteSelect(block.id); }; + const handleMouseUp = () => { + const selection = window.getSelection(); + if (!selection || selection.isCollapsed || !blockRef.current) { + setSelectedNodes(null); + closeModal(); + return; + } + + const range = selection.getRangeAt(0); + if (!blockRef.current.contains(range.commonAncestorContainer)) { + setSelectedNodes(null); + closeModal(); + return; + } + + const nodes = blockCRDTRef.current.crdt.LinkedList.getNodesBetween( + range.startOffset, + range.endOffset, + ); + + if (nodes.length > 0) { + setSelectedNodes(nodes); + openModal(); + } + }; + + const handleStyleSelect = (styleType: TextStyleType) => { + if (selectedNodes) { + onTextStyleUpdate(styleType, block.id, selectedNodes); + closeModal(); + } + }; + return ( // TODO: eslint 규칙을 수정해야 할까? // TODO: ol일때 index 순서 처리 @@ -103,6 +139,7 @@ export const Block: React.FC = memo( onKeyDown={onKeyDown} onInput={handleInput} onClick={() => onClick(block.id)} + onMouseUp={handleMouseUp} contentEditable suppressContentEditableWarning className={textContainerStyle({ @@ -112,6 +149,14 @@ export const Block: React.FC = memo( {blockCRDTRef.current.crdt.read()} + handleStyleSelect("bold")} + onItalicSelect={() => handleStyleSelect("italic")} + onUnderlineSelect={() => handleStyleSelect("underline")} + onStrikeSelect={() => handleStyleSelect("strikethrough")} + /> ); },