diff --git a/client/package.json b/client/package.json index 1cc9814..f889fb6 100644 --- a/client/package.json +++ b/client/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite --host", - "build": "tsc -b && vite build", + "build": "tsc -b && panda codegen && vite build", "lint": "eslint \"src/**/*.{ts,tsx}\" --fix", "preview": "vite preview", "prepare": "panda codegen" diff --git a/client/src/components/modal/InviteModal.tsx b/client/src/components/modal/InviteModal.tsx index e6779dc..9a116c9 100644 --- a/client/src/components/modal/InviteModal.tsx +++ b/client/src/components/modal/InviteModal.tsx @@ -1,5 +1,5 @@ // InviteModal.tsx -import { useState } from "react"; +import { useState, useEffect, useRef } from "react"; import { modalContentContainer, titleText, descriptionText, emailInput } from "./InviteModal.style"; import { Modal } from "./modal"; @@ -11,6 +11,13 @@ interface InviteModalProps { export const InviteModal = ({ isOpen, onClose, onInvite }: InviteModalProps) => { const [email, setEmail] = useState(""); + const inputRef = useRef(null); + + useEffect(() => { + if (isOpen) { + inputRef.current?.focus(); + } + }, [isOpen]); const handleInvite = () => { onInvite(email); @@ -30,6 +37,7 @@ export const InviteModal = ({ isOpen, onClose, onInvite }: InviteModalProps) =>

워크스페이스 초대

초대할 사용자의 이메일을 입력해주세요

setEmail(e.target.value)} placeholder="이메일 주소 입력" diff --git a/client/src/components/modal/modal.tsx b/client/src/components/modal/modal.tsx index d4107ea..c5b90d5 100644 --- a/client/src/components/modal/modal.tsx +++ b/client/src/components/modal/modal.tsx @@ -53,7 +53,16 @@ export const Modal = ({ return createPortal( {isOpen && ( -
+
e.stopPropagation()} + onKeyDown={(e) => { + if (e.key === "Escape") { + secondaryButtonOnClick?.(); + } + }} + > { openModal: openInviteModal, closeModal: closeInviteModal, } = useModal(); - + const currentRole = useWorkspaceStore((state) => state.currentRole); const handleMenuClick = () => { setIsOpen((prev) => !prev); }; @@ -81,6 +82,15 @@ export const MenuButton = () => { workspaceId: workspace.id, }); }; + + const handleInviteModal = () => { + if (isInviteModalOpen) return; + if (currentRole === "editor") { + addToast("Editor 권한으로는 초대할 수 없습니다."); + return; + } + openInviteModal(); + }; return ( <> - + state.setCurrentRole); const isActive = workspace?.id === id; // 현재 워크스페이스 확인 const handleClick = () => { if (!isActive) { switchWorkspace(userId, id); + setCurrentRole(role); addToast(`워크스페이스(${name})에 접속하였습니다.`); } }; diff --git a/client/src/stores/useSocketStore.ts b/client/src/stores/useSocketStore.ts index 0d191ce..b5720f9 100644 --- a/client/src/stores/useSocketStore.ts +++ b/client/src/stores/useSocketStore.ts @@ -15,6 +15,7 @@ import { } from "@noctaCrdt/Interfaces"; import { io, Socket } from "socket.io-client"; import { create } from "zustand"; +import { useWorkspaceStore } from "./useWorkspaceStore"; class BatchProcessor { private batch: any[] = []; @@ -136,7 +137,7 @@ export const useSocketStore = create((set, get) => ({ socket.on("workspace", (workspace: WorkSpaceSerializedProps) => { const { setWorkspace } = get(); - setWorkspace(workspace); // 수정된 부분 + setWorkspace(workspace); }); socket.on("workspace/connections", (connections: Record) => { @@ -153,12 +154,23 @@ export const useSocketStore = create((set, get) => ({ socket.on("workspace/list", (workspaces: WorkspaceListItem[]) => { set({ availableWorkspaces: workspaces }); + const { availableWorkspaces } = get(); + console.log(availableWorkspaces); + const { workspace } = get(); + const currentWorkspace = availableWorkspaces.find((ws) => ws.id === workspace!.id); + if (currentWorkspace) { + useWorkspaceStore.getState().setCurrentRole(currentWorkspace.role); + } }); socket.on("error", (error: Error) => { console.error("Socket error:", error); }); + socket.on("workspace/role", (data: { role: "owner" | "editor" }) => { + useWorkspaceStore.getState().setCurrentRole(data.role); + }); + socket.connect(); }, diff --git a/client/src/stores/useWorkspaceStore.ts b/client/src/stores/useWorkspaceStore.ts new file mode 100644 index 0000000..4cd1d1b --- /dev/null +++ b/client/src/stores/useWorkspaceStore.ts @@ -0,0 +1,15 @@ +import { create } from "zustand"; + +// 워크스페이스 권한 타입 정의 + +interface WorkspaceStore { + // 현재 선택된 워크스페이스의 권한 + currentRole: string | null; + // 권한 설정 함수 + setCurrentRole: (role: string | null) => void; +} + +export const useWorkspaceStore = create((set) => ({ + currentRole: null, + setCurrentRole: (role) => set({ currentRole: role }), +})); diff --git a/server/src/workspace/workspace.gateway.ts b/server/src/workspace/workspace.gateway.ts index 07372f9..b8351f6 100644 --- a/server/src/workspace/workspace.gateway.ts +++ b/server/src/workspace/workspace.gateway.ts @@ -279,13 +279,17 @@ export class WorkspaceGateway implements OnGatewayInit, OnGatewayConnection, OnG this.logger.log(`Sending workspace list to client ${client.id}`); server.to(socketId).emit("workspace/list", workspaceList); } - // 5. 초대한 사용자에게 성공 메시지 client.emit("invite/workspace/success", { email: data.email, workspaceId: data.workspaceId, message: `${targetUser.name}유저를 초대하였습니다.`, }); + const sentUserUpdatedWorkspaces = await this.workSpaceService.getUserWorkspaces( + client.data.userId, + ); + // 6. 초대한 사용자의 UI에 최신 workspace/list 반영 + client.emit("workspace/list", sentUserUpdatedWorkspaces); } catch (error) { this.logger.error( `Workspace Invite 처리 중 오류 발생 - Client ID: ${clientInfo.clientId}`,