From 375cbb148327f57e1df4c0842a2614247750fedf Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 23:42:09 +0900 Subject: [PATCH 01/15] =?UTF-8?q?feat:=20InviteModal=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #71 --- .../src/components/modal/InviteModal.style.ts | 39 +++++++++++++++++ client/src/components/modal/InviteModal.tsx | 42 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 client/src/components/modal/InviteModal.style.ts create mode 100644 client/src/components/modal/InviteModal.tsx diff --git a/client/src/components/modal/InviteModal.style.ts b/client/src/components/modal/InviteModal.style.ts new file mode 100644 index 00000000..cc6cb8c8 --- /dev/null +++ b/client/src/components/modal/InviteModal.style.ts @@ -0,0 +1,39 @@ +// InviteModal.style.ts +import { css } from "@styled-system/css"; + +export const modalContentContainer = css({ + display: "flex", + gap: "16px", + flexDirection: "column", + width: "400px", + padding: "16px", +}); + +export const titleText = css({ + color: "gray.800", + fontSize: "xl", + fontWeight: "bold", +}); + +export const descriptionText = css({ + color: "gray.600", + fontSize: "sm", +}); + +export const emailInput = css({ + outline: "none", + border: "1px solid", + borderColor: "gray.200", + borderRadius: "md", + // 기본 input 스타일 추가 + width: "100%", + padding: "8px 12px", + fontSize: "sm", + _placeholder: { + color: "gray.400", + }, + _focus: { + borderColor: "blue.500", + boxShadow: "0 0 0 1px blue.500", + }, +}); diff --git a/client/src/components/modal/InviteModal.tsx b/client/src/components/modal/InviteModal.tsx new file mode 100644 index 00000000..e6779dce --- /dev/null +++ b/client/src/components/modal/InviteModal.tsx @@ -0,0 +1,42 @@ +// InviteModal.tsx +import { useState } from "react"; +import { modalContentContainer, titleText, descriptionText, emailInput } from "./InviteModal.style"; +import { Modal } from "./modal"; + +interface InviteModalProps { + isOpen: boolean; + onClose: () => void; + onInvite: (email: string) => void; +} + +export const InviteModal = ({ isOpen, onClose, onInvite }: InviteModalProps) => { + const [email, setEmail] = useState(""); + + const handleInvite = () => { + onInvite(email); + setEmail(""); + onClose(); + }; + + return ( + +
+

워크스페이스 초대

+

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

