From cdbc688a21125f57074eb2430f01e661feff43b3 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 15 Nov 2024 01:56:54 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20mongoDB=20BlockCRDT=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=98=EA=B3=A0=20nestJS=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=ED=99=9C=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 불필요한 주석 제거 - BlockCRDT구조에 따라 역직렬화 코드 추가 #122 --- server/src/crdt/crdt.gateway.ts | 29 +---------- server/src/crdt/crdt.service.ts | 92 +++++++++++++++------------------ 2 files changed, 44 insertions(+), 77 deletions(-) diff --git a/server/src/crdt/crdt.gateway.ts b/server/src/crdt/crdt.gateway.ts index 44f0e3c7..af08eef9 100644 --- a/server/src/crdt/crdt.gateway.ts +++ b/server/src/crdt/crdt.gateway.ts @@ -17,13 +17,13 @@ import { @WebSocketGateway({ cors: { - origin: "*", // 실제 배포 시에는 보안을 위해 적절히 설정하세요 + origin: "*", }, }) export class CrdtGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { private server: Server; private clientIdCounter: number = 1; - private clientMap: Map = new Map(); // socket.id -> clientId + private clientMap: Map = new Map(); constructor(private readonly crdtService: CrdtService) {} @@ -31,10 +31,6 @@ export class CrdtGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa this.server = server; } - /** - * 초기에 연결될때, 클라이언트에 숫자id및 문서정보를 송신한다. - * @param client 클라이언트 socket 정보 - */ async handleConnection(client: Socket) { console.log(`클라이언트 연결: ${client.id}`); const assignedId = (this.clientIdCounter += 1); @@ -44,37 +40,21 @@ export class CrdtGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa client.emit("document", currentCRDT); } - /** - * 연결이 끊어지면 클라이언트 맵에서 클라이언트 삭제 - * @param client 클라이언트 socket 정보 - */ handleDisconnect(client: Socket) { console.log(`클라이언트 연결 해제: ${client.id}`); this.clientMap.delete(client.id); } - /** - * 클라이언트로부터 받은 원격 삽입 연산 - * @param data 클라이언트가 송신한 Node 정보 - * @param client 클라이언트 번호 - */ @SubscribeMessage("insert") async handleInsert( @MessageBody() data: RemoteInsertOperation, @ConnectedSocket() client: Socket, ): Promise { console.log(`Insert 연산 수신 from ${client.id}:`, data); - await this.crdtService.handleInsert(data); - client.broadcast.emit("insert", data); } - /** - * 클라이언트로부터 받은 원격 삭제 연산 - * @param data 클라이언트가 송신한 Node 정보 - * @param client 클라이언트 번호 - */ @SubscribeMessage("delete") async handleDelete( @MessageBody() data: RemoteDeleteOperation, @@ -85,11 +65,6 @@ export class CrdtGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa client.broadcast.emit("delete", data); } - /** - * 추후 caret 표시 기능을 위해 받아놓음 + 추후 개선때 인덱스 계산할때 캐럿으로 계산하면 용이할듯 하여 데이터로 만듦 - * @param data 클라이언트가 송신한 caret 정보 - * @param client 클라이언트 번호 - */ @SubscribeMessage("cursor") handleCursor(@MessageBody() data: CursorPosition, @ConnectedSocket() client: Socket): void { console.log(`Cursor 위치 수신 from ${client.id}:`, data); diff --git a/server/src/crdt/crdt.service.ts b/server/src/crdt/crdt.service.ts index 552a6147..52b56106 100644 --- a/server/src/crdt/crdt.service.ts +++ b/server/src/crdt/crdt.service.ts @@ -1,52 +1,61 @@ -// src/crdt/crdt.service.ts import { Injectable, OnModuleInit } from "@nestjs/common"; import { InjectModel } from "@nestjs/mongoose"; -import { Doc, DocumentDocument } from "../schemas/document.schema"; +import { Doc, DocumentDocument } from "./schemas/document.schema"; import { Model } from "mongoose"; -import { CRDT } from "@noctaCrdt/Crdt"; -import { NodeId, Node } from "@noctaCrdt/Node"; - -interface RemoteInsertOperation { - node: Node; -} - -interface RemoteDeleteOperation { - targetId: NodeId | null; - clock: number; -} +import { BlockCRDT } from "@noctaCrdt/Crdt"; +import { RemoteInsertOperation, RemoteDeleteOperation } from "@noctaCrdt/Interfaces"; +import { CharId } from "@noctaCrdt/NodeId"; +import { Char } from "@noctaCrdt/Node"; @Injectable() export class CrdtService implements OnModuleInit { - private crdt: CRDT; + private crdt: BlockCRDT; constructor(@InjectModel(Doc.name) private documentModel: Model) { - this.crdt = new CRDT(0); // 초기 클라이언트 ID는 0으로 설정 (서버 자체) + this.crdt = new BlockCRDT(0); } async onModuleInit() { try { const doc = await this.getDocument(); + if (doc && doc.crdt) { + this.crdt = new BlockCRDT(0); + try { + // 저장된 CRDT 상태를 복원 + this.crdt.clock = doc.crdt.clock; + this.crdt.client = doc.crdt.client; - if (doc && doc.content) { - this.crdt = new CRDT(0); - let contentArray: string[]; + // LinkedList 복원 + if (doc.crdt.LinkedList.head) { + this.crdt.LinkedList.head = new CharId( + doc.crdt.LinkedList.head.clock, + doc.crdt.LinkedList.head.client, + ); + } - try { - contentArray = JSON.parse(doc.content) as string[]; + this.crdt.LinkedList.nodeMap = {}; + for (const [key, node] of Object.entries(doc.crdt.LinkedList.nodeMap)) { + const reconstructedNode = new Char( + node.value, + new CharId(node.id.clock, node.id.client), + ); + + if (node.next) { + reconstructedNode.next = new CharId(node.next.clock, node.next.client); + } + if (node.prev) { + reconstructedNode.prev = new CharId(node.prev.clock, node.prev.client); + } + + this.crdt.LinkedList.nodeMap[key] = reconstructedNode; + } } catch (e) { - console.error("Invalid JSON in document content:", doc.content); + console.error("Error reconstructing CRDT:", e); } - contentArray.forEach((char, index) => { - this.crdt.localInsert(index, char); - }); } } catch (error) { console.error("Error during CrdtService initialization:", error); } } - - /** - * MongoDB에서 문서를 가져옵니다. - */ async getDocument(): Promise { let doc = await this.documentModel.findOne(); if (!doc) { @@ -56,49 +65,32 @@ export class CrdtService implements OnModuleInit { return doc; } - /** - * MongoDB에 문서를 업데이트합니다. - */ async updateDocument(): Promise { - const content = JSON.stringify(this.crdt.spread()); + const serializedCRDT = this.crdt.serialize(); const doc = await this.documentModel.findOneAndUpdate( {}, - { content, updatedAt: new Date() }, + { crdt: serializedCRDT, updatedAt: new Date() }, { new: true, upsert: true }, ); - ("d"); - if (!doc) { - throw new Error("문서가 저장되지 않았습니다."); - } + if (!doc) throw new Error("문서 저장 실패"); return doc; } - /** - * 삽입 연산을 처리하고 문서를 업데이트합니다. - */ async handleInsert(operation: RemoteInsertOperation): Promise { this.crdt.remoteInsert(operation); await this.updateDocument(); } - /** - * 삭제 연산을 처리하고 문서를 업데이트합니다. - */ async handleDelete(operation: RemoteDeleteOperation): Promise { this.crdt.remoteDelete(operation); await this.updateDocument(); } - /** - * 현재 CRDT의 텍스트를 반환합니다. - */ getText(): string { return this.crdt.read(); } - /** - * CRDT 인스턴스를 반환하는 Getter 메서드 - */ - getCRDT(): CRDT { + + getCRDT(): BlockCRDT { return this.crdt; } } From 80d28e69aff123b6bc98690b027430a89ddfa672 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 15 Nov 2024 02:00:04 +0900 Subject: [PATCH 2/6] =?UTF-8?q?refactor:=20schema=20=ED=8C=8C=EC=9D=BC=20c?= =?UTF-8?q?rdt=20=ED=8F=B4=EB=8D=94=20=EB=82=B4=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - crdt에서 일어나는 건 crdt폴더 내에서만 일어나도록 feature화 #122 --- server/src/crdt/crdt.module.ts | 2 +- server/src/crdt/schemas/document.schema.ts | 31 ++++++++++++++++++++++ server/src/schemas/document.schema.ts | 15 ----------- 3 files changed, 32 insertions(+), 16 deletions(-) create mode 100644 server/src/crdt/schemas/document.schema.ts delete mode 100644 server/src/schemas/document.schema.ts diff --git a/server/src/crdt/crdt.module.ts b/server/src/crdt/crdt.module.ts index 5ba3e960..e9bd8244 100644 --- a/server/src/crdt/crdt.module.ts +++ b/server/src/crdt/crdt.module.ts @@ -1,7 +1,7 @@ import { Module } from "@nestjs/common"; import { CrdtService } from "./crdt.service"; import { MongooseModule } from "@nestjs/mongoose"; -import { Doc, DocumentSchema } from "../schemas/document.schema"; +import { Doc, DocumentSchema } from "./schemas/document.schema"; import { CrdtGateway } from "./crdt.gateway"; @Module({ diff --git a/server/src/crdt/schemas/document.schema.ts b/server/src/crdt/schemas/document.schema.ts new file mode 100644 index 00000000..5b63d596 --- /dev/null +++ b/server/src/crdt/schemas/document.schema.ts @@ -0,0 +1,31 @@ +import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; +import { Document } from "mongoose"; + +export type DocumentDocument = Document & Doc; + +@Schema() +export class Doc { + @Prop({ type: Object, required: true }) + crdt: { + clock: number; + client: number; + LinkedList: { + head: { + clock: number; + client: number; + } | null; + nodeMap: { + [key: string]: { + id: { clock: number; client: number }; + value: string; + next: { clock: number; client: number } | null; + prev: { clock: number; client: number } | null; + }; + }; + }; + }; + + @Prop({ default: Date.now }) + updatedAt: Date; +} +export const DocumentSchema = SchemaFactory.createForClass(Doc); diff --git a/server/src/schemas/document.schema.ts b/server/src/schemas/document.schema.ts deleted file mode 100644 index 7dcdc6bc..00000000 --- a/server/src/schemas/document.schema.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; -import { Document } from "mongoose"; - -export type DocumentDocument = Document & Doc; - -@Schema() -export class Doc { - @Prop({ required: true }) - content: string; - - @Prop({ default: Date.now }) - updatedAt: Date; -} - -export const DocumentSchema = SchemaFactory.createForClass(Doc); From 5083d628653399e06a3cf9b709d25d1268873cc7 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 15 Nov 2024 02:00:54 +0900 Subject: [PATCH 3/6] =?UTF-8?q?build:=20tsconfig=EC=97=90=20schema=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 #122 --- server/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/tsconfig.json b/server/tsconfig.json index cc09eecb..f53101b9 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -30,6 +30,6 @@ "esModuleInterop": true, "resolveJsonModule": true }, - "include": ["src/**/*", "test/**/*"], + "include": ["src/**/*", "test/**/*", "schemas"], "exclude": ["node_modules", "dist"] } From 8323a725825b2b176c48f3924b358a141982f277 Mon Sep 17 00:00:00 2001 From: hyonun <119800605+hyonun321@users.noreply.github.com> Date: Thu, 14 Nov 2024 22:44:01 +0900 Subject: [PATCH 4/6] Update README.md --- README.md | 81 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 5c35ee85..f2a5f633 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ ![Group 38 (1)](https://github.com/user-attachments/assets/a882c5c5-b205-43cc-9a16-2f5e87dbd6aa) -## 📋 프로젝트 이름 : `Nocta` + +![image](https://github.com/user-attachments/assets/ce48d2e5-ca40-43e6-8d64-0f874312f065) +

