Skip to content

Commit

Permalink
Merge pull request #191 from boostcampwm-2024/Feature/#183_게스트,_유저별_w…
Browse files Browse the repository at this point in the history
…orkspace를_생성하고_불러오기

Feature/#183 게스트, 유저별 workspace를 생성하고 불러오기
  • Loading branch information
github-actions[bot] authored Nov 26, 2024
2 parents 1169885 + 8cc6dc3 commit c65fde7
Show file tree
Hide file tree
Showing 15 changed files with 531 additions and 557 deletions.
8 changes: 6 additions & 2 deletions @noctaCrdt/Crdt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CRDTSerializedProps,
RemoteBlockReorderOperation,
RemoteBlockUpdateOperation,
serializedEditorDataProps,
RemoteCharUpdateOperation,
TextColorType,
BackgroundColorType,
Expand Down Expand Up @@ -134,7 +135,7 @@ export class EditorCRDT extends CRDT<Block> {

newNode.next = operation.node.next;
newNode.prev = operation.node.prev;

newNode.indent = operation.node.indent;
this.LinkedList.insertById(newNode);

this.clock = Math.max(this.clock, operation.node.id.clock) + 1;
Expand Down Expand Up @@ -193,7 +194,7 @@ export class EditorCRDT extends CRDT<Block> {
this.clock = Math.max(this.clock, clock) + 1;
}

serialize(): CRDTSerializedProps<Block> {
serialize(): serializedEditorDataProps {
return {
...super.serialize(),
currentBlock: this.currentBlock ? this.currentBlock.serialize() : null,
Expand Down Expand Up @@ -241,6 +242,8 @@ export class BlockCRDT extends CRDT<Char> {
blockId,
pageId,
style: node.style || [],
color: node.color,
backgroundColor: node.backgroundColor,
};

return operation;
Expand Down Expand Up @@ -324,6 +327,7 @@ export class BlockCRDT extends CRDT<Char> {

remoteUpdate(operation: RemoteCharUpdateOperation): void {
const updatedChar = this.LinkedList.nodeMap[JSON.stringify(operation.node.id)];
console.log("remoteUpdate", updatedChar);
if (operation.node.style && operation.node.style.length > 0) {
updatedChar.style = [...operation.node.style];
}
Expand Down
9 changes: 5 additions & 4 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { useErrorStore } from "@stores/useErrorStore";
import { useEffect } from "react";
import { ErrorModal } from "@components/modal/ErrorModal";
import { WorkSpace } from "@features/workSpace/WorkSpace";
import { useErrorStore } from "@stores/useErrorStore";
import { useUserInfo } from "@stores/useUserStore";
import { useSocketStore } from "./stores/useSocketStore";

const App = () => {
// TODO 라우터, react query 설정
const { isErrorModalOpen, errorMessage } = useErrorStore();

const { id } = useUserInfo();
useEffect(() => {
const socketStore = useSocketStore.getState();
socketStore.init();
socketStore.init(id);
return () => {
setTimeout(() => {
socketStore.cleanup();
}, 0);
};
}, []);
}, [id]);

return (
<>
Expand Down
6 changes: 3 additions & 3 deletions client/src/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import {
export const Sidebar = ({
pages,
handlePageAdd,
handlePageSelect,
handlePageOpen,
}: {
pages: Page[];
handlePageAdd: () => void;
handlePageSelect: ({ pageId }: { pageId: string }) => void;
handlePageOpen: ({ pageId }: { pageId: string }) => void;
}) => {
const visiblePages = pages.filter((page) => page.isVisible);
const isMaxVisiblePage = visiblePages.length >= MAX_VISIBLE_PAGE;
Expand All @@ -43,7 +43,7 @@ export const Sidebar = ({
openModal();
return;
}
handlePageSelect({ pageId: id });
handlePageOpen({ pageId: id });
};

const handleAddPageButtonClick = () => {
Expand Down
160 changes: 95 additions & 65 deletions client/src/features/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import {
editorTitle,
addNewBlockButton,
} from "./Editor.style";
import { Block } from "./components/block/Block.tsx";
import { Block } from "./components/block/Block";
import { useBlockDragAndDrop } from "./hooks/useBlockDragAndDrop";
import { useBlockOptionSelect } from "./hooks/useBlockOption.ts";
import { useBlockOptionSelect } from "./hooks/useBlockOption";
import { useMarkdownGrammer } from "./hooks/useMarkdownGrammer";
import { useTextOptionSelect } from "./hooks/useTextOptions.ts";
import { getTextOffset } from "./utils/domSyncUtils.ts";
Expand All @@ -35,17 +35,20 @@ interface EditorProps {
onTitleChange: (title: string) => void;
pageId: string;
serializedEditorData: serializedEditorDataProps;
updatePageData: (pageId: string, newData: serializedEditorDataProps) => void;
}

interface ClipboardMetadata {
value: string;
style: string[];
color: TextColorType | undefined;
backgroundColor: BackgroundColorType | undefined;
}

// TODO: pageId, editorCRDT를 props로 받아와야함
export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorProps) => {
export const Editor = ({
onTitleChange,
pageId,
serializedEditorData,
updatePageData,
}: EditorProps) => {
const {
sendCharInsertOperation,
sendCharDeleteOperation,
Expand All @@ -54,17 +57,27 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
sendBlockDeleteOperation,
sendBlockUpdateOperation,
} = useSocketStore();
const { clientId } = useSocketStore();

const editorCRDTInstance = useMemo(() => {
const editor = new EditorCRDT(serializedEditorData.client);
editor.deserialize(serializedEditorData);
return editor;
}, [serializedEditorData]);
let newEditorCRDT;
if (serializedEditorData) {
newEditorCRDT = new EditorCRDT(serializedEditorData.client);
newEditorCRDT.deserialize(serializedEditorData);
} else {
newEditorCRDT = new EditorCRDT(clientId ? clientId : 0);
}
return newEditorCRDT;
}, [serializedEditorData, clientId]);

const editorCRDT = useRef<EditorCRDT>(editorCRDTInstance);

// editorState도 editorCRDT가 변경될 때마다 업데이트
const [editorState, setEditorState] = useState<EditorStateProps>({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
});

const { sensors, handleDragEnd } = useBlockDragAndDrop({
editorCRDT: editorCRDT.current,
editorState,
Expand Down Expand Up @@ -109,25 +122,27 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
};

const handleBlockClick = (blockId: BlockId, e: React.MouseEvent<HTMLDivElement>) => {
const selection = window.getSelection();
if (!selection) return;
if (editorCRDT) {
const selection = window.getSelection();
if (!selection) return;

const clickedElement = (e.target as HTMLElement).closest(
'[contenteditable="true"]',
) as HTMLDivElement;
if (!clickedElement) return;
const clickedElement = (e.target as HTMLElement).closest(
'[contenteditable="true"]',
) as HTMLDivElement;
if (!clickedElement) return;

editorCRDT.current.currentBlock =
editorCRDT.current.LinkedList.nodeMap[JSON.stringify(blockId)];
const caretPosition = getAbsoluteCaretPosition(clickedElement);
editorCRDT.current.currentBlock =
editorCRDT.current.LinkedList.nodeMap[JSON.stringify(blockId)];
const caretPosition = getAbsoluteCaretPosition(clickedElement);

// 계산된 캐럿 위치 저장
editorCRDT.current.currentBlock.crdt.currentCaret = caretPosition;
// 계산된 캐럿 위치 저장
editorCRDT.current.currentBlock.crdt.currentCaret = caretPosition;
}
};

const handleBlockInput = useCallback(
(e: React.FormEvent<HTMLDivElement>, block: CRDTBlock) => {
if (!block) return;
if (!block || !editorCRDT) return;
if ((e.nativeEvent as InputEvent).isComposing) {
return;
}
Expand Down Expand Up @@ -179,7 +194,7 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
linkedList: editorCRDT.current.LinkedList,
});
},
[sendCharInsertOperation, sendCharDeleteOperation],
[sendCharInsertOperation, sendCharDeleteOperation, editorCRDT, pageId, updatePageData],
);

const handleKeyDown = (
Expand Down Expand Up @@ -319,48 +334,54 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
});
};

const handleCompositionEnd = (e: React.CompositionEvent<HTMLDivElement>, block: CRDTBlock) => {
const event = e.nativeEvent as CompositionEvent;
const characters = [...event.data];
const selection = window.getSelection();
const caretPosition = selection?.focusOffset || 0;
const startPosition = caretPosition - characters.length;

characters.forEach((char, index) => {
const insertPosition = startPosition + index;
const charNode = block.crdt.localInsert(insertPosition, char, block.id, pageId);
const handleCompositionEnd = useCallback(
(e: React.CompositionEvent<HTMLDivElement>, block: CRDTBlock) => {
if (!editorCRDT) return;
const event = e.nativeEvent as CompositionEvent;
const characters = [...event.data];
const selection = window.getSelection();
const caretPosition = selection?.focusOffset || 0;
const startPosition = caretPosition - characters.length;

characters.forEach((char, index) => {
const insertPosition = startPosition + index;
const charNode = block.crdt.localInsert(insertPosition, char, block.id, pageId);

sendCharInsertOperation({
node: charNode.node,
blockId: block.id,
pageId,
sendCharInsertOperation({
node: charNode.node,
blockId: block.id,
pageId,
});
});
});

block.crdt.currentCaret = caretPosition;
};
block.crdt.currentCaret = caretPosition;
updatePageData(pageId, editorCRDT.current.serialize());
},
[editorCRDT, pageId, sendCharInsertOperation, updatePageData],
);

const subscriptionRef = useRef(false);

useEffect(() => {
if (!editorCRDT.current.currentBlock) return;
// TODO: 값이 제대로 들어왔는데 왜 안되는지 확인 필요
if (!editorCRDT || !editorCRDT.current.currentBlock) return;
setCaretPosition({
blockId: editorCRDT.current.currentBlock.id,
linkedList: editorCRDT.current.LinkedList,
position: editorCRDT.current.currentBlock?.crdt.currentCaret,
pageId,
});
// 서윤님 피드백 반영
}, [editorCRDT.current.currentBlock?.id.serialize()]);

useEffect(() => {
if (!editorCRDT) return;
if (subscriptionRef.current) return;
subscriptionRef.current = true;

const unsubscribe = subscribeToRemoteOperations({
onRemoteBlockInsert: (operation) => {
console.log(operation, "block : 입력 확인합니다이");
if (!editorCRDT.current) return;
if (operation.pageId !== pageId) return;
editorCRDT.current.remoteInsert(operation);
setEditorState({
clock: editorCRDT.current.clock,
Expand All @@ -370,7 +391,7 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr

onRemoteBlockDelete: (operation) => {
console.log(operation, "block : 삭제 확인합니다이");
if (!editorCRDT.current) return;
if (operation.pageId !== pageId) return;
editorCRDT.current.remoteDelete(operation);
setEditorState({
clock: editorCRDT.current.clock,
Expand All @@ -380,31 +401,35 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr

onRemoteCharInsert: (operation) => {
console.log(operation, "char : 입력 확인합니다이");
if (!editorCRDT.current) return;
if (operation.pageId !== pageId) return;
const targetBlock =
editorCRDT.current.LinkedList.nodeMap[JSON.stringify(operation.blockId)];
targetBlock.crdt.remoteInsert(operation);
setEditorState({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
});
if (targetBlock) {
targetBlock.crdt.remoteInsert(operation);
setEditorState({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
});
}
},

onRemoteCharDelete: (operation) => {
console.log(operation, "char : 삭제 확인합니다이");
if (!editorCRDT.current) return;
if (operation.pageId !== pageId) return;
const targetBlock =
editorCRDT.current.LinkedList.nodeMap[JSON.stringify(operation.blockId)];
targetBlock.crdt.remoteDelete(operation);
setEditorState({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
});
if (targetBlock) {
targetBlock.crdt.remoteDelete(operation);
setEditorState({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
});
}
},

onRemoteBlockUpdate: (operation) => {
console.log(operation, "block : 업데이트 확인합니다이");
if (!editorCRDT.current) return;
if (operation.pageId !== pageId) return;
editorCRDT.current.remoteUpdate(operation.node, operation.pageId);
setEditorState({
clock: editorCRDT.current.clock,
Expand All @@ -414,7 +439,7 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr

onRemoteBlockReorder: (operation) => {
console.log(operation, "block : 재정렬 확인합니다이");
if (!editorCRDT.current) return;
if (operation.pageId !== pageId) return;
editorCRDT.current.remoteReorder(operation);
setEditorState({
clock: editorCRDT.current.clock,
Expand All @@ -424,7 +449,8 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr

onRemoteCharUpdate: (operation) => {
console.log(operation, "char : 업데이트 확인합니다이");
if (!editorCRDT.current) return;
if (!editorCRDT) return;
if (operation.pageId !== pageId) return;
const targetBlock =
editorCRDT.current.LinkedList.nodeMap[JSON.stringify(operation.blockId)];
targetBlock.crdt.remoteUpdate(operation);
Expand All @@ -443,21 +469,25 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
subscriptionRef.current = false;
unsubscribe?.();
};
}, []);
}, [editorCRDT, subscribeToRemoteOperations, pageId]);

const addNewBlock = () => {
if (!editorCRDT) return;
const index = editorCRDT.current.LinkedList.spread().length;

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

// 로딩 상태 체크
if (!editorCRDT || !editorState) {
return <div>Loading editor data...</div>;
}
return (
<div className={editorContainer}>
<div className={editorTitleContainer}>
Expand Down
Loading

0 comments on commit c65fde7

Please sign in to comment.