From f1ef4b4a193b139a46f26bc615ff91d52e349caf Mon Sep 17 00:00:00 2001 From: hyehyeon-moon Date: Wed, 28 Jun 2023 01:39:00 +0900 Subject: [PATCH 1/5] Feat: Add apis about validation of invitation code and joining on planet --- src/api/domain/user.api.ts | 38 +++++++++++++++++++++++++++++++-- src/types/user/model.type.ts | 11 ++++++++++ src/types/user/request.type.ts | 4 +++- src/types/user/response.type.ts | 4 +++- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/api/domain/user.api.ts b/src/api/domain/user.api.ts index 26b825cf..698b6c20 100644 --- a/src/api/domain/user.api.ts +++ b/src/api/domain/user.api.ts @@ -1,17 +1,28 @@ -import { useMutation, UseMutationOptions, useQuery } from '@tanstack/react-query'; +import { + useMutation, + UseMutationOptions, + useQuery, + useQueryClient, + UseQueryOptions, +} from '@tanstack/react-query'; import { AxiosError } from 'axios'; import privateApi from '~/api/config/privateApi'; +import { InvitationCodeValidationResponse, PlanetJoinRequest } from '~/types/user'; import { CharacterCreateRequest } from '~/types/user'; import { UserInfoResponse } from '~/types/user'; +import publicApi from '../config/publicApi'; + export const userQueryKey = { userInfo: () => ['userInfo'], + invitationCodeIsValid: () => ['invitaion', 'code', 'valid'], }; export const getUserInfo = () => privateApi.get(`/user/profile`); -export const useGetUserInfo = () => useQuery(userQueryKey.userInfo(), () => getUserInfo()); +export const useGetUserInfo = (options?: UseQueryOptions) => + useQuery(userQueryKey.userInfo(), () => getUserInfo(), options); export const postCharacterCreate = (characterName: CharacterCreateRequest) => privateApi.post(`/user/character`, { character: characterName }); @@ -23,3 +34,26 @@ export const usePostCharacterCreate = ( mutationFn: postCharacterCreate, ...options, }); + +export const getInvitationCodeIsValid = async (invitationCode: string) => { + return await publicApi.get(`/invitation/${invitationCode}`); +}; + +export const useGetInvitationCodeIsValid = (invitationCode: string) => + useQuery(userQueryKey.invitationCodeIsValid(), () => getInvitationCodeIsValid(invitationCode)); + +export const postPlanetJoin = async (body: PlanetJoinRequest) => { + return await privateApi.post(`/join/planet`, body); +}; + +export const usePostPlanetJoin = ( + options?: Omit, 'mutationFn'>, +) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: postPlanetJoin, + onSuccess: () => queryClient.invalidateQueries(userQueryKey.userInfo()), + ...options, + }); +}; diff --git a/src/types/user/model.type.ts b/src/types/user/model.type.ts index f892790b..558a1433 100644 --- a/src/types/user/model.type.ts +++ b/src/types/user/model.type.ts @@ -7,6 +7,17 @@ export type UserInfoModel = { gender: string; ageRange: string; profileImageUrl: string; + isCharacterCreated: boolean; + planetIds: number[]; }; export type CharacterCreateModel = CharacterNameModel; + +export type InvitationCodeValidationModel = { + planetId: number; +}; + +export type PlanetJoinModel = { + userId: number; + planetId: number; +}; diff --git a/src/types/user/request.type.ts b/src/types/user/request.type.ts index d2892bcd..f142759e 100644 --- a/src/types/user/request.type.ts +++ b/src/types/user/request.type.ts @@ -1,7 +1,9 @@ // user.d.ts 정리 후 절대경로 적용 -import { CharacterCreateModel } from './model.type'; +import { CharacterCreateModel, PlanetJoinModel } from './model.type'; import { UserInfoResponse } from './response.type'; export type UserInfoRequest = Omit; export type CharacterCreateRequest = CharacterCreateModel; + +export type PlanetJoinRequest = PlanetJoinModel; diff --git a/src/types/user/response.type.ts b/src/types/user/response.type.ts index 4af1ff43..0e5d0926 100644 --- a/src/types/user/response.type.ts +++ b/src/types/user/response.type.ts @@ -1,3 +1,5 @@ -import { UserInfoModel } from './model.type'; +import { InvitationCodeValidationModel, UserInfoModel } from './model.type'; export type UserInfoResponse = UserInfoModel; + +export type InvitationCodeValidationResponse = InvitationCodeValidationModel; From d1af0cf8670a15d3118fd95ce0c7a0d77fabe8c0 Mon Sep 17 00:00:00 2001 From: hyehyeon-moon Date: Wed, 28 Jun 2023 01:42:08 +0900 Subject: [PATCH 2/5] Feat: Add mock and mockhandler --- src/mocks/user/user.mock.ts | 2 ++ src/mocks/user/user.mockHandler.ts | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/mocks/user/user.mock.ts b/src/mocks/user/user.mock.ts index a761269e..0f7aae60 100644 --- a/src/mocks/user/user.mock.ts +++ b/src/mocks/user/user.mock.ts @@ -9,4 +9,6 @@ export const createUserInfo = (): UserInfoModel => ({ gender: faker.person.gender(), ageRange: '', profileImageUrl: faker.image.avatar(), + isCharacterCreated: true, + planetIds: [1], }); diff --git a/src/mocks/user/user.mockHandler.ts b/src/mocks/user/user.mockHandler.ts index fcfc72e8..0f0637d6 100644 --- a/src/mocks/user/user.mockHandler.ts +++ b/src/mocks/user/user.mockHandler.ts @@ -10,4 +10,10 @@ export const userMockHandler = [ rest.post(`${ROOT_API_URL}/user/character`, (req, res, ctx) => { return res(ctx.status(200), ctx.json({})); }), + rest.get(`${ROOT_API_URL}/invitation/:code`, (req, res, ctx) => { + return res(ctx.status(200), ctx.json({ planetId: 1 })); + }), + rest.post(`${ROOT_API_URL}/join/planet`, (req, res, ctx) => { + return res(ctx.status(200), ctx.json({})); + }), ]; From 0c213d06ee3afc59c02874d36745101243e04b7e Mon Sep 17 00:00:00 2001 From: hyehyeon-moon Date: Wed, 28 Jun 2023 01:46:14 +0900 Subject: [PATCH 3/5] Fix: Fix the return value when user is not logged in --- src/api/domain/community.api.ts | 6 ++++-- src/utils/auth/getUserId.client.ts | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/api/domain/community.api.ts b/src/api/domain/community.api.ts index db5ea41f..b15b33b0 100644 --- a/src/api/domain/community.api.ts +++ b/src/api/domain/community.api.ts @@ -60,7 +60,8 @@ export const usePostCommunityCreate = () => { return useMutation({ mutationFn: (community: CreateCommunityRequest) => postCommunityCreate(community), onSuccess: () => { - queryClient.invalidateQueries(communityQueryKey.communityList(userId)); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + queryClient.invalidateQueries(communityQueryKey.communityList(userId!)); //TODO: BE 응답형태 변경 후 반영 router.replace('/admin/community/create/result'); }, @@ -76,7 +77,8 @@ export const usePostCommunityUpdate = (communityId: number) => { return useMutation({ mutationFn: (community: CreateCommunityRequest) => postCommunityUpdate(communityId, community), onSuccess: () => { - queryClient.invalidateQueries(communityQueryKey.communityList(userId)); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + queryClient.invalidateQueries(communityQueryKey.communityList(userId!)); }, }); }; diff --git a/src/utils/auth/getUserId.client.ts b/src/utils/auth/getUserId.client.ts index b207b87f..a085da06 100644 --- a/src/utils/auth/getUserId.client.ts +++ b/src/utils/auth/getUserId.client.ts @@ -1,13 +1,13 @@ import { UserIdNotFoundError } from './error'; import { getAuthTokensByCookie } from './tokenHandlers'; -export const getUserIdClient = (): number => { +export const getUserIdClient = (): number | undefined => { try { if (typeof document === 'undefined') throw new UserIdNotFoundError(); const { userId } = getAuthTokensByCookie(document.cookie); if (userId !== undefined) return userId; - throw new UserIdNotFoundError(); + throw new UserIdNotFoundError(); // 비로그인한 사용자의 경우 } catch (e) { - return -1; + return undefined; } }; From 3cd68bb19ddb8132955ed0636ff2f27c36d382b3 Mon Sep 17 00:00:00 2001 From: hyehyeon-moon Date: Thu, 29 Jun 2023 00:07:08 +0900 Subject: [PATCH 4/5] Feat: Add invitation page --- src/app/invitation/[code]/page.tsx | 58 ++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/app/invitation/[code]/page.tsx diff --git a/src/app/invitation/[code]/page.tsx b/src/app/invitation/[code]/page.tsx new file mode 100644 index 00000000..7e4c9d71 --- /dev/null +++ b/src/app/invitation/[code]/page.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { useRouter } from 'next/navigation'; + +import { + useGetInvitationCodeIsValid, + useGetUserInfo, + usePostPlanetJoin, +} from '~/api/domain/user.api'; +import { getUserIdClient } from '~/utils/auth/getUserId.client'; + +const InvitationPage = ({ params }: { params: { code: string } }) => { + const router = useRouter(); + const invitationCode = params.code; + + // TODO: 잘못된 행성 코드인 경우, 에러 처리(error.page) + const { data: validPlanet, isLoading: isValidPlanetLoading } = useGetInvitationCodeIsValid( + params.code, + ); + const planetId = validPlanet?.planetId; + const userId = getUserIdClient(); + const { + data: userInfo, + isRefetching, + isInitialLoading, + } = useGetUserInfo({ + enabled: !!(planetId && userId), + }); + const { mutateAsync } = usePostPlanetJoin(); + + const onClick = async () => { + const searchParams = new URLSearchParams({ + redirectUri: `/invitation/${invitationCode}`, + }).toString(); + if (planetId && userId) { + await mutateAsync({ planetId, userId }); + if (userInfo?.isCharacterCreated) { + router.push(`/planet/${planetId}`); + } else { + router.push(`/onboarding?${searchParams}`); + } + } else { + router.push(`/auth/signin?${searchParams}`); + } + }; + if (isValidPlanetLoading) return null; + + return ( +
+

