From 15192a06966e19a5789aa52f014fb4c4d5f04790 Mon Sep 17 00:00:00 2001 From: dbswl701 Date: Fri, 21 Feb 2025 13:30:18 +0900 Subject: [PATCH 01/15] =?UTF-8?q?feat(DEVING-25):=20chip=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/preview/chip/page.tsx | 40 +++++++++++++++++++++++++++++++++++ src/components/ui/Chip.tsx | 28 ++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/app/preview/chip/page.tsx create mode 100644 src/components/ui/Chip.tsx diff --git a/src/app/preview/chip/page.tsx b/src/app/preview/chip/page.tsx new file mode 100644 index 0000000..e28fbff --- /dev/null +++ b/src/app/preview/chip/page.tsx @@ -0,0 +1,40 @@ +'use client'; + +import Chip from '@/components/ui/Chip'; +import React, { useState } from 'react'; + +export default function ChipPreview() { + const [position, setPosition] = useState(''); + + return ( +
+ All + All +
+
+ setPosition('Frontend')} + > + 프론트엔드 + + setPosition('Backend')} + > + 백엔드 + + setPosition('Designer')} + > + 디자이너 + +
+
+
+ ); +} diff --git a/src/components/ui/Chip.tsx b/src/components/ui/Chip.tsx new file mode 100644 index 0000000..50fa4cc --- /dev/null +++ b/src/components/ui/Chip.tsx @@ -0,0 +1,28 @@ +import { cn } from '@/util/cn'; +import React from 'react'; + +interface IChipProps extends React.ComponentPropsWithRef<'div'> { + isActive?: boolean; +} + +const Chip = React.forwardRef( + ({ children, className, isActive = false, ...props }, ref) => { + return ( +
+ {children} +
+ ); + }, +); + +Chip.displayName = 'Chip'; + +export default Chip; From 54440cb2bdac8f1d57784a9d6ea322ea2311b08d Mon Sep 17 00:00:00 2001 From: dbswl701 Date: Fri, 21 Feb 2025 14:35:41 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat(DEVING-25):=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EA=B2=80=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/signup/components/ChipContainer.tsx | 35 ++++++ src/app/signup/page.tsx | 122 ++++++++++++++++---- src/hooks/mutations/useUserMutation.ts | 29 ++++- src/service/api/user.ts | 7 +- 4 files changed, 168 insertions(+), 25 deletions(-) create mode 100644 src/app/signup/components/ChipContainer.tsx diff --git a/src/app/signup/components/ChipContainer.tsx b/src/app/signup/components/ChipContainer.tsx new file mode 100644 index 0000000..b34884a --- /dev/null +++ b/src/app/signup/components/ChipContainer.tsx @@ -0,0 +1,35 @@ +import Chip from '@/components/ui/Chip'; + +export const ChipContainer = ({ + position, + setPosition, +}: { + position: string; + setPosition: (value: React.SetStateAction) => void; +}) => { + return ( +
+ setPosition('Frontend')} + > + 프론트엔드 + + setPosition('Backend')} + > + 백엔드 + + setPosition('Designer')} + > + 디자이너 + +
+ ); +}; diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index df258ab..2a41062 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -2,12 +2,19 @@ import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; +import { useNameCheckMutation } from '@/hooks/mutations/useUserMutation'; import Link from 'next/link'; +import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; +import { ChipContainer } from './components/ChipContainer'; + interface ISignupFormData { - id: string; - pw: string; + name: string; + email: string; + position: string; + password: string; + passwordCheck: string; } export default function Signup() { @@ -15,13 +22,69 @@ export default function Signup() { register, handleSubmit, watch, - formState: { errors }, + setError, + formState: { errors, dirtyFields }, } = useForm({ mode: 'onBlur', }); + const [position, setPosition] = useState(''); + + console.log(watch('name')); const onSubmit = (data: ISignupFormData) => { console.log('로그인 데이터: ', data); }; + + /** + * TODO + * 포커스 1초 되 유효성 검사 - 닉네임, 이메일, 비밀번호, 비밀번호 확인 + */ + + /** + * TODO + * 중복확인 플로우 체크 + * 1. 처음에는 중복확인 버튼 비활성화(''인 경우) + * 2. 입력이 있다면 중복확인 버튼 활성화 + * - 입력 중에는 비활성화 + * 3. 중복확인 성공시 + * - 해당 인풋 성공 표시(보더 색 변경), disabled 없이 그대로 유지 + * - 중복확인 버튼 비활성화 + * 3.1. 중복확인 성공 후 다시 입력 시 + * - 중복확인 버튼 활성화 + * - 해당 인풋 성공 처리 취소 + * 4. 중복확인 실패시 + * - 해당 인풋 에러메시지 표시 + * - 중복확인 버튼은 계속 활성화 + */ + + // 닉네임 중복 체크 확인. + // 1. 처음에는 중복확인 버튼 비활성화 + const [isNameCheck, setIsNameCheck] = useState(false); + + // 2. 입력이 있다면 중복확인 버튼 활성화 + console.log('dirtyFields: ', dirtyFields?.name); + + useEffect(() => { + if (dirtyFields?.name) { + setIsNameCheck(false); + } + }, [dirtyFields, watch('name')]); + + // 3. 중복확인 로직 수행 + const { mutate } = useNameCheckMutation({ + onSuccessCallback: () => setIsNameCheck(true), + onErrorCallback: () => + setError('name', { + type: 'checkFaile', + message: '이미 존재하는 닉네임입니다.', + }), + }); + + const handleNameCheck = () => { + const name = watch('name'); + console.log('name: ', name); + mutate(name); + }; + return (
-
@@ -83,25 +160,26 @@ export default function Signup() { -
+
*/} +
-
@@ -110,17 +188,17 @@ export default function Signup() { 비밀번호 확인
diff --git a/src/hooks/mutations/useUserMutation.ts b/src/hooks/mutations/useUserMutation.ts index 93e8278..f8ba09c 100644 --- a/src/hooks/mutations/useUserMutation.ts +++ b/src/hooks/mutations/useUserMutation.ts @@ -1,6 +1,6 @@ import { setAccessToken } from '@/lib/serverActions'; import { useMutation } from '@tanstack/react-query'; -import { postLogin } from 'service/api/user'; +import { postLogin, postNameCheck } from 'service/api/user'; const useLoginMutation = ({ onSuccessCallback, @@ -26,4 +26,29 @@ const useLoginMutation = ({ }); }; -export { useLoginMutation }; +// 닉네임 중복 검사 +const useNameCheckMutation = ({ + onSuccessCallback, + onErrorCallback, +}: { + onSuccessCallback: () => void; + onErrorCallback: () => void; +}) => { + return useMutation({ + mutationFn: (name: string) => postNameCheck(name), + onSuccess: () => { + // 중복 검사 성공 + /** + * TODO + * - 중복확인 버튼 비활성화 + */ + onSuccessCallback(); + }, + onError: () => { + // 중복 검사 실패 + onErrorCallback(); + }, + }); +}; + +export { useLoginMutation, useNameCheckMutation }; diff --git a/src/service/api/user.ts b/src/service/api/user.ts index 8836b4c..bbfb413 100644 --- a/src/service/api/user.ts +++ b/src/service/api/user.ts @@ -11,4 +11,9 @@ const postLogin = async ({ return res; }; -export { postLogin }; + +const postNameCheck = async (name: string) => { + const res = await basicAPI.get(`/api/v1/auths/signup/name?name=${name}`); + return res; +}; +export { postLogin, postNameCheck }; From 92772d8b2fe43e0209cb6deb4e9cd0baa4f136f7 Mon Sep 17 00:00:00 2001 From: dbswl701 Date: Fri, 21 Feb 2025 14:45:24 +0900 Subject: [PATCH 03/15] =?UTF-8?q?feat(DEVING-25):=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EA=B2=80=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/signup/page.tsx | 47 +++++++++++++++++++++----- src/hooks/mutations/useUserMutation.ts | 31 +++++++++++++++-- src/service/api/user.ts | 10 ++++-- 3 files changed, 74 insertions(+), 14 deletions(-) diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index 2a41062..5781138 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -2,7 +2,10 @@ import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; -import { useNameCheckMutation } from '@/hooks/mutations/useUserMutation'; +import { + useEmailCheckMutation, + useNameCheckMutation, +} from '@/hooks/mutations/useUserMutation'; import Link from 'next/link'; import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; @@ -59,6 +62,7 @@ export default function Signup() { // 닉네임 중복 체크 확인. // 1. 처음에는 중복확인 버튼 비활성화 const [isNameCheck, setIsNameCheck] = useState(false); + const [isEmailCheck, setIsEmailCheck] = useState(false); // 2. 입력이 있다면 중복확인 버튼 활성화 console.log('dirtyFields: ', dirtyFields?.name); @@ -67,22 +71,39 @@ export default function Signup() { if (dirtyFields?.name) { setIsNameCheck(false); } - }, [dirtyFields, watch('name')]); + if (dirtyFields?.email) { + setIsEmailCheck(false); + } + }, [dirtyFields, watch('name'), watch('email')]); // 3. 중복확인 로직 수행 - const { mutate } = useNameCheckMutation({ + const { mutate: nameCheckMutate } = useNameCheckMutation({ onSuccessCallback: () => setIsNameCheck(true), onErrorCallback: () => setError('name', { - type: 'checkFaile', + type: 'checkFail', message: '이미 존재하는 닉네임입니다.', }), }); + const { mutate: emailCheckMutate } = useEmailCheckMutation({ + onSuccessCallback: () => setIsEmailCheck(true), + onErrorCallback: () => + setError('email', { + type: 'checkFail', + message: '이미 존재하는 이메일입니다.', + }), + }); + const handleNameCheck = () => { const name = watch('name'); console.log('name: ', name); - mutate(name); + nameCheckMutate(name); + }; + + const handleEmailCheck = () => { + const email = watch('email'); + emailCheckMutate(email); }; return ( @@ -124,23 +145,31 @@ export default function Signup() {
-
diff --git a/src/hooks/mutations/useUserMutation.ts b/src/hooks/mutations/useUserMutation.ts index f8ba09c..7ff48e4 100644 --- a/src/hooks/mutations/useUserMutation.ts +++ b/src/hooks/mutations/useUserMutation.ts @@ -1,6 +1,6 @@ import { setAccessToken } from '@/lib/serverActions'; import { useMutation } from '@tanstack/react-query'; -import { postLogin, postNameCheck } from 'service/api/user'; +import { getEmailCheck, getNameCheck, postLogin } from 'service/api/user'; const useLoginMutation = ({ onSuccessCallback, @@ -35,7 +35,7 @@ const useNameCheckMutation = ({ onErrorCallback: () => void; }) => { return useMutation({ - mutationFn: (name: string) => postNameCheck(name), + mutationFn: (name: string) => getNameCheck(name), onSuccess: () => { // 중복 검사 성공 /** @@ -51,4 +51,29 @@ const useNameCheckMutation = ({ }); }; -export { useLoginMutation, useNameCheckMutation }; +// 이메일 중복 검사 +const useEmailCheckMutation = ({ + onSuccessCallback, + onErrorCallback, +}: { + onSuccessCallback: () => void; + onErrorCallback: () => void; +}) => { + return useMutation({ + mutationFn: (email: string) => getEmailCheck(email), + onSuccess: () => { + // 중복 검사 성공 + /** + * TODO + * - 중복확인 버튼 비활성화 + */ + onSuccessCallback(); + }, + onError: () => { + // 중복 검사 실패 + onErrorCallback(); + }, + }); +}; + +export { useLoginMutation, useNameCheckMutation, useEmailCheckMutation }; diff --git a/src/service/api/user.ts b/src/service/api/user.ts index bbfb413..fd6b776 100644 --- a/src/service/api/user.ts +++ b/src/service/api/user.ts @@ -12,8 +12,14 @@ const postLogin = async ({ return res; }; -const postNameCheck = async (name: string) => { +const getNameCheck = async (name: string) => { const res = await basicAPI.get(`/api/v1/auths/signup/name?name=${name}`); return res; }; -export { postLogin, postNameCheck }; + +const getEmailCheck = async (email: string) => { + const res = await basicAPI.get(`/api/v1/auths/signup/email?email=${email}`); + return res; +}; + +export { postLogin, getNameCheck, getEmailCheck }; From 380ab9e2c1b03cd3a55bdcfa0209767f58f3825e Mon Sep 17 00:00:00 2001 From: dbswl701 Date: Fri, 21 Feb 2025 15:18:13 +0900 Subject: [PATCH 04/15] =?UTF-8?q?feat(DEVING-25):=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/signup/page.tsx | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index 5781138..eb99bb6 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -34,7 +34,8 @@ export default function Signup() { console.log(watch('name')); const onSubmit = (data: ISignupFormData) => { - console.log('로그인 데이터: ', data); + const formData = { ...data, position }; + console.log('회원가입 데이터: ', formData); }; /** @@ -179,17 +180,6 @@ export default function Signup() { - {/*
- - - -
*/}
@@ -207,6 +197,11 @@ export default function Signup() { value: 6, message: '비밀번호는 최소 6자 이상이어야 합니다.', }, + pattern: { + value: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6,}$/, + message: + '비밀번호는 영어와 숫자 포함 6자 이상이어야 합니다.', + }, })} errorMessage={errors.password?.message} /> @@ -222,10 +217,9 @@ export default function Signup() { placeholder="비밀번호를 입력해주세요." {...register('passwordCheck', { required: '비밀번호를 입력해주세요.', - minLength: { - value: 6, - message: '비밀번호는 최소 6자 이상이어야 합니다.', - }, + validate: (value) => + value === watch('password') || + '비밀번호가 일치하지 않습니다.', })} errorMessage={errors.passwordCheck?.message} /> @@ -238,9 +232,9 @@ export default function Signup() {
-

비밀번호를 잊으셨나요?

- - 비밀번호 수정 +

이미 회원이신가요?

+ + 로그인
From 7f6bfc95f354f6fdb5ea278dc6330d387cb61445 Mon Sep 17 00:00:00 2001 From: dbswl701 Date: Fri, 21 Feb 2025 16:31:20 +0900 Subject: [PATCH 05/15] =?UTF-8?q?feat(DEVING-25):=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85API=20=EC=97=B0=EB=8F=99=20=EC=A0=84=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/signup/components/ChipContainer.tsx | 2 +- src/app/signup/page.tsx | 89 ++++++++++++++++++--- 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/src/app/signup/components/ChipContainer.tsx b/src/app/signup/components/ChipContainer.tsx index b34884a..678526d 100644 --- a/src/app/signup/components/ChipContainer.tsx +++ b/src/app/signup/components/ChipContainer.tsx @@ -5,7 +5,7 @@ export const ChipContainer = ({ setPosition, }: { position: string; - setPosition: (value: React.SetStateAction) => void; + setPosition: (value: string) => void; }) => { return (
diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index eb99bb6..39def6d 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -25,18 +25,16 @@ export default function Signup() { register, handleSubmit, watch, + trigger, setError, + setValue, formState: { errors, dirtyFields }, } = useForm({ mode: 'onBlur', + defaultValues: { + position: '', + }, }); - const [position, setPosition] = useState(''); - - console.log(watch('name')); - const onSubmit = (data: ISignupFormData) => { - const formData = { ...data, position }; - console.log('회원가입 데이터: ', formData); - }; /** * TODO @@ -65,17 +63,23 @@ export default function Signup() { const [isNameCheck, setIsNameCheck] = useState(false); const [isEmailCheck, setIsEmailCheck] = useState(false); + // 회원가입 버튼 활성화 여부 + const [isActiveBtn, setIsActiveBtn] = useState(false); + // 2. 입력이 있다면 중복확인 버튼 활성화 - console.log('dirtyFields: ', dirtyFields?.name); + console.log('dirtyFields: ', dirtyFields); useEffect(() => { if (dirtyFields?.name) { setIsNameCheck(false); } + }, [watch('name')]); + + useEffect(() => { if (dirtyFields?.email) { setIsEmailCheck(false); } - }, [dirtyFields, watch('name'), watch('email')]); + }, [watch('email')]); // 3. 중복확인 로직 수행 const { mutate: nameCheckMutate } = useNameCheckMutation({ @@ -100,13 +104,60 @@ export default function Signup() { const name = watch('name'); console.log('name: ', name); nameCheckMutate(name); + trigger('name'); }; const handleEmailCheck = () => { const email = watch('email'); emailCheckMutate(email); + trigger('email'); + }; + + const onSubmit = (data: ISignupFormData) => { + // const formData = { ...data, position }; + console.log('회원가입 데이터: ', data); + + /** + * TODO + * - 닉네임 중복검사 했는지 확인 + * - 이메일 중복검사 했는지 확인 + * - position 선택했는지 확인 + * - 비밀번호 입력했는지 확인 + * - 비밀번호 확인 입력했는지 확인 + */ + + if (!isNameCheck) { + setError('name', { + type: 'nameCheck', + message: '닉네임 중복확인이 필요합니다.', + }); + } + if (!isEmailCheck) { + setError('email', { + type: 'emailCheck', + message: '이메일 중복확인이 필요합니다.', + }); + } + if (watch('position') === '') { + setError('position', { + type: 'positionCheck', + message: '포지션을 선택해 주세요.', + }); + } + if (errors) { + return; + } }; + // 포지션 클릭 + const handleClickPosition = (value: string) => { + setValue('position', value); + trigger('position'); + console.log('position watch: ', watch('position')); + }; + + console.log('error: ', errors); + console.log('all watch: ', watch()); return (
포지션 - + + {errors.position?.message && ( +

+ 포지션을 선택해 주세요. +

+ )} +
@@ -203,6 +268,7 @@ export default function Signup() { '비밀번호는 영어와 숫자 포함 6자 이상이어야 합니다.', }, })} + state={dirtyFields.password ? 'success' : 'default'} errorMessage={errors.password?.message} />
@@ -221,13 +287,14 @@ export default function Signup() { value === watch('password') || '비밀번호가 일치하지 않습니다.', })} + state={dirtyFields.passwordCheck ? 'success' : 'default'} errorMessage={errors.passwordCheck?.message} />
-
From b95dd8d2538b511ea10caf3dbc92eb7c69de569d Mon Sep 17 00:00:00 2001 From: dbswl701 Date: Fri, 21 Feb 2025 17:04:02 +0900 Subject: [PATCH 06/15] =?UTF-8?q?feat(DEVING-25):=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/signup/page.tsx | 31 +++++++++++------------ src/hooks/mutations/useUserMutation.ts | 34 ++++++++++++++++++++++++-- src/service/api/user.ts | 8 +++++- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index 39def6d..a6a78a5 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -5,14 +5,16 @@ import { Input } from '@/components/ui/Input'; import { useEmailCheckMutation, useNameCheckMutation, + useSignupMutation, } from '@/hooks/mutations/useUserMutation'; import Link from 'next/link'; +import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { ChipContainer } from './components/ChipContainer'; -interface ISignupFormData { +export interface ISignupFormData { name: string; email: string; position: string; @@ -64,21 +66,15 @@ export default function Signup() { const [isEmailCheck, setIsEmailCheck] = useState(false); // 회원가입 버튼 활성화 여부 - const [isActiveBtn, setIsActiveBtn] = useState(false); + const router = useRouter(); // 2. 입력이 있다면 중복확인 버튼 활성화 - console.log('dirtyFields: ', dirtyFields); - useEffect(() => { - if (dirtyFields?.name) { - setIsNameCheck(false); - } + setIsNameCheck(false); }, [watch('name')]); useEffect(() => { - if (dirtyFields?.email) { - setIsEmailCheck(false); - } + setIsEmailCheck(false); }, [watch('email')]); // 3. 중복확인 로직 수행 @@ -102,7 +98,6 @@ export default function Signup() { const handleNameCheck = () => { const name = watch('name'); - console.log('name: ', name); nameCheckMutate(name); trigger('name'); }; @@ -113,8 +108,11 @@ export default function Signup() { trigger('email'); }; + const { mutate: singupMutate } = useSignupMutation({ + onSuccessCallback: () => router.push('/login'), + }); + const onSubmit = (data: ISignupFormData) => { - // const formData = { ...data, position }; console.log('회원가입 데이터: ', data); /** @@ -144,20 +142,19 @@ export default function Signup() { message: '포지션을 선택해 주세요.', }); } - if (errors) { + + if (Object.keys(errors).length) { return; } + singupMutate(data); }; // 포지션 클릭 const handleClickPosition = (value: string) => { setValue('position', value); trigger('position'); - console.log('position watch: ', watch('position')); }; - console.log('error: ', errors); - console.log('all watch: ', watch()); return (
-
diff --git a/src/hooks/mutations/useUserMutation.ts b/src/hooks/mutations/useUserMutation.ts index 7ff48e4..8f9861a 100644 --- a/src/hooks/mutations/useUserMutation.ts +++ b/src/hooks/mutations/useUserMutation.ts @@ -1,6 +1,12 @@ +import { ISignupFormData } from '@/app/signup/page'; import { setAccessToken } from '@/lib/serverActions'; import { useMutation } from '@tanstack/react-query'; -import { getEmailCheck, getNameCheck, postLogin } from 'service/api/user'; +import { + getEmailCheck, + getNameCheck, + postLogin, + postSignup, +} from 'service/api/user'; const useLoginMutation = ({ onSuccessCallback, @@ -76,4 +82,28 @@ const useEmailCheckMutation = ({ }); }; -export { useLoginMutation, useNameCheckMutation, useEmailCheckMutation }; +// 회원가입 +const useSignupMutation = ({ + onSuccessCallback, +}: { + onSuccessCallback: () => void; +}) => { + return useMutation({ + mutationFn: (data: ISignupFormData) => postSignup(data), + onSuccess: () => { + /** + * TODO + * - 로그인 페이지로 리다이렉트 + * - 회원가입 성공 토스트바 + */ + onSuccessCallback(); + }, + }); +}; + +export { + useLoginMutation, + useNameCheckMutation, + useEmailCheckMutation, + useSignupMutation, +}; diff --git a/src/service/api/user.ts b/src/service/api/user.ts index fd6b776..f074708 100644 --- a/src/service/api/user.ts +++ b/src/service/api/user.ts @@ -1,3 +1,4 @@ +import { ISignupFormData } from '@/app/signup/page'; import { basicAPI } from '@/lib/axios/basicApi'; const postLogin = async ({ @@ -22,4 +23,9 @@ const getEmailCheck = async (email: string) => { return res; }; -export { postLogin, getNameCheck, getEmailCheck }; +const postSignup = async (data: ISignupFormData) => { + const res = await basicAPI.post('/api/v1/auths/signup', data); + return res; +}; + +export { postLogin, getNameCheck, getEmailCheck, postSignup }; From c5f7eb2c15772fd6239f3fff7c31ba232e3d0562 Mon Sep 17 00:00:00 2001 From: dbswl701 Date: Fri, 21 Feb 2025 17:21:39 +0900 Subject: [PATCH 07/15] =?UTF-8?q?refactor(DEVING-25):=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EB=A1=9C=EC=A7=81=EA=B3=BC=20ui=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EB=A6=AC=ED=8E=99=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/login/components/LoginForm.tsx | 12 +- src/app/signup/components/SignupForm.tsx | 172 +++++++++++++ src/app/signup/page.tsx | 294 +---------------------- src/hooks/useLoginForm.ts | 6 +- src/hooks/useSignupForm.ts | 158 ++++++++++++ 5 files changed, 346 insertions(+), 296 deletions(-) create mode 100644 src/app/signup/components/SignupForm.tsx create mode 100644 src/hooks/useSignupForm.ts diff --git a/src/app/login/components/LoginForm.tsx b/src/app/login/components/LoginForm.tsx index bc39a0c..046689c 100644 --- a/src/app/login/components/LoginForm.tsx +++ b/src/app/login/components/LoginForm.tsx @@ -2,12 +2,15 @@ import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; -import { useLoginForm } from '@/hooks/useLoginForm'; +import useLoginForm from '@/hooks/useLoginForm'; import Link from 'next/link'; +import { useRouter } from 'next/navigation'; const LoginForm = () => { + const router = useRouter(); const { register, handleSubmit, onSubmit, errors, setFocusedField } = useLoginForm(); + return ( { - diff --git a/src/app/signup/components/SignupForm.tsx b/src/app/signup/components/SignupForm.tsx new file mode 100644 index 0000000..13cd29a --- /dev/null +++ b/src/app/signup/components/SignupForm.tsx @@ -0,0 +1,172 @@ +'use client'; + +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/Input'; +import useSignUpForm from '@/hooks/useSignupForm'; +import Link from 'next/link'; + +import { ChipContainer } from './ChipContainer'; + +const SignupForm = () => { + const { + handleSubmit, + onSubmit, + register, + errors, + isNameCheck, + handleNameCheck, + isEmailCheck, + handleEmailCheck, + watch, + handleClickPosition, + dirtyFields, + } = useSignUpForm(); + return ( + +
+

+ 회원가입 +

+
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ + + {errors.position?.message && ( +

+ {errors.position?.message} +

+ )} + +
+ +
+ + +
+ +
+ + + value === watch('password') || + '비밀번호가 일치하지 않습니다.', + })} + state={dirtyFields.passwordCheck ? 'success' : 'default'} + errorMessage={errors.passwordCheck?.message} + /> +
+
+
+
+ +
+
+

이미 회원이신가요?

+ + 로그인 + +
+ + ); +}; + +export default SignupForm; diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index a6a78a5..b80a883 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -1,18 +1,4 @@ -'use client'; - -import { Button } from '@/components/ui/Button'; -import { Input } from '@/components/ui/Input'; -import { - useEmailCheckMutation, - useNameCheckMutation, - useSignupMutation, -} from '@/hooks/mutations/useUserMutation'; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import { useForm } from 'react-hook-form'; - -import { ChipContainer } from './components/ChipContainer'; +import SignupForm from './components/SignupForm'; export interface ISignupFormData { name: string; @@ -23,285 +9,9 @@ export interface ISignupFormData { } export default function Signup() { - const { - register, - handleSubmit, - watch, - trigger, - setError, - setValue, - formState: { errors, dirtyFields }, - } = useForm({ - mode: 'onBlur', - defaultValues: { - position: '', - }, - }); - - /** - * TODO - * 포커스 1초 되 유효성 검사 - 닉네임, 이메일, 비밀번호, 비밀번호 확인 - */ - - /** - * TODO - * 중복확인 플로우 체크 - * 1. 처음에는 중복확인 버튼 비활성화(''인 경우) - * 2. 입력이 있다면 중복확인 버튼 활성화 - * - 입력 중에는 비활성화 - * 3. 중복확인 성공시 - * - 해당 인풋 성공 표시(보더 색 변경), disabled 없이 그대로 유지 - * - 중복확인 버튼 비활성화 - * 3.1. 중복확인 성공 후 다시 입력 시 - * - 중복확인 버튼 활성화 - * - 해당 인풋 성공 처리 취소 - * 4. 중복확인 실패시 - * - 해당 인풋 에러메시지 표시 - * - 중복확인 버튼은 계속 활성화 - */ - - // 닉네임 중복 체크 확인. - // 1. 처음에는 중복확인 버튼 비활성화 - const [isNameCheck, setIsNameCheck] = useState(false); - const [isEmailCheck, setIsEmailCheck] = useState(false); - - // 회원가입 버튼 활성화 여부 - const router = useRouter(); - - // 2. 입력이 있다면 중복확인 버튼 활성화 - useEffect(() => { - setIsNameCheck(false); - }, [watch('name')]); - - useEffect(() => { - setIsEmailCheck(false); - }, [watch('email')]); - - // 3. 중복확인 로직 수행 - const { mutate: nameCheckMutate } = useNameCheckMutation({ - onSuccessCallback: () => setIsNameCheck(true), - onErrorCallback: () => - setError('name', { - type: 'checkFail', - message: '이미 존재하는 닉네임입니다.', - }), - }); - - const { mutate: emailCheckMutate } = useEmailCheckMutation({ - onSuccessCallback: () => setIsEmailCheck(true), - onErrorCallback: () => - setError('email', { - type: 'checkFail', - message: '이미 존재하는 이메일입니다.', - }), - }); - - const handleNameCheck = () => { - const name = watch('name'); - nameCheckMutate(name); - trigger('name'); - }; - - const handleEmailCheck = () => { - const email = watch('email'); - emailCheckMutate(email); - trigger('email'); - }; - - const { mutate: singupMutate } = useSignupMutation({ - onSuccessCallback: () => router.push('/login'), - }); - - const onSubmit = (data: ISignupFormData) => { - console.log('회원가입 데이터: ', data); - - /** - * TODO - * - 닉네임 중복검사 했는지 확인 - * - 이메일 중복검사 했는지 확인 - * - position 선택했는지 확인 - * - 비밀번호 입력했는지 확인 - * - 비밀번호 확인 입력했는지 확인 - */ - - if (!isNameCheck) { - setError('name', { - type: 'nameCheck', - message: '닉네임 중복확인이 필요합니다.', - }); - } - if (!isEmailCheck) { - setError('email', { - type: 'emailCheck', - message: '이메일 중복확인이 필요합니다.', - }); - } - if (watch('position') === '') { - setError('position', { - type: 'positionCheck', - message: '포지션을 선택해 주세요.', - }); - } - - if (Object.keys(errors).length) { - return; - } - singupMutate(data); - }; - - // 포지션 클릭 - const handleClickPosition = (value: string) => { - setValue('position', value); - trigger('position'); - }; - return (
-
-
-

- 회원가입 -

-
-
- -
- - -
-
- -
- -
- - -
-
- -
- - - {errors.position?.message && ( -

- 포지션을 선택해 주세요. -

- )} - -
- -
- - -
- -
- - - value === watch('password') || - '비밀번호가 일치하지 않습니다.', - })} - state={dirtyFields.passwordCheck ? 'success' : 'default'} - errorMessage={errors.passwordCheck?.message} - /> -
-
-
-
- -
-
-

