Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#175 캐럿 관리방식 변경 #176

Merged
merged 12 commits into from
Nov 25, 2024
Merged
11 changes: 11 additions & 0 deletions @noctaCrdt/Crdt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@
this.LinkedList = new LinkedListClass();
}

localInsert(index: number, value: string, blockId?: BlockId, pageId?: string): any {

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'index' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'value' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'blockId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'pageId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'index' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'value' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'blockId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'pageId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type
// 기본 CRDT에서는 구현하지 않고, 하위 클래스에서 구현
throw new Error("Method not implemented.");
}

localDelete(index: number, blockId?: BlockId, pageId?: string): any {

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'index' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'blockId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'pageId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'index' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'blockId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'pageId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type
// 기본 CRDT에서는 구현하지 않고, 하위 클래스에서 구현
throw new Error("Method not implemented.");
}

remoteInsert(operation: any): void {

Check warning on line 35 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'operation' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 35 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'operation' is defined but never used. Allowed unused args must match /^_/u
// 기본 CRDT에서는 구현하지 않고, 하위 클래스에서 구현
throw new Error("Method not implemented.");
}
Expand Down Expand Up @@ -103,6 +103,17 @@
return operation;
}

localUpdate(block: Block, pageId: string): RemoteBlockUpdateOperation {
const updatedBlock = this.LinkedList.nodeMap[JSON.stringify(block.id)];
updatedBlock.animation = block.animation;
updatedBlock.icon = block.icon;
updatedBlock.indent = block.indent;
updatedBlock.style = block.style;
updatedBlock.type = block.type;
// this.LinkedList.nodeMap[JSON.stringify(block.id)] = block;
return { node: updatedBlock, pageId };
}

