From 49eb3340b5e9a5b3a4a5b2f42630dce77ed7585d Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Thu, 28 Nov 2024 01:40:48 +0900 Subject: [PATCH 01/23] =?UTF-8?q?style:=20glassContainer=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 불투명인 경우의 glassContainer가 필요해서 선언 #219 --- .../styles/recipes/glassContainerRecipe.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/client/src/styles/recipes/glassContainerRecipe.ts b/client/src/styles/recipes/glassContainerRecipe.ts index c9be1a8a..8f3bd556 100644 --- a/client/src/styles/recipes/glassContainerRecipe.ts +++ b/client/src/styles/recipes/glassContainerRecipe.ts @@ -32,5 +32,27 @@ export const glassContainerRecipe = defineRecipe({ border: "2px solid token(colors.white/40)", }, }, + background: { + non: { + background: "token(colors.white/95)", + }, + }, + boxShadow: { + all: { + boxShadow: "lg", + }, + top: { + boxShadow: "0 -4px 6px -1px rgb(0 0 0 / 0.1), 0 -2px 4px -2px rgb(0 0 0 / 0.1)", + }, + bottom: { + boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", + }, + left: { + boxShadow: "-4px 0 6px -1px rgb(0 0 0 / 0.1), -2px 0 4px -2px rgb(0 0 0 / 0.1)", + }, + right: { + boxShadow: "4px 0 6px -1px rgb(0 0 0 / 0.1), 2px 0 4px -2px rgb(0 0 0 / 0.1)", + }, + }, }, }); From c0899c6ba17a49ac9800ba5a9bac95f3023d59d2 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Thu, 28 Nov 2024 01:41:29 +0900 Subject: [PATCH 02/23] =?UTF-8?q?feat:=20workspaceSelectModal=20=EC=B0=BD?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hover 됐을때 표현되게 처리 - 디자인에 맞는 style 작성 #219 --- .../components/menuButton/MenuButton.style.ts | 31 +++---- .../components/menuButton/MenuButton.tsx | 34 +++++++- .../components/WorkspaceSelectModal.style.ts | 20 +++++ .../components/WorkspaceSelectModal.tsx | 80 +++++++++++++++++++ 4 files changed, 147 insertions(+), 18 deletions(-) create mode 100644 client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts create mode 100644 client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx diff --git a/client/src/components/sidebar/components/menuButton/MenuButton.style.ts b/client/src/components/sidebar/components/menuButton/MenuButton.style.ts index 497e23a2..eaf9244f 100644 --- a/client/src/components/sidebar/components/menuButton/MenuButton.style.ts +++ b/client/src/components/sidebar/components/menuButton/MenuButton.style.ts @@ -1,28 +1,29 @@ +import { SIDE_BAR } from "@constants/size"; import { css } from "@styled-system/css"; export const menuItemWrapper = css({ display: "flex", - gap: "lg", + gap: "md", alignItems: "center", - borderRightRadius: "md", - width: "300px", + width: "100%", padding: "md", - boxShadow: "sm", cursor: "pointer", }); export const textBox = css({ - textStyle: "display-medium20", - color: "gray.900", + color: "gray.700", + fontSize: "md", }); -export const menuDropdown = css({ - zIndex: "dropdown", - position: "absolute", - top: "100%", - right: "0", - borderRadius: "md", - width: "100px", - marginTop: "sm", - boxShadow: "md", +export const menuButtonContainer = css({ + position: "relative", + // 버튼과 모달 사이의 간격을 채우는 패딩 추가 + _before: { + position: "absolute", + top: "100%", + left: 0, + width: "100%", + height: "4px", // top: calc(100% + 4px)와 동일한 값 + content: '""', + }, }); diff --git a/client/src/components/sidebar/components/menuButton/MenuButton.tsx b/client/src/components/sidebar/components/menuButton/MenuButton.tsx index c2eee56c..a3447940 100644 --- a/client/src/components/sidebar/components/menuButton/MenuButton.tsx +++ b/client/src/components/sidebar/components/menuButton/MenuButton.tsx @@ -1,16 +1,44 @@ +import { useState } from "react"; import { useUserInfo } from "@stores/useUserStore"; -import { menuItemWrapper, textBox } from "./MenuButton.style"; +import { menuItemWrapper, textBox, menuButtonContainer } from "./MenuButton.style"; import { MenuIcon } from "./components/MenuIcon"; +import { WorkspaceSelectModal } from "./components/WorkspaceSelectModal"; export const MenuButton = () => { const { name } = useUserInfo(); + const [isHovered, setIsHovered] = useState(false); + const [isModalHovered, setIsModalHovered] = useState(false); + + const handleClose = () => { + if (!isHovered && !isModalHovered) { + setIsHovered(false); + } + }; return ( - <> + - + setIsModalHovered(true)} + onMouseLeave={() => { + setIsModalHovered(false); + handleClose(); + }} + /> + ); }; diff --git a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts new file mode 100644 index 00000000..0b3a7ef8 --- /dev/null +++ b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts @@ -0,0 +1,20 @@ +import { css, cx } from "@styled-system/css"; +import { glassContainer } from "@styled-system/recipes"; + +export const workspaceListContainer = css({ + display: "flex", + gap: "sm", + flexDirection: "column", + padding: "md", +}); + +export const workspaceModalContainer = cx( + glassContainer({ + borderRadius: "bottom", + background: "non", + boxShadow: "bottom", + }), + css({ + display: "flex", + }), +); diff --git a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx new file mode 100644 index 00000000..c88605c9 --- /dev/null +++ b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx @@ -0,0 +1,80 @@ +import { motion } from "framer-motion"; +import { useEffect, useRef } from "react"; +import { SIDE_BAR } from "@constants/size"; +import { useSocketStore } from "@src/stores/useSocketStore"; +import { css } from "@styled-system/css"; +import { workspaceListContainer, workspaceModalContainer } from "./WorkspaceSelectModal.style"; + +interface WorkspaceSelectModalProps { + isOpen: boolean; + userName: string | null; + onMouseEnter: () => void; + onMouseLeave: () => void; +} + +export const WorkspaceSelectModal = ({ + isOpen, + userName, + onMouseEnter, + onMouseLeave, +}: WorkspaceSelectModalProps) => { + const modalRef = useRef(null); + const { availableWorkspaces } = useSocketStore(); // 소켓 스토어에서 직접 워크스페이스 목록 가져오기 + + const informText = userName + ? availableWorkspaces.length > 0 + ? "" // 워크스페이스 목록이 표시될 것이므로 비워둠 + : "접속할 수 있는 워크스페이스가 없습니다." + : "로그인 해주세요"; + return ( + +
+ {userName && availableWorkspaces.length > 0 ? ( + availableWorkspaces.map((workspace) => ( +
+ {workspace.id} +
+ )) + ) : ( +

+ {informText} +

+ )} +
+
+ ); +}; From a5799cbc84559e6cc1bf69f964182ebd2030b082 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Thu, 28 Nov 2024 01:42:14 +0900 Subject: [PATCH 03/23] =?UTF-8?q?feat:=20workspace=20list=20=EB=B0=9B?= =?UTF-8?q?=EC=95=84=EC=98=A4=EB=8A=94=20socket=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #67 --- client/src/stores/useSocketStore.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/client/src/stores/useSocketStore.ts b/client/src/stores/useSocketStore.ts index 5dd33371..cd72460a 100644 --- a/client/src/stores/useSocketStore.ts +++ b/client/src/stores/useSocketStore.ts @@ -12,6 +12,7 @@ import { CursorPosition, WorkSpaceSerializedProps, } from "@noctaCrdt/Interfaces"; +import { WorkSpace as WorkspaceClass } from "@noctaCrdt/WorkSpace"; import { io, Socket } from "socket.io-client"; import { create } from "zustand"; @@ -19,6 +20,8 @@ interface SocketStore { socket: Socket | null; clientId: number | null; workspace: WorkSpaceSerializedProps | null; + availableWorkspaces: WorkspaceClass[]; + init: (accessToken: string | null) => void; cleanup: () => void; fetchWorkspaceData: () => WorkSpaceSerializedProps | null; @@ -35,6 +38,9 @@ interface SocketStore { sendCursorPosition: (position: CursorPosition) => void; subscribeToRemoteOperations: (handlers: RemoteOperationHandlers) => (() => void) | undefined; subscribeToPageOperations: (handlers: PageOperationsHandlers) => (() => void) | undefined; + subscribeToWorkspaceOperations: ( + handlers: WorkspaceOperationHandlers, + ) => (() => void) | undefined; setWorkspace: (workspace: WorkSpaceSerializedProps) => void; } @@ -55,10 +61,15 @@ interface PageOperationsHandlers { onRemotePageUpdate: (operation: RemotePageUpdateOperation) => void; } +interface WorkspaceOperationHandlers { + onWorkspaceListUpdate: (workspaces: WorkspaceClass[]) => void; +} + export const useSocketStore = create((set, get) => ({ socket: null, clientId: null, workspace: null, + availableWorkspaces: [], // 새로 추가 init: (id: string | null) => { const { socket: existingSocket } = get(); @@ -214,4 +225,19 @@ export const useSocketStore = create((set, get) => ({ socket.off("update/page", handlers.onRemotePageUpdate); }; }, + + subscribeToWorkspaceOperations: (handlers: WorkspaceOperationHandlers) => { + const { socket } = get(); + if (!socket) return; + + socket.on("workspace/list", (workspaces: WorkspaceClass[]) => { + console.log("수신함", workspaces); + set({ availableWorkspaces: workspaces }); + handlers.onWorkspaceListUpdate(workspaces); + }); + + return () => { + socket.off("workspace/list", handlers.onWorkspaceListUpdate); + }; + }, })); From cae395daa5cd3f07665cd35221162d91df29813c Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Thu, 28 Nov 2024 14:54:00 +0900 Subject: [PATCH 04/23] =?UTF-8?q?chore:=20=EB=94=94=EB=B2=84=EA=B9=85?= =?UTF-8?q?=EC=9A=A9=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 소켓관련 수정 - workspace/list 의 수신 시기 고민중 #219 --- client/src/App.tsx | 17 ++++++++++++ .../components/WorkspaceSelectModal.tsx | 2 +- client/src/stores/useSocketStore.ts | 27 ++++--------------- server/src/crdt/crdt.gateway.ts | 6 +++++ 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 5462bfa0..0c623466 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -8,10 +8,27 @@ import { useSocketStore } from "./stores/useSocketStore"; const App = () => { // TODO 라우터, react query 설정 const { isErrorModalOpen, errorMessage } = useErrorStore(); + const { id } = useUserInfo(); + useEffect(() => { const socketStore = useSocketStore.getState(); socketStore.init(id); + + // // 소켓이 연결된 후에 이벤트 리스너 등록 + // const { socket } = socketStore; + // socket?.on("connect", () => { + // const unsubscribe = socketStore.subscribeToWorkspaceOperations({ + // onWorkspaceListUpdate: (workspaces) => { + // console.log("Workspace list updated:", workspaces); + // }, + // }); + + // return () => { + // if (unsubscribe) unsubscribe(); + // }; + // }); + return () => { setTimeout(() => { socketStore.cleanup(); diff --git a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx index c88605c9..44762703 100644 --- a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx +++ b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx @@ -1,5 +1,5 @@ import { motion } from "framer-motion"; -import { useEffect, useRef } from "react"; +import { useRef } from "react"; import { SIDE_BAR } from "@constants/size"; import { useSocketStore } from "@src/stores/useSocketStore"; import { css } from "@styled-system/css"; diff --git a/client/src/stores/useSocketStore.ts b/client/src/stores/useSocketStore.ts index cd72460a..5115fb5f 100644 --- a/client/src/stores/useSocketStore.ts +++ b/client/src/stores/useSocketStore.ts @@ -38,9 +38,6 @@ interface SocketStore { sendCursorPosition: (position: CursorPosition) => void; subscribeToRemoteOperations: (handlers: RemoteOperationHandlers) => (() => void) | undefined; subscribeToPageOperations: (handlers: PageOperationsHandlers) => (() => void) | undefined; - subscribeToWorkspaceOperations: ( - handlers: WorkspaceOperationHandlers, - ) => (() => void) | undefined; setWorkspace: (workspace: WorkSpaceSerializedProps) => void; } @@ -61,10 +58,6 @@ interface PageOperationsHandlers { onRemotePageUpdate: (operation: RemotePageUpdateOperation) => void; } -interface WorkspaceOperationHandlers { - onWorkspaceListUpdate: (workspaces: WorkspaceClass[]) => void; -} - export const useSocketStore = create((set, get) => ({ socket: null, clientId: null, @@ -112,6 +105,11 @@ export const useSocketStore = create((set, get) => ({ console.log("Disconnected from server"); }); + socket.on("workspace/list", (workspaces: WorkspaceClass[]) => { + console.log("Received workspace list:", workspaces); + set({ availableWorkspaces: workspaces }); + }); + socket.on("error", (error: Error) => { console.error("Socket error:", error); }); @@ -225,19 +223,4 @@ export const useSocketStore = create((set, get) => ({ socket.off("update/page", handlers.onRemotePageUpdate); }; }, - - subscribeToWorkspaceOperations: (handlers: WorkspaceOperationHandlers) => { - const { socket } = get(); - if (!socket) return; - - socket.on("workspace/list", (workspaces: WorkspaceClass[]) => { - console.log("수신함", workspaces); - set({ availableWorkspaces: workspaces }); - handlers.onWorkspaceListUpdate(workspaces); - }); - - return () => { - socket.off("workspace/list", handlers.onWorkspaceListUpdate); - }; - }, })); diff --git a/server/src/crdt/crdt.gateway.ts b/server/src/crdt/crdt.gateway.ts index 2bdad3ea..0c1ed93d 100644 --- a/server/src/crdt/crdt.gateway.ts +++ b/server/src/crdt/crdt.gateway.ts @@ -81,6 +81,12 @@ export class CrdtGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa this.clientMap.set(client.id, clientInfo); client.emit("assign/clientId", assignedId); + // 클라이언트가 구독을 설정할 시간을 주기 위해 약간의 지연 + setTimeout(() => { + console.log("쏘긴쏜다"); + client.emit("workspace/list", "test"); + }, 100); + client.broadcast.emit("userJoined", { clientId: assignedId }); this.logger.log(`클라이언트 연결 성공 - Socket ID: ${client.id}, Client ID: ${assignedId}`); this.logger.debug(`현재 연결된 클라이언트 수: ${this.clientMap.size}`); From f15fff988c5e4c0832b78373e069ede0dfe3862f Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Thu, 28 Nov 2024 14:56:16 +0900 Subject: [PATCH 05/23] =?UTF-8?q?chore=20:=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/sidebar/components/menuButton/MenuButton.style.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/components/sidebar/components/menuButton/MenuButton.style.ts b/client/src/components/sidebar/components/menuButton/MenuButton.style.ts index eaf9244f..8ff2be60 100644 --- a/client/src/components/sidebar/components/menuButton/MenuButton.style.ts +++ b/client/src/components/sidebar/components/menuButton/MenuButton.style.ts @@ -1,4 +1,3 @@ -import { SIDE_BAR } from "@constants/size"; import { css } from "@styled-system/css"; export const menuItemWrapper = css({ From 83bd98708a1e1992bede1734098e86ea5a978e45 Mon Sep 17 00:00:00 2001 From: minjungw00 Date: Thu, 28 Nov 2024 18:13:37 +0900 Subject: [PATCH 06/23] =?UTF-8?q?feat:=20=EC=9B=8C=ED=81=AC=EC=8A=A4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EA=B6=8C=ED=95=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #219 --- @noctaCrdt/Interfaces.ts | 1 + @noctaCrdt/WorkSpace.ts | 12 ++- server/src/app.module.ts | 7 +- server/src/auth/schemas/user.schema.ts | 3 + .../schemas/workspace.schema.ts | 9 +- .../workspace/workspace.controller.spec.ts | 18 ++++ server/src/workspace/workspace.controller.ts | 56 ++++++++++++ .../workspace.gateway.ts} | 8 +- .../workspace.module.ts} | 10 +-- .../workspace.service.ts} | 88 +++++++++++++++++-- 10 files changed, 186 insertions(+), 26 deletions(-) rename server/src/{crdt => workspace}/schemas/workspace.schema.ts (95%) create mode 100644 server/src/workspace/workspace.controller.spec.ts create mode 100644 server/src/workspace/workspace.controller.ts rename server/src/{crdt/crdt.gateway.ts => workspace/workspace.gateway.ts} (98%) rename server/src/{crdt/crdt.module.ts => workspace/workspace.module.ts} (60%) rename server/src/{crdt/crdt.service.ts => workspace/workspace.service.ts} (58%) diff --git a/@noctaCrdt/Interfaces.ts b/@noctaCrdt/Interfaces.ts index b265c3f0..7fee4418 100644 --- a/@noctaCrdt/Interfaces.ts +++ b/@noctaCrdt/Interfaces.ts @@ -177,6 +177,7 @@ export interface RemotePageUpdateOperation { } export interface WorkSpaceSerializedProps { id: string; + name: string; pageList: Page[]; authUser: Map; } diff --git a/@noctaCrdt/WorkSpace.ts b/@noctaCrdt/WorkSpace.ts index d3e969b1..4b0fd796 100644 --- a/@noctaCrdt/WorkSpace.ts +++ b/@noctaCrdt/WorkSpace.ts @@ -4,18 +4,21 @@ import { EditorCRDT } from "./Crdt"; export class WorkSpace { id: string; + name: string; pageList: Page[]; authUser: Map; - constructor(id: string, pageList: Page[]) { - this.id = id; - this.pageList = pageList; - this.authUser = new Map(); + constructor(id?: string, name?: string, pageList?: Page[], authUser?: Map) { + this.id = id ? id : crypto.randomUUID(); + this.name = name ? name : "Untitled"; + this.pageList = pageList ? pageList : []; + this.authUser = authUser ? authUser : new Map(); } serialize(): WorkSpaceSerializedProps { return { id: this.id, + name: this.name, pageList: this.pageList, authUser: this.authUser, }; @@ -23,6 +26,7 @@ export class WorkSpace { deserialize(data: WorkSpaceSerializedProps): void { this.id = data.id; + this.name = data.name; this.pageList = data.pageList.map((pageData) => { const page = new Page(); page.deserialize(pageData); diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 87dc11ac..434057b6 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -4,7 +4,8 @@ import { AppService } from "./app.service"; import { ConfigModule, ConfigService } from "@nestjs/config"; import { MongooseModule } from "@nestjs/mongoose"; import { AuthModule } from "./auth/auth.module"; -import { CrdtModule } from "./crdt/crdt.module"; +import { WorkspaceModule } from "./workspace/workspace.module"; +import { WorkspaceController } from "./workspace/workspace.controller"; @Module({ imports: [ @@ -22,9 +23,9 @@ import { CrdtModule } from "./crdt/crdt.module"; }), }), AuthModule, - CrdtModule, + WorkspaceModule, ], - controllers: [AppController], + controllers: [AppController, WorkspaceController], providers: [AppService], }) export class AppModule {} diff --git a/server/src/auth/schemas/user.schema.ts b/server/src/auth/schemas/user.schema.ts index bdf2ae9c..41ebadf9 100644 --- a/server/src/auth/schemas/user.schema.ts +++ b/server/src/auth/schemas/user.schema.ts @@ -23,6 +23,9 @@ export class User { @Prop() refreshToken: string; + + @Prop({ type: [String], default: [] }) + workspaces: string[]; } export const UserSchema = SchemaFactory.createForClass(User); diff --git a/server/src/crdt/schemas/workspace.schema.ts b/server/src/workspace/schemas/workspace.schema.ts similarity index 95% rename from server/src/crdt/schemas/workspace.schema.ts rename to server/src/workspace/schemas/workspace.schema.ts index 494f8abb..1b9852ce 100644 --- a/server/src/crdt/schemas/workspace.schema.ts +++ b/server/src/workspace/schemas/workspace.schema.ts @@ -205,14 +205,17 @@ export class Page { // Main Workspace Document Schema @Schema({ minimize: false }) export class Workspace { - @Prop({ required: true }) + @Prop({ required: true, default: () => crypto.randomUUID() }) id: string; + @Prop({ type: String, default: "Untitled" }) + name: string; + @Prop({ type: [Page], default: [] }) pageList: Page[]; - @Prop({ type: Object, of: Object }) - authUser: object; + @Prop({ type: Map, default: new Map() }) + authUser: Map; @Prop({ default: Date.now }) updatedAt: Date; diff --git a/server/src/workspace/workspace.controller.spec.ts b/server/src/workspace/workspace.controller.spec.ts new file mode 100644 index 00000000..c356b5f9 --- /dev/null +++ b/server/src/workspace/workspace.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { WorkspaceController } from './workspace.controller'; + +describe('WorkspaceController', () => { + let controller: WorkspaceController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [WorkspaceController], + }).compile(); + + controller = module.get(WorkspaceController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/server/src/workspace/workspace.controller.ts b/server/src/workspace/workspace.controller.ts new file mode 100644 index 00000000..d7584bd7 --- /dev/null +++ b/server/src/workspace/workspace.controller.ts @@ -0,0 +1,56 @@ +import { Controller, Post, Delete, Get, Param, Body, Req, UseGuards } from "@nestjs/common"; +import { WorkSpaceService } from "./workspace.service"; +import { ApiTags } from "@nestjs/swagger"; +import { JwtAuthGuard } from "src/auth/guards/jwt-auth.guard"; +import { Request as ExpressRequest, Response as ExpressResponse } from "express"; +import { Workspace } from "./schemas/workspace.schema"; + +@ApiTags("workspace") +@UseGuards(JwtAuthGuard) +@Controller("workspace") +export class WorkspaceController { + constructor(private readonly workspaceService: WorkSpaceService) {} + + // 워크스페이스 생성 + @Post("create") + async createWorkspace( + @Req() req: ExpressRequest, + @Body("name") name?: string, + ): Promise { + const userId = req.user.id; + return this.workspaceService.createWorkspace(userId, name); + } + + // 워크스페이스 삭제 + // 삭제한 워크스페이스에서 작업중인 사람들을 내쫓기 + @Delete("delete") + async deleteWorkspace( + @Req() req: ExpressRequest, + @Body("workspaceId") workspaceId: string, + ): Promise { + const userId = req.user.id; + return this.workspaceService.deleteWorkspace(userId, workspaceId); + } + + // 유저의 워크스페이스 목록 조회 + @Get("findAll") + async getUserWorkspaces(@Req() req: ExpressRequest): Promise { + const userId = req.user.id; + return this.workspaceService.getUserWorkspaces(userId); + } + + // 워크스페이스에 유저 초대 + @Post("invite") + async inviteUser( + @Req() req: ExpressRequest, + @Body("workspaceId") workspaceId: string, + @Body("invitedUserId") invitedUserId: string, + ): Promise { + const ownerId = req.user.id; + return this.workspaceService.inviteUserToWorkspace(ownerId, workspaceId, invitedUserId); + } + + // 소켓에 관여해야 하는 부분 + // 삭제할 워크스페이스에 있는 사람들 내쫓기 + // 워크스페이스 권한이 생기면 접속중인 사람에게 알리기 +} diff --git a/server/src/crdt/crdt.gateway.ts b/server/src/workspace/workspace.gateway.ts similarity index 98% rename from server/src/crdt/crdt.gateway.ts rename to server/src/workspace/workspace.gateway.ts index 12b41021..db865a85 100644 --- a/server/src/crdt/crdt.gateway.ts +++ b/server/src/workspace/workspace.gateway.ts @@ -9,7 +9,7 @@ import { WsException, } from "@nestjs/websockets"; import { Socket, Server } from "socket.io"; -import { workSpaceService } from "./crdt.service"; +import { WorkSpaceService } from "./workspace.service"; import { RemoteBlockDeleteOperation, RemoteCharDeleteOperation, @@ -60,12 +60,12 @@ interface BatchOperation { path: "/api/socket.io", transports: ["websocket", "polling"], }) -export class CrdtGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { - private readonly logger = new Logger(CrdtGateway.name); +export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { + private readonly logger = new Logger(WorkspaceGateway.name); private clientIdCounter: number = 1; private clientMap: Map = new Map(); private batchMap: Map = new Map(); - constructor(private readonly workSpaceService: workSpaceService) {} + constructor(private readonly workSpaceService: WorkSpaceService) {} afterInit(server: Server) { this.workSpaceService.setServer(server); diff --git a/server/src/crdt/crdt.module.ts b/server/src/workspace/workspace.module.ts similarity index 60% rename from server/src/crdt/crdt.module.ts rename to server/src/workspace/workspace.module.ts index c79c9616..c8543417 100644 --- a/server/src/crdt/crdt.module.ts +++ b/server/src/workspace/workspace.module.ts @@ -1,8 +1,8 @@ import { Module } from "@nestjs/common"; -import { workSpaceService } from "./crdt.service"; +import { WorkSpaceService } from "./workspace.service"; import { MongooseModule } from "@nestjs/mongoose"; import { Workspace, WorkspaceSchema } from "./schemas/workspace.schema"; -import { CrdtGateway } from "./crdt.gateway"; +import { WorkspaceGateway } from "./workspace.gateway"; import { AuthModule } from "../auth/auth.module"; @Module({ @@ -10,7 +10,7 @@ import { AuthModule } from "../auth/auth.module"; AuthModule, MongooseModule.forFeature([{ name: Workspace.name, schema: WorkspaceSchema }]), ], - providers: [workSpaceService, CrdtGateway], - exports: [workSpaceService], + providers: [WorkSpaceService, WorkspaceGateway], + exports: [WorkSpaceService], }) -export class CrdtModule {} +export class WorkspaceModule {} diff --git a/server/src/crdt/crdt.service.ts b/server/src/workspace/workspace.service.ts similarity index 58% rename from server/src/crdt/crdt.service.ts rename to server/src/workspace/workspace.service.ts index 05ea397d..4094f6f6 100644 --- a/server/src/crdt/crdt.service.ts +++ b/server/src/workspace/workspace.service.ts @@ -4,19 +4,21 @@ import { Workspace, WorkspaceDocument } from "./schemas/workspace.schema"; import { WorkSpace as CRDTWorkSpace } from "@noctaCrdt/WorkSpace"; import { Model } from "mongoose"; import { Server } from "socket.io"; -// import { EditorCRDT, BlockCRDT } from "@noctaCrdt/Crdt"; -// import { Page as CRDTPage } from "@noctaCrdt/Page"; import { WorkSpaceSerializedProps } from "@noctaCrdt/Interfaces"; import { Page } from "@noctaCrdt/Page"; import { Block } from "@noctaCrdt/Node"; import { BlockId } from "@noctaCrdt/NodeId"; +import { User, UserDocument } from "../auth/schemas/user.schema"; @Injectable() -export class workSpaceService implements OnModuleInit { - private readonly logger = new Logger(workSpaceService.name); +export class WorkSpaceService implements OnModuleInit { + private readonly logger = new Logger(WorkSpaceService.name); private workspaces: Map; private server: Server; - constructor(@InjectModel(Workspace.name) private workspaceModel: Model) {} + constructor( + @InjectModel(Workspace.name) private workspaceModel: Model, + @InjectModel(User.name) private userModel: Model, + ) {} getServer() { return this.server; @@ -30,7 +32,7 @@ export class workSpaceService implements OnModuleInit { async onModuleInit() { this.workspaces = new Map(); // 게스트 워크스페이스 초기화 - const guestWorkspace = new CRDTWorkSpace("guest", []); + const guestWorkspace = new CRDTWorkSpace("guest", "Guest"); this.workspaces.set("guest", guestWorkspace); // 주기적으로 인메모리 DB 정리 작업 실행 @@ -95,7 +97,7 @@ export class workSpaceService implements OnModuleInit { // DB에서 찾기 const workspaceJSON = await this.workspaceModel.findOne({ id: userId }); - const workspace = new CRDTWorkSpace(userId, []); + const workspace = new CRDTWorkSpace(); if (workspaceJSON) { // DB에 있으면 JSON을 객체로 복원 @@ -126,4 +128,76 @@ export class workSpaceService implements OnModuleInit { } return page.crdt.LinkedList.nodeMap[JSON.stringify(blockId)]; } + + // 워크스페이스 생성 + async createWorkspace(userId: string, name: string): Promise { + const newWorkspace = await this.workspaceModel.create({ + name: name, + authUser: { userId: "owner" }, + }); + + // 유저 정보 업데이트 + await this.userModel.updateOne({ id: userId }, { $push: { workspaces: newWorkspace.id } }); + + return newWorkspace; + } + + // 워크스페이스 삭제 + async deleteWorkspace(userId: string, workspaceId: string): Promise { + const workspace = await this.workspaceModel.findOne({ id: workspaceId }); + + if (!workspace) { + throw new Error(`Workspace with id ${workspaceId} not found`); + } + + // 권한 확인 + if (!workspace.authUser.has(userId) || workspace.authUser.get(userId) !== "owner") { + throw new Error(`User ${userId} does not have permission to delete this workspace`); + } + + // 관련 유저들의 workspaces 목록 업데이트 + await this.userModel.updateMany( + { workspaces: workspaceId }, + { $pull: { workspaces: workspaceId } }, + ); + + await this.workspaceModel.deleteOne({ id: workspaceId }); + } + + // 워크스페이스 조회 + async getUserWorkspaces(userId: string): Promise { + const user = await this.userModel.findOne({ id: userId }); + if (!user) { + throw new Error(`User with id ${userId} not found`); + } + + return this.workspaceModel.find({ id: { $in: user.workspaces } }); + } + + // 워크스페이스에 유저 초대 + async inviteUserToWorkspace( + ownerId: string, + workspaceId: string, + invitedUserId: string, + ): Promise { + const workspace = await this.workspaceModel.findOne({ id: workspaceId }); + + if (!workspace) { + throw new Error(`Workspace with id ${workspaceId} not found`); + } + + // 권한 확인 + if (!workspace.authUser.has(ownerId) || workspace.authUser.get(ownerId) !== "owner") { + throw new Error(`User ${ownerId} does not have permission to invite users to this workspace`); + } + + // 워크스페이스에 유저 추가 + if (!workspace.authUser.has(invitedUserId)) { + workspace.authUser.set(invitedUserId, "editor"); + await workspace.save(); + + // 유저 정보 업데이트 + await this.userModel.updateOne({ id: invitedUserId }, { $push: { workspaces: workspaceId } }); + } + } } From 532ea0c26119feed45df71c5ad73d3cf2c8bedcb Mon Sep 17 00:00:00 2001 From: minjungw00 Date: Fri, 29 Nov 2024 01:14:03 +0900 Subject: [PATCH 07/23] =?UTF-8?q?fix:=20socket=EC=9D=B4=20workspaceId?= =?UTF-8?q?=EB=A5=BC=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EB=B0=A9?= =?UTF-8?q?=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95(=EC=A7=84=ED=96=89=EC=A4=91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #219 --- .../components/WorkspaceSelectModal.style.ts | 1 + client/src/features/workSpace/WorkSpace.tsx | 6 +- .../workspace/workspace.controller.spec.ts | 26 +++- server/src/workspace/workspace.controller.ts | 6 +- server/src/workspace/workspace.gateway.ts | 124 +++++++++++------- server/src/workspace/workspace.module.ts | 32 ++++- server/src/workspace/workspace.service.ts | 24 ++-- 7 files changed, 150 insertions(+), 69 deletions(-) diff --git a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts index 0b3a7ef8..3dc21d0d 100644 --- a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts +++ b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts @@ -10,6 +10,7 @@ export const workspaceListContainer = css({ export const workspaceModalContainer = cx( glassContainer({ + border: "md", borderRadius: "bottom", background: "non", boxShadow: "bottom", diff --git a/client/src/features/workSpace/WorkSpace.tsx b/client/src/features/workSpace/WorkSpace.tsx index b3d92c97..b27bc047 100644 --- a/client/src/features/workSpace/WorkSpace.tsx +++ b/client/src/features/workSpace/WorkSpace.tsx @@ -30,7 +30,11 @@ export const WorkSpace = () => { useEffect(() => { if (workspaceMetadata) { - const newWorkspace = new WorkSpaceClass(workspaceMetadata.id, workspaceMetadata.pageList); + const newWorkspace = new WorkSpaceClass( + workspaceMetadata.id, + workspaceMetadata.name, + workspaceMetadata.pageList, + ); newWorkspace.deserialize(workspaceMetadata); setWorkspace(newWorkspace); diff --git a/server/src/workspace/workspace.controller.spec.ts b/server/src/workspace/workspace.controller.spec.ts index c356b5f9..264ea4e6 100644 --- a/server/src/workspace/workspace.controller.spec.ts +++ b/server/src/workspace/workspace.controller.spec.ts @@ -1,18 +1,34 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { WorkspaceController } from './workspace.controller'; +import { Test, TestingModule } from "@nestjs/testing"; +import { WorkspaceController } from "./workspace.controller"; +import { WorkSpaceService } from "./workspace.service"; +import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard"; +import { ExecutionContext } from "@nestjs/common"; -describe('WorkspaceController', () => { +describe("WorkspaceController", () => { let controller: WorkspaceController; beforeEach(async () => { + const mockJwtAuthGuard = { + canActivate: (context: ExecutionContext) => true, + }; + const module: TestingModule = await Test.createTestingModule({ controllers: [WorkspaceController], - }).compile(); + providers: [ + { + provide: WorkSpaceService, + useValue: {}, // 필요한 경우 모의 서비스 구현 + }, + ], + }) + .overrideGuard(JwtAuthGuard) + .useValue(mockJwtAuthGuard) + .compile(); controller = module.get(WorkspaceController); }); - it('should be defined', () => { + it("should be defined", () => { expect(controller).toBeDefined(); }); }); diff --git a/server/src/workspace/workspace.controller.ts b/server/src/workspace/workspace.controller.ts index d7584bd7..889399d3 100644 --- a/server/src/workspace/workspace.controller.ts +++ b/server/src/workspace/workspace.controller.ts @@ -1,8 +1,8 @@ -import { Controller, Post, Delete, Get, Param, Body, Req, UseGuards } from "@nestjs/common"; +import { Controller, Post, Delete, Get, Body, Req, UseGuards } from "@nestjs/common"; import { WorkSpaceService } from "./workspace.service"; import { ApiTags } from "@nestjs/swagger"; -import { JwtAuthGuard } from "src/auth/guards/jwt-auth.guard"; -import { Request as ExpressRequest, Response as ExpressResponse } from "express"; +import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard"; +import { Request as ExpressRequest } from "express"; import { Workspace } from "./schemas/workspace.schema"; @ApiTags("workspace") diff --git a/server/src/workspace/workspace.gateway.ts b/server/src/workspace/workspace.gateway.ts index db865a85..4cba30de 100644 --- a/server/src/workspace/workspace.gateway.ts +++ b/server/src/workspace/workspace.gateway.ts @@ -36,19 +36,22 @@ interface ClientInfo { interface BatchOperation { event: string; - operation: - | RemotePageCreateOperation - | RemotePageDeleteOperation - | RemotePageUpdateOperation - | RemoteBlockInsertOperation - | RemoteBlockDeleteOperation - | RemoteBlockUpdateOperation - | RemoteBlockReorderOperation - | RemoteCharInsertOperation - | RemoteCharDeleteOperation - | RemoteCharUpdateOperation; + operation: Operation; } +type Operation = + | RemotePageCreateOperation + | RemotePageDeleteOperation + | RemotePageUpdateOperation + | RemoteBlockInsertOperation + | RemoteBlockDeleteOperation + | RemoteBlockUpdateOperation + | RemoteBlockReorderOperation + | RemoteCharInsertOperation + | RemoteCharDeleteOperation + | RemoteCharUpdateOperation + | CursorPosition; + @WebSocketGateway({ cors: { origin: @@ -71,7 +74,13 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG this.workSpaceService.setServer(server); } - emitOperation(clientId: string, roomId: string, event: string, operation: any, batch: boolean) { + emitOperation( + clientId: string, + roomId: string, + event: string, + operation: Operation, + batch: boolean, + ) { const key = `${clientId}:${roomId}`; if (batch) { if (!this.batchMap.has(key)) { @@ -95,9 +104,23 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG userId = "guest"; } client.data.userId = userId; - client.join(userId); - // userId라는 방. - const currentWorkSpace = (await this.workSpaceService.getWorkspace(userId)).serialize(); + + const workspaces = await this.workSpaceService.getUserWorkspaces(userId); + let workspaceId = ""; + if (userId === "guest") { + client.join("guest"); + workspaceId = "guest"; + } else if (workspaces.length === 0) { + const workspace = await this.workSpaceService.createWorkspace(userId, "My Workspace"); + client.join(workspace.id); + workspaceId = workspace.id; + } else { + client.join(workspaces[0]); + [workspaceId] = workspaces; + } + client.data.workspaceId = workspaceId; + + const currentWorkSpace = (await this.workSpaceService.getWorkspace(workspaceId)).serialize(); client.emit("workspace", currentWorkSpace); const assignedId = (this.clientIdCounter += 1); @@ -166,9 +189,9 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG try { const { pageId } = data; - const { userId } = client.data; + const { workspaceId } = client.data; // 워크스페이스에서 해당 페이지 찾기 - const currentPage = await this.workSpaceService.getPage(userId, pageId); + const currentPage = await this.workSpaceService.getPage(workspaceId, pageId); if (!currentPage) { throw new WsException(`Page with id ${pageId} not found`); } @@ -240,8 +263,8 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG `Page Create 연산 수신 - Client ID: ${clientInfo?.clientId}, Data:`, JSON.stringify(data), ); - const { userId } = client.data; - const workspace = await this.workSpaceService.getWorkspace(userId); + const { workspaceId } = client.data; + const workspace = await this.workSpaceService.getWorkspace(workspaceId); const newEditorCRDT = new EditorCRDT(data.clientId); const newPage = new Page(nanoid(), "새로운 페이지", "Docs", newEditorCRDT); workspace.pageList.push(newPage); @@ -253,7 +276,7 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG page: newPage.serialize(), } as RemotePageCreateOperation; client.emit("create/page", operation); - this.emitOperation(client.id, userId, "create/page", operation, batch); + this.emitOperation(client.id, workspaceId, "create/page", operation, batch); } catch (error) { this.logger.error( `Page Create 연산 처리 중 오류 발생 - Client ID: ${clientInfo?.clientId}`, @@ -278,11 +301,11 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG `Page Delete 연산 수신 - Client ID: ${clientInfo?.clientId}, Data:`, JSON.stringify(data), ); - const { userId } = client.data; + const { workspaceId } = client.data; // 현재 워크스페이스 가져오기 - const currentWorkspace = await this.workSpaceService.getWorkspace(userId); + const currentWorkspace = await this.workSpaceService.getWorkspace(workspaceId); // pageList에서 해당 페이지 찾기 - const pageIndex = await this.workSpaceService.getPageIndex(userId, data.pageId); + const pageIndex = await this.workSpaceService.getPageIndex(workspaceId, data.pageId); if (pageIndex === -1) { throw new Error(`Page with id ${data.pageId} not found`); } @@ -291,12 +314,12 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG const operation = { type: "pageDelete", - workspaceId: data.workspaceId, + workspaceId, pageId: data.pageId, clientId: data.clientId, } as RemotePageDeleteOperation; client.emit("delete/page", operation); - this.emitOperation(client.id, userId, "delete/page", operation, batch); + this.emitOperation(client.id, workspaceId, "delete/page", operation, batch); this.logger.debug(`Page ${data.pageId} successfully deleted`); } catch (error) { @@ -330,8 +353,7 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG ); const { pageId, title, icon, workspaceId } = data; - const { userId } = client.data; - const currentPage = await this.workSpaceService.getPage(userId, data.pageId); + const currentPage = await this.workSpaceService.getPage(workspaceId, data.pageId); if (!currentPage) { throw new Error(`Page with id ${data.pageId} not found`); } @@ -353,7 +375,7 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG clientId: clientInfo.clientId, } as RemotePageUpdateOperation; client.emit("update/page", operation); - this.emitOperation(client.id, userId, "update/page", operation, batch); + this.emitOperation(client.id, workspaceId, "update/page", operation, batch); this.logger.log(`Page ${pageId} updated successfully by client ${clientInfo.clientId}`); } catch (error) { @@ -381,8 +403,8 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG JSON.stringify(data), ); - const { userId } = client.data; - const currentPage = await this.workSpaceService.getPage(userId, data.pageId); + const { workspaceId } = client.data; + const currentPage = await this.workSpaceService.getPage(workspaceId, data.pageId); if (!currentPage) { throw new Error(`Page with id ${data.pageId} not found`); } @@ -418,8 +440,8 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG `Block Delete 연산 수신 - Client ID: ${clientInfo?.clientId}, Data:`, JSON.stringify(data), ); - const { userId } = client.data; - const currentPage = await this.workSpaceService.getPage(userId, data.pageId); + const { workspaceId } = client.data; + const currentPage = await this.workSpaceService.getPage(workspaceId, data.pageId); if (!currentPage) { throw new Error(`Page with id ${data.pageId} not found`); } @@ -457,8 +479,8 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG JSON.stringify(data), ); - const { userId } = client.data; - const currentPage = await this.workSpaceService.getPage(userId, data.pageId); + const { workspaceId } = client.data; + const currentPage = await this.workSpaceService.getPage(workspaceId, data.pageId); if (!currentPage) { throw new Error(`Page with id ${data.pageId} not found`); } @@ -494,8 +516,8 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG `Block Reorder 연산 수신 - Client ID: ${clientInfo?.clientId}, Data:`, JSON.stringify(data), ); - const { userId } = client.data; - const currentPage = await this.workSpaceService.getPage(userId, data.pageId); + const { workspaceId } = client.data; + const currentPage = await this.workSpaceService.getPage(workspaceId, data.pageId); if (!currentPage) { throw new Error(`Page with id ${data.pageId} not found`); } @@ -535,8 +557,12 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG JSON.stringify(data), ); - const { userId } = client.data; - const currentBlock = await this.workSpaceService.getBlock(userId, data.pageId, data.blockId); + 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`); } @@ -577,8 +603,12 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG `Char Delete 연산 수신 - Client ID: ${clientInfo?.clientId}, Data:`, JSON.stringify(data), ); - const { userId } = client.data; - const currentBlock = await this.workSpaceService.getBlock(userId, data.pageId, data.blockId); + 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`); } @@ -616,8 +646,12 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG `Char Update 연산 수신 - Client ID: ${clientInfo?.clientId}, Data:`, JSON.stringify(data), ); - const { userId } = client.data; - const currentBlock = await this.workSpaceService.getBlock(userId, data.pageId, data.blockId); + 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`); } @@ -660,8 +694,8 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG clientId: clientInfo?.clientId, position: data.position, } as CursorPosition; - const { userId } = client.data; - this.emitOperation(client.id, userId, "cursor", operation, batch); + const { workspaceId } = client.data; + this.emitOperation(client.id, workspaceId, "cursor", operation, batch); } catch (error) { this.logger.error( `Cursor 업데이트 중 오류 발생 - Client ID: ${clientInfo?.clientId}`, @@ -730,7 +764,7 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG } } - private async processOperation(operation: any, client: Socket) { + private async processOperation(operation: Operation, client: Socket) { try { switch (operation.type) { case "blockInsert": diff --git a/server/src/workspace/workspace.module.ts b/server/src/workspace/workspace.module.ts index c8543417..333e252c 100644 --- a/server/src/workspace/workspace.module.ts +++ b/server/src/workspace/workspace.module.ts @@ -4,13 +4,39 @@ import { MongooseModule } from "@nestjs/mongoose"; import { Workspace, WorkspaceSchema } from "./schemas/workspace.schema"; import { WorkspaceGateway } from "./workspace.gateway"; import { AuthModule } from "../auth/auth.module"; +import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard"; +import { JwtModule } from "@nestjs/jwt"; +import { ConfigModule, ConfigService } from "@nestjs/config"; +import { JwtStrategy } from "../auth/strategies/jwt.strategy"; +import { JwtRefreshTokenStrategy } from "../auth/strategies/jwt-refresh-token.strategy"; +import { JwtRefreshTokenAuthGuard } from "../auth/guards/jwt-refresh-token-auth.guard"; +import { User, UserSchema } from "../auth/schemas/user.schema"; @Module({ imports: [ AuthModule, - MongooseModule.forFeature([{ name: Workspace.name, schema: WorkspaceSchema }]), + MongooseModule.forFeature([ + { name: Workspace.name, schema: WorkspaceSchema }, + { name: User.name, schema: UserSchema }, + ]), + JwtModule.registerAsync({ + global: true, + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + secret: config.get("JWT_SECRET"), + signOptions: { expiresIn: "1h" }, + }), + }), + ], + exports: [WorkSpaceService, JwtModule], + providers: [ + WorkSpaceService, + WorkspaceGateway, + JwtStrategy, + JwtRefreshTokenStrategy, + JwtAuthGuard, + JwtRefreshTokenAuthGuard, ], - providers: [WorkSpaceService, WorkspaceGateway], - exports: [WorkSpaceService], }) export class WorkspaceModule {} diff --git a/server/src/workspace/workspace.service.ts b/server/src/workspace/workspace.service.ts index 4094f6f6..129fc5b6 100644 --- a/server/src/workspace/workspace.service.ts +++ b/server/src/workspace/workspace.service.ts @@ -87,15 +87,15 @@ export class WorkSpaceService implements OnModuleInit { } } - async getWorkspace(userId: string): Promise { + async getWorkspace(workspaceId: string): Promise { // 인메모리에서 먼저 찾기 - const cachedWorkspace = this.workspaces.get(userId); + const cachedWorkspace = this.workspaces.get(workspaceId); if (cachedWorkspace) { return cachedWorkspace; } // DB에서 찾기 - const workspaceJSON = await this.workspaceModel.findOne({ id: userId }); + const workspaceJSON = await this.workspaceModel.findOne({ id: workspaceId }); const workspace = new CRDTWorkSpace(); @@ -109,20 +109,20 @@ export class WorkSpaceService implements OnModuleInit { } // 메모리에 캐시하고 반환 - this.workspaces.set(userId, workspace); + this.workspaces.set(workspaceId, workspace); return workspace; } - async getPage(userId: string, pageId: string): Promise { - return (await this.getWorkspace(userId)).pageList.find((page) => page.id === pageId); + async getPage(workspaceId: string, pageId: string): Promise { + return (await this.getWorkspace(workspaceId)).pageList.find((page) => page.id === pageId); } - async getPageIndex(userId: string, pageId: string): Promise { - return (await this.getWorkspace(userId)).pageList.findIndex((page) => page.id === pageId); + async getPageIndex(workspaceId: string, pageId: string): Promise { + return (await this.getWorkspace(workspaceId)).pageList.findIndex((page) => page.id === pageId); } - async getBlock(userId: string, pageId: string, blockId: BlockId): Promise { - const page = await this.getPage(userId, pageId); + async getBlock(workspaceId: string, pageId: string, blockId: BlockId): Promise { + const page = await this.getPage(workspaceId, pageId); if (!page) { throw new Error(`Page with id ${pageId} not found`); } @@ -132,7 +132,7 @@ export class WorkSpaceService implements OnModuleInit { // 워크스페이스 생성 async createWorkspace(userId: string, name: string): Promise { const newWorkspace = await this.workspaceModel.create({ - name: name, + name, authUser: { userId: "owner" }, }); @@ -168,7 +168,7 @@ export class WorkSpaceService implements OnModuleInit { async getUserWorkspaces(userId: string): Promise { const user = await this.userModel.findOne({ id: userId }); if (!user) { - throw new Error(`User with id ${userId} not found`); + return []; } return this.workspaceModel.find({ id: { $in: user.workspaces } }); From b2b9ffa0b3dcc4ff83060a45b6da29179f48303c Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 19:47:10 +0900 Subject: [PATCH 08/23] =?UTF-8?q?fix=20:=20glassContianer=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/styles/recipes/glassContainerRecipe.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/styles/recipes/glassContainerRecipe.ts b/client/src/styles/recipes/glassContainerRecipe.ts index 8f3bd556..def2acae 100644 --- a/client/src/styles/recipes/glassContainerRecipe.ts +++ b/client/src/styles/recipes/glassContainerRecipe.ts @@ -33,7 +33,7 @@ export const glassContainerRecipe = defineRecipe({ }, }, background: { - non: { + none: { background: "token(colors.white/95)", }, }, From 6ef0c9d3d662e825c7a1dbd7139bb883d8d4a2c3 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 19:47:19 +0900 Subject: [PATCH 09/23] =?UTF-8?q?chore:=20=EB=B9=8C=EB=93=9C=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/features/workSpace/WorkSpace.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/src/features/workSpace/WorkSpace.tsx b/client/src/features/workSpace/WorkSpace.tsx index b3d92c97..b27bc047 100644 --- a/client/src/features/workSpace/WorkSpace.tsx +++ b/client/src/features/workSpace/WorkSpace.tsx @@ -30,7 +30,11 @@ export const WorkSpace = () => { useEffect(() => { if (workspaceMetadata) { - const newWorkspace = new WorkSpaceClass(workspaceMetadata.id, workspaceMetadata.pageList); + const newWorkspace = new WorkSpaceClass( + workspaceMetadata.id, + workspaceMetadata.name, + workspaceMetadata.pageList, + ); newWorkspace.deserialize(workspaceMetadata); setWorkspace(newWorkspace); From 590073644815527e3b9489fa3c0c4968cf1bf715 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 19:47:42 +0900 Subject: [PATCH 10/23] =?UTF-8?q?chore:=20=EC=98=A4=ED=83=80=EC=88=98?= =?UTF-8?q?=EC=A0=95=20non=20->=20none?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menuButton/components/WorkspaceSelectModal.style.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts index 0b3a7ef8..b268f69a 100644 --- a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts +++ b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts @@ -11,7 +11,7 @@ export const workspaceListContainer = css({ export const workspaceModalContainer = cx( glassContainer({ borderRadius: "bottom", - background: "non", + background: "none", boxShadow: "bottom", }), css({ From 8e3e0e60450d73e8b80567c1fa143b7ac42c15dd Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 19:47:55 +0900 Subject: [PATCH 11/23] =?UTF-8?q?refactor:=20=ED=98=B8=EB=B2=84=EA=B0=80?= =?UTF-8?q?=20=EC=95=84=EB=8B=88=EB=9D=BC=20=ED=81=B4=EB=A6=AD=ED=95=B4?= =?UTF-8?q?=EC=95=BC=20=EB=AA=A8=EB=8B=AC=EC=B0=BD=20=EB=82=98=EC=98=A4?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #219 --- .../components/menuButton/MenuButton.style.ts | 6 ++- .../components/menuButton/MenuButton.tsx | 46 +++++++++---------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/client/src/components/sidebar/components/menuButton/MenuButton.style.ts b/client/src/components/sidebar/components/menuButton/MenuButton.style.ts index 8ff2be60..fbe93299 100644 --- a/client/src/components/sidebar/components/menuButton/MenuButton.style.ts +++ b/client/src/components/sidebar/components/menuButton/MenuButton.style.ts @@ -4,9 +4,12 @@ export const menuItemWrapper = css({ display: "flex", gap: "md", alignItems: "center", - width: "100%", + width: "250px", padding: "md", cursor: "pointer", + "&:hover": { + backgroundColor: "gray.100", + }, }); export const textBox = css({ @@ -16,7 +19,6 @@ export const textBox = css({ export const menuButtonContainer = css({ position: "relative", - // 버튼과 모달 사이의 간격을 채우는 패딩 추가 _before: { position: "absolute", top: "100%", diff --git a/client/src/components/sidebar/components/menuButton/MenuButton.tsx b/client/src/components/sidebar/components/menuButton/MenuButton.tsx index a3447940..e42ff756 100644 --- a/client/src/components/sidebar/components/menuButton/MenuButton.tsx +++ b/client/src/components/sidebar/components/menuButton/MenuButton.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useUserInfo } from "@stores/useUserStore"; import { menuItemWrapper, textBox, menuButtonContainer } from "./MenuButton.style"; import { MenuIcon } from "./components/MenuIcon"; @@ -6,39 +6,35 @@ import { WorkspaceSelectModal } from "./components/WorkspaceSelectModal"; export const MenuButton = () => { const { name } = useUserInfo(); - const [isHovered, setIsHovered] = useState(false); - const [isModalHovered, setIsModalHovered] = useState(false); + const [isOpen, setIsOpen] = useState(false); - const handleClose = () => { - if (!isHovered && !isModalHovered) { - setIsHovered(false); + const handleMenuClick = () => { + setIsOpen((prev) => !prev); // 토글 형태로 변경 + }; + + // 모달 외부 클릭시 닫기 처리를 위한 함수 + const handleClickOutside = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if (!target.closest(`.${menuButtonContainer}`)) { + setIsOpen(false); } }; + // 외부 클릭 이벤트 리스너 등록 + useEffect(() => { + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + return ( - - setIsModalHovered(true)} - onMouseLeave={() => { - setIsModalHovered(false); - handleClose(); - }} - /> + ); }; From c12490a4f3030a348a71082caddc39b39b3e40d9 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 19:49:10 +0900 Subject: [PATCH 12/23] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=A7=A4=EA=B0=9C=EB=B3=80=EC=88=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menuButton/components/WorkspaceSelectModal.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx index 44762703..cdebd3c3 100644 --- a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx +++ b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx @@ -8,16 +8,9 @@ import { workspaceListContainer, workspaceModalContainer } from "./WorkspaceSele interface WorkspaceSelectModalProps { isOpen: boolean; userName: string | null; - onMouseEnter: () => void; - onMouseLeave: () => void; } -export const WorkspaceSelectModal = ({ - isOpen, - userName, - onMouseEnter, - onMouseLeave, -}: WorkspaceSelectModalProps) => { +export const WorkspaceSelectModal = ({ isOpen, userName }: WorkspaceSelectModalProps) => { const modalRef = useRef(null); const { availableWorkspaces } = useSocketStore(); // 소켓 스토어에서 직접 워크스페이스 목록 가져오기 @@ -45,8 +38,6 @@ export const WorkspaceSelectModal = ({ pointerEvents: isOpen ? "auto" : "none", display: isOpen ? "block" : "none", }} - onMouseEnter={onMouseEnter} - onMouseLeave={onMouseLeave} >
{userName && availableWorkspaces.length > 0 ? ( From 132f9a80a070ff7c14e2a85783ac1923ce0b4a79 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 21:14:12 +0900 Subject: [PATCH 13/23] =?UTF-8?q?style:=20=EB=AF=B8=EC=84=B8=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 1px 삐져나온것 수정 #219 --- .../components/menuButton/components/WorkspaceSelectModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx index cdebd3c3..6d23ec83 100644 --- a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx +++ b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx @@ -32,7 +32,7 @@ export const WorkspaceSelectModal = ({ isOpen, userName }: WorkspaceSelectModalP style={{ position: "absolute", top: "calc(100% + 4px)", - left: 0, + left: -1, width: SIDE_BAR.WIDTH, zIndex: 20, pointerEvents: isOpen ? "auto" : "none", From e2129aca11b8db0e1769351b59fd70903f539a35 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 22:28:51 +0900 Subject: [PATCH 14/23] =?UTF-8?q?feat:=20workspaceListItem=20=EC=9A=A9=20i?= =?UTF-8?q?nterface=20=EC=84=A0=EC=96=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #219 --- @noctaCrdt/Interfaces.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/@noctaCrdt/Interfaces.ts b/@noctaCrdt/Interfaces.ts index 7fee4418..7c41e506 100644 --- a/@noctaCrdt/Interfaces.ts +++ b/@noctaCrdt/Interfaces.ts @@ -190,3 +190,9 @@ export interface RemoteBlockReorderOperation { client: number; pageId: string; } +export interface WorkspaceListItem { + id: string; + name: string; + role: string; + memberCount?: number; +} From 71fc3cea921d2f47085b9496f755a9a24c19942d Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 22:29:38 +0900 Subject: [PATCH 15/23] =?UTF-8?q?refactor:=20useUserInfo=20id=20=EB=A5=BC?= =?UTF-8?q?=20=EC=A2=80=EB=8D=94=20=EB=AA=85=ED=99=95=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?userId=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #219 --- client/src/App.tsx | 6 +++--- client/src/stores/useUserStore.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 0c623466..b968ceea 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -9,11 +9,11 @@ const App = () => { // TODO 라우터, react query 설정 const { isErrorModalOpen, errorMessage } = useErrorStore(); - const { id } = useUserInfo(); + const { userId } = useUserInfo(); useEffect(() => { const socketStore = useSocketStore.getState(); - socketStore.init(id); + socketStore.init(userId, null); // // 소켓이 연결된 후에 이벤트 리스너 등록 // const { socket } = socketStore; @@ -34,7 +34,7 @@ const App = () => { socketStore.cleanup(); }, 0); }; - }, [id]); + }, [userId]); return ( <> diff --git a/client/src/stores/useUserStore.ts b/client/src/stores/useUserStore.ts index 243595a3..152affc4 100644 --- a/client/src/stores/useUserStore.ts +++ b/client/src/stores/useUserStore.ts @@ -63,7 +63,7 @@ export const useUserInfo = () => useUserStore( // state 바뀜에 따라 재렌더링 되도록 useShallow((state) => ({ - id: state.id, + userId: state.id, name: state.name, accessToken: state.accessToken, })), From 2be53de75d4dcaf4e0f36fed0baa445a428b03da Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 22:29:55 +0900 Subject: [PATCH 16/23] =?UTF-8?q?fix:=20=EB=A9=94=EB=89=B4=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=A2=80=EB=8D=94=20=EC=9E=98=20=EC=B0=BE=EA=B2=8C?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=9D=B4=EB=A6=84=20=EB=B6=80?= =?UTF-8?q?=EC=97=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #219 --- .../components/sidebar/components/menuButton/MenuButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/sidebar/components/menuButton/MenuButton.tsx b/client/src/components/sidebar/components/menuButton/MenuButton.tsx index e42ff756..4b9a3948 100644 --- a/client/src/components/sidebar/components/menuButton/MenuButton.tsx +++ b/client/src/components/sidebar/components/menuButton/MenuButton.tsx @@ -15,7 +15,7 @@ export const MenuButton = () => { // 모달 외부 클릭시 닫기 처리를 위한 함수 const handleClickOutside = (e: MouseEvent) => { const target = e.target as HTMLElement; - if (!target.closest(`.${menuButtonContainer}`)) { + if (!target.closest(`.menu_button_container`)) { setIsOpen(false); } }; @@ -29,7 +29,7 @@ export const MenuButton = () => { }, []); return ( - + ); +}; From f856e54d034416138f21acaa3290063b09d25d5b Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 22:32:23 +0900 Subject: [PATCH 19/23] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EC=8A=A4=ED=82=A4=EB=A7=88?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=A9=94=EC=86=8C=EB=93=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기본적으로 workspace가 유저정보를 가지고 있다. - 이는 workspace위주의 동작환경이라 설정 #219 --- server/src/workspace/workspace.service.ts | 29 +++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/server/src/workspace/workspace.service.ts b/server/src/workspace/workspace.service.ts index 129fc5b6..0642ce62 100644 --- a/server/src/workspace/workspace.service.ts +++ b/server/src/workspace/workspace.service.ts @@ -4,7 +4,7 @@ import { Workspace, WorkspaceDocument } from "./schemas/workspace.schema"; import { WorkSpace as CRDTWorkSpace } from "@noctaCrdt/WorkSpace"; import { Model } from "mongoose"; import { Server } from "socket.io"; -import { WorkSpaceSerializedProps } from "@noctaCrdt/Interfaces"; +import { WorkSpaceSerializedProps, WorkspaceListItem } from "@noctaCrdt/Interfaces"; import { Page } from "@noctaCrdt/Page"; import { Block } from "@noctaCrdt/Node"; import { BlockId } from "@noctaCrdt/NodeId"; @@ -133,7 +133,7 @@ export class WorkSpaceService implements OnModuleInit { async createWorkspace(userId: string, name: string): Promise { const newWorkspace = await this.workspaceModel.create({ name, - authUser: { userId: "owner" }, + authUser: new Map([[userId, "owner"]]), // 올바른 형태 }); // 유저 정보 업데이트 @@ -164,14 +164,33 @@ export class WorkSpaceService implements OnModuleInit { await this.workspaceModel.deleteOne({ id: workspaceId }); } - // 워크스페이스 조회 - async getUserWorkspaces(userId: string): Promise { + async getUserWorkspaces(userId: string): Promise { + if (userId === "guest") { + return [ + { + id: "guest", + name: "Guest Workspace", + role: "editor", + memberCount: 0, + }, + ]; + } + const user = await this.userModel.findOne({ id: userId }); if (!user) { return []; } - return this.workspaceModel.find({ id: { $in: user.workspaces } }); + const workspaces = await this.workspaceModel.find({ + id: { $in: user.workspaces }, + }); + + return workspaces.map((workspace) => ({ + id: workspace.id, + name: workspace.name, + role: workspace.authUser.get(userId) || "editor", + memberCount: workspace.authUser.size, + })); } // 워크스페이스에 유저 초대 From f6f66d5a33687e3218ce8b1ff6790459a75f8d23 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 22:33:24 +0900 Subject: [PATCH 20/23] =?UTF-8?q?feat:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20?= =?UTF-8?q?=EC=8A=A4=ED=82=A4=EB=A7=88=EC=99=80=20=ED=95=B4=EB=8B=B9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=9C=A0=EC=A0=80=20id=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EA=B2=8C=20workspace=EB=A5=BC=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=86=A1=EC=8B=A0=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 그외 interface 관련 정보 변경 #219 --- server/src/workspace/workspace.controller.ts | 3 +- server/src/workspace/workspace.gateway.ts | 33 +++++++++++++++----- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/server/src/workspace/workspace.controller.ts b/server/src/workspace/workspace.controller.ts index 889399d3..8d4800b7 100644 --- a/server/src/workspace/workspace.controller.ts +++ b/server/src/workspace/workspace.controller.ts @@ -4,6 +4,7 @@ import { ApiTags } from "@nestjs/swagger"; import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard"; import { Request as ExpressRequest } from "express"; import { Workspace } from "./schemas/workspace.schema"; +import { WorkspaceListItem } from "@noctaCrdt/Interfaces"; @ApiTags("workspace") @UseGuards(JwtAuthGuard) @@ -34,7 +35,7 @@ export class WorkspaceController { // 유저의 워크스페이스 목록 조회 @Get("findAll") - async getUserWorkspaces(@Req() req: ExpressRequest): Promise { + async getUserWorkspaces(@Req() req: ExpressRequest): Promise { const userId = req.user.id; return this.workspaceService.getUserWorkspaces(userId); } diff --git a/server/src/workspace/workspace.gateway.ts b/server/src/workspace/workspace.gateway.ts index 4cba30de..0690262d 100644 --- a/server/src/workspace/workspace.gateway.ts +++ b/server/src/workspace/workspace.gateway.ts @@ -115,11 +115,23 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG client.join(workspace.id); workspaceId = workspace.id; } else { - client.join(workspaces[0]); - [workspaceId] = workspaces; + const requestedWorkspaceId = client.handshake.query.workspaceId; + + // workspaces가 WorkspaceListItem[]이므로 id 비교로 수정 + if ( + requestedWorkspaceId && + workspaces.some((workspace) => workspace.id === requestedWorkspaceId) + ) { + // 요청한 워크스페이스가 있고 접근 권한이 있으면 해당 워크스페이스 사용 + workspaceId = requestedWorkspaceId as string; + } else { + // 없으면 첫 번째 워크스페이스 사용 + workspaceId = workspaces[0].id; // WorkspaceListItem의 id 속성 접근 + } + client.join(workspaceId); } - client.data.workspaceId = workspaceId; + client.data.workspaceId = workspaceId; const currentWorkSpace = (await this.workSpaceService.getWorkspace(workspaceId)).serialize(); client.emit("workspace", currentWorkSpace); @@ -131,10 +143,17 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG this.clientMap.set(client.id, clientInfo); client.emit("assign/clientId", assignedId); - // 클라이언트가 구독을 설정할 시간을 주기 위해 약간의 지연 - setTimeout(() => { - console.log("쏘긴쏜다"); - client.emit("workspace/list", "test"); + setTimeout(async () => { + const workspaces = await this.workSpaceService.getUserWorkspaces(userId); + const workspaceList = workspaces.map((workspace) => ({ + id: workspace.id, + name: workspace.name, + role: workspace.role, + memberCount: workspace.memberCount, + })); + + this.logger.log(`Sending workspace list to client ${client.id}`); + client.emit("workspace/list", workspaceList); }, 100); client.broadcast.emit("userJoined", { clientId: assignedId }); From eb26fba7f6ab10822b7404b88496ba78d8d285fc Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 22:45:59 +0900 Subject: [PATCH 21/23] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BD=98=EC=86=94=EB=A1=9C=EA=B7=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menuButton/components/components/WorkspaceSelectItem.tsx | 1 - client/src/stores/useSocketStore.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/components/sidebar/components/menuButton/components/components/WorkspaceSelectItem.tsx b/client/src/components/sidebar/components/menuButton/components/components/WorkspaceSelectItem.tsx index 56777cee..0952ec05 100644 --- a/client/src/components/sidebar/components/menuButton/components/components/WorkspaceSelectItem.tsx +++ b/client/src/components/sidebar/components/menuButton/components/components/WorkspaceSelectItem.tsx @@ -25,7 +25,6 @@ export const WorkspaceSelectItem = ({ const { userId } = useUserInfo(); const switchWorkspace = useSocketStore((state) => state.switchWorkspace); const handleClick = () => { - console.log("Selected workspace:", id, name, role, memberCount, userName); switchWorkspace(userId, id); }; diff --git a/client/src/stores/useSocketStore.ts b/client/src/stores/useSocketStore.ts index ac3ad462..03c450e2 100644 --- a/client/src/stores/useSocketStore.ts +++ b/client/src/stores/useSocketStore.ts @@ -170,7 +170,7 @@ export const useSocketStore = create((set, get) => ({ switchWorkspace: (userId: string | null, workspaceId: string | null) => { const { socket, init } = get(); - + console.log(userId, workspaceId); // 기존 연결 정리 if (socket) { socket.disconnect(); From 949bfcce08de13cbd0fd2a16552ffa2fb4595708 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 22:50:34 +0900 Subject: [PATCH 22/23] =?UTF-8?q?style:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=EA=B2=8C=20=EB=8B=A4=EB=A5=B8=20=EC=9B=8C=ED=81=AC?= =?UTF-8?q?=EC=8A=A4=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=95=88=EB=82=B4?= =?UTF-8?q?=EB=A9=98=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 다른 워크스페이스 기능은 회원전용 입니다로 수정 #219 --- .../components/WorkspaceSelectModal.style.ts | 8 +++++++ .../components/WorkspaceSelectModal.tsx | 21 +++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts index ef40b3bc..a1b43b59 100644 --- a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts +++ b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.style.ts @@ -19,3 +19,11 @@ export const workspaceModalContainer = cx( display: "flex", }), ); + +export const textBox = css({ + padding: "lg", + color: "gray.500", + textAlign: "center", + fontSize: "md", + whiteSpace: "pre-line", +}); diff --git a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx index 54802562..65193f2a 100644 --- a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx +++ b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx @@ -3,7 +3,11 @@ import { useRef } from "react"; import { SIDE_BAR } from "@constants/size"; import { useSocketStore } from "@src/stores/useSocketStore"; import { css } from "@styled-system/css"; -import { workspaceListContainer, workspaceModalContainer } from "./WorkspaceSelectModal.style"; +import { + workspaceListContainer, + workspaceModalContainer, + textBox, +} from "./WorkspaceSelectModal.style"; import { WorkspaceSelectItem } from "./components/WorkspaceSelectItem"; interface WorkspaceSelectModalProps { @@ -17,9 +21,9 @@ export const WorkspaceSelectModal = ({ isOpen, userName }: WorkspaceSelectModalP const informText = userName ? availableWorkspaces.length > 0 - ? "" // 워크스페이스 목록이 표시될 것이므로 비워둠 + ? "" : "접속할 수 있는 워크스페이스가 없습니다." - : "로그인 해주세요"; + : `다른 워크스페이스 기능은\n 회원전용 입니다`; return ( )) ) : ( -

- {informText} -

+

{informText}

)}
From 4ebee79a11070c6378f65677cbc18c5bafe42da0 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 22:57:40 +0900 Subject: [PATCH 23/23] =?UTF-8?q?chore:=20=EB=B9=8C=EB=93=9C=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menuButton/components/WorkspaceSelectModal.tsx | 1 - .../components/components/WorkspaceSelectItem.tsx | 8 +------- server/src/workspace/workspace.gateway.ts | 2 +- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx index 65193f2a..7f3980a8 100644 --- a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx +++ b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx @@ -2,7 +2,6 @@ import { motion } from "framer-motion"; import { useRef } from "react"; import { SIDE_BAR } from "@constants/size"; import { useSocketStore } from "@src/stores/useSocketStore"; -import { css } from "@styled-system/css"; import { workspaceListContainer, workspaceModalContainer, diff --git a/client/src/components/sidebar/components/menuButton/components/components/WorkspaceSelectItem.tsx b/client/src/components/sidebar/components/menuButton/components/components/WorkspaceSelectItem.tsx index 0952ec05..cdd64344 100644 --- a/client/src/components/sidebar/components/menuButton/components/components/WorkspaceSelectItem.tsx +++ b/client/src/components/sidebar/components/menuButton/components/components/WorkspaceSelectItem.tsx @@ -15,13 +15,7 @@ interface WorkspaceSelectItemProps extends WorkspaceListItem { userName: string; } -export const WorkspaceSelectItem = ({ - id, - name, - role, - memberCount, - userName, -}: WorkspaceSelectItemProps) => { +export const WorkspaceSelectItem = ({ id, name, role, memberCount }: WorkspaceSelectItemProps) => { const { userId } = useUserInfo(); const switchWorkspace = useSocketStore((state) => state.switchWorkspace); const handleClick = () => { diff --git a/server/src/workspace/workspace.gateway.ts b/server/src/workspace/workspace.gateway.ts index 564f9710..1d12ce66 100644 --- a/server/src/workspace/workspace.gateway.ts +++ b/server/src/workspace/workspace.gateway.ts @@ -328,7 +328,7 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG if (pageIndex === -1) { throw new Error(`Page with id ${data.pageId} not found`); } - const pageTitle = (await this.workSpaceService.getPage(userId, data.pageId)).title; + const pageTitle = (await this.workSpaceService.getPage(workspaceId, data.pageId)).title; currentWorkspace.pageList.splice(pageIndex, 1); const operation = {