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

Dep 167 Add flow of invitation #122

Merged
merged 5 commits into from
Jun 29, 2023
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
6 changes: 4 additions & 2 deletions src/api/domain/community.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
Expand All @@ -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!));
},
});
};
38 changes: 36 additions & 2 deletions src/api/domain/user.api.ts
Original file line number Diff line number Diff line change
@@ -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<UserInfoResponse>(`/user/profile`);

export const useGetUserInfo = () => useQuery(userQueryKey.userInfo(), () => getUserInfo());
export const useGetUserInfo = (options?: UseQueryOptions<UserInfoResponse>) =>
useQuery<UserInfoResponse>(userQueryKey.userInfo(), () => getUserInfo(), options);

export const postCharacterCreate = (characterName: CharacterCreateRequest) =>
privateApi.post(`/user/character`, { character: characterName });
Expand All @@ -23,3 +34,26 @@ export const usePostCharacterCreate = (
mutationFn: postCharacterCreate,
...options,
});

export const getInvitationCodeIsValid = async (invitationCode: string) => {
return await publicApi.get<InvitationCodeValidationResponse>(`/invitation/${invitationCode}`);
};

export const useGetInvitationCodeIsValid = (invitationCode: string) =>
useQuery(userQueryKey.invitationCodeIsValid(), () => getInvitationCodeIsValid(invitationCode));

export const postPlanetJoin = async (body: PlanetJoinRequest) => {
return await privateApi.post<InvitationCodeValidationResponse>(`/join/planet`, body);
};
Comment on lines +38 to +47
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public api가 한 파일(api.ts)에서 private api와 함께 선언되고 서버 컴포넌트에서 함께 사용(invitation page)되어도 문제없는 것을 확인했습니다.


export const usePostPlanetJoin = (
options?: Omit<UseMutationOptions<unknown, AxiosError, PlanetJoinRequest>, 'mutationFn'>,
) => {
const queryClient = useQueryClient();

return useMutation<unknown, AxiosError, PlanetJoinRequest>({
mutationFn: postPlanetJoin,
onSuccess: () => queryClient.invalidateQueries(userQueryKey.userInfo()),
...options,
});
};
64 changes: 64 additions & 0 deletions src/app/invitation/[code]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use client';

import { useRouter } from 'next/navigation';

import {
useGetInvitationCodeIsValid,
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();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useRouter 를 사용하기 위해서 client component 로 선언했습니다.

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}`);
}
};
Comment on lines +34 to +48
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래 flow를 구현했습니다.
image

if (isValidPlanetLoading) return null;

return (
<Template>
<Template.Title className="text-grey-900">
<h1>{title}</h1>
</Template.Title>
<Template.Content />
<Template.Button disabled={isInitialLoading || isRefetching} onClick={onClick}>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용자의 데이터를 가져오는 동안 button을 비활성화 합니다.
enabled option 이 걸릴 경우, isLoading: true 로 반환됩니다. (참고 : issue)
따라서 이에 대한 방안책으로 isInitialLoading || isRefetching을 사용합니다.

시작하기
</Template.Button>
</Template>
);
};

export default InvitationPage;
15 changes: 10 additions & 5 deletions src/components/Template/TemplateButton.tsx
Original file line number Diff line number Diff line change
@@ -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<Omit<ButtonProps, 'children'>> & {
children: ReactNode | string;
className?: string;
onClick?: () => void;
};

export const TemplateButton = ({ children, className, onClick }: TemplateButtonProps) => {
export const TemplateButton = ({ children, className, onClick, ...rest }: TemplateButtonProps) => {
return (
<Button size="large" color="primary" className={tw('mb-15pxr', className)} onClick={onClick}>
<Button
{...rest}
size="large"
color="primary"
className={tw('mb-15pxr', className)}
onClick={onClick}
>
{children}
</Button>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/Template/TemplateContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ReactNode } from 'react';
import { tw } from '~/utils/tailwind.util';

type TemplateContentProps = {
children: ReactNode;
children?: ReactNode;
className?: string;
};

Expand Down
2 changes: 2 additions & 0 deletions src/mocks/user/user.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ export const createUserInfo = (): UserInfoModel => ({
gender: faker.person.gender(),
ageRange: '',
profileImageUrl: faker.image.avatar(),
isCharacterCreated: true,
planetIds: [1],
});
6 changes: 6 additions & 0 deletions src/mocks/user/user.mockHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({}));
}),
];
11 changes: 11 additions & 0 deletions src/types/user/model.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Comment on lines +16 to +23
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아직 api 명세가 나오지 않아 임의로 구현해 놓았습니다.

4 changes: 3 additions & 1 deletion src/types/user/request.type.ts
Original file line number Diff line number Diff line change
@@ -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<UserInfoResponse, 'userId'>;

export type CharacterCreateRequest = CharacterCreateModel;

export type PlanetJoinRequest = PlanetJoinModel;
4 changes: 3 additions & 1 deletion src/types/user/response.type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { UserInfoModel } from './model.type';
import { InvitationCodeValidationModel, UserInfoModel } from './model.type';

export type UserInfoResponse = UserInfoModel;

export type InvitationCodeValidationResponse = InvitationCodeValidationModel;
6 changes: 3 additions & 3 deletions src/utils/auth/getUserId.client.ts
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines -4 to +11
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용자의 로그인 여부를 에러 없이 순수하게 알아내기 위해 사용합니다.
예) 로그인 사용자, 비로그인 사용자 모두 초대코드를 들고 입장할 수 있으며 해당 페이지에서 로그인 여부를 구분을 해주어야 합니다
-1 보다 undefined 가 처리하기 용이할 것 같아 수정했습니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-1 보다 undefined가 왜 처리가 용이한지 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그인 여부를 -1 로 주게 된다면 아래와 같은 코드가 나올 것 같았어요.

const userId = getUserId();
if(userId === NOT_LOGIN)

이것보다 아래와 같이 사용하는 게 편하다고 생각했습니다.

const userId = getUserId();
if(!userId)

cookie에서 가져오는 값이기 때문에 -1이라는 임의의 숫자보다는 undefined 가 의미상 더 맞다고 생각되었어요. (확실히 하려면 null 이긴 하지만...)

저의 개인적인 생각일 뿐입니다!

}
};