remoteUpdate(block: Block, pageId: string): RemoteBlockUpdateOperation {
const updatedBlock = this.LinkedList.nodeMap[JSON.stringify(block.id)];
updatedBlock.animation = block.animation;
Expand Down
2 changes: 0 additions & 2 deletions @noctaCrdt/LinkedList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ export abstract class LinkedList<T extends Node<NodeId>> {

deleteNode(id: T["id"]): void {
const nodeToDelete = this.getNode(id);
console.log(this.nodeMap);
console.log("nodeToDelete", nodeToDelete, id);
if (!nodeToDelete) return;

if (this.head && id.equals(this.head)) {
Expand Down
6 changes: 3 additions & 3 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useEffect } from "react";
import { useRefreshQuery } from "@apis/auth";
import { ErrorModal } from "@components/modal/ErrorModal";
import { WorkSpace } from "@features/workSpace/WorkSpace";
import { useErrorStore } from "@stores/useErrorStore";
import { useUserInfo } from "@stores/useUserStore";
import { useEffect } from "react";
import { ErrorModal } from "@components/modal/ErrorModal";
import { WorkSpace } from "@features/workSpace/WorkSpace";
import { useSocketStore } from "./stores/useSocketStore";

const App = () => {
Expand Down
16 changes: 8 additions & 8 deletions client/src/constants/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ export const OPTION_CATEGORIES = {
id: "type",
label: "전환",
options: [
{ id: "p", label: "p" },
{ id: "h1", label: "h1" },
{ id: "h2", label: "h2" },
{ id: "h3", label: "h3" },
{ id: "ul", label: "ul" },
{ id: "ol", label: "ol" },
{ id: "checkbox", label: "checkbox" },
{ id: "blockquote", label: "blockquote" },
{ id: "p", label: "기본" },
{ id: "h1", label: "제목 1" },
{ id: "h2", label: "제목 2" },
{ id: "h3", label: "제목 3" },
{ id: "ul", label: "리스트" },
{ id: "ol", label: "순서 리스트" },
{ id: "checkbox", label: "체크박스" },
{ id: "blockquote", label: "인용문" },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

대신 해주셨군요!! 감사합니다!! 🙇‍♂️

] as { id: ElementType; label: string }[],
},
ANIMATION: {
Expand Down
13 changes: 13 additions & 0 deletions client/src/features/editor/Editor.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,16 @@ export const checkbox = css({
backgroundColor: "blue.500",
},
});

export const addNewBlockButton = css({
display: "flex",
gap: "spacing.sm",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

후후 요거 그냥 "sm"해도 동작될겁니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 수정하겠습니다!

requestAnimationFrame 없어도 동작하는군요! 이곳저곳 수정하면서 디버깅 하고 되나 안되나 확인하는 식으로 구현해서 중복되는 코드들이 많네요... 이부분도 반영해서 수정하겠습니다!

borderRadius: "4px",
padding: "spacing.sm",
color: "gray.900",
opacity: 0.8,
cursor: "pointer",
"&:hover": {
opacity: 1,
},
});
143 changes: 75 additions & 68 deletions client/src/features/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ import {
RemoteCharInsertOperation,
serializedEditorDataProps,
} from "node_modules/@noctaCrdt/Interfaces.ts";
import { useRef, useState, useCallback, useEffect, useMemo } from "react";
import { useRef, useState, useCallback, useEffect, useMemo, useLayoutEffect } from "react";
import { useSocketStore } from "@src/stores/useSocketStore.ts";
import { editorContainer, editorTitleContainer, editorTitle } from "./Editor.style";
import { setCaretPosition } from "@src/utils/caretUtils.ts";
import {
editorContainer,
editorTitleContainer,
editorTitle,
addNewBlockButton,
} from "./Editor.style";
import { Block } from "./components/block/Block.tsx";
import { useBlockDragAndDrop } from "./hooks/useBlockDragAndDrop";
import { useBlockOptionSelect } from "./hooks/useBlockOption.ts";
Expand All @@ -25,7 +31,6 @@ interface EditorProps {
export interface EditorStateProps {
clock: number;
linkedList: BlockLinkedList;
currentBlock: BlockId | null;
}
// TODO: pageId, editorCRDT를 props로 받아와야함
export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorProps) => {
Expand All @@ -47,7 +52,6 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
const [editorState, setEditorState] = useState<EditorStateProps>({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
currentBlock: null as BlockId | null,
});
const { sensors, handleDragEnd } = useBlockDragAndDrop({
editorCRDT: editorCRDT.current,
Expand Down Expand Up @@ -84,37 +88,9 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
onTitleChange(e.target.value);
};

const handleBlockClick = (blockId: BlockId, e: React.MouseEvent<HTMLDivElement>) => {
try {
const block = editorState.linkedList.getNode(blockId);
if (!block) {
console.warn("Block not found:", blockId);
return;
}

const selection = window.getSelection();
const range = document.caretRangeFromPoint(e.clientX, e.clientY);

if (!selection || !range) {
console.warn("Selection or range not available");
return;
}

// 새로운 Range로 Selection 설정
selection.removeAllRanges();
selection.addRange(range);

// 현재 캐럿 위치를 저장
const caretPosition = selection.focusOffset;
block.crdt.currentCaret = caretPosition;

setEditorState((prev) => ({
...prev,
currentBlock: blockId,
}));
} catch (error) {
console.error("Error handling block click:", error);
}
const handleBlockClick = (blockId: BlockId) => {
editorCRDT.current.currentBlock =
editorCRDT.current.LinkedList.nodeMap[JSON.stringify(blockId)];
};

const handleBlockInput = useCallback(
Expand All @@ -136,29 +112,55 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
if (caretPosition === 0) {
const [addedChar] = newContent;
charNode = block.crdt.localInsert(0, addedChar, block.id, pageId);
block.crdt.currentCaret = 1;
editorCRDT.current.currentBlock!.crdt.currentCaret = caretPosition;
requestAnimationFrame(() => {
setCaretPosition({
blockId: block.id,
linkedList: editorCRDT.current.LinkedList,
position: caretPosition,
});
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다!!!

requestAnimationFram이 뭔가 보니, 이전의 프레임 렌더가 다 끝나고 실행하는 함수 같더라구요.
그렇다면 캐럿 연산이 다 끝나고 저 setCaretPosition이 실행된다고 볼 수 있겠네요!

이 중복되는 코드는

const updateEditorState = () => {
        editorCRDT.current.currentBlock!.crdt.currentCaret = caretPosition;
        requestAnimationFrame(() => {
          setCaretPosition({
            blockId: block.id,
            linkedList: editorCRDT.current.LinkedList,
            position: caretPosition,
          });
        });

로 줄여서

updateEditorState()

로 사용해도 되지 않을까.. 싶습니다 !

Comment on lines +115 to +122
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 로직이 중복해서 나오네요

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 해당 로직들이 필요한 이유가 있을까요? 이게 없으면 어떤 부분이 동작하지 않는걸까요??
이 부분을 다 지워봤을때, 특별히 동작이 안되던 부분이 없어서 질문드립니당!

          requestAnimationFrame(() => {
            setCaretPosition({
              blockId: block.id,
              linkedList: editorCRDT.current.LinkedList,
              position: caretPosition,
            });
          });

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인해보니 이 부분의 문제가 아니었던 것 같습니다. 다른 부분 수정하면서 조금 헷갈렸네요... 이내용 현재 구현하고 있는 텍스트 옵션 PR 올릴때 수정하도록 하겠습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 서윤님 피드백 주신 내용을 보면 requestAnimationFrame 없이도 동작하는 것 같습니다. 이부분은 삭제하겠습니다!

} else if (caretPosition > currentContent.length) {
const addedChar = newContent[newContent.length - 1];
charNode = block.crdt.localInsert(currentContent.length, addedChar, block.id, pageId);
block.crdt.currentCaret = caretPosition;
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);
block.crdt.currentCaret = caretPosition;
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) {
// 문자가 삭제된 경우
operationNode = block.crdt.localDelete(caretPosition, block.id, pageId);
block.crdt.currentCaret = caretPosition;
sendCharDeleteOperation(operationNode);
editorCRDT.current.currentBlock!.crdt.currentCaret = caretPosition;
requestAnimationFrame(() => {
setCaretPosition({
blockId: block.id,
linkedList: editorCRDT.current.LinkedList,
position: caretPosition,
});
});
}

setEditorState((prev) => ({
setEditorState({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
currentBlock: prev.currentBlock,
}));
});
},
[sendCharInsertOperation, sendCharDeleteOperation],
);
Expand Down Expand Up @@ -186,6 +188,15 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr

const subscriptionRef = useRef(false);

useLayoutEffect(() => {
if (!editorCRDT.current.currentBlock) return;
setCaretPosition({
blockId: editorCRDT.current.currentBlock.id,
linkedList: editorCRDT.current.LinkedList,
position: editorCRDT.current.currentBlock?.crdt.currentCaret,
});
}, [editorCRDT.current.currentBlock?.crdt.read().length]);
Copy link
Member

@pipisebastian pipisebastian Nov 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2. 옵션창 -> 삭제버튼 누를시, 캐럿이 제대로 이동하지 않는 문제

  • 다음과 같이 변경해줘야합니다. 해당 useLayoutEffect가 도는 조건은, length가 바뀔때라서, 만약 1번째 블록이 aa, 2번째 블록이 bb면 length가 같다고해서 삭제버튼을 누르더라도 캐럿이 이동하지 않더라구요!
  • 그렇다고 crdt의 value값을 의존성으로 넣는다고 해도, 1번째 블록이 aa, 2번째 블록이 aa면 동작하지 않더라구요..
  • 그래서 id 자체를 의존성으로 넣어줬더니 잘 동작했습니다.
Suggested change
}, [editorCRDT.current.currentBlock?.crdt.read().length]);
}, [editorCRDT.current.currentBlock?.id.serialize()]);
2024-11-23.5.50.39.mov

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 branch 만들때 블록 옵션 기능이 없었어서 이부분은 확인하지 못했던 것 같습니다. 현재 작업중인 텍스트 옵션 pr 올릴때 피드백 주신 부분 반영해서 올리겠습니다!


useEffect(() => {
if (subscriptionRef.current) return;
subscriptionRef.current = true;
Expand All @@ -195,22 +206,20 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
console.log(operation, "block : 입력 확인합니다이");
if (!editorCRDT.current) return;
editorCRDT.current.remoteInsert(operation);
setEditorState((prev) => ({
setEditorState({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
currentBlock: prev.currentBlock,
}));
});
Comment on lines 207 to +212
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이쪽은 묶으려면 묶을 수도 있겠는데 투머치일 것 같다는 생각이 들어서 굳이 묶을 필요는 없어보이네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

묶는게 정확이 어떤 걸 묶는다는 걸까요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

비슷한 로직이 아래에도 중복해서 나오는 것 같아서 함수로 묶을 수 있겠다고 생각했는데 해보니까 좀 어거지로 묶이는 것 같더라구요.. 코멘트를 달기는 했는데 이 코멘트는 그냥 거르셔도 될 것 같습니다!

},

onRemoteBlockDelete: (operation) => {
console.log(operation, "block : 삭제 확인합니다이");
if (!editorCRDT.current) return;
editorCRDT.current.remoteDelete(operation);
setEditorState((prev) => ({
setEditorState({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
currentBlock: prev.currentBlock,
}));
});
},

onRemoteCharInsert: (operation) => {
Expand All @@ -219,11 +228,10 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
const targetBlock =
editorCRDT.current.LinkedList.nodeMap[JSON.stringify(operation.blockId)];
targetBlock.crdt.remoteInsert(operation);
setEditorState((prev) => ({
setEditorState({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
currentBlock: prev.currentBlock,
}));
});
},

onRemoteCharDelete: (operation) => {
Expand All @@ -232,11 +240,10 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
const targetBlock =
editorCRDT.current.LinkedList.nodeMap[JSON.stringify(operation.blockId)];
targetBlock.crdt.remoteDelete(operation);
setEditorState((prev) => ({
setEditorState({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
currentBlock: prev.currentBlock,
}));
});
},

onRemoteBlockUpdate: (operation) => {
Expand All @@ -245,22 +252,20 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
// ??
console.log("타입", operation.node);
editorCRDT.current.remoteUpdate(operation.node, operation.pageId);
setEditorState((prev) => ({
setEditorState({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
currentBlock: prev.currentBlock,
}));
});
},

onRemoteBlockReorder: (operation) => {
console.log(operation, "block : 재정렬 확인합니다이");
if (!editorCRDT.current) return;
editorCRDT.current.remoteReorder(operation);
setEditorState((prev) => ({
setEditorState({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
currentBlock: prev.currentBlock,
}));
});
},

onRemoteCursor: (position) => {
Expand All @@ -274,18 +279,16 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
};
}, []);

const tempBlock = () => {
const addNewBlock = () => {
const index = editorCRDT.current.LinkedList.spread().length;

// 로컬 삽입을 수행하고 연산 객체를 반환받음
const operation = editorCRDT.current.localInsert(index, "");
sendBlockInsertOperation({ node: operation.node, pageId });
console.log("operation clock", operation.node);
setEditorState(() => ({
setEditorState({
clock: operation.node.id.clock,
linkedList: editorCRDT.current.LinkedList,
currentBlock: operation.node.id,
}));
});
};

return (
Expand All @@ -309,7 +312,7 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
key={`${block.id.client}-${block.id.clock}`}
id={`${block.id.client}-${block.id.clock}`}
block={block}
isActive={block.id === editorState.currentBlock}
isActive={block.id === editorCRDT.current.currentBlock?.id}
onInput={handleBlockInput}
onCompositionEnd={handleCompositionEnd}
onKeyDown={handleKeyDown}
Expand All @@ -322,7 +325,11 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
))}
</SortableContext>
</DndContext>
<div onClick={tempBlock}>임시</div>
{editorState.linkedList.spread().length === 0 && (
<div className={addNewBlockButton} onClick={addNewBlock}>
클릭해서 새로운 블록을 추가하세요
</div>
)}
</div>
</div>
);
Expand Down
Loading
Loading