Skip to content

Commit

Permalink
Merge pull request #139 from hhhminme/feat/register
Browse files Browse the repository at this point in the history
회원가입 로직 기능을 추가합니다.
  • Loading branch information
InSeong-So authored May 16, 2024
2 parents 0257100 + 90ad0c0 commit 9aed6e1
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 47 deletions.
13 changes: 13 additions & 0 deletions apps/web/app/(auth)/_api/index.ts
Original file line number Diff line number Diff line change
@@ -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<UserType | UserErrorType | undefined> {
return http.post({
url: "/users",
body: {
user: req,
},
schema: UserSchema,
});
}
11 changes: 11 additions & 0 deletions apps/web/app/(auth)/_component/error-message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
interface ErrorMessageProps {
message?: string;
}

export function ErrorMessage({ message }: ErrorMessageProps): JSX.Element {
return (
<ul className="error-messages">
<li>{message}</li>
</ul>
);
}
88 changes: 88 additions & 0 deletions apps/web/app/(auth)/_component/register-form.tsx
Original file line number Diff line number Diff line change
@@ -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<RegisterReq>();

const { mutate } = useRegister();

const router = useRouter();

const onSubmit: SubmitHandler<RegisterReq> = 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
<form onSubmit={handleSubmit(onSubmit)}>
<fieldset className="form-group">
<input
{...register("username", { required: "Username can't be blank" })}
className="form-control form-control-lg"
placeholder="Username"
type="text"
/>
</fieldset>
{errors.username ? <ErrorMessage message={errors.username.message} /> : null}

<fieldset className="form-group">
<input
{...register("email", {
required: "Email can't be blank",
validate: email => isEmailValid(email) || "Invalid email format",
})}
className="form-control form-control-lg"
placeholder="Email"
type="text"
/>
</fieldset>
{errors.email ? <ErrorMessage message={errors.email.message} /> : null}

<fieldset className="form-group">
<input
{...register("password", { required: "Password can't be blank" })}
className="form-control form-control-lg"
placeholder="Password"
type="password"
/>
</fieldset>
{errors.password ? <ErrorMessage message={errors.password.message} /> : null}

<button className="btn btn-lg btn-primary pull-xs-right" type="submit">
Sign up
</button>
</form>
);
}
9 changes: 9 additions & 0 deletions apps/web/app/(auth)/_hooks/use-register.tsx
Original file line number Diff line number Diff line change
@@ -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),
});
}
28 changes: 28 additions & 0 deletions apps/web/app/(auth)/_types/index.ts
Original file line number Diff line number Diff line change
@@ -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<typeof UserErrorsSchema>;

export type UserType = z.infer<typeof UserSchema>;
21 changes: 3 additions & 18 deletions apps/web/app/(auth)/register/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { RegisterForm } from "../_component/register-form";

export default function RegisterPage(): JSX.Element {
return (
<div className="auth-page">
Expand All @@ -9,24 +11,7 @@ export default function RegisterPage(): JSX.Element {
<a href="/login">Have an account?</a>
</p>

<ul className="error-messages">
<li>That email is already taken</li>
</ul>

<form>
<fieldset className="form-group">
<input className="form-control form-control-lg" placeholder="Username" type="text" />
</fieldset>
<fieldset className="form-group">
<input className="form-control form-control-lg" placeholder="Email" type="text" />
</fieldset>
<fieldset className="form-group">
<input className="form-control form-control-lg" placeholder="Password" type="password" />
</fieldset>
<button className="btn btn-lg btn-primary pull-xs-right" type="button">
Sign up
</button>
</form>
<RegisterForm />
</div>
</div>
</div>
Expand Down
54 changes: 25 additions & 29 deletions apps/web/lib/http.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { z, ZodType } from "zod";
import { ZodError } from "zod";
import { UserErrorsSchema } from "../app/(auth)/_types";

const HTTP_ERRORS = {
BAD_REQUEST: "잘못된 요청: 요청이 잘못되었습니다.",
UNAUTHORIZED: "인증되지 않음: 인증이 필요합니다.",
FORBIDDEN: "금지됨 : 이 리소스에 액세스 권한이 없습니다.",
NOT_FOUND: "찾을 수 없음: 요청한 리소스를 찾을 수 없습니다.",
INTERNAL_SERVER_ERROR: "내부 서버 오류: 서버에서 오류가 발생했습니다.",
ALREADY_TOKEN: "토큰 : 이미 토큰이 있습니다.",
};

export const httpErrorHandler = (e: unknown, statusCode?: number): never => {
Expand All @@ -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:
Expand Down Expand Up @@ -46,23 +50,19 @@ export const buildUrl = (url: string): string => `${"https://api.realworld.io/ap

export const http = {
async get<Response>({ url, accessToken, schema }: { url: string; accessToken?: string; schema: ZodType<Response> }) {
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<typeof schema> = 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<Request, Response>({
url,
Expand All @@ -75,23 +75,19 @@ export const http = {
schema: ZodType<Response>;
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<typeof schema> = 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;
},
};
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9aed6e1

Please sign in to comment.