Skip to content

Commit

Permalink
Merge pull request #274 from boostcampwm-2024/Feature/#270_체크박스_기능_추가
Browse files Browse the repository at this point in the history
Feature/#270 체크박스 기능 추가
  • Loading branch information
github-actions[bot] authored Dec 2, 2024
2 parents d16ebd2 + e65f8ae commit 68034be
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 12 deletions.
7 changes: 7 additions & 0 deletions @noctaCrdt/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions @noctaCrdt/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export class Block extends Node<BlockId> {
icon: string;
crdt: BlockCRDT;
listIndex?: number;
isChecked?: boolean;

constructor(value: string, id: BlockId) {
super(value, id);
Expand All @@ -72,6 +73,7 @@ export class Block extends Node<BlockId> {
icon: this.icon,
crdt: this.crdt.serialize(),
listIndex: this.listIndex ? this.listIndex : null,
isChecked: this.isChecked ? this.isChecked : null,
};
}

Expand All @@ -87,6 +89,7 @@ export class Block extends Node<BlockId> {
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;
}
}
Expand Down
22 changes: 14 additions & 8 deletions client/src/features/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData
sendBlockInsertOperation,
sendBlockDeleteOperation,
sendBlockUpdateOperation,
sendBlockCheckboxOperation,
} = useSocketStore();
const { clientId } = useSocketStore();
const [displayTitle, setDisplayTitle] = useState(pageTitle);
Expand Down Expand Up @@ -86,6 +87,7 @@ export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData
handleRemoteBlockReorder,
handleRemoteCharUpdate,
handleRemoteCursor,
handleRemoteBlockCheckbox,
addNewBlock,
} = useEditorOperation({ editorCRDT, pageId, setEditorState, isSameLocalChange });

