Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#260 workspace의member count 실시간 미동기화 현상 #266

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 9 additions & 1 deletion client/src/components/modal/InviteModal.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -11,6 +11,13 @@ interface InviteModalProps {

export const InviteModal = ({ isOpen, onClose, onInvite }: InviteModalProps) => {
const [email, setEmail] = useState("");
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
if (isOpen) {
inputRef.current?.focus();
}
}, [isOpen]);

const handleInvite = () => {
onInvite(email);
Expand All @@ -30,6 +37,7 @@ export const InviteModal = ({ isOpen, onClose, onInvite }: InviteModalProps) =>
<h2 className={titleText}>워크스페이스 초대</h2>
<p className={descriptionText}>초대할 사용자의 이메일을 입력해주세요</p>
<input
ref={inputRef}
className={emailInput}
onChange={(e) => setEmail(e.target.value)}
placeholder="이메일 주소 입력"
Expand Down
11 changes: 10 additions & 1 deletion client/src/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,16 @@ export const Modal = ({
return createPortal(
<AnimatePresence>
{isOpen && (
<div className={container}>
<div
role="dialog"
className={container}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === "Escape") {
secondaryButtonOnClick?.();
}
}}
>
<motion.div
initial={overlayAnimation.initial}
animate={overlayAnimation.animate}
Expand Down
14 changes: 12 additions & 2 deletions client/src/components/sidebar/components/menuButton/MenuButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { InviteModal } from "@src/components/modal/InviteModal";
import { useModal } from "@src/components/modal/useModal";
import { useSocketStore } from "@src/stores/useSocketStore";
import { useToastStore } from "@src/stores/useToastStore";
import { useWorkspaceStore } from "@src/stores/useWorkspaceStore";
import { useUserInfo } from "@stores/useUserStore";
import { menuItemWrapper, textBox, menuButtonContainer } from "./MenuButton.style";
import { MenuIcon } from "./components/MenuIcon";
Expand All @@ -18,7 +19,7 @@ export const MenuButton = () => {
openModal: openInviteModal,
closeModal: closeInviteModal,
} = useModal();

const currentRole = useWorkspaceStore((state) => state.currentRole);
const handleMenuClick = () => {
setIsOpen((prev) => !prev);
};
Expand Down Expand Up @@ -81,6 +82,15 @@ export const MenuButton = () => {
workspaceId: workspace.id,
});
};

const handleInviteModal = () => {
if (isInviteModalOpen) return;
if (currentRole === "editor") {
addToast("Editor 권한으로는 초대할 수 없습니다.");
return;
}
openInviteModal();
};
return (
<>
<button
Expand All @@ -92,7 +102,7 @@ export const MenuButton = () => {
<MenuIcon />
<p className={textBox}>{name ?? "Nocta"}</p>
</button>
<WorkspaceSelectModal isOpen={isOpen} userName={name} onInviteClick={openInviteModal} />
<WorkspaceSelectModal isOpen={isOpen} userName={name} onInviteClick={handleInviteModal} />
<InviteModal
isOpen={isInviteModalOpen}
onClose={closeInviteModal}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { WorkspaceListItem } from "@noctaCrdt/Interfaces"; // 이전에 만든
import { useSocketStore } from "@src/stores/useSocketStore";
import { useToastStore } from "@src/stores/useToastStore";
import { useUserInfo } from "@src/stores/useUserStore";
import { useWorkspaceStore } from "@src/stores/useWorkspaceStore";
import {
itemContainer,
itemContent,
Expand All @@ -28,10 +29,12 @@ export const WorkspaceSelectItem = ({
const { userId } = useUserInfo();
const { workspace, switchWorkspace } = useSocketStore();
const { addToast } = useToastStore();
const setCurrentRole = useWorkspaceStore((state) => state.setCurrentRole);
const isActive = workspace?.id === id; // 현재 워크스페이스 확인
const handleClick = () => {
if (!isActive) {
switchWorkspace(userId, id);
setCurrentRole(role);
addToast(`워크스페이스(${name})에 접속하였습니다.`);
}
};
Expand Down
14 changes: 13 additions & 1 deletion client/src/stores/useSocketStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -136,7 +137,7 @@ export const useSocketStore = create<SocketStore>((set, get) => ({

socket.on("workspace", (workspace: WorkSpaceSerializedProps) => {
const { setWorkspace } = get();
setWorkspace(workspace); // 수정된 부분
setWorkspace(workspace);
});

socket.on("workspace/connections", (connections: Record<string, number>) => {
Expand All @@ -153,12 +154,23 @@ export const useSocketStore = create<SocketStore>((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();
},

Expand Down
15 changes: 15 additions & 0 deletions client/src/stores/useWorkspaceStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { create } from "zustand";

// 워크스페이스 권한 타입 정의

interface WorkspaceStore {
// 현재 선택된 워크스페이스의 권한
currentRole: string | null;
// 권한 설정 함수
setCurrentRole: (role: string | null) => void;
}

export const useWorkspaceStore = create<WorkspaceStore>((set) => ({
currentRole: null,
setCurrentRole: (role) => set({ currentRole: role }),
}));
6 changes: 5 additions & 1 deletion server/src/workspace/workspace.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down
Loading