당신을 디프만 행성으로 초대합니다

+ +
+ ); +}; + +export default InvitationPage; From 71fbfd66b5c14136e6851cc75e2b70fd24669f37 Mon Sep 17 00:00:00 2001 From: hyehyeon-moon Date: Thu, 29 Jun 2023 00:20:15 +0900 Subject: [PATCH 5/5] Feat: Add style of invitation page --- src/app/invitation/[code]/page.tsx | 18 ++++++++++++------ src/components/Template/TemplateButton.tsx | 15 ++++++++++----- src/components/Template/TemplateContent.tsx | 2 +- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/app/invitation/[code]/page.tsx b/src/app/invitation/[code]/page.tsx index 7e4c9d71..7354fae9 100644 --- a/src/app/invitation/[code]/page.tsx +++ b/src/app/invitation/[code]/page.tsx @@ -7,8 +7,11 @@ import { useGetUserInfo, usePostPlanetJoin, } from '~/api/domain/user.api'; +import { Template } from '~/components/Template'; import { getUserIdClient } from '~/utils/auth/getUserId.client'; +const title = '당신을 디프만 행성으로\n 초대합니다'; + const InvitationPage = ({ params }: { params: { code: string } }) => { const router = useRouter(); const invitationCode = params.code; @@ -46,12 +49,15 @@ const InvitationPage = ({ params }: { params: { code: string } }) => { if (isValidPlanetLoading) return null; return ( -
-

당신을 디프만 행성으로 초대합니다

- -
+ ); }; diff --git a/src/components/Template/TemplateButton.tsx b/src/components/Template/TemplateButton.tsx index 77efd58b..423fd561 100644 --- a/src/components/Template/TemplateButton.tsx +++ b/src/components/Template/TemplateButton.tsx @@ -1,17 +1,22 @@ import { ReactNode } from 'react'; -import { Button } from '~/components/Button/Button'; +import { Button, ButtonProps } from '~/components/Button/Button'; import { tw } from '~/utils/tailwind.util'; -type TemplateButtonProps = { +type TemplateButtonProps = Partial> & { children: ReactNode | string; className?: string; - onClick?: () => void; }; -export const TemplateButton = ({ children, className, onClick }: TemplateButtonProps) => { +export const TemplateButton = ({ children, className, onClick, ...rest }: TemplateButtonProps) => { return ( - ); diff --git a/src/components/Template/TemplateContent.tsx b/src/components/Template/TemplateContent.tsx index ea2d56ba..0028bb5a 100644 --- a/src/components/Template/TemplateContent.tsx +++ b/src/components/Template/TemplateContent.tsx @@ -3,7 +3,7 @@ import { ReactNode } from 'react'; import { tw } from '~/utils/tailwind.util'; type TemplateContentProps = { - children: ReactNode; + children?: ReactNode; className?: string; };