diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 0a5b522f..42b9ebd1 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -3,6 +3,7 @@ import React from 'react'; import type { Preview } from '@storybook/react'; import '@/styles/global.css'; +import Layout from '../src/v1/layout/Layout'; import ToastProvider from '../src/ui/Base/Toast/ToastProvider'; const preview: Preview = { @@ -22,7 +23,9 @@ const preview: Preview = { decorators: [ Story => ( - + + + ), ], diff --git a/public/icons/close.svg b/public/icons/close.svg index 24f9bfeb..df8f680d 100644 --- a/public/icons/close.svg +++ b/public/icons/close.svg @@ -1,4 +1,4 @@ - - + + diff --git a/src/app/profile/me/edit/page.tsx b/src/app/profile/me/edit/page.tsx index 537bc550..e5604c35 100644 --- a/src/app/profile/me/edit/page.tsx +++ b/src/app/profile/me/edit/page.tsx @@ -1,31 +1,25 @@ 'use client'; +import { Suspense } from 'react'; import useAllJobQuery from '@/queries/job/useAllJobQuery'; import useMyProfileQuery from '@/queries/user/useMyProfileQuery'; -import AuthRequired from '@/ui/AuthRequired'; -import TopNavigation from '@/ui/common/TopNavigation'; -import ProfileForm from '@/ui/Profile/ProfileForm'; + import { isAuthed } from '@/utils/helpers'; -import { Skeleton, VStack } from '@chakra-ui/react'; -import { Suspense } from 'react'; +import AuthRequired from '@/ui/AuthRequired'; + +import EditProfile from '@/v1/profile/EditProfile'; + +/** + * @todo + * Fallback UI 추가하기 + */ -const EditMyPage = () => { +const EditProfilePage = () => { return ( - - - - - - - - } - > - - - + + + ); }; @@ -35,8 +29,8 @@ const Contents = () => { const { data: profileData } = useMyProfileQuery(); return allJobQuery.isSuccess ? ( - + ) : null; }; -export default EditMyPage; +export default EditProfilePage; diff --git a/src/components/ContextProvider.tsx b/src/components/ContextProvider.tsx index 9d5e6c07..f926c7a8 100644 --- a/src/components/ContextProvider.tsx +++ b/src/components/ContextProvider.tsx @@ -7,6 +7,7 @@ import ChakraThemeProvider from '@/components/ChakraThemeProvider'; import ReactQueryProvider from '@/components/ReactQueryProvider'; import { ReactNode } from 'react'; import ErrorPage from '@/app/error'; +import ToastProvider from '@/ui/Base/Toast/ToastProvider'; const ContextProvider = ({ children }: { children: ReactNode }) => { return ( @@ -15,7 +16,9 @@ const ContextProvider = ({ children }: { children: ReactNode }) => { - {children} + + {children} + diff --git a/src/stories/Base/InputLength.stories.tsx b/src/stories/Base/InputLength.stories.tsx new file mode 100644 index 00000000..9a3b10eb --- /dev/null +++ b/src/stories/Base/InputLength.stories.tsx @@ -0,0 +1,76 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { SubmitHandler, useForm } from 'react-hook-form'; + +import Button from '@/ui/Base/Button'; +import Input from '@/ui/Base/Input'; +import InputLength from '@/ui/Base/InputLength'; +import ErrorMessage from '@/ui/Base/ErrorMessage'; + +const meta: Meta = { + title: 'Base/InputLength', + component: InputLength, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +type DefaultValues = { + password: string; +}; + +const InputLengthUseWithForm = () => { + const { + register, + watch, + handleSubmit, + formState: { errors }, + } = useForm({ + mode: 'all', + }); + + const handleSubmitForm: SubmitHandler = ({ password }) => { + alert(`password: ${password}`); + }; + + return ( +
+
+ +
+ + {errors.password && ( + {errors.password.message} + )} +
+
+ +
+ ); +}; + +export const Default: Story = { + render: () => , +}; diff --git a/src/ui/Base/InputLength.tsx b/src/ui/Base/InputLength.tsx new file mode 100644 index 00000000..b66db197 --- /dev/null +++ b/src/ui/Base/InputLength.tsx @@ -0,0 +1,22 @@ +type InputLengthProps = { + currentLength: number; + isError: boolean; + maxLength: number; +}; + +const InputLength = ({ + currentLength, + isError, + maxLength, +}: InputLengthProps) => { + const textColor = isError ? 'text-warning-800 ' : 'text-main-900'; + + return ( +
+ {currentLength ? currentLength : 0}/ + {maxLength} +
+ ); +}; + +export default InputLength; diff --git a/src/ui/Base/TopNavigation.tsx b/src/ui/Base/TopNavigation.tsx index 0bd033c1..380b2661 100644 --- a/src/ui/Base/TopNavigation.tsx +++ b/src/ui/Base/TopNavigation.tsx @@ -16,7 +16,7 @@ const TopNavigation = ({ children }: TopNavigationProps) => { const LeftItem = ({ children }: ItemProps) => { return ( -
+
{children}
); @@ -32,13 +32,15 @@ const textAligns = { const CenterItem = ({ children, textAlign = 'center' }: CenterItemProps) => { const alignClassName = textAligns[textAlign]; return ( -
{children}
+
+ {children} +
); }; const RightItem = ({ children }: ItemProps) => { return ( -
+
{children}
); diff --git a/src/v1/profile/EditProfile.tsx b/src/v1/profile/EditProfile.tsx new file mode 100644 index 00000000..bd9d16c0 --- /dev/null +++ b/src/v1/profile/EditProfile.tsx @@ -0,0 +1,193 @@ +'use client'; + +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { SubmitHandler, useForm } from 'react-hook-form'; + +import type { APIJobGroup } from '@/types/job'; +import type { APIUser } from '@/types/user'; + +import { isAxiosError } from 'axios'; +import useMyProfileMutation from '@/queries/user/useMyProfileMutation'; + +import { IconClose } from '@public/icons'; + +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 UserProfileProps = { + profile: Pick; + jobGroups: APIJobGroup[]; +}; + +type FormValues = { + nickname: string; + jobGroup: string; + job: string; +}; + +const EditProfile = ({ profile, jobGroups }: UserProfileProps) => { + const { + register, + watch, + handleSubmit, + formState: { errors }, + } = useForm({ + mode: 'all', + defaultValues: { + nickname: profile.nickname || '', + jobGroup: profile.job.jobGroupName || '', + job: profile.job.jobName || '', + }, + }); + + const router = useRouter(); + const myProfileMutation = useMyProfileMutation(); + const toast = useToast(); + + const showToastEditSuccess = () => + toast.show({ + type: 'success', + message: '프로필 수정 완료!', + duration: 3000, + }); + + const showToastEditFailed = () => + toast.show({ + type: 'error', + message: '알 수 없는 에러가 발생했어요.', + duration: 3000, + }); + + const handleSubmitForm: SubmitHandler = ({ + nickname, + jobGroup, + job, + }) => { + myProfileMutation.mutateAsync( + { + nickname, + job: { jobGroup, jobName: job }, + }, + { + onSuccess: () => { + router.replace('/profile/me'); + showToastEditSuccess(); + }, + onError: error => { + if (isAxiosError(error) && error.response) { + console.error(error.response.data); + showToastEditFailed(); + } + }, + } + ); + }; + + return ( + <> + + + + + + + + + 프로필 수정 + + + + + 완료 + + + + +
+
+ + 닉네임 + +
+ +
+ + {errors.nickname && ( + {errors.nickname.message} + )} +
+
+
+ +
+ + 직업/직군 + + +
+ + {errors.jobGroup && ( + {errors.jobGroup.message} + )} +
+ +
+ + {errors.job && {errors.job.message}} +
+
+
+ + ); +}; + +export default EditProfile;