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/#183 게스트, 유저별 workspace를 생성하고 불러오기 #191

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
70311db
feat: socketStore에 accessToken의존성 부여
hyonun321 Nov 24, 2024
83454e7
refactor: Map으로 workspace 관리하도록 변경
hyonun321 Nov 24, 2024
5ff0e2e
feat: CRDT라이브러리에 AuthModule import 선언
hyonun321 Nov 24, 2024
72735c7
refactor: 사용자별로 client.join 하여 socket을 송신받도록 수정
hyonun321 Nov 24, 2024
e31934d
chore: 불필요한 로그 제거, import 선언문 위치 수정
hyonun321 Nov 24, 2024
df45a85
chore: 불필요한 선언문 제거
hyonun321 Nov 24, 2024
b00ed2b
refactor: accessToken 이 아닌, id값을 그대로 사용하여 workspace 생성
hyonun321 Nov 25, 2024
dbd235d
feat: pageId 별로 crdt 연산 수행하게 수정
hyonun321 Nov 25, 2024
45d28b3
chore: 불필요한 콘솔로그 및 주석 제거
hyonun321 Nov 25, 2024
8ea5866
feat: page 컴포넌트 누를때 마다 socket을 연결하여 서버로부터 최신 page 인스턴스 정보 받아오기
hyonun321 Nov 25, 2024
f76e17f
chore: 린트에러 수정
hyonun321 Nov 25, 2024
cfafade
Merge branch 'dev' into Feature/#183_게스트,_유저별_workspace를_생성하고_불러오기
hyonun321 Nov 25, 2024
44ee6ac
chore: lint에러 수정
hyonun321 Nov 25, 2024
03d7d40
chore: 빌드에러 수정
hyonun321 Nov 25, 2024
71cbb2c
feat: 페이지를 킬때 socket을 연결하고, pages 를 렌더할때 통신하여 page정보를 가져오도록 수정
hyonun321 Nov 25, 2024
60aead7
feat: editorCRDT 에디터별 상태로 관리
hyonun321 Nov 25, 2024
301046d
chore: lint에러 수정
hyonun321 Nov 25, 2024
2207a7b
Merge branch 'dev' of https://github.com/boostcampwm-2024/web33-Nocta…
hyonun321 Nov 26, 2024
e31faa4
chore: 병합 처리
hyonun321 Nov 26, 2024
cc68ebd
Merge branch 'dev' of https://github.com/boostcampwm-2024/web33-Nocta…
hyonun321 Nov 26, 2024
8cc6dc3
fix: 병합과정에서 생긴 문제 해결
Ludovico7 Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
CRDTSerializedProps,
RemoteBlockReorderOperation,
RemoteBlockUpdateOperation,
serializedEditorDataProps,
RemoteCharUpdateOperation,
TextColorType,
BackgroundColorType,
Expand All @@ -25,17 +26,17 @@
this.LinkedList = new LinkedListClass();
}

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

Check warning on line 29 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 29 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 29 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 29 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 29 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 29 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 29 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 29 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 29 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 29 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 34 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 34 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 34 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 34 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 34 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 34 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 34 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 34 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 39 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 39 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 @@ -134,7 +135,7 @@

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 @@
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 @@
blockId,
pageId,
style: node.style || [],
color: node.color,
backgroundColor: node.backgroundColor,
};

return operation;
Expand Down Expand Up @@ -324,6 +327,7 @@

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
Loading