From b4447055d3c89ee391e17ae76680d18ed886023a Mon Sep 17 00:00:00 2001 From: Ji Hyeong Lee <115636461+Jihyeong00@users.noreply.github.com> Date: Thu, 23 May 2024 13:37:13 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20zod=EB=A5=BC=20=EC=9D=B4=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +- pnpm-lock.yaml | 30 +++++++++ src/features/auth/hook/index.ts | 2 + src/features/auth/hook/use-sign-in-form.ts | 30 +++++++++ src/features/auth/hook/use-sign-up-form.ts | 31 +++++++++ src/features/auth/schema/index.ts | 2 + .../auth/schema/sign-in-form-schema.ts | 8 +++ .../auth/schema/sign-up-form-schema.ts | 15 +++++ src/features/auth/schema/user-schema.ts | 13 ++++ src/features/auth/ui/ConfirmField.tsx | 27 -------- src/features/auth/ui/SignInForm.tsx | 34 +++++----- src/features/auth/ui/SignUpForm.tsx | 63 +++++-------------- src/features/search/schema/index.ts | 1 + .../search/schema/search-keyword-schema.ts | 3 + src/features/search/ui/HeaderSearchBar.tsx | 18 +++++- src/shared/ui/ConfirmField.tsx | 35 +++++++++++ src/shared/ui/TextFiled.tsx | 46 ++++++++------ .../ui/confirm-field.module.scss | 0 src/shared/ui/index.ts | 2 + 19 files changed, 250 insertions(+), 115 deletions(-) create mode 100644 src/features/auth/hook/index.ts create mode 100644 src/features/auth/hook/use-sign-in-form.ts create mode 100644 src/features/auth/hook/use-sign-up-form.ts create mode 100644 src/features/auth/schema/index.ts create mode 100644 src/features/auth/schema/sign-in-form-schema.ts create mode 100644 src/features/auth/schema/sign-up-form-schema.ts create mode 100644 src/features/auth/schema/user-schema.ts delete mode 100644 src/features/auth/ui/ConfirmField.tsx create mode 100644 src/features/search/schema/index.ts create mode 100644 src/features/search/schema/search-keyword-schema.ts create mode 100644 src/shared/ui/ConfirmField.tsx rename src/{features/auth => shared}/ui/confirm-field.module.scss (100%) diff --git a/package.json b/package.json index 8486118..d6be28f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@faker-js/faker": "^8.4.1", + "@hookform/resolvers": "^3.4.2", "@lottiefiles/react-lottie-player": "^3.5.3", "@mswjs/http-middleware": "^0.10.1", "@mui/icons-material": "^5.15.15", @@ -37,9 +38,11 @@ "next": "14.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.51.5", "react-intersection-observer": "^9.10.0", "react-youtube": "^10.1.0", - "sass": "^1.75.0" + "sass": "^1.75.0", + "zod": "^3.23.8" }, "devDependencies": { "@next/eslint-plugin-next": "^14.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa9785d..4f2d4f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@faker-js/faker': specifier: ^8.4.1 version: 8.4.1 + '@hookform/resolvers': + specifier: ^3.4.2 + version: 3.4.2(react-hook-form@7.51.5) '@lottiefiles/react-lottie-player': specifier: ^3.5.3 version: 3.5.3(react@18.2.0) @@ -68,6 +71,9 @@ dependencies: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-hook-form: + specifier: ^7.51.5 + version: 7.51.5(react@18.2.0) react-intersection-observer: specifier: ^9.10.0 version: 9.10.0(react-dom@18.2.0)(react@18.2.0) @@ -77,6 +83,9 @@ dependencies: sass: specifier: ^1.75.0 version: 1.75.0 + zod: + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@next/eslint-plugin-next': @@ -380,6 +389,14 @@ packages: resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} dev: false + /@hookform/resolvers@3.4.2(react-hook-form@7.51.5): + resolution: {integrity: sha512-1m9uAVIO8wVf7VCDAGsuGA0t6Z3m6jVGAN50HkV9vYLl0yixKK/Z1lr01vaRvYCkIKGoy1noVRxMzQYb4y/j1Q==} + peerDependencies: + react-hook-form: ^7.0.0 + dependencies: + react-hook-form: 7.51.5(react@18.2.0) + dev: false + /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -3619,6 +3636,15 @@ packages: scheduler: 0.23.0 dev: false + /react-hook-form@7.51.5(react@18.2.0): + resolution: {integrity: sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==} + engines: {node: '>=12.22.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + react: 18.2.0 + dev: false + /react-intersection-observer@9.10.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-PgdIX+RdFLl2WWtYn1p2IN86brHBE6NwzWm3hBxy1FoCl/isy7cMeq4HKjoRR4S2wRu7o3hzlJRi50/XAvVbaw==} peerDependencies: @@ -4431,3 +4457,7 @@ packages: transitivePeerDependencies: - supports-color dev: false + + /zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + dev: false diff --git a/src/features/auth/hook/index.ts b/src/features/auth/hook/index.ts new file mode 100644 index 0000000..a6d0edb --- /dev/null +++ b/src/features/auth/hook/index.ts @@ -0,0 +1,2 @@ +export { useSignInForm } from './use-sign-in-form' +export { useSignUpForm } from './use-sign-up-form' diff --git a/src/features/auth/hook/use-sign-in-form.ts b/src/features/auth/hook/use-sign-in-form.ts new file mode 100644 index 0000000..5f077dd --- /dev/null +++ b/src/features/auth/hook/use-sign-in-form.ts @@ -0,0 +1,30 @@ +'use client' + +import { zodResolver } from '@hookform/resolvers/zod' +import { SubmitHandler, useForm } from 'react-hook-form' +import { z } from 'zod' + +import { signInFormSchema } from '@/features/auth/schema' + +type SignInFormSchema = z.infer + +export function useSignInForm() { + const onSubmit: SubmitHandler = data => { + console.info('로그인을 시도합니다', data) + } + + const { + handleSubmit, + formState: { errors, isSubmitting }, + register, + } = useForm({ + resolver: zodResolver(signInFormSchema), + mode: 'onSubmit', + defaultValues: { + email: '', + password: '', + }, + }) + + return { handleSubmit: handleSubmit(onSubmit), errors, register, isSubmitting } +} diff --git a/src/features/auth/hook/use-sign-up-form.ts b/src/features/auth/hook/use-sign-up-form.ts new file mode 100644 index 0000000..5c91f01 --- /dev/null +++ b/src/features/auth/hook/use-sign-up-form.ts @@ -0,0 +1,31 @@ +'use client' + +import { zodResolver } from '@hookform/resolvers/zod' +import { SubmitHandler, useForm } from 'react-hook-form' +import { z } from 'zod' + +import { signUpFormSchema } from '@/features/auth/schema' + +type SignUpFormSchema = z.infer + +export function useSignUpForm() { + const onSubmit: SubmitHandler = data => { + console.info('회원가입을 시도합니다.', data) + } + + const { + handleSubmit, + formState: { errors }, + register, + } = useForm>({ + resolver: zodResolver(signUpFormSchema), + defaultValues: { + email: '', + password: '', + confirmPassword: '', + nickname: '', + }, + }) + + return { handleSubmit: handleSubmit(onSubmit), errors, register } +} diff --git a/src/features/auth/schema/index.ts b/src/features/auth/schema/index.ts new file mode 100644 index 0000000..310a117 --- /dev/null +++ b/src/features/auth/schema/index.ts @@ -0,0 +1,2 @@ +export { signInFormSchema } from './sign-in-form-schema' +export { signUpFormSchema } from './sign-up-form-schema' diff --git a/src/features/auth/schema/sign-in-form-schema.ts b/src/features/auth/schema/sign-in-form-schema.ts new file mode 100644 index 0000000..8d683cb --- /dev/null +++ b/src/features/auth/schema/sign-in-form-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod' + +import { email, password } from './user-schema' + +export const signInFormSchema = z.object({ + email, + password, +}) diff --git a/src/features/auth/schema/sign-up-form-schema.ts b/src/features/auth/schema/sign-up-form-schema.ts new file mode 100644 index 0000000..131ec04 --- /dev/null +++ b/src/features/auth/schema/sign-up-form-schema.ts @@ -0,0 +1,15 @@ +import { z } from 'zod' + +import { email, nickname, password } from './user-schema' + +export const signUpFormSchema = z + .object({ + nickname, + email, + password, + confirmPassword: password, + }) + .refine(data => data.password === data.confirmPassword, { + message: '비밀번호가 일치하지 않습니다.', + path: ['confirmPassword'], + }) diff --git a/src/features/auth/schema/user-schema.ts b/src/features/auth/schema/user-schema.ts new file mode 100644 index 0000000..081f93a --- /dev/null +++ b/src/features/auth/schema/user-schema.ts @@ -0,0 +1,13 @@ +import { z } from 'zod' + +export const email = z.string().email({ message: '이메일 형식에 맞춰 작성해주세요' }) + +export const nickname = z + .string() + .min(2, { message: '최소 2글자 이상의 닉네임을 작성해주세요' }) + .max(6, { message: '최소 6글자 미만의 닉네임을 작성해주세요' }) + +export const password = z + .string() + .min(6, { message: '최소 6글자 이상의 비밀번호를 작성해주세요' }) + .max(20, { message: '최대 20글자 이하의 비밀번호를 작성해주세요' }) diff --git a/src/features/auth/ui/ConfirmField.tsx b/src/features/auth/ui/ConfirmField.tsx deleted file mode 100644 index 7f8b209..0000000 --- a/src/features/auth/ui/ConfirmField.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { ComponentPropsWithoutRef } from 'react' - -import { poppins } from '@/shared/font' -import { TextFiled } from '@/shared/ui/TextFiled' - -import styles from './confirm-field.module.scss' - -type Props = { - confirmAPIFn: () => void -} & ComponentPropsWithoutRef - -export function ConfirmField({ disabled, confirmAPIFn, name, onChange, icon, value, error }: Props) { - return ( -
-
- - -
- {error && {error}} -
- ) -} diff --git a/src/features/auth/ui/SignInForm.tsx b/src/features/auth/ui/SignInForm.tsx index da82b9b..81294d5 100644 --- a/src/features/auth/ui/SignInForm.tsx +++ b/src/features/auth/ui/SignInForm.tsx @@ -1,35 +1,33 @@ 'use client' import { KeyRound, Mail } from 'lucide-react' -import { ChangeEventHandler, useState } from 'react' -import { TextFiled } from '@/shared/ui/TextFiled' +import { useSignInForm } from '@/features/auth/hook' +import { TextFiled } from '@/shared/ui' import styles from './SignForm.module.scss' import { SubmitButton } from './SubmitButton' export function SignInForm() { - const [id, setId] = useState(() => '') - const [pw, setPw] = useState(() => '') + const { handleSubmit, register, errors, isSubmitting } = useSignInForm() - const onIdChange: ChangeEventHandler = e => { - setId(e.target.value) - } - - const onPasswordChange: ChangeEventHandler = e => { - setPw(e.target.value) - } return ( -
- } /> + + } + {...register('email')} + /> } /> - Login + Login ) } diff --git a/src/features/auth/ui/SignUpForm.tsx b/src/features/auth/ui/SignUpForm.tsx index 7cb158f..856a300 100644 --- a/src/features/auth/ui/SignUpForm.tsx +++ b/src/features/auth/ui/SignUpForm.tsx @@ -1,39 +1,16 @@ 'use client' import { KeyRound, KeySquare, Mail, UserRound } from 'lucide-react' -import { ChangeEventHandler, useState } from 'react' +import { useState } from 'react' -import { TextFiled } from '@/shared/ui/TextFiled' +import { useSignUpForm } from '@/features/auth/hook' +import { ConfirmField, TextFiled } from '@/shared/ui' -import { ConfirmField } from './ConfirmField' import styles from './SignForm.module.scss' import { SubmitButton } from './SubmitButton' export function SignUpForm() { - const [nickname, setNickname] = useState(() => '') - const [email, setEmail] = useState(() => '') - const [pw, setPw] = useState(() => '') - const [confirmPw, setConfirmPw] = useState(() => '') const [checked, setChecked] = useState([false, false]) - - const onNicknameChange: ChangeEventHandler = e => { - nickNameReset() - setNickname(e.target.value) - } - - const onEmailChange: ChangeEventHandler = e => { - emailReset() - setEmail(e.target.value) - } - - const onPasswordChange: ChangeEventHandler = e => { - setPw(e.target.value) - } - - const onConfirmPasswordChange: ChangeEventHandler = e => { - setConfirmPw(e.target.value) - } - const nickNameChecked = (newState: boolean) => { setChecked(prevState => [newState, prevState[1]]) } @@ -50,48 +27,40 @@ export function SignUpForm() { emailChecked(true) } - const nickNameReset = () => { - nickNameChecked(false) - } + const { handleSubmit, register, errors } = useSignUpForm() - const emailReset = () => { - emailChecked(false) - } return ( -
+ } + {...register('nickname')} /> } + {...register('email')} /> } + {...register('password')} /> } + {...register('confirmPassword')} /> !value).length}>Sign Up diff --git a/src/features/search/schema/index.ts b/src/features/search/schema/index.ts new file mode 100644 index 0000000..08cef73 --- /dev/null +++ b/src/features/search/schema/index.ts @@ -0,0 +1 @@ +export { searchKeywordSchema } from './search-keyword-schema' diff --git a/src/features/search/schema/search-keyword-schema.ts b/src/features/search/schema/search-keyword-schema.ts new file mode 100644 index 0000000..e33e11e --- /dev/null +++ b/src/features/search/schema/search-keyword-schema.ts @@ -0,0 +1,3 @@ +import { z } from 'zod' + +export const searchKeywordSchema = z.string().min(2, '검색의 최소 길이 (2글자)를 넘지 않습니다.') diff --git a/src/features/search/ui/HeaderSearchBar.tsx b/src/features/search/ui/HeaderSearchBar.tsx index f8d7957..12b44ed 100644 --- a/src/features/search/ui/HeaderSearchBar.tsx +++ b/src/features/search/ui/HeaderSearchBar.tsx @@ -4,7 +4,9 @@ import classNames from 'classnames' import Image from 'next/image' import { useRouter } from 'next/navigation' import { useState } from 'react' +import { ZodError } from 'zod' +import { searchKeywordSchema } from '@/features/search/schema' import { SITE_PATH } from '@/shared/constants' import { quando } from '@/shared/font' @@ -17,9 +19,21 @@ export function HeaderSearchBar() { const onSubmit = (e: FormData) => { const keyword = e.get('keyword') - if (typeof keyword !== 'string') return + if (typeof keyword !== 'string') { + return + } - router.push(SITE_PATH.search(keyword)) + try { + searchKeywordSchema.parse(keyword) + + router.push(SITE_PATH.search(keyword)) + } catch (error) { + if (error instanceof ZodError) { + console.error('Validation failed. Errors:', error.errors) + } else if (error instanceof Error) { + console.error('Unexpected error during validation:', error.message) + } + } } const resetValue = () => { diff --git a/src/shared/ui/ConfirmField.tsx b/src/shared/ui/ConfirmField.tsx new file mode 100644 index 0000000..c8ba459 --- /dev/null +++ b/src/shared/ui/ConfirmField.tsx @@ -0,0 +1,35 @@ +import React, { forwardRef, ReactNode } from 'react' +import { ComponentPropsWithRef } from 'react' +import { FieldError } from 'react-hook-form' + +import { poppins } from '@/shared/font' + +import styles from './confirm-field.module.scss' + +type Props = { + icon: ReactNode + error?: FieldError + btnDisabled: boolean + confirmAPIFn: () => void +} & ComponentPropsWithRef<'input'> // 'input' 유형의 props를 포함시킴 + +export const ConfirmField = forwardRef( + ({ btnDisabled, confirmAPIFn, icon, error, ...rest }, ref) => { + return ( +
+
+ + +
+ {error && {error.message}} +
+ ) + }, +) + +ConfirmField.displayName = 'ConfirmField' diff --git a/src/shared/ui/TextFiled.tsx b/src/shared/ui/TextFiled.tsx index f79b9cb..4d4f86b 100644 --- a/src/shared/ui/TextFiled.tsx +++ b/src/shared/ui/TextFiled.tsx @@ -1,4 +1,6 @@ -import { ComponentPropsWithoutRef, ReactNode } from 'react' +import React, { forwardRef } from 'react' +import { ReactNode } from 'react' +import { FieldError } from 'react-hook-form' import { poppins } from '@/shared/font' @@ -6,23 +8,27 @@ import styles from './text-filed.module.scss' type Props = { icon: ReactNode - error?: string -} & ComponentPropsWithoutRef<'input'> + error?: FieldError +} & React.ComponentPropsWithRef<'input'> -export function TextFiled({ type = 'text', name, onChange, icon, value, error }: Props) { - return ( -
- - {error && {error}} -
- ) -} +export const TextFiled = forwardRef( + ({ type = 'text', placeholder, icon, error, ...rest }, ref) => { + return ( +
+ + {error && {error.message}} +
+ ) + }, +) + +TextFiled.displayName = 'TextFiled' diff --git a/src/features/auth/ui/confirm-field.module.scss b/src/shared/ui/confirm-field.module.scss similarity index 100% rename from src/features/auth/ui/confirm-field.module.scss rename to src/shared/ui/confirm-field.module.scss diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index 0179ca1..046e0e5 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -1,8 +1,10 @@ export { Button } from './Button' export { Chip } from './Chip' +export { ConfirmField } from './ConfirmField' export { DetailLinkButton } from './DetailLinkButton' export { HomeButton } from './HomeButton' export { Modal } from './Modal' export { ProfileImage } from './ProfileImage' export { RQProvider } from './RQProvider' +export { TextFiled } from './TextFiled' export { YoutubePlayer } from './YoutubePlayer'