From 60e5b7c6f5c2b605735347597cb19c03d33e9a27 Mon Sep 17 00:00:00 2001 From: pipisebastian Date: Tue, 3 Dec 2024 01:05:09 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20blokc=20interface=20isChecked=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #270 --- @noctaCrdt/Node.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/@noctaCrdt/Node.ts b/@noctaCrdt/Node.ts index d7220560..c0f6104e 100644 --- a/@noctaCrdt/Node.ts +++ b/@noctaCrdt/Node.ts @@ -51,6 +51,7 @@ export class Block extends Node { icon: string; crdt: BlockCRDT; listIndex?: number; + isChecked?: boolean; constructor(value: string, id: BlockId) { super(value, id); @@ -72,6 +73,7 @@ export class Block extends Node { icon: this.icon, crdt: this.crdt.serialize(), listIndex: this.listIndex ? this.listIndex : null, + isChecked: this.isChecked ? this.isChecked : null, }; } @@ -87,6 +89,7 @@ export class Block extends Node { block.icon = data.icon; block.crdt = BlockCRDT.deserialize(data.crdt); block.listIndex = data.listIndex ? data.listIndex : null; + block.isChecked = data.isChecked ? data.isChecked : null; return block; } } From 86c398dfe5114d05e2f9a348a15666565388fc80 Mon Sep 17 00:00:00 2001 From: pipisebastian Date: Tue, 3 Dec 2024 01:05:47 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20remote=20checkbox=20interface=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #270 --- @noctaCrdt/Interfaces.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/@noctaCrdt/Interfaces.ts b/@noctaCrdt/Interfaces.ts index 63fba8a8..82659583 100644 --- a/@noctaCrdt/Interfaces.ts +++ b/@noctaCrdt/Interfaces.ts @@ -113,6 +113,13 @@ export interface RemoteBlockDeleteOperation { pageId: string; } +export interface RemoteBlockCheckboxOperation { + type: "blockCheckbox"; + blockId: BlockId; + isChecked: boolean; + pageId: string; +} + export interface RemoteCharDeleteOperation { type: "charDelete"; targetId: CharId; From 4ef910ec38d1ee39e25297e25bf7c0994ab9785a Mon Sep 17 00:00:00 2001 From: pipisebastian Date: Tue, 3 Dec 2024 01:06:42 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=EC=84=9C=EB=B2=84=20remote=20check?= =?UTF-8?q?box=20gateway=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #270 --- server/src/workspace/workspace.gateway.ts | 45 +++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/server/src/workspace/workspace.gateway.ts b/server/src/workspace/workspace.gateway.ts index b8351f6e..80c45c8a 100644 --- a/server/src/workspace/workspace.gateway.ts +++ b/server/src/workspace/workspace.gateway.ts @@ -20,6 +20,7 @@ import { RemoteBlockUpdateOperation, RemotePageCreateOperation, RemoteBlockReorderOperation, + RemoteBlockCheckboxOperation, RemoteCharUpdateOperation, CursorPosition, } from "@noctaCrdt/Interfaces"; @@ -669,6 +670,50 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG } } + /** + * 블록 Checkbox 연산 처리 + */ + @SubscribeMessage("checkbox/block") + async handleBlockCheckbox( + @MessageBody() data: RemoteBlockCheckboxOperation, + @ConnectedSocket() client: Socket, + ): Promise { + const clientInfo = this.clientMap.get(client.id); + try { + this.logger.debug( + `Block checkbox 연산 수신 - Client ID: ${clientInfo?.clientId}, Data:`, + JSON.stringify(data), + ); + const { workspaceId } = client.data; + const currentBlock = await this.workSpaceService.getBlock( + workspaceId, + data.pageId, + data.blockId, + ); + + if (!currentBlock) { + throw new Error(`Block with id ${data.blockId} not found`); + } + + currentBlock.isChecked = data.isChecked; + + const operation = { + type: "blockCheckbox", + blockId: data.blockId, + pageId: data.pageId, + isChecked: data.isChecked, + }; + + client.broadcast.to(data.pageId).emit("checkbox/block", operation); + } catch (error) { + this.logger.error( + `Block Checkbox 연산 처리 중 오류 발생 - Client ID: ${clientInfo?.clientId}`, + error.stack, + ); + throw new WsException(`Checkbox 연산 실패: ${error.message}`); + } + } + /** * 글자 삽입 연산 처리 */ From a618f79ffb25799fbc62fe7078087a639d6e8594 Mon Sep 17 00:00:00 2001 From: pipisebastian Date: Tue, 3 Dec 2024 01:07:22 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=20remote=20checkbox=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #270 --- client/src/stores/useSocketStore.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/src/stores/useSocketStore.ts b/client/src/stores/useSocketStore.ts index b5720f98..70f5a0ce 100644 --- a/client/src/stores/useSocketStore.ts +++ b/client/src/stores/useSocketStore.ts @@ -6,6 +6,7 @@ import { RemoteCharDeleteOperation, RemoteBlockUpdateOperation, RemoteBlockReorderOperation, + RemoteBlockCheckboxOperation, RemoteCharUpdateOperation, RemotePageDeleteOperation, RemotePageUpdateOperation, @@ -77,6 +78,7 @@ interface SocketStore { subscribeToPageOperations: (handlers: PageOperationsHandlers) => (() => void) | undefined; setWorkspace: (workspace: WorkSpaceSerializedProps) => void; sendOperation: (operation: any) => void; + sendBlockCheckboxOperation: (operation: RemoteBlockCheckboxOperation) => void; } interface RemoteOperationHandlers { @@ -89,6 +91,7 @@ interface RemoteOperationHandlers { onRemoteCharUpdate: (operation: RemoteCharUpdateOperation) => void; onRemoteCursor: (position: CursorPosition) => void; onBatchOperations: (batch: any[]) => void; + onRemoteBlockCheckbox: (operation: RemoteBlockCheckboxOperation) => void; } interface PageOperationsHandlers { @@ -274,6 +277,11 @@ export const useSocketStore = create((set, get) => ({ // sendOperation(operation); }, + sendBlockCheckboxOperation: (operation: RemoteBlockCheckboxOperation) => { + const { socket } = get(); + socket?.emit("checkbox/block", operation); + }, + subscribeToRemoteOperations: (handlers: RemoteOperationHandlers) => { const { socket } = get(); if (!socket) return; @@ -287,6 +295,7 @@ export const useSocketStore = create((set, get) => ({ socket.on("update/char", handlers.onRemoteCharUpdate); socket.on("cursor", handlers.onRemoteCursor); socket.on("batch/operations", handlers.onBatchOperations); + socket.on("checkbox/block", handlers.onRemoteBlockCheckbox); return () => { socket.off("update/block", handlers.onRemoteBlockUpdate); @@ -298,6 +307,7 @@ export const useSocketStore = create((set, get) => ({ socket.off("update/char", handlers.onRemoteCharUpdate); socket.off("cursor", handlers.onRemoteCursor); socket.off("batch/operations", handlers.onBatchOperations); + socket.off("checkbox/block", handlers.onRemoteBlockCheckbox); }; }, From 69026b905a38d927398dd000d97be4c20031e66a Mon Sep 17 00:00:00 2001 From: pipisebastian Date: Tue, 3 Dec 2024 01:07:38 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=EC=B2=B4=ED=81=AC=EB=B0=95?= =?UTF-8?q?=EC=8A=A4=20ui=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #270 --- .../components/IconBlock/IconBlock.style.ts | 10 +++++++++ .../editor/components/IconBlock/IconBlock.tsx | 22 +++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/client/src/features/editor/components/IconBlock/IconBlock.style.ts b/client/src/features/editor/components/IconBlock/IconBlock.style.ts index be2c8720..2a95384b 100644 --- a/client/src/features/editor/components/IconBlock/IconBlock.style.ts +++ b/client/src/features/editor/components/IconBlock/IconBlock.style.ts @@ -32,5 +32,15 @@ export const iconStyle = cva({ backgroundColor: "white", }, }, + isChecked: { + true: { + color: "white", + backgroundColor: "#7272FF", + }, + false: { + color: "gray.600", + backgroundColor: "white", + }, + }, }, }); diff --git a/client/src/features/editor/components/IconBlock/IconBlock.tsx b/client/src/features/editor/components/IconBlock/IconBlock.tsx index 70c466db..61c0632e 100644 --- a/client/src/features/editor/components/IconBlock/IconBlock.tsx +++ b/client/src/features/editor/components/IconBlock/IconBlock.tsx @@ -5,9 +5,17 @@ interface IconBlockProps { type: ElementType; index: number | undefined; indent?: number; + isChecked?: boolean; + onCheckboxClick?: () => void; } -export const IconBlock = ({ type, index = 1, indent = 0 }: IconBlockProps) => { +export const IconBlock = ({ + type, + index = 1, + indent = 0, + isChecked = false, + onCheckboxClick, +}: IconBlockProps) => { const getIcon = () => { switch (type) { case "ul": @@ -21,7 +29,17 @@ export const IconBlock = ({ type, index = 1, indent = 0 }: IconBlockProps) => { case "ol": return {`${index}.`}; case "checkbox": - return ; + return ( + + {isChecked ? "✓" : ""} + + ); default: return null; } From e65f8ae67e59cae36abff5dac2e21e1756daec37 Mon Sep 17 00:00:00 2001 From: pipisebastian Date: Tue, 3 Dec 2024 01:07:54 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=EC=B2=B4=ED=81=AC=EB=B0=95?= =?UTF-8?q?=EC=8A=A4=20=ED=86=A0=EA=B8=80=20=EA=B8=B0=EB=8A=A5=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #270 --- client/src/features/editor/Editor.tsx | 22 +++++++++------ .../editor/components/block/Block.tsx | 15 ++++++++++- .../editor/hooks/useBlockOperation.ts | 27 ++++++++++++++++++- .../editor/hooks/useEditorOperation.ts | 17 ++++++++++++ 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/client/src/features/editor/Editor.tsx b/client/src/features/editor/Editor.tsx index fe8e25d9..312adffd 100644 --- a/client/src/features/editor/Editor.tsx +++ b/client/src/features/editor/Editor.tsx @@ -42,6 +42,7 @@ export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData sendBlockInsertOperation, sendBlockDeleteOperation, sendBlockUpdateOperation, + sendBlockCheckboxOperation, } = useSocketStore(); const { clientId } = useSocketStore(); const [displayTitle, setDisplayTitle] = useState(pageTitle); @@ -86,6 +87,7 @@ export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData handleRemoteBlockReorder, handleRemoteCharUpdate, handleRemoteCursor, + handleRemoteBlockCheckbox, addNewBlock, } = useEditorOperation({ editorCRDT, pageId, setEditorState, isSameLocalChange }); @@ -121,14 +123,16 @@ export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData sendCharInsertOperation, }); - const { handleBlockClick, handleBlockInput, handleKeyDown } = useBlockOperation({ - editorCRDT: editorCRDT.current, - setEditorState, - pageId, - onKeyDown, - handleHrInput, - isLocalChange, - }); + const { handleBlockClick, handleBlockInput, handleKeyDown, handleCheckboxToggle } = + useBlockOperation({ + editorCRDT: editorCRDT.current, + setEditorState, + pageId, + onKeyDown, + handleHrInput, + isLocalChange, + sendBlockCheckboxOperation, + }); const { onTextStyleUpdate, onTextColorUpdate, onTextBackgroundColorUpdate } = useTextOptionSelect( { @@ -275,6 +279,7 @@ export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData onRemoteBlockReorder: handleRemoteBlockReorder, onRemoteCharUpdate: handleRemoteCharUpdate, onRemoteCursor: handleRemoteCursor, + onRemoteBlockCheckbox: handleRemoteBlockCheckbox, onBatchOperations: (batch) => { for (const item of batch) { switch (item.event) { @@ -377,6 +382,7 @@ export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData onTextColorUpdate={onTextColorUpdate} onTextBackgroundColorUpdate={onTextBackgroundColorUpdate} dragBlockList={dragBlockList} + onCheckboxToggle={handleCheckboxToggle} /> ))} diff --git a/client/src/features/editor/components/block/Block.tsx b/client/src/features/editor/components/block/Block.tsx index 6d0300e9..5d398792 100644 --- a/client/src/features/editor/components/block/Block.tsx +++ b/client/src/features/editor/components/block/Block.tsx @@ -66,6 +66,7 @@ interface BlockProps { blockId: BlockId, nodes: Array, ) => void; + onCheckboxToggle: (blockId: BlockId, isChecked: boolean) => void; } export const Block: React.FC = memo( ({ @@ -88,6 +89,7 @@ export const Block: React.FC = memo( onTextStyleUpdate, onTextColorUpdate, onTextBackgroundColorUpdate, + onCheckboxToggle, }: BlockProps) => { const blockRef = useRef(null); const { isOpen, openModal, closeModal } = useModal(); @@ -267,6 +269,10 @@ export const Block: React.FC = memo( } }; + const handleCheckboxClick = () => { + onCheckboxToggle(block.id, !block.isChecked); + }; + const Indicator = () => (
= memo( onCopySelect={handleCopySelect} onDeleteSelect={handleDeleteSelect} /> - + +
handleKeyDown(e, blockRef.current, block)} diff --git a/client/src/features/editor/hooks/useBlockOperation.ts b/client/src/features/editor/hooks/useBlockOperation.ts index 34f707b6..240a7324 100644 --- a/client/src/features/editor/hooks/useBlockOperation.ts +++ b/client/src/features/editor/hooks/useBlockOperation.ts @@ -1,5 +1,5 @@ import { EditorCRDT } from "@noctaCrdt/Crdt"; -import { RemoteCharInsertOperation } from "@noctaCrdt/Interfaces"; +import { RemoteBlockCheckboxOperation, RemoteCharInsertOperation } from "@noctaCrdt/Interfaces"; import { Block } from "@noctaCrdt/Node"; import { BlockId } from "@noctaCrdt/NodeId"; import { useCallback } from "react"; @@ -15,6 +15,7 @@ interface UseBlockOperationProps { onKeyDown: (e: React.KeyboardEvent) => void; handleHrInput: (block: Block, content: string) => boolean; isLocalChange: React.MutableRefObject; + sendBlockCheckboxOperation: (operation: RemoteBlockCheckboxOperation) => void; } export const useBlockOperation = ({ @@ -24,6 +25,7 @@ export const useBlockOperation = ({ onKeyDown, handleHrInput, isLocalChange, + sendBlockCheckboxOperation, }: UseBlockOperationProps) => { const { sendCharInsertOperation, sendCharDeleteOperation } = useSocketStore(); @@ -257,9 +259,32 @@ export const useBlockOperation = ({ [editorCRDT.LinkedList, sendCharDeleteOperation, pageId, onKeyDown], ); + const handleCheckboxToggle = useCallback( + (blockId: BlockId, isChecked: boolean) => { + const operation = { + type: "blockCheckbox", + blockId, + pageId, + isChecked, + } as RemoteBlockCheckboxOperation; + + sendBlockCheckboxOperation(operation); + const targetBlock = editorCRDT.LinkedList.nodeMap[JSON.stringify(blockId)]; + if (targetBlock) { + targetBlock.isChecked = isChecked; + setEditorState({ + clock: editorCRDT.clock, + linkedList: editorCRDT.LinkedList, + }); + } + }, + [editorCRDT, pageId, sendBlockCheckboxOperation], + ); + return { handleBlockClick, handleBlockInput, handleKeyDown, + handleCheckboxToggle, }; }; diff --git a/client/src/features/editor/hooks/useEditorOperation.ts b/client/src/features/editor/hooks/useEditorOperation.ts index 1a96758f..8fd4616d 100644 --- a/client/src/features/editor/hooks/useEditorOperation.ts +++ b/client/src/features/editor/hooks/useEditorOperation.ts @@ -7,6 +7,7 @@ import { RemoteBlockReorderOperation, RemoteCharUpdateOperation, RemoteBlockInsertOperation, + RemoteBlockCheckboxOperation, } from "@noctaCrdt/Interfaces"; import { TextLinkedList } from "@noctaCrdt/LinkedList"; import { CharId } from "@noctaCrdt/NodeId"; @@ -151,6 +152,21 @@ export const useEditorOperation = ({ [pageId, editorCRDT], ); + const handleRemoteBlockCheckbox = useCallback( + (operation: RemoteBlockCheckboxOperation) => { + if (operation.pageId !== pageId) return; + const targetBlock = editorCRDT.current.LinkedList.nodeMap[JSON.stringify(operation.blockId)]; + if (targetBlock) { + targetBlock.isChecked = operation.isChecked; + setEditorState({ + clock: editorCRDT.current.clock, + linkedList: editorCRDT.current.LinkedList, + }); + } + }, + [pageId, editorCRDT], + ); + const handleRemoteCharUpdate = useCallback( (operation: RemoteCharUpdateOperation) => { if (!editorCRDT) return; @@ -190,6 +206,7 @@ export const useEditorOperation = ({ handleRemoteBlockReorder, handleRemoteCharUpdate, handleRemoteCursor, + handleRemoteBlockCheckbox, addNewBlock, }; };