diff --git a/apps/web/app/(auth)/_api/index.ts b/apps/web/app/(auth)/_api/index.ts new file mode 100644 index 00000000..ade54a1c --- /dev/null +++ b/apps/web/app/(auth)/_api/index.ts @@ -0,0 +1,13 @@ +import { http } from "../../../lib/http"; +import { UserSchema } from "../_types"; +import type { UserType, RegisterReq, UserErrorType } from "../_types"; + +export function register(req: RegisterReq): Promise { + return http.post({ + url: "/users", + body: { + user: req, + }, + schema: UserSchema, + }); +} diff --git a/apps/web/app/(auth)/_component/error-message.tsx b/apps/web/app/(auth)/_component/error-message.tsx new file mode 100644 index 00000000..087ea67d --- /dev/null +++ b/apps/web/app/(auth)/_component/error-message.tsx @@ -0,0 +1,11 @@ +interface ErrorMessageProps { + message?: string; +} + +export function ErrorMessage({ message }: ErrorMessageProps): JSX.Element { + return ( + + ); +} diff --git a/apps/web/app/(auth)/_component/register-form.tsx b/apps/web/app/(auth)/_component/register-form.tsx new file mode 100644 index 00000000..d39ce54c --- /dev/null +++ b/apps/web/app/(auth)/_component/register-form.tsx @@ -0,0 +1,88 @@ +"use client"; + +import type { SubmitHandler } from "react-hook-form"; +import { useForm } from "react-hook-form"; +import { useRouter } from "next/navigation"; +import type { RegisterReq } from "../_types"; +import { useRegister } from "../_hooks/use-register"; +import { UserSchema, UserType } from "../_types"; +import { ErrorMessage } from "./error-message"; + +export function RegisterForm(): JSX.Element { + const { + register, + handleSubmit, + formState: { errors }, + setError, + resetField, + } = useForm(); + + const { mutate } = useRegister(); + + const router = useRouter(); + + const onSubmit: SubmitHandler = data => { + mutate(data, { + onSuccess: () => { + router.push("/login"); + }, + onError: () => { + setError("email", { + message: "email has already been taken", + }); + setError("username", { + message: "username has already been taken", + }); + + resetField("password"); + }, + }); + }; + + const isEmailValid = (email: string) => { + const emailPattern = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i; + return emailPattern.test(email); + }; + + return ( + // eslint-disable-next-line @typescript-eslint/no-misused-promises +
+
+ +
+ {errors.username ? : null} + +
+ isEmailValid(email) || "Invalid email format", + })} + className="form-control form-control-lg" + placeholder="Email" + type="text" + /> +
+ {errors.email ? : null} + +
+ +
+ {errors.password ? : null} + + + + ); +} diff --git a/apps/web/app/(auth)/_hooks/use-register.tsx b/apps/web/app/(auth)/_hooks/use-register.tsx new file mode 100644 index 00000000..76a90fcc --- /dev/null +++ b/apps/web/app/(auth)/_hooks/use-register.tsx @@ -0,0 +1,9 @@ +import { useMutation } from "@tanstack/react-query"; +import type { RegisterReq } from "../_types"; +import { register } from "../_api"; + +export function useRegister() { + return useMutation({ + mutationFn: (req: RegisterReq) => register(req), + }); +} diff --git a/apps/web/app/(auth)/_types/index.ts b/apps/web/app/(auth)/_types/index.ts new file mode 100644 index 00000000..c5c611af --- /dev/null +++ b/apps/web/app/(auth)/_types/index.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; + +export interface RegisterReq { + username: string; + email: string; + password: string; +} + +export const UserErrorsSchema = z.object({ + errors: z.object({ + email: z.array(z.string()), + username: z.array(z.string()), + }), +}); + +export const UserSchema = z.object({ + user: z.object({ + email: z.string(), + token: z.string(), + username: z.string(), + bio: z.string(), + image: z.string(), + }), +}); + +export type UserErrorType = z.infer; + +export type UserType = z.infer; diff --git a/apps/web/app/(auth)/register/page.tsx b/apps/web/app/(auth)/register/page.tsx index 03737e48..bce9c155 100644 --- a/apps/web/app/(auth)/register/page.tsx +++ b/apps/web/app/(auth)/register/page.tsx @@ -1,3 +1,5 @@ +import { RegisterForm } from "../_component/register-form"; + export default function RegisterPage(): JSX.Element { return (
@@ -9,24 +11,7 @@ export default function RegisterPage(): JSX.Element { Have an account?

-
    -
  • That email is already taken
  • -
- -
-
- -
-
- -
-
- -
- -
+
diff --git a/apps/web/lib/http.ts b/apps/web/lib/http.ts index 884d8c73..a5216a03 100644 --- a/apps/web/lib/http.ts +++ b/apps/web/lib/http.ts @@ -1,5 +1,6 @@ import type { z, ZodType } from "zod"; import { ZodError } from "zod"; +import { UserErrorsSchema } from "../app/(auth)/_types"; const HTTP_ERRORS = { BAD_REQUEST: "잘못된 요청: 요청이 잘못되었습니다.", @@ -7,6 +8,7 @@ const HTTP_ERRORS = { FORBIDDEN: "금지됨 : 이 리소스에 액세스 권한이 없습니다.", NOT_FOUND: "찾을 수 없음: 요청한 리소스를 찾을 수 없습니다.", INTERNAL_SERVER_ERROR: "내부 서버 오류: 서버에서 오류가 발생했습니다.", + ALREADY_TOKEN: "토큰 : 이미 토큰이 있습니다.", }; export const httpErrorHandler = (e: unknown, statusCode?: number): never => { @@ -19,6 +21,8 @@ export const httpErrorHandler = (e: unknown, statusCode?: number): never => { throw new Error(HTTP_ERRORS.FORBIDDEN); case 404: throw new Error(HTTP_ERRORS.NOT_FOUND); + case 422: + throw new Error(HTTP_ERRORS.ALREADY_TOKEN); case 500: throw new Error(HTTP_ERRORS.INTERNAL_SERVER_ERROR); default: @@ -46,23 +50,19 @@ export const buildUrl = (url: string): string => `${"https://api.realworld.io/ap export const http = { async get({ url, accessToken, schema }: { url: string; accessToken?: string; schema: ZodType }) { - try { - const headers = createHeaders(accessToken); + const headers = createHeaders(accessToken); - const res = await fetch(buildUrl(url), { - method: "GET", - headers, - }); + const res = await fetch(buildUrl(url), { + method: "GET", + headers, + }); - if (!res.ok) { - httpErrorHandler(new Error(`HTTP Error: ${res.statusText}`), res.status); - } - - const obj: z.infer = schema.parse(await res.json()); - return obj; - } catch (e) { - httpErrorHandler(e); + if (!res.ok) { + httpErrorHandler(new Error(`HTTP Error: ${res.statusText}`), res.status); } + + const obj = schema.parse(await res.json()); + return obj; }, async post({ url, @@ -75,23 +75,19 @@ export const http = { schema: ZodType; body: Request; }) { - try { - const headers = createHeaders(accessToken); + const headers = createHeaders(accessToken); - const res = await fetch(buildUrl(url), { - method: "POST", - headers, - body: JSON.stringify(body), - }); + const res = await fetch(buildUrl(url), { + method: "POST", + headers, + body: JSON.stringify(body), + }); - if (!res.ok) { - httpErrorHandler(new Error(`HTTP Error: ${res.statusText}`), res.status); - } - - const obj: z.infer = schema.parse(await res.json()); - return obj; - } catch (e) { - httpErrorHandler(e); + if (!res.ok) { + httpErrorHandler(new Error(`HTTP Error: ${res.statusText}`), res.status); } + + const obj = schema.parse(await res.json()); + return obj; }, }; diff --git a/apps/web/package.json b/apps/web/package.json index a68e2c81..4d3ee55f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -16,6 +16,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-error-boundary": "^4.0.11", + "react-hook-form": "^7.46.1", "ui": "workspace:*" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab483d4f..eba58e10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: react-error-boundary: specifier: ^4.0.11 version: 4.0.11(react@18.2.0) + react-hook-form: + specifier: ^7.46.1 + version: 7.46.1(react@18.2.0) ui: specifier: workspace:* version: link:../../packages/ui @@ -4708,6 +4711,15 @@ packages: react: 18.2.0 dev: false + /react-hook-form@7.46.1(react@18.2.0): + resolution: {integrity: sha512-0GfI31LRTBd5tqbXMGXT1Rdsv3rnvy0FjEk8Gn9/4tp6+s77T7DPZuGEpBRXOauL+NhyGT5iaXzdIM2R6F/E+w==} + engines: {node: '>=12.22.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + react: 18.2.0 + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: true