이미 회원이신가요?

- - 로그인 - -
-
+
); } diff --git a/src/hooks/useLoginForm.ts b/src/hooks/useLoginForm.ts index a5d723d..143f77f 100644 --- a/src/hooks/useLoginForm.ts +++ b/src/hooks/useLoginForm.ts @@ -11,7 +11,7 @@ interface ILoginFormData { password: string; } -export function useLoginForm() { +const useLoginForm = () => { const { register, handleSubmit, @@ -80,4 +80,6 @@ export function useLoginForm() { setFocusedField, onSubmit, }; -} +}; + +export default useLoginForm; diff --git a/src/hooks/useSignupForm.ts b/src/hooks/useSignupForm.ts new file mode 100644 index 0000000..6d707f2 --- /dev/null +++ b/src/hooks/useSignupForm.ts @@ -0,0 +1,158 @@ +import { ISignupFormData } from '@/app/signup/page'; +import { + useEmailCheckMutation, + useNameCheckMutation, + useSignupMutation, +} from '@/hooks/mutations/useUserMutation'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; + +const useSignUpForm = () => { + const { + register, + handleSubmit, + watch, + trigger, + setError, + setValue, + formState: { errors, dirtyFields }, + } = useForm({ + mode: 'onBlur', + defaultValues: { + position: '', + }, + }); + + /** + * TODO + * 포커스 1초 되 유효성 검사 - 닉네임, 이메일, 비밀번호, 비밀번호 확인 + */ + + /** + * TODO + * 중복확인 플로우 체크 + * 1. 처음에는 중복확인 버튼 비활성화(''인 경우) + * 2. 입력이 있다면 중복확인 버튼 활성화 + * - 입력 중에는 비활성화 + * 3. 중복확인 성공시 + * - 해당 인풋 성공 표시(보더 색 변경), disabled 없이 그대로 유지 + * - 중복확인 버튼 비활성화 + * 3.1. 중복확인 성공 후 다시 입력 시 + * - 중복확인 버튼 활성화 + * - 해당 인풋 성공 처리 취소 + * 4. 중복확인 실패시 + * - 해당 인풋 에러메시지 표시 + * - 중복확인 버튼은 계속 활성화 + */ + + // 닉네임 중복 체크 확인. + // 1. 처음에는 중복확인 버튼 비활성화 + const [isNameCheck, setIsNameCheck] = useState(false); + const [isEmailCheck, setIsEmailCheck] = useState(false); + + const router = useRouter(); + + // 2. 입력이 있다면 중복확인 버튼 활성화 + useEffect(() => { + setIsNameCheck(false); + }, [watch('name')]); + + useEffect(() => { + setIsEmailCheck(false); + }, [watch('email')]); + + // 3. 중복확인 로직 수행 + const { mutate: nameCheckMutate } = useNameCheckMutation({ + onSuccessCallback: () => setIsNameCheck(true), + onErrorCallback: () => + setError('name', { + type: 'checkFail', + message: '이미 존재하는 닉네임입니다.', + }), + }); + + const { mutate: emailCheckMutate } = useEmailCheckMutation({ + onSuccessCallback: () => setIsEmailCheck(true), + onErrorCallback: () => + setError('email', { + type: 'checkFail', + message: '이미 존재하는 이메일입니다.', + }), + }); + + const handleNameCheck = () => { + const name = watch('name'); + nameCheckMutate(name); + trigger('name'); + }; + + const handleEmailCheck = () => { + const email = watch('email'); + emailCheckMutate(email); + trigger('email'); + }; + + const { mutate: singupMutate } = useSignupMutation({ + onSuccessCallback: () => router.push('/login'), + }); + + const onSubmit = (data: ISignupFormData) => { + console.log('회원가입 데이터: ', data); + + /** + * TODO + * - 닉네임 중복검사 했는지 확인 + * - 이메일 중복검사 했는지 확인 + * - position 선택했는지 확인 + * - 비밀번호 입력했는지 확인 + * - 비밀번호 확인 입력했는지 확인 + */ + + if (!isNameCheck) { + setError('name', { + type: 'nameCheck', + message: '닉네임 중복확인이 필요합니다.', + }); + } + if (!isEmailCheck) { + setError('email', { + type: 'emailCheck', + message: '이메일 중복확인이 필요합니다.', + }); + } + // if (watch('position') === '') { + // setError('position', { + // type: 'positionCheck', + // message: '포지션을 선택해 주세요.', + // }); + // } + + if (Object.keys(errors).length) { + return; + } + singupMutate(data); + }; + + // 포지션 클릭 + const handleClickPosition = (value: string) => { + setValue('position', value); + trigger('position'); + }; + + return { + handleSubmit, + onSubmit, + register, + errors, + isNameCheck, + handleNameCheck, + isEmailCheck, + handleEmailCheck, + watch, + handleClickPosition, + dirtyFields, + }; +}; + +export default useSignUpForm; From 94809a7e7c749041ba2ba90a86a08bd391f81404 Mon Sep 17 00:00:00 2001 From: dbswl701 Date: Sun, 23 Feb 2025 18:40:15 +0900 Subject: [PATCH 08/15] =?UTF-8?q?feat(DEVING-25):=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=EC=B0=BD=20=ED=8F=AC=EC=BB=A4=EC=8A=A4=201=EC=B4=88=20?= =?UTF-8?q?=EB=92=A4=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/signup/components/SignupForm.tsx | 21 +++++++- src/hooks/useLoginForm.ts | 14 +----- src/hooks/useSignupForm.ts | 63 ++++++++---------------- 3 files changed, 42 insertions(+), 56 deletions(-) diff --git a/src/app/signup/components/SignupForm.tsx b/src/app/signup/components/SignupForm.tsx index 13cd29a..a0b4853 100644 --- a/src/app/signup/components/SignupForm.tsx +++ b/src/app/signup/components/SignupForm.tsx @@ -20,6 +20,7 @@ const SignupForm = () => { watch, handleClickPosition, dirtyFields, + setFocusedField, } = useSignUpForm(); return (
{ id="name" className="h-full" placeholder="닉네임을 입력해주세요." - {...register('name', { required: '닉네임을 입력해주세요.' })} + {...register('name', { + required: '닉네임을 입력해주세요.', + minLength: { + value: 2, + message: '최소 2자 이상 입력해 주세요.', + }, + maxLength: { + value: 10, + message: '최대 10자 이하로 입력해 주세요.', + }, + pattern: { + value: /^[가-힣a-zA-Z0-9]+$/, + message: '한글(완성형), 영어, 숫자만 입력할 수 있습니다.', + }, + })} errorMessage={errors.name?.message} state={isNameCheck ? 'success' : 'default'} + onFocus={() => setFocusedField('name')} /> + + + + ); +} diff --git a/src/assets/icon/check_icon.svg b/src/assets/icon/check_icon.svg new file mode 100644 index 0000000..770a655 --- /dev/null +++ b/src/assets/icon/check_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icon/warning_icon.svg b/src/assets/icon/warning_icon.svg new file mode 100644 index 0000000..cb1e6e2 --- /dev/null +++ b/src/assets/icon/warning_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/common/Toast.tsx b/src/components/common/Toast.tsx new file mode 100644 index 0000000..1504b71 --- /dev/null +++ b/src/components/common/Toast.tsx @@ -0,0 +1,99 @@ +import { cn } from '@/util/cn'; +import { type VariantProps, cva } from 'class-variance-authority'; +import React, { forwardRef, useEffect, useState } from 'react'; + +import CheckIcon from '../../assets/icon/check_icon.svg'; +import WarningIcon from '../../assets/icon/warning_icon.svg'; +import { Button } from '../ui/Button'; + +const ToastVariants = cva( + 'typo-head4 flex h-[64px] w-fit items-center rounded-[20px] border border-main bg-BG p-[16px] text-white transition-opacity duration-300 ease-in-out', + { + variants: { + variant: { + default: '', + success: '', + error: 'border-warning', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +export interface ToastProps + extends React.HTMLAttributes, + VariantProps { + duration?: number; // 표시 시간 (ms) + onDismiss?: () => void; // fade out 후 호출 + btnText?: string; + onBtnClick?: () => void; +} + +const Toast = forwardRef( + ( + { + className, + variant = 'default', + children, + duration = 3000, + onDismiss, + btnText, + onBtnClick, + ...props + }, + ref, + ) => { + const [visible, setVisible] = useState(false); + + // 컴포넌트 마운트 후 fade in 효과 적용 + useEffect(() => { + setVisible(true); + const timer = setTimeout(() => { + setVisible(false); + }, duration); + return () => clearTimeout(timer); + }, [duration]); + + // fade out 애니메이션(300ms) 후 onDismiss 호출 + useEffect(() => { + if (!visible) { + const timer = setTimeout(() => { + onDismiss && onDismiss(); + }, 300); + return () => clearTimeout(timer); + } + }, [visible, onDismiss]); + + return ( +
+ {variant === 'success' ? ( + + ) : variant === 'error' ? ( + + ) : null} + {children} + {btnText && ( + + )} +
+ ); + }, +); + +Toast.displayName = 'Toast'; + +export { Toast, ToastVariants }; diff --git a/src/components/common/ToastContext.tsx b/src/components/common/ToastContext.tsx new file mode 100644 index 0000000..414c3fc --- /dev/null +++ b/src/components/common/ToastContext.tsx @@ -0,0 +1,80 @@ +'use client'; + +import React, { ReactNode, createContext, useContext, useState } from 'react'; + +import { Toast } from './Toast'; + +interface ToastItem { + id: number; + message: string; + variant: 'default' | 'success' | 'error'; + duration: number; + btnText?: string; + onClick?: () => void; +} + +interface ToastContextType { + showToast: ( + message: string, + variant?: 'default' | 'success' | 'error', + options?: { duration?: number; btnText?: string; onClick?: () => void }, + ) => void; +} + +const ToastContext = createContext(undefined); + +export const ToastProvider = ({ children }: { children: ReactNode }) => { + const [toasts, setToasts] = useState([]); + + const showToast = ( + message: string, + variant: 'default' | 'success' | 'error' = 'default', + options?: { duration?: number; btnText?: string; onClick?: () => void }, + ) => { + const id = Date.now(); + const duration = options?.duration ?? 3000; + setToasts((prev) => [ + ...prev, + { + id, + message, + variant, + duration, + btnText: options?.btnText, + onClick: options?.onClick, + }, + ]); + }; + + const removeToast = (id: number) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }; + + return ( + + {children} +
+ {toasts.map((toast) => ( + removeToast(toast.id)} + btnText={toast.btnText} + onClick={toast.onClick} + > + {toast.message} + + ))} +
+
+ ); +}; + +export const useToast = () => { + const context = useContext(ToastContext); + if (!context) { + throw new Error('useToast must be used within a ToastProvider'); + } + return context; +}; From e4e3ac04df71210998eea96b4d936c65f475269f Mon Sep 17 00:00:00 2001 From: dbswl701 Date: Fri, 28 Feb 2025 22:10:53 +0900 Subject: [PATCH 11/15] =?UTF-8?q?refactor(DEVING-25):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=A6=AC=EB=A0=8C=EB=8D=94=EB=A7=81=20=EA=B0=90?= =?UTF-8?q?=EC=86=8C=20=EC=9C=84=ED=95=9C=20=EB=A6=AC=ED=8E=99=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/login/components/EmailInput.tsx | 50 ++++++++++++++++++++++ src/app/login/components/LoginForm.tsx | 37 ++++++---------- src/app/login/components/PasswordInput.tsx | 39 +++++++++++++++++ src/hooks/mutations/useUserMutation.ts | 5 ++- src/hooks/useDebounde.ts | 4 +- src/hooks/useLoginForm.ts | 36 ++-------------- 6 files changed, 113 insertions(+), 58 deletions(-) create mode 100644 src/app/login/components/EmailInput.tsx create mode 100644 src/app/login/components/PasswordInput.tsx diff --git a/src/app/login/components/EmailInput.tsx b/src/app/login/components/EmailInput.tsx new file mode 100644 index 0000000..eb49a4e --- /dev/null +++ b/src/app/login/components/EmailInput.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { Input } from '@/components/ui/Input'; +import useDebounce from '@/hooks/useDebounde'; +import { ILoginFormData } from '@/hooks/useLoginForm'; +import { loginEmailValidation } from '@/util/validation'; +import { useCallback } from 'react'; +import { + Control, + FieldErrors, + UseFormRegister, + UseFormTrigger, + useWatch, +} from 'react-hook-form'; + +export interface IInputProps { + control: Control; // ✅ control 타입 지정 + register: UseFormRegister; // ✅ register 타입 지정 + errors: FieldErrors; // ✅ errors 타입 지정 + trigger: UseFormTrigger; // ✅ trigger 타입 지정 +} + +const EmailInput = ({ control, register, errors, trigger }: IInputProps) => { + // `useWatch`를 개별 컴포넌트에서 호출하여 최적화 + const email = useWatch({ control, name: 'email' }); + + useDebounce({ + value: email, + callBack: useCallback(() => { + trigger('email'); + }, [email]), + }); + + return ( + <> + + + + ); +}; + +export default EmailInput; diff --git a/src/app/login/components/LoginForm.tsx b/src/app/login/components/LoginForm.tsx index 8659bf6..e2963ad 100644 --- a/src/app/login/components/LoginForm.tsx +++ b/src/app/login/components/LoginForm.tsx @@ -10,9 +10,12 @@ import { import Link from 'next/link'; import { useRouter } from 'next/navigation'; +import EmailInput from './EmailInput'; +import PasswordInput from './PasswordInput'; + const LoginForm = () => { const router = useRouter(); - const { register, handleSubmit, onSubmit, errors, setFocusedField } = + const { register, handleSubmit, onSubmit, errors, control, trigger } = useLoginForm(); return ( @@ -23,30 +26,18 @@ const LoginForm = () => {

로그인

- - setFocusedField('email')} + - - setFocusedField('password')} + -

