Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/api/auth/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export interface SigninRequest {
email: string;
password: string;
}
export interface SigninFormData extends SigninRequest {
isRememberEmail: boolean;
}

export interface MyInfoResponse {
id: number;
name: string;
Expand Down
27 changes: 24 additions & 3 deletions src/app/auths/signin/_components/SignInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,24 @@ import { Eye, EyeOff } from 'lucide-react';
const SignInForm = () => {
const { value: isShowPassword, toggle: toggleIsShowPassword } = useBoolean();

const localEmail =
typeof window !== 'undefined'
? (localStorage.getItem('rememberEmail') ?? '')
: '';
const rememberEmail = !!localEmail;

const { onSubmit, register, handleSubmit, isSubmitting, errors } =
useSignInForm();
useSignInForm({
defaultValues: {
email: localEmail,
isRememberEmail: rememberEmail,
},
});

return (
<form className="flex flex-col gap-6" onSubmit={handleSubmit(onSubmit)}>
<InputForm
label="아이디"
label="이메일"
name="email"
placeholder="이메일을 입력해주세요."
register={register('email', {
Expand Down Expand Up @@ -52,7 +63,17 @@ const SignInForm = () => {
</button>
}
/>

<div className="flex items-center gap-2">
<input
type="checkbox"
aria-label="이메일 기억하기"
className="h-4 w-4"
{...register('isRememberEmail')}
/>
<label htmlFor="rememberEmail" className="text-sm">
이메일 기억하기
</label>
</div>
<Button
role="button"
type="submit"
Expand Down
113 changes: 110 additions & 3 deletions src/app/auths/signin/_components/SigninForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand All @@ -19,17 +20,78 @@ describe('SignInForm 렌더링 테스트', () => {
handleSubmit: (fn: FormEventHandler) => (e: FormEvent) => fn(e),
isSubmitting: false,
errors: {},
onSubmit: jest.fn(), // 여기서 직접 jest.fn() 사용
onSubmit: jest.fn(),
setValue: jest.fn(),
});
});

it('이메일과 비밀번호 입력 필드가 렌더링되어야 한다', () => {
render(<SignInForm />);
expect(screen.getByLabelText('아이디')).toBeInTheDocument();
expect(screen.getByLabelText('이메일')).toBeInTheDocument();
expect(screen.getByLabelText('비밀번호')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /로그인/i })).toBeInTheDocument();
});

it('이메일 기억하기 체크박스가 렌더링되어야 한다', () => {
render(<SignInForm />);
expect(screen.getByLabelText('이메일 기억하기')).toBeInTheDocument();
});

it('이메일 기억하기 체크박스가 클릭되면 체크된다', () => {
render(<SignInForm />);
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(<SignInForm />);

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(<SignInForm />);

await waitFor(() => {
expect(mockSetValue).toHaveBeenCalledWith('email', 'test@email.com');
expect(mockSetValue).toHaveBeenCalledWith('rememberEmail', true);
});

localStorage.clear();
});
it('비밀번호 토글 버튼을 클릭하면 입력 타입이 바뀐다', () => {
render(<SignInForm />);
const toggleButton = screen.getByRole('button', {
Expand All @@ -50,6 +112,7 @@ describe('SignInForm 렌더링 테스트', () => {
isSubmitting: false,
errors: {},
onSubmit: mockSubmit,
setValue: jest.fn(),
});

render(<SignInForm />);
Expand All @@ -71,6 +134,7 @@ describe('SignInForm 에러 메시지 테스트', () => {
isSubmitting: false,
errors: { email: { message: '이메일을 입력해주세요' } },
onSubmit: jest.fn(),
setValue: jest.fn(),
});
});

Expand All @@ -86,6 +150,7 @@ describe('SignInForm 에러 메시지 테스트', () => {
isSubmitting: false,
errors: { password: { message: '비밀번호를 입력해주세요' } },
onSubmit: jest.fn(),
setValue: jest.fn(),
});

render(<SignInForm />);
Expand All @@ -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(),
});
});

Expand All @@ -110,13 +177,53 @@ describe('SignInForm 제출 상태 테스트', () => {
expect(submitButton).toBeDisabled();
});

it('이메일 기억하기 체크박스가 클릭된 상태에서 제출되면 이메일이 로컬스토리지에 저장된다', async () => {
localStorage.clear();

mockUseSignInForm.mockReturnValue({
register: jest.fn(() => ({ name: '', onChange: jest.fn() })),
handleSubmit: (fn: SubmitHandler<SigninFormData>) => () =>
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(<SignInForm />);

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() })),
handleSubmit: (fn: FormEventHandler) => (e: FormEvent) => fn(e),
isSubmitting: false,
errors: { email: { message: '이메일을 입력해주세요' } },
onSubmit: jest.fn(),
setValue: jest.fn(),
});

render(<SignInForm />);
Expand Down
65 changes: 44 additions & 21 deletions src/hooks/api/auth/useSignInForm.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,58 @@
import { SigninRequest } from '@/api/auth/type';
import { SubmitHandler, useForm } from 'react-hook-form';
import { SigninFormData } from '@/api/auth/type';
import { SubmitHandler, useForm, UseFormProps } from 'react-hook-form';
import { usePostSignin } from './usePostSignin';

export const useSignInForm = () => {
export function useSignInForm(options: UseFormProps<SigninFormData>) {
const { mutate: signIn } = usePostSignin();

const {
register,
handleSubmit,
setError,
setValue,
formState: { isSubmitting, errors },
} = useForm<SigninRequest>();
} = useForm<SigninFormData>({
mode: 'onChange',
...options,
});

const onSubmit: SubmitHandler<SigninRequest> = (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<SigninFormData> = (data) => {
if (data.isRememberEmail) {
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,
});
Comment on lines +35 to +41
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

react hook form의 setError은 사용해본적이 없는데 setError하면 register내부에 유효성 검사에 걸린 것과 마찬가지로
폼 에러 메시지가 출력되나요? (errorData.message가 어떤식으로 출력되는지 텍스트나 스크린샷 첨부해주시면 감사할 것 같습니다!)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

  • 단순 입력 규격에 관한 유효성 검사이외에 서버 요청에 반환되는 에러를 필드에 보여주려고 setError 썼습니다!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 그렇군요 스크린샷 감사합니다! 지금은 서버에서 반환되는 메시지가 한글인데 나중에 Supabase 인증으로 변경되면
영어로 된 메시지가 출력될까요? 서버에서 한글 메시지를 반환하도록 변경하거나, 반환하는 메시지를 우리가 커스텀할 수 있는 지 확인해보면 좋을 것 같네요!

}
},
}
);
};

return { onSubmit, register, handleSubmit, isSubmitting, errors };
};
return {
onSubmit,
register,
handleSubmit,
isSubmitting,
errors,
setValue,
};
}

export default useSignInForm;