diff --git a/src/app/mocks/handlers.ts b/src/app/mocks/handlers.ts index 0f3710c..7016a01 100644 --- a/src/app/mocks/handlers.ts +++ b/src/app/mocks/handlers.ts @@ -1 +1,3 @@ -export const handlers = []; +import { signupHandlers } from "./signup-handlers"; + +export const handlers = [...signupHandlers]; diff --git a/src/app/mocks/signup-handlers.ts b/src/app/mocks/signup-handlers.ts new file mode 100644 index 0000000..bf7c22d --- /dev/null +++ b/src/app/mocks/signup-handlers.ts @@ -0,0 +1,9 @@ +import { checkEmailHandler } from "@pages/auth/signup/api/check-email/index.mock"; +import { checkNicknameHandler } from "@pages/auth/signup/api/check-nickname/index.mock"; +import { signupHandler } from "@pages/auth/signup/api/signup/index.mock"; + +export const signupHandlers = [ + checkEmailHandler, + checkNicknameHandler, + signupHandler, +]; diff --git a/src/pages/auth/signup/api/check-email/index.mock.ts b/src/pages/auth/signup/api/check-email/index.mock.ts new file mode 100644 index 0000000..719ba33 --- /dev/null +++ b/src/pages/auth/signup/api/check-email/index.mock.ts @@ -0,0 +1,16 @@ +import { CHECK_EMAIL_ENDPOINT } from "@pages/auth/signup/api/check-email"; +import { http, HttpResponse } from "msw"; + +export const checkEmailHandler = http.get( + CHECK_EMAIL_ENDPOINT, + ({ request }) => { + const url = new URL(request.url); + const email = url.searchParams.get("email"); + + return HttpResponse.json({ + success: true, + available: true, + message: "사용 가능한 이메일입니다.", + }); + }, +); diff --git a/src/pages/auth/signup/api/check-email/index.ts b/src/pages/auth/signup/api/check-email/index.ts index d39d247..46a3589 100644 --- a/src/pages/auth/signup/api/check-email/index.ts +++ b/src/pages/auth/signup/api/check-email/index.ts @@ -1,11 +1,13 @@ import { apiRequester } from "@shared/api/api-requester"; +export const CHECK_EMAIL_ENDPOINT = `/api/signup/check-email`; + export const checkEmail = (email: string) => { return apiRequester<{ success: boolean; available: boolean; message: string; - }>(`/api/signup/check-email?email=${email}`, { + }>(`${CHECK_EMAIL_ENDPOINT}?email=${email}`, { method: "GET", }); }; diff --git a/src/pages/auth/signup/api/check-nickname/index.mock.ts b/src/pages/auth/signup/api/check-nickname/index.mock.ts new file mode 100644 index 0000000..471d288 --- /dev/null +++ b/src/pages/auth/signup/api/check-nickname/index.mock.ts @@ -0,0 +1,16 @@ +import { CHECK_NICKNAME_ENDPOINT } from "@pages/auth/signup/api/check-nickname"; +import { http, HttpResponse } from "msw"; + +export const checkNicknameHandler = http.get( + CHECK_NICKNAME_ENDPOINT, + ({ request }) => { + const url = new URL(request.url); + const nickname = url.searchParams.get("nickname"); + + return HttpResponse.json({ + success: true, + available: true, + message: "사용 가능한 닉네임입니다.", + }); + }, +); diff --git a/src/pages/auth/signup/api/check-nickname/index.ts b/src/pages/auth/signup/api/check-nickname/index.ts index b4586cd..3be4dda 100644 --- a/src/pages/auth/signup/api/check-nickname/index.ts +++ b/src/pages/auth/signup/api/check-nickname/index.ts @@ -1,11 +1,13 @@ import { apiRequester } from "@shared/api/api-requester"; +export const CHECK_NICKNAME_ENDPOINT = `/api/signup/check-nickname`; + export const checkNickname = (nickname: string) => { return apiRequester<{ success: boolean; available: boolean; message: string; - }>(`/api/signup/check-nickname?nickname=${nickname}`, { + }>(`${CHECK_NICKNAME_ENDPOINT}?nickname=${nickname}`, { method: "GET", }); }; diff --git a/src/pages/auth/signup/api/signup/index.mock.ts b/src/pages/auth/signup/api/signup/index.mock.ts new file mode 100644 index 0000000..5a4a3ac --- /dev/null +++ b/src/pages/auth/signup/api/signup/index.mock.ts @@ -0,0 +1,8 @@ +import { SIGNUP_ENDPOINT } from "@pages/auth/signup/api/signup"; +import { http, HttpResponse } from "msw"; + +export const signupHandler = http.post(SIGNUP_ENDPOINT, ({}) => { + return HttpResponse.json({ + success: true, + }); +}); diff --git a/src/pages/auth/signup/api/signup/index.ts b/src/pages/auth/signup/api/signup/index.ts index 12ba618..76467f0 100644 --- a/src/pages/auth/signup/api/signup/index.ts +++ b/src/pages/auth/signup/api/signup/index.ts @@ -1,12 +1,14 @@ import { SignUpFormValues } from "../../model/signup-form-values"; import { apiRequester } from "@shared/api/api-requester"; +export const SIGNUP_ENDPOINT = "/api/signup"; + export const signup = (payload: SignUpFormValues) => { return apiRequester<{ success: boolean; available: boolean; message: string; - }>(`/api/signup`, { + }>(SIGNUP_ENDPOINT, { method: "POST", body: JSON.stringify(payload), }); diff --git a/src/pages/auth/signup/ui/email-field/index.tsx b/src/pages/auth/signup/ui/email-field/index.tsx index 871d746..0061ef9 100644 --- a/src/pages/auth/signup/ui/email-field/index.tsx +++ b/src/pages/auth/signup/ui/email-field/index.tsx @@ -1,65 +1,44 @@ -import { checkEmail } from "@pages/auth/signup/api/check-email"; -import { useFormContext, useFormState } from "react-hook-form"; import { TextField } from "@shared/ui/text-field"; -import { ApiError, HttpStatus } from "@shared/api"; +import { ChangeEvent } from "react"; type EmailFieldProps = { isValidDuplicateEmail: boolean; - setIsValidDuplicateEmail: (v: boolean) => void; + value: string; + onChange: (e: ChangeEvent) => void; + errorMessage?: string; + successMessage?: string; + isCheckDisabled: boolean; + checkDuplicateEmail: () => void; }; export const EmailField = ({ + value, + onChange, + errorMessage, isValidDuplicateEmail, - setIsValidDuplicateEmail, + successMessage, + isCheckDisabled, + checkDuplicateEmail, }: EmailFieldProps) => { - const { control, register, getValues, setError } = useFormContext<{ - email: string; - }>(); - const { touchedFields, errors } = useFormState<{ email: string }>({ - control, - }); return (
아이디
setIsValidDuplicateEmail(false), - })} + value={value} + onChange={onChange} /> { - const email = getValues("email"); - checkEmail(email) - .then((response) => { - if (response.available) { - setIsValidDuplicateEmail(true); - } else { - setError("email", { - type: "duplicate", - message: response.message, - }); - } - }) - .catch((error) => { - if (error instanceof ApiError) { - if (error.status === HttpStatus.BAD_REQUEST) { - setError("email", { - type: "invalid_format", - message: error.message, - }); - } - } - }); + checkDuplicateEmail(); }} > 중복 확인 diff --git a/src/pages/auth/signup/ui/nickname-field/nickname-field.tsx b/src/pages/auth/signup/ui/nickname-field/nickname-field.tsx index e847e18..7962cc6 100644 --- a/src/pages/auth/signup/ui/nickname-field/nickname-field.tsx +++ b/src/pages/auth/signup/ui/nickname-field/nickname-field.tsx @@ -37,6 +37,7 @@ export const NicknameField = ({ })} /> { + beforeEach(() => { + render( + + + , + ); + }); + + it("모든 필수 필드를 렌더링한다", () => { + expect(screen.getByLabelText("아이디")).toBeInTheDocument(); + expect(screen.getByLabelText("닉네임")).toBeInTheDocument(); + expect(screen.getByLabelText("비밀번호")).toBeInTheDocument(); + expect(screen.getByLabelText("비밀번호 확인")).toBeInTheDocument(); + expect(screen.getByLabelText("동의함")).toBeInTheDocument(); //이용약관 + }); + + //복잡한 제출 로직 테스트 + it("모든 필수 필드 입력 시 제출을 허용한다", async () => { + const signupSpy = vi.fn(); + server.use( + http.post("/api/signup", async ({ request }) => { + signupSpy(await request.json()); + return HttpResponse.json({ + success: true, + message: "회원가입이 완료되었습니다.", + }); + }), + ); + const user = userEvent.setup(); + expect(screen.getByRole("button", { name: "회원가입" })).toBeDisabled(); + + // 이메일 검증 + await user.type(screen.getByLabelText("아이디"), TEST_EMAIL); + await user.tab(); + await user.click(screen.getByRole("button", { name: "이메일 중복 확인" })); + expect( + await screen.findByText("사용 가능한 이메일입니다."), + ).toBeInTheDocument(); + + // 닉네임 검증 + await user.type(screen.getByLabelText("닉네임"), TEST_NICKNAME); + await user.tab(); + await user.click(screen.getByRole("button", { name: "닉네임 중복 확인" })); + expect( + await screen.findByText("사용 가능한 닉네임입니다."), + ).toBeInTheDocument(); + + // 비밀번호, 비밀번호 확인 + await user.type(screen.getByLabelText("비밀번호"), TEST_PASSWORD); + await user.type( + screen.getByLabelText("비밀번호 확인"), + TEST_CONFIRM_PASSWORD, + ); + + // 약관 동의 체크 + await user.click(screen.getByLabelText("동의함")); + + // 버튼 활성화 테스트 + expect(screen.getByRole("button", { name: "회원가입" })).toBeEnabled(); + + // 제출 + await user.click(screen.getByRole("button", { name: "회원가입" })); + await waitFor(() => { + expect(signupSpy).toHaveBeenCalledWith( + expect.objectContaining({ + email: TEST_EMAIL, + nickname: TEST_NICKNAME, + password: TEST_PASSWORD, + confirmPassword: TEST_CONFIRM_PASSWORD, + }), + ); + }); + }); + // 유효성검사 테스트 + + // 조건부렌더링 테스트 +}); diff --git a/src/pages/auth/signup/ui/signup-page/index.tsx b/src/pages/auth/signup/ui/signup-page/index.tsx index a832134..e859c8f 100644 --- a/src/pages/auth/signup/ui/signup-page/index.tsx +++ b/src/pages/auth/signup/ui/signup-page/index.tsx @@ -9,9 +9,9 @@ import { PATH } from "@shared/routes"; import { Button } from "@shared/ui"; import Link from "next/link"; -import { FormProvider, useForm } from "react-hook-form"; +import { FormProvider, useController, useForm } from "react-hook-form"; -import { useState } from "react"; +import { ChangeEvent, useState } from "react"; import { useRouter } from "next/navigation"; import { EmailField } from "@pages/auth/signup/ui/email-field"; @@ -22,6 +22,8 @@ import { TermsAgreementField } from "@pages/auth/signup/ui/term-agreement-field" import { useSignup } from "@pages/auth/signup/model/use-signup"; import { SideBanner } from "@widgets/side-banner"; +import { checkEmail } from "@pages/auth/signup/api/check-email"; +import { ApiError, HttpStatus } from "@shared/api"; export const SignupPage = () => { const methods = useForm({ @@ -38,7 +40,9 @@ export const SignupPage = () => { const router = useRouter(); const { handleSubmit, - formState: { isValid }, + control, + setError, + formState: { isValid, errors }, } = methods; const [isValidDuplicateEmail, setIsValidDuplicateEmail] = useState(false); const [isValidDuplicateNickname, setIsValidDuplicateNickname] = @@ -49,6 +53,35 @@ export const SignupPage = () => { const isFormValid = isValidDuplicateEmail && isValidDuplicateNickname && isValid; + const { field: emailField } = useController({ + control, + name: "email", + }); + + const checkDuplicateEmail = () => { + const email = emailField.value; + checkEmail(email) + .then((response) => { + if (response.available) { + setIsValidDuplicateEmail(true); + } else { + setError("email", { + type: "duplicate", + message: response.message, + }); + } + }) + .catch((error) => { + if (error instanceof ApiError) { + if (error.status === HttpStatus.BAD_REQUEST) { + setError("email", { + type: "invalid_format", + message: error.message, + }); + } + } + }); + }; return (
@@ -72,7 +105,14 @@ export const SignupPage = () => { {/* 이메일 */} emailField.onChange(e.target.value)} + isCheckDisabled={ + !emailField.value || errors.email?.type === "invalid_format" + } + checkDuplicateEmail={checkDuplicateEmail} /> {/* 닉네임 */} { return (
- 이용약관 +

이용약관