Expand Down Expand Up @@ -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(
{
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -377,6 +382,7 @@ export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData
onTextColorUpdate={onTextColorUpdate}
onTextBackgroundColorUpdate={onTextBackgroundColorUpdate}
dragBlockList={dragBlockList}
onCheckboxToggle={handleCheckboxToggle}
/>
))}
</SortableContext>
Expand Down
10 changes: 10 additions & 0 deletions client/src/features/editor/components/IconBlock/IconBlock.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,15 @@ export const iconStyle = cva({
backgroundColor: "white",
},
},
isChecked: {
true: {
color: "white",
backgroundColor: "#7272FF",
},
false: {
color: "gray.600",
backgroundColor: "white",
},
},
},
});
22 changes: 20 additions & 2 deletions client/src/features/editor/components/IconBlock/IconBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand All @@ -21,7 +29,17 @@ export const IconBlock = ({ type, index = 1, indent = 0 }: IconBlockProps) => {
case "ol":
return <span className={iconStyle({ type: "ol" })}>{`${index}.`}</span>;
case "checkbox":
return <span className={iconStyle({ type: "checkbox" })} />;
return (
<span
onClick={onCheckboxClick}
className={iconStyle({
type: "checkbox",
isChecked,
})}
>
{isChecked ? "✓" : ""}
</span>
);
default:
return null;
}
Expand Down
15 changes: 14 additions & 1 deletion client/src/features/editor/components/block/Block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ interface BlockProps {
blockId: BlockId,
nodes: Array<Char>,
) => void;
onCheckboxToggle: (blockId: BlockId, isChecked: boolean) => void;
}
export const Block: React.FC<BlockProps> = memo(
({
Expand All @@ -88,6 +89,7 @@ export const Block: React.FC<BlockProps> = memo(
onTextStyleUpdate,
onTextColorUpdate,
onTextBackgroundColorUpdate,
onCheckboxToggle,
}: BlockProps) => {
const blockRef = useRef<HTMLDivElement>(null);
const { isOpen, openModal, closeModal } = useModal();
Expand Down Expand Up @@ -267,6 +269,10 @@ export const Block: React.FC<BlockProps> = memo(
}
};

const handleCheckboxClick = () => {
onCheckboxToggle(block.id, !block.isChecked);
};

const Indicator = () => (
<div
className={dropIndicatorStyle({
Expand Down Expand Up @@ -306,7 +312,14 @@ export const Block: React.FC<BlockProps> = memo(
onCopySelect={handleCopySelect}
onDeleteSelect={handleDeleteSelect}
/>
<IconBlock type={block.type} index={block.listIndex} indent={block.indent} />

<IconBlock
type={block.type}
index={block.listIndex}
indent={block.indent}
isChecked={block.isChecked}
onCheckboxClick={handleCheckboxClick}
/>
<div
ref={blockRef}
onKeyDown={(e) => handleKeyDown(e, blockRef.current, block)}
Expand Down
27 changes: 26 additions & 1 deletion client/src/features/editor/hooks/useBlockOperation.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -15,6 +15,7 @@ interface UseBlockOperationProps {
onKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void;
handleHrInput: (block: Block, content: string) => boolean;
isLocalChange: React.MutableRefObject<boolean>;
sendBlockCheckboxOperation: (operation: RemoteBlockCheckboxOperation) => void;
}

export const useBlockOperation = ({
Expand All @@ -24,6 +25,7 @@ export const useBlockOperation = ({
onKeyDown,
handleHrInput,
isLocalChange,
sendBlockCheckboxOperation,
}: UseBlockOperationProps) => {
const { sendCharInsertOperation, sendCharDeleteOperation } = useSocketStore();

Expand Down Expand Up @@ -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,
};
};
17 changes: 17 additions & 0 deletions client/src/features/editor/hooks/useEditorOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
RemoteBlockReorderOperation,
RemoteCharUpdateOperation,
RemoteBlockInsertOperation,
RemoteBlockCheckboxOperation,
} from "@noctaCrdt/Interfaces";
import { TextLinkedList } from "@noctaCrdt/LinkedList";
import { CharId } from "@noctaCrdt/NodeId";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -190,6 +206,7 @@ export const useEditorOperation = ({
handleRemoteBlockReorder,
handleRemoteCharUpdate,
handleRemoteCursor,
handleRemoteBlockCheckbox,
addNewBlock,
};
};
10 changes: 10 additions & 0 deletions client/src/stores/useSocketStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
RemoteCharDeleteOperation,
RemoteBlockUpdateOperation,
RemoteBlockReorderOperation,
RemoteBlockCheckboxOperation,
RemoteCharUpdateOperation,
RemotePageDeleteOperation,
RemotePageUpdateOperation,
Expand Down Expand Up @@ -78,6 +79,7 @@ interface SocketStore {
subscribeToPageOperations: (handlers: PageOperationsHandlers) => (() => void) | undefined;
setWorkspace: (workspace: WorkSpaceSerializedProps) => void;
sendOperation: (operation: any) => void;
sendBlockCheckboxOperation: (operation: RemoteBlockCheckboxOperation) => void;
}

interface RemoteOperationHandlers {
Expand All @@ -90,6 +92,7 @@ interface RemoteOperationHandlers {
onRemoteCharUpdate: (operation: RemoteCharUpdateOperation) => void;
onRemoteCursor: (position: CursorPosition) => void;
onBatchOperations: (batch: any[]) => void;
onRemoteBlockCheckbox: (operation: RemoteBlockCheckboxOperation) => void;
}

interface PageOperationsHandlers {
Expand Down Expand Up @@ -292,6 +295,11 @@ export const useSocketStore = create<SocketStore>((set, get) => ({
// sendOperation(operation);
},

sendBlockCheckboxOperation: (operation: RemoteBlockCheckboxOperation) => {
const { socket } = get();
socket?.emit("checkbox/block", operation);
},

subscribeToRemoteOperations: (handlers: RemoteOperationHandlers) => {
const { socket } = get();
if (!socket) return;
Expand All @@ -305,6 +313,7 @@ export const useSocketStore = create<SocketStore>((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);
Expand All @@ -316,6 +325,7 @@ export const useSocketStore = create<SocketStore>((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);
};
},

Expand Down
45 changes: 45 additions & 0 deletions server/src/workspace/workspace.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
RemoteBlockUpdateOperation,
RemotePageCreateOperation,
RemoteBlockReorderOperation,
RemoteBlockCheckboxOperation,
RemoteCharUpdateOperation,
CursorPosition,
} from "@noctaCrdt/Interfaces";
Expand Down Expand Up @@ -719,6 +720,50 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG
}
}

/**
* 블록 Checkbox 연산 처리
*/
@SubscribeMessage("checkbox/block")
async handleBlockCheckbox(
@MessageBody() data: RemoteBlockCheckboxOperation,
@ConnectedSocket() client: Socket,
): Promise<void> {
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}`);
}
}

/**
* 글자 삽입 연산 처리
*/
Expand Down

0 comments on commit 68034be

Please sign in to comment.