비밀번호를 잊으셨나요?

diff --git a/src/app/login/components/PasswordInput.tsx b/src/app/login/components/PasswordInput.tsx new file mode 100644 index 0000000..c2eb663 --- /dev/null +++ b/src/app/login/components/PasswordInput.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { Input } from '@/components/ui/Input'; +import useDebounce from '@/hooks/useDebounde'; +import { loginPasswordValidation } from '@/util/validation'; +import { useCallback } from 'react'; +import { useWatch } from 'react-hook-form'; + +import { IInputProps } from './EmailInput'; + +const PasswordInput = ({ control, register, errors, trigger }: IInputProps) => { + // `useWatch`를 개별 컴포넌트에서 호출하여 최적화 + const password = useWatch({ control, name: 'password' }); + + useDebounce({ + value: password, + callBack: useCallback(() => { + trigger('password'); + }, [password]), + }); + + return ( + <> + + + + ); +}; + +export default PasswordInput; diff --git a/src/hooks/mutations/useUserMutation.ts b/src/hooks/mutations/useUserMutation.ts index 8f9861a..4f5bbf8 100644 --- a/src/hooks/mutations/useUserMutation.ts +++ b/src/hooks/mutations/useUserMutation.ts @@ -1,4 +1,5 @@ import { ISignupFormData } from '@/app/signup/page'; +import { useToast } from '@/components/common/ToastContext'; import { setAccessToken } from '@/lib/serverActions'; import { useMutation } from '@tanstack/react-query'; import { @@ -13,6 +14,7 @@ const useLoginMutation = ({ }: { onSuccessCallback: () => void; }) => { + const { showToast } = useToast(); return useMutation({ mutationFn: ({ email, password }: { email: string; password: string }) => postLogin({ email, password }), @@ -23,11 +25,12 @@ const useLoginMutation = ({ await setAccessToken(accessToken); } + showToast('로그인 성공', 'success'); // 메인페이지로 리다이렉트 onSuccessCallback(); }, onError: () => { - console.log('로그인 에러'); + showToast('이메일 또는 비밀번호가 틀렸습니다.', 'error'); }, }); }; diff --git a/src/hooks/useDebounde.ts b/src/hooks/useDebounde.ts index f0f587e..1d665ef 100644 --- a/src/hooks/useDebounde.ts +++ b/src/hooks/useDebounde.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; /** * 특정 값이 변경된 후 지정된 시간이 지나면 콜백 함수를 실행하는 Debounce 훅 @@ -17,7 +17,7 @@ const useDebounce = ({ callBack?: () => void; }) => { useEffect(() => { - if (!value) return; + if (value === null || value === undefined) return; const timer = setTimeout(() => { if (callBack) { callBack(); diff --git a/src/hooks/useLoginForm.ts b/src/hooks/useLoginForm.ts index 29e6bda..5d597bf 100644 --- a/src/hooks/useLoginForm.ts +++ b/src/hooks/useLoginForm.ts @@ -1,11 +1,9 @@ import { useRouter } from 'next/navigation'; -import { useCallback, useState } from 'react'; -import { useForm, useWatch } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; import { useLoginMutation } from './mutations/useUserMutation'; -import useDebounce from './useDebounde'; -interface ILoginFormData { +export interface ILoginFormData { email: string; password: string; } @@ -22,33 +20,6 @@ const useLoginForm = () => { }); const router = useRouter(); - const [focusedField, setFocusedField] = useState<'email' | 'password' | null>( - null, - ); - - // `useWatch`를 사용하여 특정 필드만 감시 (렌더링 최소화) - const email = useWatch({ control, name: 'email' }); - const password = useWatch({ control, name: 'password' }); - - // 입력창 포커스 1초 뒤 유효성 검사 - useDebounce({ - value: email, - callBack: useCallback(() => { - if (focusedField === 'email') { - trigger(focusedField); - } - }, [focusedField, trigger]), - }); - - // 비밀번호 포커스 1초 뒤 유효성 검사 - useDebounce({ - value: password, - callBack: useCallback(() => { - if (focusedField === 'password') { - trigger(focusedField); - } - }, [focusedField, trigger]), - }); const { mutate } = useLoginMutation({ onSuccessCallback: () => router.push('/'), @@ -62,8 +33,9 @@ const useLoginForm = () => { register, handleSubmit, errors, - setFocusedField, onSubmit, + control, + trigger, }; }; From c1fdeb673dcdc1798d2d17a712b41cd83034184c Mon Sep 17 00:00:00 2001 From: dbswl701 Date: Fri, 28 Feb 2025 22:11:22 +0900 Subject: [PATCH 12/15] =?UTF-8?q?refactor(DEVING-25):=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EB=A6=AC=EB=A0=8C=EB=8D=94=EB=A7=81=20?= =?UTF-8?q?=EA=B0=90=EC=86=8C=20=EC=9C=84=ED=95=9C=20=EB=A6=AC=ED=8E=99?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/signup/components/EmailInput.tsx | 67 ++++++++ src/app/signup/components/NameInput.tsx | 87 ++++++++++ .../signup/components/PasswordCheckInput.tsx | 48 ++++++ src/app/signup/components/PasswordInput.tsx | 55 ++++++ src/app/signup/components/PositionInput.tsx | 34 ++++ src/app/signup/components/SignupForm.tsx | 159 ++++++------------ src/hooks/useSignupForm.ts | 51 ++---- src/util/validation.ts | 13 +- 8 files changed, 363 insertions(+), 151 deletions(-) create mode 100644 src/app/signup/components/EmailInput.tsx create mode 100644 src/app/signup/components/NameInput.tsx create mode 100644 src/app/signup/components/PasswordCheckInput.tsx create mode 100644 src/app/signup/components/PasswordInput.tsx create mode 100644 src/app/signup/components/PositionInput.tsx diff --git a/src/app/signup/components/EmailInput.tsx b/src/app/signup/components/EmailInput.tsx new file mode 100644 index 0000000..3f744e7 --- /dev/null +++ b/src/app/signup/components/EmailInput.tsx @@ -0,0 +1,67 @@ +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/Input'; +import useDebounce from '@/hooks/useDebounde'; +import { emailValidation } from '@/util/validation'; +import { Dispatch, SetStateAction, useCallback, useEffect } from 'react'; +import { useWatch } from 'react-hook-form'; + +import { ISignupInputProps } from './NameInput'; + +export interface IEmailInputProps extends ISignupInputProps { + isEmailCheck: boolean; + handleEmailCheck: () => void; + setIsEmailCheck: Dispatch>; +} + +const EmailInput = ({ + register, + errors, + isEmailCheck, + handleEmailCheck, + setIsEmailCheck, + control, + trigger, +}: IEmailInputProps) => { + const email = useWatch({ control, name: 'email' }); + + // 입력이 있다면 중복확인 버튼 활성화 + useEffect(() => { + setIsEmailCheck(false); + }, [email]); + + useDebounce({ + value: email, + callBack: useCallback(() => { + trigger?.('email'); + }, [email]), + }); + + return ( +
+ +
+ + +
+
+ ); +}; +export default EmailInput; diff --git a/src/app/signup/components/NameInput.tsx b/src/app/signup/components/NameInput.tsx new file mode 100644 index 0000000..813fd24 --- /dev/null +++ b/src/app/signup/components/NameInput.tsx @@ -0,0 +1,87 @@ +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/Input'; +import useDebounce from '@/hooks/useDebounde'; +import { nameValidation } from '@/util/validation'; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useState, +} from 'react'; +import { + Control, + FieldErrors, + UseFormRegister, + UseFormTrigger, + useWatch, +} from 'react-hook-form'; + +import { ISignupFormData } from '../page'; + +export interface ISignupInputProps { + control: Control; // ✅ control 타입 지정 + register: UseFormRegister; // ✅ register 타입 지정 + errors: FieldErrors; // ✅ errors 타입 지정 + trigger?: UseFormTrigger; // ✅ trigger 타입 지정 +} + +export interface INameInputProps extends ISignupInputProps { + isNameCheck: boolean; + handleNameCheck: () => void; + setIsNameCheck: Dispatch>; +} + +const NameInput = ({ + register, + errors, + isNameCheck, + handleNameCheck, + setIsNameCheck, + control, + trigger, +}: INameInputProps) => { + const name = useWatch({ control, name: 'name' }); + + useEffect(() => { + setIsNameCheck(false); + }, [name]); + // 중복확인 로직 수행 + + useDebounce({ + value: name, + callBack: useCallback(() => { + trigger?.('name'); + }, [name]), + }); + + return ( +
+ +
+ setFocusedField('name')} + /> + +
+
+ ); +}; +export default NameInput; diff --git a/src/app/signup/components/PasswordCheckInput.tsx b/src/app/signup/components/PasswordCheckInput.tsx new file mode 100644 index 0000000..69f325d --- /dev/null +++ b/src/app/signup/components/PasswordCheckInput.tsx @@ -0,0 +1,48 @@ +import { Input } from '@/components/ui/Input'; +import useDebounce from '@/hooks/useDebounde'; +import { passwordCheckValidation } from '@/util/validation'; +import { useCallback } from 'react'; +import { Control, useWatch } from 'react-hook-form'; + +import { ISignupFormData } from '../page'; +import { IPasswordInputProps } from './PasswordInput'; + +interface IPWCheckInputProps extends IPasswordInputProps { + control: Control; // ✅ control 타입 지정 +} + +const PasswordCheckInput = ({ + register, + dirtyFields, + errors, + control, + trigger, +}: IPWCheckInputProps) => { + const password = useWatch({ control, name: 'password' }); + const passwordCheck = useWatch({ control, name: 'passwordCheck' }); + + useDebounce({ + value: passwordCheck, + callBack: useCallback(() => { + trigger?.('passwordCheck'); + }, [passwordCheck]), + }); + + return ( +
+ + +
+ ); +}; + +export default PasswordCheckInput; diff --git a/src/app/signup/components/PasswordInput.tsx b/src/app/signup/components/PasswordInput.tsx new file mode 100644 index 0000000..f8c9ae7 --- /dev/null +++ b/src/app/signup/components/PasswordInput.tsx @@ -0,0 +1,55 @@ +import { Input } from '@/components/ui/Input'; +import useDebounce from '@/hooks/useDebounde'; +import { passwordValidation } from '@/util/validation'; +import { useCallback } from 'react'; +import { FieldErrors, UseFormRegister, useWatch } from 'react-hook-form'; + +import { ISignupFormData } from '../page'; +import { ISignupInputProps } from './NameInput'; + +export interface IPasswordInputProps extends ISignupInputProps { + register: UseFormRegister; // ✅ register 타입 지정 + dirtyFields: Partial< + Readonly<{ + name?: boolean | undefined; + email?: boolean | undefined; + position?: boolean | undefined; + password?: boolean | undefined; + passwordCheck?: boolean | undefined; + }> + >; + errors: FieldErrors; // ✅ errors 타입 지정 +} + +const PasswordInput = ({ + register, + control, + trigger, + dirtyFields, + errors, +}: IPasswordInputProps) => { + const password = useWatch({ control, name: 'password' }); + useDebounce({ + value: password, + callBack: useCallback(() => { + trigger?.('password'); + }, [password]), + }); + + return ( +
+ + +
+ ); +}; +export default PasswordInput; diff --git a/src/app/signup/components/PositionInput.tsx b/src/app/signup/components/PositionInput.tsx new file mode 100644 index 0000000..472fa5c --- /dev/null +++ b/src/app/signup/components/PositionInput.tsx @@ -0,0 +1,34 @@ +import { positionValidation } from '@/util/validation'; +import { useWatch } from 'react-hook-form'; + +import { ChipContainer } from './ChipContainer'; +import { ISignupInputProps } from './NameInput'; + +interface IPositionInputProps extends ISignupInputProps { + handleClickPosition: (value: string) => void; +} + +const PositionInput = ({ + control, + handleClickPosition, + errors, + register, +}: IPositionInputProps) => { + const position = useWatch({ control, name: 'position' }); + return ( +
+ + + {errors.position?.message && ( +

+ {errors.position?.message} +

+ )} + +
+ ); +}; + +export default PositionInput; diff --git a/src/app/signup/components/SignupForm.tsx b/src/app/signup/components/SignupForm.tsx index 275a2dc..1b5868d 100644 --- a/src/app/signup/components/SignupForm.tsx +++ b/src/app/signup/components/SignupForm.tsx @@ -1,18 +1,14 @@ 'use client'; import { Button } from '@/components/ui/Button'; -import { Input } from '@/components/ui/Input'; import useSignUpForm from '@/hooks/useSignupForm'; -import { - emailValidation, - nameValidation, - passwordCheckValidation, - passwordValidation, - positionValidation, -} from '@/util/validation'; import Link from 'next/link'; -import { ChipContainer } from './ChipContainer'; +import EmailInput from './EmailInput'; +import NameInput from './NameInput'; +import PasswordCheckInput from './PasswordCheckInput'; +import PasswordInput from './PasswordInput'; +import PositionInput from './PositionInput'; const SignupForm = () => { const { @@ -24,10 +20,12 @@ const SignupForm = () => { handleNameCheck, isEmailCheck, handleEmailCheck, - watch, handleClickPosition, dirtyFields, - setFocusedField, + control, + setIsNameCheck, + setIsEmailCheck, + trigger, } = useSignUpForm(); return ( { 회원가입
-
- -
- setFocusedField('name')} - /> - -
-
+ -
- -
- setFocusedField('email')} - /> - -
-
+ -
- - - {errors.position?.message && ( -

- {errors.position?.message} -

- )} - -
+ -
- - setFocusedField('password')} - /> -
+ -
- - setFocusedField('passwordCheck')} - /> -
+
diff --git a/src/hooks/useSignupForm.ts b/src/hooks/useSignupForm.ts index 44debd6..79ed050 100644 --- a/src/hooks/useSignupForm.ts +++ b/src/hooks/useSignupForm.ts @@ -5,11 +5,9 @@ import { useSignupMutation, } from '@/hooks/mutations/useUserMutation'; import { useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useForm } from 'react-hook-form'; -import useDebounce from './useDebounde'; - const useSignUpForm = () => { const { register, @@ -19,6 +17,7 @@ const useSignUpForm = () => { setError, setValue, formState: { errors, dirtyFields }, + control, } = useForm({ mode: 'onBlur', defaultValues: { @@ -26,34 +25,12 @@ const useSignUpForm = () => { }, }); - const [focusedField, setFocusedField] = useState< - 'name' | 'email' | 'position' | 'password' | 'passwordCheck' | null - >(null); - - useDebounce({ - value: watch(focusedField!), - callBack: () => { - if (focusedField) { - trigger(focusedField); - } - }, - }); - // 처음에는 중복확인 버튼 비활성화 const [isNameCheck, setIsNameCheck] = useState(false); const [isEmailCheck, setIsEmailCheck] = useState(false); const router = useRouter(); - // 입력이 있다면 중복확인 버튼 활성화 - useEffect(() => { - setIsNameCheck(false); - }, [watch('name')]); - - useEffect(() => { - setIsEmailCheck(false); - }, [watch('email')]); - // 중복확인 로직 수행 const { mutate: nameCheckMutate } = useNameCheckMutation({ onSuccessCallback: () => setIsNameCheck(true), @@ -73,16 +50,22 @@ const useSignUpForm = () => { }), }); - const handleNameCheck = () => { + const handleNameCheck = async () => { const name = watch('name'); - nameCheckMutate(name); - trigger('name'); + const isValid = await trigger('name'); + + if (isValid) { + nameCheckMutate(name); + } }; - const handleEmailCheck = () => { + const handleEmailCheck = async () => { const email = watch('email'); - emailCheckMutate(email); - trigger('email'); + const isValid = await trigger('email'); + + if (isValid) { + emailCheckMutate(email); + } }; // 회원가입 제출 @@ -125,10 +108,12 @@ const useSignUpForm = () => { handleNameCheck, isEmailCheck, handleEmailCheck, - watch, handleClickPosition, dirtyFields, - setFocusedField, + control, + setIsNameCheck, + setIsEmailCheck, + trigger, }; }; diff --git a/src/util/validation.ts b/src/util/validation.ts index 46824f8..82a16a2 100644 --- a/src/util/validation.ts +++ b/src/util/validation.ts @@ -27,18 +27,19 @@ export const passwordValidation = { required: '비밀번호를 입력해주세요.', minLength: { value: 6, - message: '비밀번호는 최소 6자 이상이어야 합니다.', + message: '비밀번호는 최소 8자 이상이어야 합니다.', }, pattern: { - value: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6,}$/, - message: '비밀번호는 영어와 숫자 포함 6자 이상이어야 합니다.', + value: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/, + message: '비밀번호는 영어와 숫자 포함 8자 이상이어야 합니다.', }, }; export const passwordCheckValidation = (password: string) => ({ - required: '비밀번호를 입력해주세요.', - validate: (value: string) => - value === password || '비밀번호가 일치하지 않습니다.', + validate: (value: string) => { + if (value === '') return true; + return value === password || '비밀번호가 일치하지 않습니다.'; + }, }); export const positionValidation = { From 0dcbd56e9bd3d6e3ed721596778ab4a33ab2f38b Mon Sep 17 00:00:00 2001 From: dbswl701 Date: Fri, 28 Feb 2025 22:16:13 +0900 Subject: [PATCH 13/15] =?UTF-8?q?refactor(DEVING-25):=20=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=EC=85=98=20=EC=84=A0=ED=83=9D=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/signup/components/PositionInput.tsx | 4 ++-- .../common/PositionSelect.tsx} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/{app/signup/components/ChipContainer.tsx => components/common/PositionSelect.tsx} (96%) diff --git a/src/app/signup/components/PositionInput.tsx b/src/app/signup/components/PositionInput.tsx index 472fa5c..af13add 100644 --- a/src/app/signup/components/PositionInput.tsx +++ b/src/app/signup/components/PositionInput.tsx @@ -1,7 +1,7 @@ +import { PositionSelect } from '@/components/common/PositionSelect'; import { positionValidation } from '@/util/validation'; import { useWatch } from 'react-hook-form'; -import { ChipContainer } from './ChipContainer'; import { ISignupInputProps } from './NameInput'; interface IPositionInputProps extends ISignupInputProps { @@ -20,7 +20,7 @@ const PositionInput = ({ - + {errors.position?.message && (

{errors.position?.message} diff --git a/src/app/signup/components/ChipContainer.tsx b/src/components/common/PositionSelect.tsx similarity index 96% rename from src/app/signup/components/ChipContainer.tsx rename to src/components/common/PositionSelect.tsx index 678526d..aabbe03 100644 --- a/src/app/signup/components/ChipContainer.tsx +++ b/src/components/common/PositionSelect.tsx @@ -1,6 +1,6 @@ import Chip from '@/components/ui/Chip'; -export const ChipContainer = ({ +export const PositionSelect = ({ position, setPosition, }: { From 303fb79e7aeb610ac9e5b1268324a22eff15c2fd Mon Sep 17 00:00:00 2001 From: dbswl701 Date: Fri, 28 Feb 2025 23:24:10 +0900 Subject: [PATCH 14/15] =?UTF-8?q?refactor(DEVING-25):=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EC=9E=AC=EC=82=AC=EC=9A=A9=20=EB=B0=8F=20=EB=A9=94=ED=83=80?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/login/components/EmailInput.tsx | 27 +++++---------- src/app/login/components/LoginForm.tsx | 5 --- src/app/login/components/PasswordInput.tsx | 13 +++++--- src/app/signup/components/EmailInput.tsx | 7 ++-- src/app/signup/components/NameInput.tsx | 33 ++++--------------- .../signup/components/PasswordCheckInput.tsx | 11 +++---- src/app/signup/components/PasswordInput.tsx | 12 +++---- src/app/signup/components/PositionInput.tsx | 5 ++- src/app/signup/components/SignupForm.tsx | 1 + src/app/signup/page.tsx | 21 ++++++++---- src/hooks/useLoginForm.ts | 6 +--- src/hooks/useSignupForm.ts | 2 +- src/types/auth.ts | 29 ++++++++++++++++ 13 files changed, 85 insertions(+), 87 deletions(-) create mode 100644 src/types/auth.ts diff --git a/src/app/login/components/EmailInput.tsx b/src/app/login/components/EmailInput.tsx index eb49a4e..244a78c 100644 --- a/src/app/login/components/EmailInput.tsx +++ b/src/app/login/components/EmailInput.tsx @@ -2,32 +2,23 @@ import { Input } from '@/components/ui/Input'; import useDebounce from '@/hooks/useDebounde'; -import { ILoginFormData } from '@/hooks/useLoginForm'; import { loginEmailValidation } from '@/util/validation'; import { useCallback } from 'react'; -import { - Control, - FieldErrors, - UseFormRegister, - UseFormTrigger, - useWatch, -} from 'react-hook-form'; +import { useWatch } from 'react-hook-form'; +import { IInputProps, ILoginFormData } from 'types/auth'; -export interface IInputProps { - control: Control; // ✅ control 타입 지정 - register: UseFormRegister; // ✅ register 타입 지정 - errors: FieldErrors; // ✅ errors 타입 지정 - trigger: UseFormTrigger; // ✅ trigger 타입 지정 -} - -const EmailInput = ({ control, register, errors, trigger }: IInputProps) => { - // `useWatch`를 개별 컴포넌트에서 호출하여 최적화 +const EmailInput = ({ + control, + register, + errors, + trigger, +}: IInputProps) => { const email = useWatch({ control, name: 'email' }); useDebounce({ value: email, callBack: useCallback(() => { - trigger('email'); + trigger?.('email'); }, [email]), }); diff --git a/src/app/login/components/LoginForm.tsx b/src/app/login/components/LoginForm.tsx index e2963ad..c8913aa 100644 --- a/src/app/login/components/LoginForm.tsx +++ b/src/app/login/components/LoginForm.tsx @@ -1,12 +1,7 @@ 'use client'; import { Button } from '@/components/ui/Button'; -import { Input } from '@/components/ui/Input'; import useLoginForm from '@/hooks/useLoginForm'; -import { - loginEmailValidation, - loginPasswordValidation, -} from '@/util/validation'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; diff --git a/src/app/login/components/PasswordInput.tsx b/src/app/login/components/PasswordInput.tsx index c2eb663..465085e 100644 --- a/src/app/login/components/PasswordInput.tsx +++ b/src/app/login/components/PasswordInput.tsx @@ -5,17 +5,20 @@ import useDebounce from '@/hooks/useDebounde'; import { loginPasswordValidation } from '@/util/validation'; import { useCallback } from 'react'; import { useWatch } from 'react-hook-form'; +import { IInputProps, ILoginFormData } from 'types/auth'; -import { IInputProps } from './EmailInput'; - -const PasswordInput = ({ control, register, errors, trigger }: IInputProps) => { - // `useWatch`를 개별 컴포넌트에서 호출하여 최적화 +const PasswordInput = ({ + control, + register, + errors, + trigger, +}: IInputProps) => { const password = useWatch({ control, name: 'password' }); useDebounce({ value: password, callBack: useCallback(() => { - trigger('password'); + trigger?.('password'); }, [password]), }); diff --git a/src/app/signup/components/EmailInput.tsx b/src/app/signup/components/EmailInput.tsx index 3f744e7..fa6d77a 100644 --- a/src/app/signup/components/EmailInput.tsx +++ b/src/app/signup/components/EmailInput.tsx @@ -1,13 +1,14 @@ +'use client'; + import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import useDebounce from '@/hooks/useDebounde'; import { emailValidation } from '@/util/validation'; import { Dispatch, SetStateAction, useCallback, useEffect } from 'react'; import { useWatch } from 'react-hook-form'; +import { IInputProps, ISignupFormData } from 'types/auth'; -import { ISignupInputProps } from './NameInput'; - -export interface IEmailInputProps extends ISignupInputProps { +export interface IEmailInputProps extends IInputProps { isEmailCheck: boolean; handleEmailCheck: () => void; setIsEmailCheck: Dispatch>; diff --git a/src/app/signup/components/NameInput.tsx b/src/app/signup/components/NameInput.tsx index 813fd24..692291a 100644 --- a/src/app/signup/components/NameInput.tsx +++ b/src/app/signup/components/NameInput.tsx @@ -1,32 +1,14 @@ +'use client'; + import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import useDebounce from '@/hooks/useDebounde'; import { nameValidation } from '@/util/validation'; -import { - Dispatch, - SetStateAction, - useCallback, - useEffect, - useState, -} from 'react'; -import { - Control, - FieldErrors, - UseFormRegister, - UseFormTrigger, - useWatch, -} from 'react-hook-form'; - -import { ISignupFormData } from '../page'; - -export interface ISignupInputProps { - control: Control; // ✅ control 타입 지정 - register: UseFormRegister; // ✅ register 타입 지정 - errors: FieldErrors; // ✅ errors 타입 지정 - trigger?: UseFormTrigger; // ✅ trigger 타입 지정 -} +import { Dispatch, SetStateAction, useCallback, useEffect } from 'react'; +import { useWatch } from 'react-hook-form'; +import { IInputProps, ISignupFormData } from 'types/auth'; -export interface INameInputProps extends ISignupInputProps { +export interface INameInputProps extends IInputProps { isNameCheck: boolean; handleNameCheck: () => void; setIsNameCheck: Dispatch>; @@ -43,10 +25,10 @@ const NameInput = ({ }: INameInputProps) => { const name = useWatch({ control, name: 'name' }); + // 중복확인 로직 수행 useEffect(() => { setIsNameCheck(false); }, [name]); - // 중복확인 로직 수행 useDebounce({ value: name, @@ -68,7 +50,6 @@ const NameInput = ({ {...register('name', nameValidation)} errorMessage={errors.name?.message} state={isNameCheck ? 'success' : 'default'} - // onFocus={() => setFocusedField('name')} />