+ setEmail(e.target.value)} + placeholder="이메일 주소 입력" + type="email" + value={email} + /> +
+
+ ); +}; From c18ec74f383580c5acbf0084e3b56131c0fb3ca6 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 23:42:32 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat:=20InviteButton=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #71 --- .../components/InviteButton.style.ts | 19 +++++++++++++++++++ .../components/components/InviteButton.tsx | 15 +++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 client/src/components/sidebar/components/menuButton/components/components/InviteButton.style.ts create mode 100644 client/src/components/sidebar/components/menuButton/components/components/InviteButton.tsx diff --git a/client/src/components/sidebar/components/menuButton/components/components/InviteButton.style.ts b/client/src/components/sidebar/components/menuButton/components/components/InviteButton.style.ts new file mode 100644 index 00000000..bac3731c --- /dev/null +++ b/client/src/components/sidebar/components/menuButton/components/components/InviteButton.style.ts @@ -0,0 +1,19 @@ +import { css } from "@styled-system/css"; + +export const inviteButtonStyle = css({ + display: "flex", + gap: "32px", + alignItems: "center", + borderTop: "1px solid", + borderColor: "gray.200", + + width: "100%", + padding: "12px 16px", + color: "gray.600", + backgroundColor: "transparent", + transition: "all 0.2s", + cursor: "pointer", + _hover: { + backgroundColor: "gray.200", + }, +}); diff --git a/client/src/components/sidebar/components/menuButton/components/components/InviteButton.tsx b/client/src/components/sidebar/components/menuButton/components/components/InviteButton.tsx new file mode 100644 index 00000000..62090c80 --- /dev/null +++ b/client/src/components/sidebar/components/menuButton/components/components/InviteButton.tsx @@ -0,0 +1,15 @@ +import Plus from "@assets/icons/plusIcon.svg?react"; +import { inviteButtonStyle } from "./InviteButton.style"; + +interface InviteButtonProps { + onClick: () => void; +} + +export const InviteButton = ({ onClick }: InviteButtonProps) => { + return ( + + ); +}; From af1260781943b4c94574c3a1ac2c4a7af0c20ed9 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Fri, 29 Nov 2024 23:43:05 +0900 Subject: [PATCH 03/15] =?UTF-8?q?feat:=20InviteButton=20=EB=88=84=EB=A5=B4?= =?UTF-8?q?=EB=A9=B4=20InviteModal=EC=9D=84=20=ED=91=9C=EC=8B=9C=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #71 --- .../components/menuButton/MenuButton.tsx | 30 +++++++++++++------ .../components/WorkspaceSelectModal.tsx | 20 +++++++++---- .../components/WorkspaceSelectItem.style.tsx | 17 ++++++++--- .../components/WorkspaceSelectItem.tsx | 10 +++++-- 4 files changed, 56 insertions(+), 21 deletions(-) diff --git a/client/src/components/sidebar/components/menuButton/MenuButton.tsx b/client/src/components/sidebar/components/menuButton/MenuButton.tsx index 4b9a3948..746bb679 100644 --- a/client/src/components/sidebar/components/menuButton/MenuButton.tsx +++ b/client/src/components/sidebar/components/menuButton/MenuButton.tsx @@ -1,4 +1,6 @@ import { useState, useEffect } from "react"; +import { InviteModal } from "@src/components/modal/InviteModal"; +import { useModal } from "@src/components/modal/useModal"; import { useUserInfo } from "@stores/useUserStore"; import { menuItemWrapper, textBox, menuButtonContainer } from "./MenuButton.style"; import { MenuIcon } from "./components/MenuIcon"; @@ -7,12 +9,16 @@ import { WorkspaceSelectModal } from "./components/WorkspaceSelectModal"; export const MenuButton = () => { const { name } = useUserInfo(); const [isOpen, setIsOpen] = useState(false); + const { + isOpen: isInviteModalOpen, + openModal: openInviteModal, + closeModal: closeInviteModal, + } = useModal(); const handleMenuClick = () => { - setIsOpen((prev) => !prev); // 토글 형태로 변경 + setIsOpen((prev) => !prev); }; - // 모달 외부 클릭시 닫기 처리를 위한 함수 const handleClickOutside = (e: MouseEvent) => { const target = e.target as HTMLElement; if (!target.closest(`.menu_button_container`)) { @@ -20,7 +26,6 @@ export const MenuButton = () => { } }; - // 외부 클릭 이벤트 리스너 등록 useEffect(() => { document.addEventListener("mousedown", handleClickOutside); return () => { @@ -28,13 +33,20 @@ export const MenuButton = () => { }; }, []); + const handleInvite = (email: string) => { + console.log("Invite user:", email); + }; + return ( - + - - + + ); }; diff --git a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx index 7f3980a8..c18d63a7 100644 --- a/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx +++ b/client/src/components/sidebar/components/menuButton/components/WorkspaceSelectModal.tsx @@ -7,22 +7,29 @@ import { workspaceModalContainer, textBox, } from "./WorkspaceSelectModal.style"; +import { InviteButton } from "./components/InviteButton"; import { WorkspaceSelectItem } from "./components/WorkspaceSelectItem"; interface WorkspaceSelectModalProps { isOpen: boolean; userName: string | null; + onInviteClick: () => void; } -export const WorkspaceSelectModal = ({ isOpen, userName }: WorkspaceSelectModalProps) => { +export const WorkspaceSelectModal = ({ + isOpen, + userName, + onInviteClick, +}: WorkspaceSelectModalProps) => { const modalRef = useRef(null); - const { availableWorkspaces } = useSocketStore(); // 소켓 스토어에서 직접 워크스페이스 목록 가져오기 + const { availableWorkspaces } = useSocketStore(); const informText = userName ? availableWorkspaces.length > 0 ? "" : "접속할 수 있는 워크스페이스가 없습니다." : `다른 워크스페이스 기능은\n 회원전용 입니다`; + return (
{userName && availableWorkspaces.length > 0 ? ( - availableWorkspaces.map((workspace) => ( - - )) + <> + {availableWorkspaces.map((workspace) => ( + + ))} + + ) : (

{informText}

)} diff --git a/client/src/components/sidebar/components/menuButton/components/components/WorkspaceSelectItem.style.tsx b/client/src/components/sidebar/components/menuButton/components/components/WorkspaceSelectItem.style.tsx index 568d9316..7154036c 100644 --- a/client/src/components/sidebar/components/menuButton/components/components/WorkspaceSelectItem.style.tsx +++ b/client/src/components/sidebar/components/menuButton/components/components/WorkspaceSelectItem.style.tsx @@ -5,14 +5,22 @@ export const itemContainer = css({ display: "flex", justifyContent: "space-between", alignItems: "center", - padding: "md", + padding: "8px", cursor: "pointer", - _hover: { backgroundColor: "gray.100" }, + _hover: { backgroundColor: "gray.200" }, }); +export const informBox = css({ + display: "flex", + gap: "16px", + justifyContent: "center", + alignItems: "center", + marginLeft: "14px", +}); export const itemContent = css({ display: "flex", - gap: "2", + flex: 1, + gap: "10", alignItems: "center", }); @@ -24,12 +32,13 @@ export const itemIcon = css({ width: "8", height: "8", fontSize: "sm", - backgroundColor: "gray.200", + backgroundColor: "gray.100", }); export const itemInfo = css({ display: "flex", flexDirection: "column", + alignItems: "center", }); export const itemName = css({ 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 cdd64344..c257f61c 100644 --- a/client/src/components/sidebar/components/menuButton/components/components/WorkspaceSelectItem.tsx +++ b/client/src/components/sidebar/components/menuButton/components/components/WorkspaceSelectItem.tsx @@ -9,6 +9,7 @@ import { itemMemberCount, itemName, itemRole, + informBox, } from "./WorkspaceSelectItem.style"; interface WorkspaceSelectItemProps extends WorkspaceListItem { @@ -28,11 +29,14 @@ export const WorkspaceSelectItem = ({ id, name, role, memberCount }: WorkspaceSe
{name.charAt(0)}
{name} - {role} +
+ {role} + {memberCount !== undefined && ( + 접속자수 : {memberCount} 명 + )} +
- - {memberCount !== undefined && 접속자수 : {memberCount} 명 } ); }; From 2775b5f80f5c7d773105756e38672767fe1656dd Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Sat, 30 Nov 2024 00:56:26 +0900 Subject: [PATCH 04/15] =?UTF-8?q?feat:=20toast=EB=A5=BC=20=ED=86=B5?= =?UTF-8?q?=ED=95=B4=20=EC=B4=88=EB=8C=80,=20=EC=B4=88=EB=8C=80=EB=90=A8,?= =?UTF-8?q?=20=EC=B4=88=EB=8C=80=EA=B1=B0=EB=B6=80=20=EB=93=B1=EC=9D=98=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 서버쪽의 workspace/list 를 초대받은 유저에게 재송신 - 유저의 아이디를 toast에 표시하게 수정 #71 --- .../components/menuButton/MenuButton.tsx | 48 +++++++++- server/src/auth/auth.service.ts | 3 + server/src/workspace/workspace.gateway.ts | 89 ++++++++++++++++++- server/src/workspace/workspace.interface.ts | 4 + 4 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 server/src/workspace/workspace.interface.ts diff --git a/client/src/components/sidebar/components/menuButton/MenuButton.tsx b/client/src/components/sidebar/components/menuButton/MenuButton.tsx index 746bb679..866b70b8 100644 --- a/client/src/components/sidebar/components/menuButton/MenuButton.tsx +++ b/client/src/components/sidebar/components/menuButton/MenuButton.tsx @@ -1,6 +1,8 @@ import { useState, useEffect } from "react"; 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 { useUserInfo } from "@stores/useUserStore"; import { menuItemWrapper, textBox, menuButtonContainer } from "./MenuButton.style"; import { MenuIcon } from "./components/MenuIcon"; @@ -9,6 +11,8 @@ import { WorkspaceSelectModal } from "./components/WorkspaceSelectModal"; export const MenuButton = () => { const { name } = useUserInfo(); const [isOpen, setIsOpen] = useState(false); + const { socket, workspace } = useSocketStore(); + const { addToast } = useToastStore(); const { isOpen: isInviteModalOpen, openModal: openInviteModal, @@ -33,10 +37,50 @@ export const MenuButton = () => { }; }, []); + useEffect(() => { + if (!socket) return; + + // 초대 성공 응답 수신 + socket.on( + "invite/workspace/success", + (data: { email: string; workspaceId: string; message: string }) => { + addToast(data.message); + closeInviteModal(); + }, + ); + + // 초대 실패 응답 수신 + socket.on( + "invite/workspace/fail", + (data: { email: string; workspaceId: string; message: string }) => { + addToast(data.message); + closeInviteModal(); + }, + ); + + // 초대 받은 경우 수신 + socket.on( + "workspace/invited", + (data: { workspaceId: string; invitedBy: string; message: string }) => { + addToast(data.message); + }, + ); + + return () => { + socket.off("invite/workspace/success"); + socket.off("invite/workspace/fail"); + socket.off("workspace/invited"); + }; + }, [socket]); + const handleInvite = (email: string) => { - console.log("Invite user:", email); - }; + if (!socket || !workspace?.id) return; + socket.emit("invite/workspace", { + email, + workspaceId: workspace.id, + }); + }; return ( <>