From b0f6303aed58d3c19b3e8278918700855ca6fdbb Mon Sep 17 00:00:00 2001 From: swimmingRiver Date: Sun, 29 Jun 2025 23:32:56 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EB=A1=9C=EC=BB=AC=20=EC=A0=80=EC=9E=A5=EC=86=8C=EC=97=90=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth/type.ts | 4 ++ .../auths/signin/_components/SignInForm.tsx | 16 ++++++- .../signin/_components/SigninForm.test.tsx | 2 +- src/hooks/api/auth/useSignInForm.ts | 46 ++++++++++++------- 4 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/api/auth/type.ts b/src/api/auth/type.ts index 87803998..2202f03c 100644 --- a/src/api/auth/type.ts +++ b/src/api/auth/type.ts @@ -13,6 +13,10 @@ export interface SigninRequest { email: string; password: string; } +export interface SigninFormData extends SigninRequest { + rememberEmail: boolean; +} + export interface MyInfoResponse { id: number; name: string; diff --git a/src/app/auths/signin/_components/SignInForm.tsx b/src/app/auths/signin/_components/SignInForm.tsx index 0d6b13b8..aa239cf5 100644 --- a/src/app/auths/signin/_components/SignInForm.tsx +++ b/src/app/auths/signin/_components/SignInForm.tsx @@ -9,14 +9,15 @@ import { Eye, EyeOff } from 'lucide-react'; const SignInForm = () => { const { value: isShowPassword, toggle: toggleIsShowPassword } = useBoolean(); - + const { value: isRememberEmail, toggle: toggleIsRememberEmail } = + useBoolean(); const { onSubmit, register, handleSubmit, isSubmitting, errors } = useSignInForm(); return (
{ hasError={!!errors.email} helperText={errors.email?.message} /> +
+ + 이메일 기억하기 +
{ it('이메일과 비밀번호 입력 필드가 렌더링되어야 한다', () => { render(); - expect(screen.getByLabelText('아이디')).toBeInTheDocument(); + expect(screen.getByLabelText('이메일')).toBeInTheDocument(); expect(screen.getByLabelText('비밀번호')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /로그인/i })).toBeInTheDocument(); }); diff --git a/src/hooks/api/auth/useSignInForm.ts b/src/hooks/api/auth/useSignInForm.ts index 5a0b1655..24848255 100644 --- a/src/hooks/api/auth/useSignInForm.ts +++ b/src/hooks/api/auth/useSignInForm.ts @@ -1,4 +1,4 @@ -import { SigninRequest } from '@/api/auth/type'; +import { SigninFormData } from '@/api/auth/type'; import { SubmitHandler, useForm } from 'react-hook-form'; import { usePostSignin } from './usePostSignin'; @@ -10,23 +10,35 @@ export const useSignInForm = () => { handleSubmit, setError, formState: { isSubmitting, errors }, - } = useForm(); + } = useForm(); - const onSubmit: SubmitHandler = (data) => { - signIn(data, { - onError: (error: Error) => { - const errorData = JSON.parse(error.message); - if ( - errorData.code === 'VALIDATION_ERROR' || - errorData.code === 'USER_NOT_FOUND' - ) { - setError('email', { type: 'manual', message: errorData.message }); - } - if (errorData.code === 'INVALID_CREDENTIALS') { - setError('password', { type: 'manual', message: errorData.message }); - } - }, - }); + const onSubmit: SubmitHandler = (data) => { + if (data.rememberEmail) { + localStorage.setItem('rememberEmail', data.email); + } else { + localStorage.removeItem('rememberEmail'); + } + + signIn( + { email: data.email, password: data.password }, + { + onError: (error: Error) => { + const errorData = JSON.parse(error.message); + if ( + errorData.code === 'VALIDATION_ERROR' || + errorData.code === 'USER_NOT_FOUND' + ) { + setError('email', { type: 'manual', message: errorData.message }); + } + if (errorData.code === 'INVALID_CREDENTIALS') { + setError('password', { + type: 'manual', + message: errorData.message, + }); + } + }, + } + ); }; return { onSubmit, register, handleSubmit, isSubmitting, errors }; From 6182ea2fa1e4524e1c57c4e4deb36ff6c859ed35 Mon Sep 17 00:00:00 2001 From: swimmingRiver Date: Sun, 29 Jun 2025 23:42:01 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EC=A0=80=EC=9E=A5=EB=90=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/auths/signin/_components/SignInForm.tsx | 16 +++++++++++----- src/hooks/api/auth/useSignInForm.ts | 3 ++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/app/auths/signin/_components/SignInForm.tsx b/src/app/auths/signin/_components/SignInForm.tsx index aa239cf5..08c8fe58 100644 --- a/src/app/auths/signin/_components/SignInForm.tsx +++ b/src/app/auths/signin/_components/SignInForm.tsx @@ -6,14 +6,22 @@ import { useSignInForm } from '@/hooks/api/auth/useSignInForm'; import { signInValidate } from '@/utils/validators/auth'; import { Eye, EyeOff } from 'lucide-react'; +import { useEffect } from 'react'; const SignInForm = () => { const { value: isShowPassword, toggle: toggleIsShowPassword } = useBoolean(); - const { value: isRememberEmail, toggle: toggleIsRememberEmail } = - useBoolean(); - const { onSubmit, register, handleSubmit, isSubmitting, errors } = + + const { onSubmit, register, handleSubmit, isSubmitting, errors, setValue } = useSignInForm(); + useEffect(() => { + const email = localStorage.getItem('rememberEmail'); + if (email) { + setValue('email', email); + setValue('rememberEmail', true); + } + }, [setValue]); + return ( { 이메일 기억하기 diff --git a/src/hooks/api/auth/useSignInForm.ts b/src/hooks/api/auth/useSignInForm.ts index 24848255..3ea3564a 100644 --- a/src/hooks/api/auth/useSignInForm.ts +++ b/src/hooks/api/auth/useSignInForm.ts @@ -9,6 +9,7 @@ export const useSignInForm = () => { register, handleSubmit, setError, + setValue, formState: { isSubmitting, errors }, } = useForm(); @@ -41,7 +42,7 @@ export const useSignInForm = () => { ); }; - return { onSubmit, register, handleSubmit, isSubmitting, errors }; + return { onSubmit, register, handleSubmit, isSubmitting, errors, setValue }; }; export default useSignInForm; From e9301c37d037e2e113bab15343590be96e94e80d Mon Sep 17 00:00:00 2001 From: swimmingRiver Date: Mon, 30 Jun 2025 17:34:28 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../signin/_components/SigninForm.test.tsx | 111 +++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/src/app/auths/signin/_components/SigninForm.test.tsx b/src/app/auths/signin/_components/SigninForm.test.tsx index 156c057d..26c375a3 100644 --- a/src/app/auths/signin/_components/SigninForm.test.tsx +++ b/src/app/auths/signin/_components/SigninForm.test.tsx @@ -3,8 +3,9 @@ import SignInForm from './SignInForm'; import '@testing-library/jest-dom'; import { FormEvent, FormEventHandler } from 'react'; import { signInValidate } from '@/utils/validators/auth'; +import { SigninFormData } from '@/api/auth/type'; +import { SubmitHandler } from 'react-hook-form'; -// 하나의 모킹 객체로 통합 const mockUseSignInForm = jest.fn(); jest.mock('@/hooks/api/auth/useSignInForm', () => ({ @@ -19,7 +20,8 @@ describe('SignInForm 렌더링 테스트', () => { handleSubmit: (fn: FormEventHandler) => (e: FormEvent) => fn(e), isSubmitting: false, errors: {}, - onSubmit: jest.fn(), // 여기서 직접 jest.fn() 사용 + onSubmit: jest.fn(), + setValue: jest.fn(), }); }); @@ -30,6 +32,66 @@ describe('SignInForm 렌더링 테스트', () => { expect(screen.getByRole('button', { name: /로그인/i })).toBeInTheDocument(); }); + it('이메일 기억하기 체크박스가 렌더링되어야 한다', () => { + render(); + expect(screen.getByLabelText('이메일 기억하기')).toBeInTheDocument(); + }); + + it('이메일 기억하기 체크박스가 클릭되면 체크된다', () => { + render(); + const rememberEmailCheckbox = screen.getByLabelText('이메일 기억하기'); + fireEvent.click(rememberEmailCheckbox); + expect(rememberEmailCheckbox).toBeChecked(); + }); + + it('저장된 이메일이 있으면 이메일 기억하기 체크박스가 체크된다', () => { + localStorage.setItem('rememberEmail', 'test@email.com'); + const mockSetValue = jest.fn(); + mockUseSignInForm.mockReturnValue({ + register: jest.fn((name) => ({ + name, + onChange: jest.fn(), + checked: name === 'rememberEmail' ? true : undefined, + })), + handleSubmit: (fn: FormEventHandler) => (e: FormEvent) => fn(e), + isSubmitting: false, + errors: {}, + onSubmit: jest.fn(), + setValue: mockSetValue, + }); + render(); + + expect(mockSetValue).toHaveBeenCalledWith('email', 'test@email.com'); + expect(mockSetValue).toHaveBeenCalledWith('rememberEmail', true); + localStorage.clear(); + }); + it('저장된 이메일이 있으면 이메일 필드에 저장된 이메일이 입력된다', async () => { + localStorage.setItem('rememberEmail', 'test@email.com'); + + const mockSetValue = jest.fn(); + + mockUseSignInForm.mockReturnValue({ + register: jest.fn((name) => ({ + name, + onChange: jest.fn(), + checked: name === 'rememberEmail' ? true : undefined, + })), + handleSubmit: (fn: FormEventHandler) => (e: FormEvent) => fn(e), + isSubmitting: false, + errors: {}, + onSubmit: jest.fn(), + setValue: mockSetValue, + }); + + render(); + + await waitFor(() => { + expect(mockSetValue).toHaveBeenCalledWith('email', 'test@email.com'); + expect(mockSetValue).toHaveBeenCalledWith('rememberEmail', true); + }); + + localStorage.clear(); + }); it('비밀번호 토글 버튼을 클릭하면 입력 타입이 바뀐다', () => { render(); const toggleButton = screen.getByRole('button', { @@ -50,6 +112,7 @@ describe('SignInForm 렌더링 테스트', () => { isSubmitting: false, errors: {}, onSubmit: mockSubmit, + setValue: jest.fn(), }); render(); @@ -71,6 +134,7 @@ describe('SignInForm 에러 메시지 테스트', () => { isSubmitting: false, errors: { email: { message: '이메일을 입력해주세요' } }, onSubmit: jest.fn(), + setValue: jest.fn(), }); }); @@ -86,6 +150,7 @@ describe('SignInForm 에러 메시지 테스트', () => { isSubmitting: false, errors: { password: { message: '비밀번호를 입력해주세요' } }, onSubmit: jest.fn(), + setValue: jest.fn(), }); render(); @@ -95,12 +160,14 @@ describe('SignInForm 에러 메시지 테스트', () => { describe('SignInForm 제출 상태 테스트', () => { beforeEach(() => { + localStorage.clear(); mockUseSignInForm.mockReturnValue({ register: jest.fn(() => ({ name: '', onChange: jest.fn() })), handleSubmit: (fn: FormEventHandler) => (e: FormEvent) => fn(e), isSubmitting: true, errors: {}, onSubmit: jest.fn(), + setValue: jest.fn(), }); }); @@ -110,6 +177,45 @@ describe('SignInForm 제출 상태 테스트', () => { expect(submitButton).toBeDisabled(); }); + it('이메일 기억하기 체크박스가 클릭된 상태에서 제출되면 이메일이 로컬스토리지에 저장된다', async () => { + localStorage.clear(); + + mockUseSignInForm.mockReturnValue({ + register: jest.fn(() => ({ name: '', onChange: jest.fn() })), + handleSubmit: (fn: SubmitHandler) => () => + fn({ + email: 'test@email.com', + password: 'password123', + rememberEmail: true, + } as SigninFormData), + isSubmitting: false, + errors: {}, + onSubmit: ({ email, rememberEmail }: SigninFormData) => { + if (rememberEmail) { + localStorage.setItem('rememberEmail', email); + } + }, + }); + + render(); + + fireEvent.change(screen.getByLabelText('이메일'), { + target: { value: 'test@email.com' }, + }); + + fireEvent.change(screen.getByLabelText('비밀번호'), { + target: { value: 'password123' }, + }); + + fireEvent.click(screen.getByLabelText('이메일 기억하기')); + + fireEvent.click(screen.getByRole('button', { name: /로그인/i })); + + await waitFor(() => { + expect(localStorage.getItem('rememberEmail')).toBe('test@email.com'); + }); + }); + it('에러가 있을 때 버튼이 비활성화된다', () => { mockUseSignInForm.mockReturnValue({ register: jest.fn(() => ({ name: '', onChange: jest.fn() })), @@ -117,6 +223,7 @@ describe('SignInForm 제출 상태 테스트', () => { isSubmitting: false, errors: { email: { message: '이메일을 입력해주세요' } }, onSubmit: jest.fn(), + setValue: jest.fn(), }); render(); From 81fbf0c9742214dfce472a9af5dbf0f98f5ea6ec Mon Sep 17 00:00:00 2001 From: swimmingRiver Date: Wed, 2 Jul 2025 23:58:41 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20useEffect=20=EB=8C=80=EC=B2=B4=20de?= =?UTF-8?q?faultValues=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auths/signin/_components/SignInForm.tsx | 43 ++++++++++--------- src/hooks/api/auth/useSignInForm.ts | 20 ++++++--- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/app/auths/signin/_components/SignInForm.tsx b/src/app/auths/signin/_components/SignInForm.tsx index 08c8fe58..5d40d216 100644 --- a/src/app/auths/signin/_components/SignInForm.tsx +++ b/src/app/auths/signin/_components/SignInForm.tsx @@ -6,21 +6,23 @@ import { useSignInForm } from '@/hooks/api/auth/useSignInForm'; import { signInValidate } from '@/utils/validators/auth'; import { Eye, EyeOff } from 'lucide-react'; -import { useEffect } from 'react'; const SignInForm = () => { const { value: isShowPassword, toggle: toggleIsShowPassword } = useBoolean(); - const { onSubmit, register, handleSubmit, isSubmitting, errors, setValue } = - useSignInForm(); + const email = + typeof window !== 'undefined' + ? (localStorage.getItem('rememberEmail') ?? '') + : ''; + const rememberEmail = !!email; - useEffect(() => { - const email = localStorage.getItem('rememberEmail'); - if (email) { - setValue('email', email); - setValue('rememberEmail', true); - } - }, [setValue]); + const { onSubmit, register, handleSubmit, isSubmitting, errors } = + useSignInForm({ + defaultValues: { + email, + rememberEmail, + }, + }); return ( @@ -35,15 +37,6 @@ const SignInForm = () => { hasError={!!errors.email} helperText={errors.email?.message} /> -
- - 이메일 기억하기 -
{ } /> - +
+ + +