Skip to content

Commit

Permalink
Merge pull request #269 from boostcampwm2023/feature/invite-member
Browse files Browse the repository at this point in the history
feat: 임시 초대 페이지 생성 및 참가 API 연결
dongind authored Apr 28, 2024
2 parents 3de3714 + a7de07d commit 80f979c
Showing 13 changed files with 137 additions and 14 deletions.
5 changes: 5 additions & 0 deletions frontend/src/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ import PrivateRoute from "./components/common/route/PrivateRoute";
import PublicRoute from "./components/common/route/PublicRoute";
import MainPage from "./pages/main/MainPage";
import LandingPage from "./pages/landing/LandingPage";
import InvitePage from "./pages/invite/InvitePage";

type RouteType = "PRIVATE" | "PUBLIC";

@@ -79,6 +80,10 @@ const router = createBrowserRouter([
{ path: ROUTER_URL.SETTINGS, element: <div>setting Page</div> },
],
},
{
path: ROUTER_URL.INVITE,
element: <InvitePage />,
},
],
},
]);
20 changes: 16 additions & 4 deletions frontend/src/apis/api/loginAPI.ts
Original file line number Diff line number Diff line change
@@ -9,27 +9,39 @@ import {
} from "../../types/DTO/authDTO";
import { useNavigate } from "react-router-dom";
import { authAPI, setAccessToken } from "../utils/authAPI";
import { STORAGE_KEY } from "../../constants/storageKey";

export const getLoginURL = async () => {
const response = await baseAPI.get<GithubOauthUrlDTO>(API_URL.GITHUB_OAUTH_URL);
const response = await baseAPI.get<GithubOauthUrlDTO>(
API_URL.GITHUB_OAUTH_URL
);
return response.data.authUrl;
};

