Skip to content

Commit

Permalink
[#415] [프로필] 프로필 생성 페이지 (#427)
Browse files Browse the repository at this point in the history
* feat: 프로필 생성 페이지 구현 (WIP)

* chore: 프로필 등록 페이지 Storybook 삭제

* feat: 프로필 등록 페이지 기능 구현

* chore: component, type 명칭 및 토스트 메시지 수정

* refactor: 쿼리 키 팩토리 패턴 적용 및 쿼리 staleTime 옵션 적용
  • Loading branch information
hanyugeon authored and gxxrxn committed Jun 17, 2024
1 parent 66005ea commit 4175612
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 32 deletions.
46 changes: 17 additions & 29 deletions src/app/profile/me/add/page.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,29 @@
'use client';

import { Suspense } from 'react';
import useAllJobQuery from '@/queries/job/useAllJobQuery';
import useMyProfileQuery from '@/queries/user/useMyProfileQuery';
import AuthRequired from '@/ui/AuthRequired';
import ProfileForm from '@/ui/Profile/ProfileForm';
import { isAuthed } from '@/utils/helpers';
import { Text, VStack } from '@chakra-ui/react';

const AdditionalProfile = () => {
const allJobQuery = useAllJobQuery({ enabled: isAuthed() });
const userProfileQuery = useMyProfileQuery({ enabled: isAuthed() });
import { isAuthed } from '@/utils/helpers';
import AuthRequired from '@/ui/AuthRequired';

const isSuccess = allJobQuery.isSuccess && userProfileQuery.isSuccess;
import AddJobProfile from '@/v1/profile/AddJobProfile';

const AddJobProfilePage = () => {
return (
<AuthRequired>
<VStack position="relative" zIndex={10} pt="6rem" gap="1rem">
<Text fontSize="lg" fontWeight="bold">
추가 정보를 입력해 주세요!
</Text>
<Text fontSize="md" textAlign="center">
추가 정보를 입력하면
<br />
<Text as="span" color="main" fontWeight="bold">
다독다독
</Text>
이 추천하는 책장을 볼 수 있어요!
</Text>
{isSuccess && (
<ProfileForm
profile={userProfileQuery.data}
jobGroups={allJobQuery.data.jobGroups}
/>
)}
</VStack>
<Suspense fallback={null}>
<Contents />
</Suspense>
</AuthRequired>
);
};

export default AdditionalProfile;
const Contents = () => {
const allJobQuery = useAllJobQuery({ enabled: isAuthed() });

return allJobQuery.isSuccess ? (
<AddJobProfile jobCategories={allJobQuery.data.jobGroups} />
) : null;
};

export default AddJobProfilePage;
6 changes: 6 additions & 0 deletions src/queries/job/key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const jobKeys = {
all: ['job'] as const,
category: () => [...jobKeys.all, 'category'] as const,
};

export default jobKeys;
9 changes: 6 additions & 3 deletions src/queries/job/useAllJobQuery/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import jobAPI from '@/apis/job';
import { useQuery, UseQueryOptions } from '@tanstack/react-query';

import jobAPI from '@/apis/job';

import jobKeys from '@/queries/job/key';

type Options = Pick<
UseQueryOptions<Awaited<ReturnType<typeof jobAPI.getAllJobs>>['data']>,
'enabled'
>;

const useAllJobQuery = (options?: Options) =>
useQuery(
['allJob'],
jobKeys.category(),
() => jobAPI.getAllJobs().then(response => response.data),
options
{ ...options, staleTime: Infinity }
);

export default useAllJobQuery;
202 changes: 202 additions & 0 deletions src/v1/profile/AddJobProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
'use client';

import { useRouter } from 'next/navigation';
import { SubmitHandler, useForm } from 'react-hook-form';

import type { APIJobGroup } from '@/types/job';

import { isAxiosError } from 'axios';
import useMyProfileMutation from '@/queries/user/useMyProfileMutation';

import TopNavigation from '@/ui/Base/TopNavigation';
import Input from '@/ui/Base/Input';
import InputLength from '@/ui/Base/InputLength';
import Select from '@/ui/Base/Select';
import ErrorMessage from '@/ui/Base/ErrorMessage';
import useToast from '@/ui/Base/Toast/useToast';

type AddJobProfileProps = {
jobCategories: APIJobGroup[];
};

type FormValues = {
nickname: string;
jobGroup: string;
job: string;
};

const AddJobProfile = ({ jobCategories }: AddJobProfileProps) => {
const {
register,
watch,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
mode: 'all',
defaultValues: {
nickname: '',
jobGroup: '',
job: '',
},
});

const router = useRouter();
const myProfileMutation = useMyProfileMutation();
const toast = useToast();

const showToastEditSuccess = () =>
toast.show({
type: 'success',
message: '프로필을 등록했어요!',
duration: 3000,
});

/**
* @todo
* showToastEditFailed()
* 범용적으로 에러 핸들링 할 수 있도록 수정
*/

const showToastEditFailed = () =>
toast.show({
type: 'error',
message: '잠시 후 다시 시도해 주세요.',
duration: 3000,
});

const handleSubmitForm: SubmitHandler<FormValues> = ({
nickname,
jobGroup,
job,
}) => {
myProfileMutation.mutateAsync(
{
nickname,
job: { jobGroup: jobGroup, jobName: job },
},
{
onSuccess: () => {
router.replace('/bookarchive');
showToastEditSuccess();
},
onError: error => {
if (isAxiosError(error) && error.response) {
console.error(error.response.data);
showToastEditFailed();
}
},
}
);
};

return (
<>
<TopNavigation>
<TopNavigation.CenterItem textAlign="center">
<span className="text-md font-normal text-black-900">
프로필 등록
</span>
</TopNavigation.CenterItem>
<TopNavigation.RightItem>
<span
onClick={handleSubmit(handleSubmitForm)}
className="cursor-pointer text-md font-bold text-main-900"
>
완료
</span>
</TopNavigation.RightItem>
</TopNavigation>

<div className="mt-[9.2rem] flex w-full flex-col gap-[3.3rem]">
<div className="flex flex-col gap-[1rem] font-normal">
<span className="text-lg text-black-700">프로필을 등록해주세요!</span>
<div className="text-sm text-placeholder">
<p>프로필을 등록하면</p>
<p>
<span className="text-main-900">다독다독</span>이 추천하는 책장을
볼 수 있어요.
</p>
</div>
</div>

<form
onSubmit={handleSubmit(handleSubmitForm)}
className="flex w-full flex-col gap-[3.2rem]"
>
<div className="flex flex-col gap-[1rem]">
<span className="h-[2.1rem] text-md font-normal text-black-700">
닉네임
</span>
<div className="flex flex-col gap-[0.5rem]">
<Input
placeholder="닉네임을 입력해주세요."
{...register('nickname', {
required: '닉네임을 입력해주세요.',
minLength: { value: 2, message: '2자 이상 입력해 주세요.' },
maxLength: { value: 10, message: '10자 이하 입력해 주세요.' },
})}
error={!!errors.nickname}
/>
<div className="flex h-[1.4rem] flex-row-reverse justify-between">
<InputLength
currentLength={watch('nickname')?.length}
isError={!!errors.nickname}
maxLength={10}
/>
{errors.nickname && (
<ErrorMessage>{errors.nickname.message}</ErrorMessage>
)}
</div>
</div>
</div>

<div className="flex flex-col gap-[1rem]">
<span className="h-[2.1rem] text-md font-normal text-black-700">
직업/직군
</span>

<div className="flex flex-col gap-[0.5rem]">
<Select
placeholder="직군을 선택해주세요."
{...register('jobGroup', {
required: '직군을 선택해주세요.',
})}
error={!!errors.jobGroup}
>
{jobCategories.map(({ name, koreanName }) => (
<Select.Option key={name} value={name}>
{koreanName}
</Select.Option>
))}
</Select>
{errors.jobGroup && (
<ErrorMessage>{errors.jobGroup.message}</ErrorMessage>
)}
</div>

<div className="flex flex-col gap-[0.5rem]">
<Select
placeholder="직업을 선택해주세요."
{...register('job', {
required: '직업을 선택해주세요.',
})}
error={!!errors.job}
>
{jobCategories
.find(({ name }) => name === watch('jobGroup'))
?.jobs.map(({ name, koreanName }) => (
<Select.Option key={name} value={name}>
{koreanName}
</Select.Option>
))}
</Select>
{errors.job && <ErrorMessage>{errors.job.message}</ErrorMessage>}
</div>
</div>
</form>
</div>
</>
);
};

export default AddJobProfile;

0 comments on commit 4175612

Please sign in to comment.