diff --git a/@noctaCrdt/Crdt.ts b/@noctaCrdt/Crdt.ts index ae7686ca..34812d0c 100644 --- a/@noctaCrdt/Crdt.ts +++ b/@noctaCrdt/Crdt.ts @@ -7,7 +7,7 @@ import { RemoteBlockInsertOperation, RemoteCharInsertOperation, CRDTSerializedProps, - RemoteReorderOperation, + RemoteBlockReorderOperation, RemoteBlockUpdateOperation, } from "./Interfaces"; @@ -149,20 +149,25 @@ export class EditorCRDT extends CRDT { targetId: BlockId; beforeId: BlockId | null; afterId: BlockId | null; - }): RemoteReorderOperation { - const operation: RemoteReorderOperation = { + pageId: string; + }): RemoteBlockReorderOperation { + const operation: RemoteBlockReorderOperation = { ...params, clock: this.clock, client: this.client, }; - this.LinkedList.reorderNodes(params); + this.LinkedList.reorderNodes({ + targetId: params.targetId, + beforeId: params.beforeId, + afterId: params.afterId, + }); this.clock += 1; return operation; } - remoteReorder(operation: RemoteReorderOperation): void { + remoteReorder(operation: RemoteBlockReorderOperation): void { const { targetId, beforeId, afterId, clock } = operation; this.LinkedList.reorderNodes({ @@ -171,9 +176,7 @@ export class EditorCRDT extends CRDT { afterId, }); - if (this.clock <= clock) { - this.clock = clock + 1; - } + this.clock = Math.max(this.clock, clock) + 1; } serialize(): CRDTSerializedProps { diff --git a/@noctaCrdt/Interfaces.ts b/@noctaCrdt/Interfaces.ts index ecee570d..d8762ced 100644 --- a/@noctaCrdt/Interfaces.ts +++ b/@noctaCrdt/Interfaces.ts @@ -93,10 +93,11 @@ export interface WorkSpaceSerializedProps { pageList: Page[]; authUser: Map; } -export interface RemoteReorderOperation { +export interface RemoteBlockReorderOperation { targetId: BlockId; beforeId: BlockId | null; afterId: BlockId | null; clock: number; client: number; + pageId: string; } diff --git a/client/src/features/editor/Editor.tsx b/client/src/features/editor/Editor.tsx index c9790bf5..f8424eb7 100644 --- a/client/src/features/editor/Editor.tsx +++ b/client/src/features/editor/Editor.tsx @@ -28,16 +28,6 @@ export interface EditorStateProps { } // TODO: pageId, editorCRDT를 props로 받아와야함 export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorProps) => { - /* - const { - sendCharInsertOperation, - sendCharDeleteOperation, - subscribeToRemoteOperations, - sendBlockInsertOperation, - sendBlockDeleteOperation, - sendBlockUpdateOperation, - } = useSocket(); - */ const { sendCharInsertOperation, sendCharDeleteOperation, @@ -62,6 +52,7 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr editorCRDT: editorCRDT.current, editorState, setEditorState, + pageId, }); const { handleKeyDown } = useMarkdownGrammer({ @@ -222,6 +213,18 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr currentBlock: prev.currentBlock, })); }, + + onRemoteBlockReorder: (operation) => { + console.log(operation, "block : 재정렬 확인합니다이"); + if (!editorCRDT.current) return; + editorCRDT.current.remoteReorder(operation); + setEditorState((prev) => ({ + clock: editorCRDT.current.clock, + linkedList: editorCRDT.current.LinkedList, + currentBlock: prev.currentBlock, + })); + }, + onRemoteCursor: (position) => { console.log(position, "커서위치 수신"); }, diff --git a/client/src/features/editor/hooks/useBlockDragAndDrop.ts b/client/src/features/editor/hooks/useBlockDragAndDrop.ts index 04e4d45b..61c35eb9 100644 --- a/client/src/features/editor/hooks/useBlockDragAndDrop.ts +++ b/client/src/features/editor/hooks/useBlockDragAndDrop.ts @@ -1,18 +1,21 @@ // hooks/useBlockDragAndDrop.ts import { DragEndEvent, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; import { EditorCRDT } from "@noctaCrdt/Crdt"; +import { useSocketStore } from "@src/stores/useSocketStore.ts"; import { EditorStateProps } from "../Editor"; interface UseBlockDragAndDropProps { editorCRDT: EditorCRDT; editorState: EditorStateProps; setEditorState: React.Dispatch>; + pageId: string; } export const useBlockDragAndDrop = ({ editorCRDT, editorState, setEditorState, + pageId, }: UseBlockDragAndDropProps) => { const sensors = useSensors( useSensor(PointerSensor, { @@ -22,6 +25,8 @@ export const useBlockDragAndDrop = ({ }), ); + const { sendBlockReorderOperation } = useSocketStore(); + const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; @@ -73,12 +78,15 @@ export const useBlockDragAndDrop = ({ } // EditorCRDT의 현재 상태로 작업 - editorCRDT.localReorder({ + const operation = editorCRDT.localReorder({ targetId: targetNode.id, beforeId: beforeNode?.id || null, afterId: afterNode?.id || null, + pageId, }); + sendBlockReorderOperation(operation); + // EditorState 업데이트 setEditorState({ clock: editorCRDT.clock, diff --git a/client/src/stores/useSocketStore.ts b/client/src/stores/useSocketStore.ts index 62bd6989..ec5f8a03 100644 --- a/client/src/stores/useSocketStore.ts +++ b/client/src/stores/useSocketStore.ts @@ -5,6 +5,7 @@ import { RemoteCharInsertOperation, RemoteCharDeleteOperation, RemoteBlockUpdateOperation, + RemoteBlockReorderOperation, CursorPosition, WorkSpaceSerializedProps, } from "@noctaCrdt/Interfaces"; @@ -24,6 +25,7 @@ interface SocketStore { sendCharInsertOperation: (operation: RemoteCharInsertOperation) => void; sendBlockDeleteOperation: (operation: RemoteBlockDeleteOperation) => void; sendCharDeleteOperation: (operation: RemoteCharDeleteOperation) => void; + sendBlockReorderOperation: (operation: RemoteBlockReorderOperation) => void; sendCursorPosition: (position: CursorPosition) => void; subscribeToRemoteOperations: (handlers: RemoteOperationHandlers) => (() => void) | undefined; subscribeToPageOperations: (handlers: PageOperationsHandlers) => (() => void) | undefined; @@ -34,6 +36,7 @@ interface RemoteOperationHandlers { onRemoteBlockUpdate: (operation: RemoteBlockUpdateOperation) => void; onRemoteBlockInsert: (operation: RemoteBlockInsertOperation) => void; onRemoteBlockDelete: (operation: RemoteBlockDeleteOperation) => void; + onRemoteBlockReorder: (operation: RemoteBlockReorderOperation) => void; onRemoteCharInsert: (operation: RemoteCharInsertOperation) => void; onRemoteCharDelete: (operation: RemoteCharDeleteOperation) => void; onRemoteCursor: (position: CursorPosition) => void; @@ -146,6 +149,11 @@ export const useSocketStore = create((set, get) => ({ socket?.emit("cursor", position); }, + sendBlockReorderOperation: (operation: RemoteBlockReorderOperation) => { + const { socket } = get(); + socket?.emit("reorder/block", operation); + }, + subscribeToRemoteOperations: (handlers: RemoteOperationHandlers) => { const { socket } = get(); if (!socket) return; @@ -153,6 +161,7 @@ export const useSocketStore = create((set, get) => ({ socket.on("update/block", handlers.onRemoteBlockUpdate); socket.on("insert/block", handlers.onRemoteBlockInsert); socket.on("delete/block", handlers.onRemoteBlockDelete); + socket.on("reorder/block", handlers.onRemoteBlockReorder); socket.on("insert/char", handlers.onRemoteCharInsert); socket.on("delete/char", handlers.onRemoteCharDelete); socket.on("cursor", handlers.onRemoteCursor); @@ -161,6 +170,7 @@ export const useSocketStore = create((set, get) => ({ socket.off("update/block", handlers.onRemoteBlockUpdate); socket.off("insert/block", handlers.onRemoteBlockInsert); socket.off("delete/block", handlers.onRemoteBlockDelete); + socket.off("reorder/block", handlers.onRemoteBlockReorder); socket.off("insert/char", handlers.onRemoteCharInsert); socket.off("delete/char", handlers.onRemoteCharDelete); socket.off("cursor", handlers.onRemoteCursor); diff --git a/server/src/crdt/crdt.gateway.ts b/server/src/crdt/crdt.gateway.ts index 23c354b1..e4e6d9ad 100644 --- a/server/src/crdt/crdt.gateway.ts +++ b/server/src/crdt/crdt.gateway.ts @@ -17,6 +17,7 @@ import { RemoteCharInsertOperation, RemoteBlockUpdateOperation, RemotePageCreateOperation, + RemoteBlockReorderOperation, CursorPosition, } from "@noctaCrdt/Interfaces"; import { Logger } from "@nestjs/common"; @@ -362,6 +363,43 @@ export class CrdtGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa } } + @SubscribeMessage("reorder/block") + async handleBlockReorder( + @MessageBody() data: RemoteBlockReorderOperation, + @ConnectedSocket() client: Socket, + ): Promise { + const clientInfo = this.clientMap.get(client.id); + try { + this.logger.debug( + `블록 Reorder 연산 수신 - Client ID: ${clientInfo?.clientId}, Data:`, + JSON.stringify(data), + ); + // 1. 워크스페이스 가져오기 + const workspace = this.workSpaceService.getWorkspace(); + + const currentPage = workspace.pageList.find((p) => p.id === data.pageId); + if (!currentPage) { + throw new Error(`Page with id ${data.pageId} not found`); + } + currentPage.crdt.remoteReorder(data); + + // 5. 다른 클라이언트들에게 업데이트된 블록 정보 브로드캐스트 + const operation = { + targetId: data.targetId, + beforeId: data.beforeId, + afterId: data.afterId, + pageId: data.pageId, + } as RemoteBlockReorderOperation; + client.broadcast.emit("reorder/block", operation); + } catch (error) { + this.logger.error( + `블록 Reorder 연산 처리 중 오류 발생 - Client ID: ${clientInfo?.clientId}`, + error.stack, + ); + throw new WsException(`Update 연산 실패: ${error.message}`); + } + } + /** * 커서 위치 업데이트 처리 */