export const postAuthCode = async (authCode: string) => {
const navigate = useNavigate();
const redirectURL = sessionStorage.getItem(STORAGE_KEY.REDIRECT);
const response = await baseAPI.post<AuthenticationDTO>(API_URL.AUTH, {
authCode,
});
if (response.status === 201) {
const body = response.data as AccessTokenResponse;
setAccessToken(body.accessToken);
window.localStorage.setItem("member", JSON.stringify(body.member));
navigate(ROUTER_URL.PROJECTS);
window.localStorage.setItem(
STORAGE_KEY.MEMBER,
JSON.stringify(body.member)
);
redirectURL
? navigate(redirectURL, { replace: true })
: navigate(ROUTER_URL.PROJECTS);
return;
}
if (response.status === 209) {
const body = response.data as TempIdTokenResponse;
navigate(ROUTER_URL.SIGNUP, { state: { tempIdToken: body.tempIdToken } });
navigate(ROUTER_URL.SIGNUP, {
state: { tempIdToken: body.tempIdToken },
replace: redirectURL ? true : false,
});
return;
}
};
6 changes: 6 additions & 0 deletions frontend/src/apis/api/projectAPI.ts
Original file line number Diff line number Diff line change
@@ -16,3 +16,9 @@ export const postCreateProject = async (body: {
const response = await authAPI.post(API_URL.PROJECT, body);
return response.data;
};

export const postJoinProject = async (inviteLinkId: string) => {
const response = await authAPI.post(API_URL.PROJECT_JOIN, { inviteLinkId });

return response;
};
6 changes: 5 additions & 1 deletion frontend/src/components/account/SignupMainSection.tsx
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import PositionInput from "./PositionInput";
import TechStackInput from "./TechStackInput";
import { SIGNUP_STEP } from "../../constants/account";
import { ROUTER_URL } from "../../constants/path";
import { STORAGE_KEY } from "../../constants/storageKey";

interface SignupMainSectionProps {
currentStepNumber: number;
@@ -45,9 +46,12 @@ const SignupMainSection = ({
techStack: techRef.current,
tempIdToken: location.state.tempIdToken,
});
const redirectURL = sessionStorage.getItem(STORAGE_KEY.REDIRECT);

if (status === 201) {
navigate(ROUTER_URL.PROJECTS);
redirectURL
? navigate(redirectURL, { replace: true })
: navigate(ROUTER_URL.PROJECTS, { replace: true });
}
};

8 changes: 5 additions & 3 deletions frontend/src/components/common/route/PublicRoute.tsx
Original file line number Diff line number Diff line change
@@ -3,10 +3,12 @@ import checkAuthentication from "../../../utils/route/checkAuthentication";
import RouteLoading from "./RouteLoading";
import { Navigate, Outlet } from "react-router-dom";
import { ROUTER_URL } from "../../../constants/path";
import { STORAGE_KEY } from "../../../constants/storageKey";

const PublicRoute = () => {
const [loadingState, setLoadingState] = useState<Boolean>(true);
const [authenticated, setAuthenticated] = useState<Boolean>(false);
const [loadingState, setLoadingState] = useState<boolean>(true);
const [authenticated, setAuthenticated] = useState<boolean>(false);
const redirectURL = sessionStorage.getItem(STORAGE_KEY.REDIRECT);

useEffect(() => {
checkAuthentication().then((result) => {
@@ -25,7 +27,7 @@ const PublicRoute = () => {
) : !authenticated ? (
<Outlet />
) : (
<Navigate to={ROUTER_URL.PROJECTS} />
<Navigate to={redirectURL ? redirectURL : ROUTER_URL.PROJECTS} />
);
};

24 changes: 23 additions & 1 deletion frontend/src/components/landing/LandingMember.tsx
Original file line number Diff line number Diff line change
@@ -8,9 +8,13 @@ import { DEFAULT_MEMBER } from "../../constants/projects";
const LandingMember = ({
member,
myInfo,
inviteLinkIdRef,
projectTitle,
}: {
member: LandingMemberDTO[];
myInfo: LandingMemberDTO;
inviteLinkIdRef: React.MutableRefObject<string>;
projectTitle: string;
}) => {
const { Dropdown, selectedOption } = useDropdown({
placeholder: "내 상태",
@@ -24,6 +28,19 @@ const LandingMember = ({
const imageUrl = myInfo.imageUrl ?? userData.imageUrl;
const username = myInfo.username ?? userData.username;

const handleInviteButtonClick = () => {
window.navigator.clipboard
.writeText(
`${window.location.origin}/projects/invite/${projectTitle.replace(
/\s/g,
""
)}/${inviteLinkIdRef.current}`
)
.then(() => {
alert("초대링크가 복사되었습니다.");
});
};

return (
<div className="w-full px-6 py-6 overflow-y-scroll rounded-lg shadow-box bg-gradient-to-tr to-light-green-linear-from from-light-green scrollbar-thin scrollbar-thumb-light-green scrollbar-track-transparent scrollbar-thumb-rounded-full">
<div className="flex flex-col gap-3">
@@ -44,7 +61,12 @@ const LandingMember = ({
/>
<div className="flex justify-between text-white">
<p className="text-xs font-bold">| 함께하는 사람들</p>
<button className="text-xxs hover:underline">초대링크 복사</button>
<button
className="text-xxs hover:underline"
onClick={handleInviteButtonClick}
>
초대링크 복사
</button>
</div>
{member.map((memberData: LandingMemberDTO) => (
<UserBlock {...memberData} key={memberData.id} />
2 changes: 2 additions & 0 deletions frontend/src/constants/path.ts
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ export const API_URL = {
NICKNAME_AVAILABLILITY: "/member/availability",
GITHUB_USERNAME: "/auth/github/username",
PROJECT: "/project",
PROJECT_JOIN: "/project/join",
};

export const ROUTER_URL = {
@@ -24,6 +25,7 @@ export const ROUTER_URL = {
SPRINT: "/projects/:projectId/sprint",
SPRINT_CREATE: "/projects/:projectId/sprint/create",
SETTINGS: "/projects/:projectId/settings",
INVITE: "projects/invite/:projectTitle/:projectId",
ERROR: "/error",
};

4 changes: 4 additions & 0 deletions frontend/src/constants/storageKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const STORAGE_KEY = {
MEMBER: "member",
REDIRECT: "redirect",
};
8 changes: 5 additions & 3 deletions frontend/src/hooks/common/socket/useLandingSocket.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Socket } from "socket.io-client";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import {
LandingDTO,
LandingLinkDTO,
@@ -31,18 +31,20 @@ const useLandingSocket = (socket: Socket) => {
const [sprint, setSprint] = useState<LandingSprintDTO | null>(null);
const [board, setBoard] = useState<LandingMemoDTO[]>([]);
const [link, setLink] = useState<LandingLinkDTO[]>([]);
const inviteLinkIdRef = useRef<string>('')

const handleOnLanding = ({ action, content }: SocketData) => {
switch (action) {
case LandingSocketEvent.INIT:
const { project, myInfo, member, sprint, board, link } =
const { project, myInfo, member, sprint, board, link, inviteLinkId } =
content as LandingDTO;
setProject(project);
setMyInfo(myInfo);
setMember(member);
setSprint(sprint);
setBoard(board);
setLink(link);
inviteLinkIdRef.current = inviteLinkId
break;
}
};
@@ -56,7 +58,7 @@ const useLandingSocket = (socket: Socket) => {
};
}, [socket]);

return { project, myInfo, member, sprint, board, link };
return { project, myInfo, member, sprint, board, link, inviteLinkIdRef };
};

export default useLandingSocket;
5 changes: 5 additions & 0 deletions frontend/src/pages/TempHomepage.tsx
Original file line number Diff line number Diff line change
@@ -45,6 +45,11 @@ const TempHomepage = () => {
에러를 던져봅시다!
</Link>
</li>
<li>
<Link to={"projects/invite/projectTitle/projectId"}>
초대
</Link>
</li>
</ul>
</div>
);
58 changes: 58 additions & 0 deletions frontend/src/pages/invite/InvitePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { checkAccessToken } from "../../apis/utils/authAPI";
import { ROUTER_URL } from "../../constants/path";
import { useEffect } from "react";
import { STORAGE_KEY } from "../../constants/storageKey";
import { postJoinProject } from "../../apis/api/projectAPI";

const InvitePage = () => {
const { projectTitle, projectId: projectUUID } = useParams();
const { pathname } = useLocation();
const navigate = useNavigate();

const handleJoinButtonClick = async () => {
const response = await postJoinProject(projectUUID!);

switch (response.status) {
case 201:
alert("프로젝트에 참여되었습니다.");
navigate("/projects");
break;
case 200:
alert("이미 참여한 프로젝트입니다.");
navigate(`/projects/${response.data.projectId}`);
break;
}
};

useEffect(() => {
if (!checkAccessToken()) {
sessionStorage.setItem(STORAGE_KEY.REDIRECT, pathname);
navigate(ROUTER_URL.LOGIN, { replace: true });
}

return () => {
if (checkAccessToken()) {
sessionStorage.removeItem(STORAGE_KEY.REDIRECT);
}
};
}, []);

return (
<div className="w-[100%] min-w-[720px] h-[600px] flex justify-center items-center">
<main>
<p>프로젝트{projectTitle}에 초대되었습니다.</p>
<p>{projectUUID}</p>
<button
type="button"
className="w-[100px] h-[50px] bg-middle-green rounded text-white"
onClick={handleJoinButtonClick}
>
참여하기
</button>
</main>
</div>
);
};

export default InvitePage;
4 changes: 2 additions & 2 deletions frontend/src/pages/landing/LandingPage.tsx
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ const LandingPage = () => {

const { socket }: { socket: Socket } = useOutletContext();

const { project, myInfo, member, sprint, link } = useLandingSocket(socket);
const { project, myInfo, member, sprint, link, inviteLinkIdRef } = useLandingSocket(socket);

return (
<div className="flex flex-col justify-between w-full h-full">
@@ -24,7 +24,7 @@ const LandingPage = () => {
</div>
<div className="h-[20.5625rem] w-full shrink-0 flex gap-9">
<LandingSprint {...{ sprint }} />
<LandingMember {...{ member, myInfo }} />
<LandingMember {...{ member, myInfo, inviteLinkIdRef }} projectTitle={project.title} />
<LandingLink {...{ link }} />
</div>
</div>
1 change: 1 addition & 0 deletions frontend/src/types/DTO/landingDTO.ts
Original file line number Diff line number Diff line change
@@ -43,4 +43,5 @@ export interface LandingDTO {
sprint: LandingSprintDTO | null;
board: LandingMemoDTO[];
link: LandingLinkDTO[];
inviteLinkId: string
}

0 comments on commit 80f979c

Please sign in to comment.