Skip to content

Commit d06b649

Browse files
authored
Merge pull request #105 from hyeonjiroh/refactor/#75/author
refactor: #75/로그인, 회원가입 페이지 API 연결
2 parents cb688b7 + 3bfa420 commit d06b649

8 files changed

Lines changed: 152 additions & 21 deletions

File tree

src/app/(before-login)/(without-navbar)/layout.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@ type AuthLayoutProps = {
99
buttonText: string;
1010
linkText: string;
1111
linkPath: string;
12+
isLoading?: boolean;
13+
isFormValid?: boolean;
1214
};
1315

1416
export const AuthLayout: React.FC<AuthLayoutProps> = ({
1517
children,
1618
buttonText,
1719
linkText,
1820
linkPath,
21+
isLoading = false,
22+
isFormValid = false,
1923
}) => {
24+
const isButtonDisabled = isLoading || !isFormValid;
2025
return (
2126
<main className="w-full min-h-screen flex justify-center items-center bg-gray-200 p-3">
2227
<section className="w-[520px] flex flex-col justify-center items-center">
@@ -41,8 +46,9 @@ export const AuthLayout: React.FC<AuthLayoutProps> = ({
4146
className="w-full"
4247
form="auth-form"
4348
type="submit"
49+
disabled={isButtonDisabled}
4450
>
45-
{buttonText}
51+
{isLoading ? "처리 중 입니다." : buttonText}
4652
</Button>
4753
</div>
4854
</div>

src/app/(before-login)/(without-navbar)/login/page.tsx

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,59 @@
11
"use client";
2-
import Input from "@/components/common/input/Input";
3-
import { AuthLayout } from "@/app/(before-login)/(without-navbar)/layout";
42
import { useForm } from "react-hook-form";
53
import { zodResolver } from "@hookform/resolvers/zod";
4+
import Input from "@/components/common/input/Input";
5+
import { AuthLayout } from "@/app/(before-login)/(without-navbar)/layout";
66
import { loginSchema, LoginFormData } from "@/lib/utils/validationSchema";
7+
import { useAlertStore } from "@/lib/store/useAlertStore";
8+
import { fetchLogin } from "@/lib/apis/authApi";
9+
import { useState } from "react";
10+
import { useRouter } from "next/navigation";
711

812
export default function Page() {
13+
const { openAlert } = useAlertStore();
14+
const [isLoading, setIsLoading] = useState(false);
15+
const router = useRouter();
16+
917
const {
1018
register,
1119
handleSubmit,
12-
formState: { errors },
20+
formState: { errors, isValid },
1321
} = useForm<LoginFormData>({
1422
resolver: zodResolver(loginSchema),
1523
mode: "onBlur",
1624
});
1725

18-
const onSubmit = (data: LoginFormData) => {
19-
console.log("로그인 데이터:", data);
26+
const onSubmit = async (data: LoginFormData) => {
27+
setIsLoading(true);
28+
try {
29+
const response = await fetchLogin(data);
30+
setIsLoading(false);
31+
localStorage.setItem("accessToken", response.accessToken);
32+
openAlert("loginSuccess");
33+
router.push("/mydashboard");
34+
} catch (error: unknown) {
35+
setIsLoading(false);
36+
if (error instanceof Error) {
37+
const errorInfo: { status: number; message: string } = JSON.parse(
38+
error.message
39+
);
40+
if (errorInfo.status === 400) {
41+
openAlert("wrongPassword");
42+
}
43+
if (errorInfo.status === 404) {
44+
openAlert("userNotFound");
45+
}
46+
}
47+
}
2048
};
49+
2150
return (
2251
<AuthLayout
2352
buttonText="로그인"
2453
linkText="회원이 아니신가요?"
2554
linkPath="/signup"
55+
isLoading={isLoading}
56+
isFormValid={isValid}
2657
>
2758
<form id="auth-form" onSubmit={handleSubmit(onSubmit)} className="w-full">
2859
<div className="pb-4">

src/app/(before-login)/(without-navbar)/signup/page.tsx

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,56 @@ import { zodResolver } from "@hookform/resolvers/zod";
44
import Input from "@/components/common/input/Input";
55
import { AuthLayout } from "@/app/(before-login)/(without-navbar)/layout";
66
import { signupSchema, SignupFormData } from "@/lib/utils/validationSchema";
7+
import { useAlertStore } from "@/lib/store/useAlertStore";
8+
import { fetchSignup } from "@/lib/apis/authApi";
9+
import { useState } from "react";
10+
import { useRouter } from "next/navigation";
711

812
export default function Page() {
13+
const { openAlert } = useAlertStore();
14+
const [isLoading, setIsLoading] = useState(false);
15+
const router = useRouter();
16+
917
const {
1018
register,
1119
handleSubmit,
12-
formState: { errors },
20+
formState: { errors, isValid },
1321
} = useForm<SignupFormData>({
1422
resolver: zodResolver(signupSchema),
1523
mode: "onBlur",
1624
defaultValues: {
1725
terms: false,
1826
},
1927
});
20-
21-
const onSubmit = (data: SignupFormData) => {
22-
console.log("회원가입 데이터:", data);
28+
const onSubmit = async (data: SignupFormData) => {
29+
setIsLoading(true);
30+
try {
31+
await fetchSignup(data);
32+
setIsLoading(false);
33+
openAlert("signupSuccess");
34+
router.push("/login");
35+
} catch (error: unknown) {
36+
setIsLoading(false);
37+
if (error instanceof Error) {
38+
const errorInfo: { status: number; message: string } = JSON.parse(
39+
error.message
40+
);
41+
if (errorInfo.status === 409) {
42+
openAlert("emailDuplicated");
43+
}
44+
}
45+
}
2346
};
2447

2548
return (
2649
<AuthLayout
2750
buttonText="가입하기"
2851
linkText="이미 회원이신가요?"
2952
linkPath="/login"
53+
isLoading={isLoading}
54+
isFormValid={isValid}
3055
>
31-
<form
32-
id="auth-form" // Button과 연결
33-
onSubmit={handleSubmit(onSubmit)}
34-
className="w-full"
35-
>
56+
<form id="auth-form" onSubmit={handleSubmit(onSubmit)} className="w-full">
3657
<div className="pb-4">
3758
<Input
3859
label="이메일"

src/components/common/alert/AlertProvider.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export default function AlertProvider() {
3232
{currentAlert === "deleteColumn" && (
3333
<Alert onConfirm={handleDeleteClick} />
3434
)}
35+
{currentAlert === "loginSuccess" && (
36+
<Alert onConfirm={() => router.push(ROUTE.MYDASHBOARD)} />
37+
)}
38+
{currentAlert === "userNotFound" && <Alert />}
39+
{currentAlert === "wrongPassword" && <Alert />}
3540
</>
3641
);
3742
}

src/components/common/alert/alertData.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@ export const alertMessages: Record<string, string> = {
33
emailDuplicated: "이미 사용 중인 이메일입니다.",
44
signupSuccess: "가입이 완료되었습니다!",
55
deleteColumn: "컬럼의 모든 카드가 삭제됩니다.",
6+
loginSuccess: "로그인 되었습니다.",
7+
userNotFound: "존재하지 않는 유저입니다.",
8+
wrongPassword: "현재 비밀번호가 틀렸습니다.",
69
};

src/lib/apis/authApi.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { LoginFormData, SignupFormData } from "@/lib/utils/validationSchema";
2+
import { BASE_URL } from "@/lib/constants/urls";
3+
4+
export async function fetchLogin(data: LoginFormData) {
5+
const requestBody = {
6+
email: data.email,
7+
password: data.password,
8+
};
9+
10+
const response = await fetch(`${BASE_URL}/auth/login`, {
11+
method: "POST",
12+
headers: {
13+
"Content-Type": "application/json",
14+
accept: "application/json",
15+
},
16+
body: JSON.stringify(requestBody),
17+
cache: "no-store",
18+
});
19+
20+
if (!response.ok) {
21+
const errorData = await response.json();
22+
throw new Error(
23+
JSON.stringify({
24+
status: response.status,
25+
message: errorData.message || "로그인에 실패했습니다.",
26+
})
27+
);
28+
}
29+
30+
return response.json();
31+
}
32+
33+
export async function fetchSignup(data: SignupFormData) {
34+
const requestBody = {
35+
email: data.email,
36+
password: data.password,
37+
nickname: data.nickname,
38+
};
39+
40+
const response = await fetch(`${BASE_URL}/users`, {
41+
method: "POST",
42+
headers: {
43+
"Content-Type": "application/json",
44+
accept: "application/json",
45+
},
46+
body: JSON.stringify(requestBody),
47+
cache: "no-store",
48+
});
49+
50+
if (!response.ok) {
51+
const errorData = await response.json();
52+
throw new Error(
53+
JSON.stringify({
54+
status: response.status,
55+
message: errorData.message || "회원가입에 실패했습니다.",
56+
})
57+
);
58+
}
59+
60+
return response.json();
61+
}

src/lib/store/useAlertStore.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ export type AlertKey =
55
| "emailDuplicated"
66
| "signupSuccess"
77
| "deleteColumn"
8+
| "loginSuccess"
9+
| "userNotFound"
10+
| "wrongPassword"
811
| null;
912

1013
type AlertState = {

src/lib/utils/validationSchema.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,29 @@ import { z } from "zod";
33
export const loginSchema = z.object({
44
email: z
55
.string()
6-
.nonempty("이메일을 입력해주세요.")
6+
.min(1, "이메일을 입력해주세요.")
77
.email("이메일 형식으로 작성해 주세요."),
88
password: z
99
.string()
10-
.nonempty("비밀번호를 입력해주세요.")
10+
.min(1, "비밀번호를 입력해주세요.")
1111
.min(8, "8자 이상 작성해 주세요."),
1212
});
1313

1414
export const signupSchema = z
1515
.object({
1616
email: z
1717
.string()
18-
.nonempty("이메일을 입력해주세요.")
18+
.min(1, "이메일을 입력해주세요.")
1919
.email("이메일 형식으로 작성해 주세요."),
2020
nickname: z
2121
.string()
22-
.nonempty("닉네임을 입력해주세요.")
22+
.min(1, "닉네임을 입력해주세요.")
2323
.max(10, "열 자 이하로 작성해주세요."),
2424
password: z
2525
.string()
26-
.nonempty("비밀번호를 입력해주세요.")
26+
.min(1, "비밀번호를 입력해주세요.")
2727
.min(8, "8자 이상 입력해주세요."),
28-
confirmPassword: z.string().nonempty("비밀번호 확인을 입력해주세요."),
28+
confirmPassword: z.string().min(1, "비밀번호 확인을 입력해주세요."),
2929
terms: z
3030
.boolean()
3131
.refine((val) => val === true, "이용약관에 동의해 주세요."),
@@ -34,5 +34,6 @@ export const signupSchema = z
3434
message: "비밀번호가 일치하지 않습니다.",
3535
path: ["confirmPassword"],
3636
});
37+
3738
export type LoginFormData = z.infer<typeof loginSchema>;
3839
export type SignupFormData = z.infer<typeof signupSchema>;

0 commit comments

Comments
 (0)