+ 배포 사이트 +

+ +## `Nocta` > 🌌 밤하늘의 별빛처럼, 자유로운 인터랙션 실시간 에디터 - 실시간 기록 협업 소프트웨어입니다. -## 👩‍💻 팀 이름 : `Glassmo` (글래스모) +## `Team Glassmo` - 글래스모피즘의 약자 @@ -22,6 +28,31 @@ | FE+BE | FE | BE | FE | | [@hyonun321](https://github.com/hyonun321) | [@Ludovico7](https://github.com/Ludovico7) | [@minjungw00](https://github.com/minjungw00) | [@pipisebastian](https://github.com/pipisebastian) | + +## 프로젝트 기능 소개 +### 1. 페이지 생성, 드래그 앤 드랍 + +사이드바의 페이지 추가 버튼을 통해 페이지를 생성하고 관리할 수 있습니다. + +![111111](https://github.com/user-attachments/assets/7bbbd091-a906-49b1-8043-13240bdf2f5b) + +### 2. 탭 브라우징: 최소화 최대화 리사이즈 + +각각의 문서를 탭브라우징 방식으로 관리할 수 있습니다. 크기를 조절하거나 드래그 앤 드랍을 통해 원하는 위치에 위치시킬 수 있습니다. + +![22222](https://github.com/user-attachments/assets/7355a84a-7ff5-44c5-a3d0-24840a468818) + +### 3. 실시간 마크다운 편집 + +마크다운 문법을 입력하면 실시간으로 마크다운 문법으로 변환합니다. + +![33333](https://github.com/user-attachments/assets/ffcf7fa5-9436-4e6b-b38f-6fbf9e813cb5) + +### 4.실시간 동시편집(구현중) + +하나의 문서를 여러 사용자가 동시에 편집이 가능합니다. CRDT 알고리즘을 통해 실시간 변경사항을 모든 사용자에게 반영합니다. + + ## 🔧 기술 스택 **Common** @@ -34,59 +65,37 @@ **Backend** -
+
**Infra**
-## 🚀 프로젝트 시작 가이드 +## 시스템 아키텍처 다이어그램 -**Frontend** +![image](https://github.com/user-attachments/assets/ab96462b-5f38-4dd9-9c72-984829fa873d) -
-Frontend (추후 변경) +## 🚀 프로젝트 시작 가이드 ```bash # 저장소 복제 git clone https://github.com/boostcampwm-2024/web33-boostproject.git -# 프로젝트 폴더로 이동 -cd frontend - # 의존성 설치 -npm install - -# 개발 서버 실행 -npm run dev +pnpm install # 프로덕션 빌드 -npm run build -``` - -
- -**Backend** +pnpm run build -
-Backend (추후 변경) +# 프로젝트 개발 모드 실행 +# Frontend: http://localhost:5173/ +# Backend: http://localhost:3000/ +pnpm run dev -```bash -# 프로젝트 폴더로 이동 -cd backend - -# 의존성 설치 -npm install - -# 개발 서버 실행 -npm run start:dev - -# 프로덕션 빌드 -npm run build +# 프로젝트 Docker 빌드 후 실행 (http://localhost/) +docker-compose up -d --build ``` -
- ## 🔗 프로젝트 링크 | 노션 | 디자인 | From 72d09104e331dfa82430b1c1a4d50e6b3ba3365a Mon Sep 17 00:00:00 2001 From: hyonun <119800605+hyonun321@users.noreply.github.com> Date: Thu, 14 Nov 2024 22:44:39 +0900 Subject: [PATCH 5/6] docs: Update README.md https -> http --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f2a5f633..fd033475 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![image](https://github.com/user-attachments/assets/ce48d2e5-ca40-43e6-8d64-0f874312f065)

- 배포 사이트 + 배포 사이트

## `Nocta` From 7448b677278ee752233767393336f8aa6cc0b7fe Mon Sep 17 00:00:00 2001 From: hyonun <119800605+hyonun321@users.noreply.github.com> Date: Thu, 14 Nov 2024 23:03:33 +0900 Subject: [PATCH 6/6] docs: Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hits 추가 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fd033475..d3b75767 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ ![image](https://github.com/user-attachments/assets/ce48d2e5-ca40-43e6-8d64-0f874312f065) -

- 배포 사이트 -

+
+ +
## `Nocta`