diff --git a/frontend/src/apis/api/inviteAPI.ts b/frontend/src/apis/api/inviteAPI.ts new file mode 100644 index 0000000..4a3a76e --- /dev/null +++ b/frontend/src/apis/api/inviteAPI.ts @@ -0,0 +1,10 @@ +import { API_URL } from "../../constants/path"; +import { authAPI } from "../utils/authAPI"; + +export const getInvitePreview = async (inviteLinkId: string) => { + const response = await authAPI.get( + `${API_URL.INVITE_PREVIEW}/${inviteLinkId}` + ); + + return response; +}; diff --git a/frontend/src/components/landing/member/LandingMember.tsx b/frontend/src/components/landing/member/LandingMember.tsx index 4dd8245..9e07052 100644 --- a/frontend/src/components/landing/member/LandingMember.tsx +++ b/frontend/src/components/landing/member/LandingMember.tsx @@ -73,6 +73,12 @@ const LandingMember = ({ projectTitle }: LandingMemberProps) => { }); } + const handleChangeInviteLinkClick = () => { + console.log("Asdfsdf"); + + socket.emit("inviteLink", { action: "update", content: {} }); + }; + return (
@@ -103,7 +109,10 @@ const LandingMember = ({ projectTitle }: LandingMemberProps) => { 초대링크 복사 | -
diff --git a/frontend/src/components/landing/project/LandingProject.tsx b/frontend/src/components/landing/project/LandingProject.tsx index 2da8e08..c134b97 100644 --- a/frontend/src/components/landing/project/LandingProject.tsx +++ b/frontend/src/components/landing/project/LandingProject.tsx @@ -4,6 +4,7 @@ import LandingProjectLink from "./LandingProjectLink"; import { useOutletContext } from "react-router-dom"; import useLandingProjectSocket from "../../../hooks/common/landing/useLandingProjectSocket"; +import useMemberStore from "../../../stores/useMemberStore"; interface LandingProjectProps { projectId: string; @@ -12,6 +13,7 @@ interface LandingProjectProps { const LandingProject = ({ projectId }: LandingProjectProps) => { const { socket }: { socket: Socket } = useOutletContext(); const { project } = useLandingProjectSocket(socket); + const { role } = useMemberStore((state) => state.myInfo); return (
@@ -22,10 +24,12 @@ const LandingProject = ({ projectId }: LandingProjectProps) => {

{project.subject}
-
+
- + {role === "LEADER" && ( + + )}
); diff --git a/frontend/src/components/landing/project/LandingProjectLink.tsx b/frontend/src/components/landing/project/LandingProjectLink.tsx index 8653fcd..d6f4bab 100644 --- a/frontend/src/components/landing/project/LandingProjectLink.tsx +++ b/frontend/src/components/landing/project/LandingProjectLink.tsx @@ -12,7 +12,7 @@ const LandingProjectLink = ({ projectId, type }: LandingProjectLinkProps) => { return (
diff --git a/frontend/src/components/main/PageLinkIcons.tsx b/frontend/src/components/main/PageLinkIcons.tsx index 78b2566..d42e53d 100644 --- a/frontend/src/components/main/PageLinkIcons.tsx +++ b/frontend/src/components/main/PageLinkIcons.tsx @@ -5,34 +5,41 @@ import SprintIcon from "../../assets/icons/sprint.svg?react"; import SettingIcon from "../../assets/icons/settings.svg?react"; import { LINK_URL } from "../../constants/path"; import { ProjectSidebarProps } from "../../types/common/main"; +import useMemberStore from "../../stores/useMemberStore"; -const PageLinkIcons = ({ pathname, projectId }: ProjectSidebarProps) => ( -
- - - - -
-); +const PageLinkIcons = ({ pathname, projectId }: ProjectSidebarProps) => { + const { role } = useMemberStore((state) => state.myInfo); + + return ( +
+ + + + {role === "LEADER" && ( + + )} +
+ ); +}; export default PageLinkIcons; diff --git a/frontend/src/components/setting/JoinRequestBlock.tsx b/frontend/src/components/setting/JoinRequestBlock.tsx new file mode 100644 index 0000000..077c84d --- /dev/null +++ b/frontend/src/components/setting/JoinRequestBlock.tsx @@ -0,0 +1,38 @@ +import useMemberStore from "../../stores/useMemberStore"; +import { SettingJoinRequestDTO } from "../../types/DTO/settingDTO"; + +interface JoinRequestBlockProps extends SettingJoinRequestDTO {} + +const JoinRequestBlock = ({ username, imageUrl }: JoinRequestBlockProps) => { + const myRole = useMemberStore((state) => state.myInfo.role); + + return ( +
+
+ {username} +

{username}

+
+
+
+ {myRole === "LEADER" && ( + <> + + + + )} +
+
+ ); +}; + +export default JoinRequestBlock; diff --git a/frontend/src/components/setting/MemberBlock.tsx b/frontend/src/components/setting/MemberBlock.tsx index e4c319f..a6ba88d 100644 --- a/frontend/src/components/setting/MemberBlock.tsx +++ b/frontend/src/components/setting/MemberBlock.tsx @@ -1,10 +1,11 @@ import useMemberStore from "../../stores/useMemberStore"; -import { LandingMemberDTO } from "../../types/DTO/landingDTO"; +import { SettingMemberDTO } from "../../types/DTO/settingDTO"; -interface MemberBlockProps extends LandingMemberDTO {} +interface MemberBlockProps extends SettingMemberDTO {} const MemberBlock = ({ username, imageUrl, role }: MemberBlockProps) => { const myRole = useMemberStore((state) => state.myInfo.role); + const myUserName = useMemberStore((state) => state.myInfo.username); return (
@@ -16,7 +17,7 @@ const MemberBlock = ({ username, imageUrl, role }: MemberBlockProps) => {

{role}

- {myRole === "LEADER" && ( + {myRole === "LEADER" && myUserName !== username && (
diff --git a/frontend/src/constants/path.ts b/frontend/src/constants/path.ts index 5f54ea7..3f10454 100644 --- a/frontend/src/constants/path.ts +++ b/frontend/src/constants/path.ts @@ -9,7 +9,8 @@ export const API_URL = { NICKNAME_AVAILABLILITY: "/member/availability", GITHUB_USERNAME: "/auth/github/username", PROJECT: "/project", - PROJECT_JOIN: "/project/join", + PROJECT_JOIN: "/project/join-request", + INVITE_PREVIEW: "/project/invite-preview", }; export const ROUTER_URL = { diff --git a/frontend/src/hooks/common/member/useUpdateUserStatus.ts b/frontend/src/hooks/common/member/useUpdateUserStatus.ts index 943f623..13db870 100644 --- a/frontend/src/hooks/common/member/useUpdateUserStatus.ts +++ b/frontend/src/hooks/common/member/useUpdateUserStatus.ts @@ -3,6 +3,7 @@ import { Socket } from "socket.io-client"; import { LandingSocketData, LandingSocketDomain, + LandingSocketInviteLinkAction, LandingSocketMemberAction, } from "../../../types/common/landing"; import { LandingDTO, LandingMemberDTO } from "../../../types/DTO/landingDTO"; @@ -76,6 +77,16 @@ const useUpdateUserStatus = ( } }; + const handleInviteLinkEvent = ( + action: LandingSocketInviteLinkAction, + content: { inviteLinkId: string } + ) => { + if (action === "update") { + alert("초대링크가 변경되었습니다."); + inviteLinkIdRef.current = content.inviteLinkId; + } + }; + const handleOnLanding = ({ domain, action, content }: LandingSocketData) => { switch (domain) { case LandingSocketDomain.INIT: @@ -84,6 +95,9 @@ const useUpdateUserStatus = ( case LandingSocketDomain.MEMBER: handleMemberEvent(action, content); break; + case LandingSocketDomain.INVITE_LINK: + handleInviteLinkEvent(action, content); + break; } }; diff --git a/frontend/src/hooks/common/socket/useSocket.ts b/frontend/src/hooks/common/socket/useSocket.ts index f575984..d0395a2 100644 --- a/frontend/src/hooks/common/socket/useSocket.ts +++ b/frontend/src/hooks/common/socket/useSocket.ts @@ -24,7 +24,7 @@ const useSocket = (projectId: string) => { alert("프로젝트가 삭제되었습니다."); setTimeout(() => { navigate(ROUTER_URL.PROJECTS); - }, 1000); + }, 500); } }; diff --git a/frontend/src/hooks/pages/setting/useSettingSocket.ts b/frontend/src/hooks/pages/setting/useSettingSocket.ts index 7f1f549..522de06 100644 --- a/frontend/src/hooks/pages/setting/useSettingSocket.ts +++ b/frontend/src/hooks/pages/setting/useSettingSocket.ts @@ -5,17 +5,28 @@ import { SettingSocketDomain, SettingSocketProjectInfoAction, } from "../../../types/common/setting"; -import { SettingDTO, SettingProjectDTO } from "../../../types/DTO/settingDTO"; +import { + SettingDTO, + SettingJoinRequestDTO, + SettingMemberDTO, + SettingProjectDTO, +} from "../../../types/DTO/settingDTO"; const useSettingSocket = (socket: Socket) => { const [projectInfo, setProjectInfo] = useState({ title: "", subject: "", }); + const [memberList, setMemberList] = useState([]); + const [joinRequestList, setJoinRequestList] = useState< + SettingJoinRequestDTO[] + >([]); const handleInitEvent = (content: SettingDTO) => { - const { project } = content; + const { project, member, joinRequestList } = content; setProjectInfo(project); + setMemberList(member); + setJoinRequestList(joinRequestList ? joinRequestList : []); }; const handleProjectInfoEvent = ( @@ -53,7 +64,7 @@ const useSettingSocket = (socket: Socket) => { }; }, []); - return { projectInfo }; + return { projectInfo, memberList, joinRequestList }; }; export default useSettingSocket; diff --git a/frontend/src/pages/invite/InvitePage.tsx b/frontend/src/pages/invite/InvitePage.tsx index d2771e2..1d56131 100644 --- a/frontend/src/pages/invite/InvitePage.tsx +++ b/frontend/src/pages/invite/InvitePage.tsx @@ -1,13 +1,21 @@ -import { useLocation, useNavigate, useParams } from "react-router-dom"; +import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; import { checkAccessToken } from "../../apis/utils/authAPI"; import { ROUTER_URL } from "../../constants/path"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { STORAGE_KEY } from "../../constants/storageKey"; import { postJoinProject } from "../../apis/api/projectAPI"; +import { InvitePreview } from "../../types/DTO/inviteDTO"; +import { getInvitePreview } from "../../apis/api/inviteAPI"; const InvitePage = () => { const { projectTitle, projectId: projectUUID } = useParams(); const { pathname } = useLocation(); + const [projectInfo, setProjectInfo] = useState({ + id: -1, + title: "", + subject: "", + leaderUsername: "", + }); const navigate = useNavigate(); const handleJoinButtonClick = async () => { @@ -15,7 +23,7 @@ const InvitePage = () => { switch (response.status) { case 201: - alert("프로젝트에 참여되었습니다."); + alert("참여 요청을 보냈습니다."); navigate("/projects"); break; case 200: @@ -35,6 +43,17 @@ const InvitePage = () => { navigate(ROUTER_URL.LOGIN, { replace: true }); } + getInvitePreview(projectUUID!) + .then((response) => { + setProjectInfo(response.data); + }) + .catch((error) => { + if (error.response.status === 404) { + alert("유효하지 않은 요청 링크입니다."); + navigate("/projects"); + } + }); + return () => { if (checkAccessToken()) { sessionStorage.removeItem(STORAGE_KEY.REDIRECT); @@ -43,18 +62,44 @@ const InvitePage = () => { }, []); return ( -
-
-

프로젝트{projectTitle}에 초대되었습니다.

-

{projectUUID}

- -
+
+
+
+
+

+ {projectInfo.leaderUsername} + 님의 +

+

+ {projectTitle} +

+

{projectInfo.subject}

+

+ {projectInfo.leaderUsername}님의 {projectInfo.title} +

+

+ 프로젝트에 참여하고 싶다면 요청을 보내세요. +

+
+ + + + +
+
+
+
); }; diff --git a/frontend/src/pages/setting/SettingPage.tsx b/frontend/src/pages/setting/SettingPage.tsx index 990bce2..b9de6b1 100644 --- a/frontend/src/pages/setting/SettingPage.tsx +++ b/frontend/src/pages/setting/SettingPage.tsx @@ -2,38 +2,21 @@ import { Socket } from "socket.io-client"; import InformationSettingSection from "../../components/setting/InformationSettingSection"; import MemberSettingSection from "../../components/setting/MemberSettingSection"; import ProjectDeleteSection from "../../components/setting/ProjectDeleteSection"; -import { LandingMemberDTO } from "../../types/DTO/landingDTO"; import { useOutletContext } from "react-router-dom"; import useSettingSocket from "../../hooks/pages/setting/useSettingSocket"; -const memberList: LandingMemberDTO[] = [ - { id: 1, username: "lesserTest", role: "LEADER", imageUrl: "", status: "on" }, - { - id: 2, - username: "lesserTest2", - role: "MEMBER", - imageUrl: "", - status: "on", - }, - { - id: 3, - username: "lesserTest3", - role: "MEMBER", - imageUrl: "", - status: "on", - }, -]; - const SettingPage = () => { const { socket }: { socket: Socket } = useOutletContext(); const { projectInfo: { title, subject }, + memberList, + joinRequestList, } = useSettingSocket(socket); return (
- +
); diff --git a/frontend/src/types/DTO/inviteDTO.ts b/frontend/src/types/DTO/inviteDTO.ts new file mode 100644 index 0000000..7a971cc --- /dev/null +++ b/frontend/src/types/DTO/inviteDTO.ts @@ -0,0 +1,6 @@ +export interface InvitePreview { + id: number; + title: string; + subject: string; + leaderUsername: string; +} diff --git a/frontend/src/types/DTO/settingDTO.ts b/frontend/src/types/DTO/settingDTO.ts index 2ffe4bc..98f59c0 100644 --- a/frontend/src/types/DTO/settingDTO.ts +++ b/frontend/src/types/DTO/settingDTO.ts @@ -12,7 +12,15 @@ export interface SettingMemberDTO { role: MemberRole; } +export interface SettingJoinRequestDTO { + id: number; + memberId: number; + username: string; + imageUrl: string; +} + export interface SettingDTO { project: SettingProjectDTO; member: SettingMemberDTO[]; + joinRequestList: SettingJoinRequestDTO[]; } diff --git a/frontend/src/types/common/landing.ts b/frontend/src/types/common/landing.ts index 18f2322..fcad701 100644 --- a/frontend/src/types/common/landing.ts +++ b/frontend/src/types/common/landing.ts @@ -10,6 +10,7 @@ export enum LandingSocketDomain { MEMO = "memo", MEMBER = "member", LINK = "link", + INVITE_LINK = "inviteLink", } export enum LandingSocketMemoAction { @@ -30,6 +31,10 @@ export enum LandingSocketLinkAction { DELETE = "delete", } +export enum LandingSocketInviteLinkAction { + UPDATE = "update", +} + interface LandingSocketInitData { domain: LandingSocketDomain.INIT; action: "init"; @@ -54,11 +59,18 @@ interface LandingSocketLinkData { content: LandingLinkDTO; } +interface LandingSocketInviteLinkData { + domain: LandingSocketDomain.INVITE_LINK; + action: LandingSocketInviteLinkAction; + content: { inviteLinkId: string }; +} + export type LandingSocketData = | LandingSocketInitData | LandingSocketMemoData | LandingSocketMemberData - | LandingSocketLinkData; + | LandingSocketLinkData + | LandingSocketInviteLinkData; export enum MemoColorStyle { yellow = "bg-[#FFD966]",