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
8 changes: 7 additions & 1 deletion src/app/(before-login)/(without-navbar)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ type AuthLayoutProps = {
buttonText: string;
linkText: string;
linkPath: string;
isLoading?: boolean;
isFormValid?: boolean;
};

export const AuthLayout: React.FC<AuthLayoutProps> = ({
children,
buttonText,
linkText,
linkPath,
isLoading = false,
isFormValid = false,
}) => {
const isButtonDisabled = isLoading || !isFormValid;
return (
<main className="w-full min-h-screen flex justify-center items-center bg-gray-200 p-3">
<section className="w-[520px] flex flex-col justify-center items-center">
Expand All @@ -41,8 +46,9 @@ export const AuthLayout: React.FC<AuthLayoutProps> = ({
className="w-full"
form="auth-form"
type="submit"
disabled={isButtonDisabled}
>
{buttonText}
{isLoading ? "처리 중 입니다." : buttonText}
</Button>
</div>
</div>
Expand Down
41 changes: 36 additions & 5 deletions src/app/(before-login)/(without-navbar)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,59 @@
"use client";
import Input from "@/components/common/input/Input";
import { AuthLayout } from "@/app/(before-login)/(without-navbar)/layout";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import Input from "@/components/common/input/Input";
import { AuthLayout } from "@/app/(before-login)/(without-navbar)/layout";
Copy link
Owner

Choose a reason for hiding this comment

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

layout.tsx는 따로 import 해줄 필요 없이 children인 페이지의 레이아웃이 됩니다. 이 import 부분 삭제해보신 뒤 문제 없는지 확인해보시고, 문제 없다면 지워도 될 것 같습니다!

import { loginSchema, LoginFormData } from "@/lib/utils/validationSchema";
import { useAlertStore } from "@/lib/store/useAlertStore";
import { fetchLogin } from "@/lib/apis/authApi";
import { useState } from "react";
import { useRouter } from "next/navigation";

export default function Page() {
const { openAlert } = useAlertStore();
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();

const {
register,
handleSubmit,
formState: { errors },
formState: { errors, isValid },
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
mode: "onBlur",
});

const onSubmit = (data: LoginFormData) => {
console.log("로그인 데이터:", data);
const onSubmit = async (data: LoginFormData) => {
setIsLoading(true);
try {
const response = await fetchLogin(data);
setIsLoading(false);
localStorage.setItem("accessToken", response.accessToken);
Copy link
Owner

Choose a reason for hiding this comment

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

localStorage에 액세스 토큰을 저장하는군요. 로그아웃 기능 구현할 때 참고하겠습니다!

openAlert("loginSuccess");
router.push("/mydashboard");
} catch (error: unknown) {
Copy link
Owner

Choose a reason for hiding this comment

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

에러 처리까지 꼼꼼하게! 너무 멋집니다!

setIsLoading(false);
if (error instanceof Error) {
const errorInfo: { status: number; message: string } = JSON.parse(
error.message
);
if (errorInfo.status === 400) {
openAlert("wrongPassword");
}
if (errorInfo.status === 404) {
openAlert("userNotFound");
}
}
}
};

return (
<AuthLayout
buttonText="로그인"
linkText="회원이 아니신가요?"
linkPath="/signup"
isLoading={isLoading}
isFormValid={isValid}
>
<form id="auth-form" onSubmit={handleSubmit(onSubmit)} className="w-full">
<div className="pb-4">
Expand Down
39 changes: 30 additions & 9 deletions src/app/(before-login)/(without-navbar)/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,56 @@ import { zodResolver } from "@hookform/resolvers/zod";
import Input from "@/components/common/input/Input";
import { AuthLayout } from "@/app/(before-login)/(without-navbar)/layout";
Copy link
Owner

Choose a reason for hiding this comment

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

이 부분도 위에서 말한 것 같이 지워도 괜찮을 것 같습니다!

import { signupSchema, SignupFormData } from "@/lib/utils/validationSchema";
import { useAlertStore } from "@/lib/store/useAlertStore";
import { fetchSignup } from "@/lib/apis/authApi";
import { useState } from "react";
import { useRouter } from "next/navigation";

export default function Page() {
const { openAlert } = useAlertStore();
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();

const {
register,
handleSubmit,
formState: { errors },
formState: { errors, isValid },
} = useForm<SignupFormData>({
resolver: zodResolver(signupSchema),
mode: "onBlur",
defaultValues: {
terms: false,
},
});

const onSubmit = (data: SignupFormData) => {
console.log("회원가입 데이터:", data);
const onSubmit = async (data: SignupFormData) => {
setIsLoading(true);
try {
await fetchSignup(data);
setIsLoading(false);
openAlert("signupSuccess");
router.push("/login");
} catch (error: unknown) {
setIsLoading(false);
if (error instanceof Error) {
const errorInfo: { status: number; message: string } = JSON.parse(
error.message
);
if (errorInfo.status === 409) {
openAlert("emailDuplicated");
}
}
}
};

return (
<AuthLayout
buttonText="가입하기"
linkText="이미 회원이신가요?"
linkPath="/login"
isLoading={isLoading}
isFormValid={isValid}
>
<form
id="auth-form" // Button과 연결
onSubmit={handleSubmit(onSubmit)}
className="w-full"
>
<form id="auth-form" onSubmit={handleSubmit(onSubmit)} className="w-full">
<div className="pb-4">
<Input
label="이메일"
Expand Down
5 changes: 5 additions & 0 deletions src/components/common/alert/AlertProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export default function AlertProvider() {
{currentAlert === "deleteColumn" && (
<Alert onConfirm={handleDeleteClick} />
)}
{currentAlert === "loginSuccess" && (
<Alert onConfirm={() => router.push(ROUTE.MYDASHBOARD)} />
)}
{currentAlert === "userNotFound" && <Alert />}
{currentAlert === "wrongPassword" && <Alert />}
</>
);
}
3 changes: 3 additions & 0 deletions src/components/common/alert/alertData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ export const alertMessages: Record<string, string> = {
emailDuplicated: "이미 사용 중인 이메일입니다.",
signupSuccess: "가입이 완료되었습니다!",
deleteColumn: "컬럼의 모든 카드가 삭제됩니다.",
loginSuccess: "로그인 되었습니다.",
userNotFound: "존재하지 않는 유저입니다.",
wrongPassword: "현재 비밀번호가 틀렸습니다.",
};
61 changes: 61 additions & 0 deletions src/lib/apis/authApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { LoginFormData, SignupFormData } from "@/lib/utils/validationSchema";
import { BASE_URL } from "@/lib/constants/urls";

export async function fetchLogin(data: LoginFormData) {
const requestBody = {
email: data.email,
password: data.password,
};

const response = await fetch(`${BASE_URL}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
accept: "application/json",
},
body: JSON.stringify(requestBody),
cache: "no-store",
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(
JSON.stringify({
status: response.status,
message: errorData.message || "로그인에 실패했습니다.",
})
);
}

return response.json();
}

export async function fetchSignup(data: SignupFormData) {
const requestBody = {
email: data.email,
password: data.password,
nickname: data.nickname,
};

const response = await fetch(`${BASE_URL}/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
accept: "application/json",
},
body: JSON.stringify(requestBody),
cache: "no-store",
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(
JSON.stringify({
status: response.status,
message: errorData.message || "회원가입에 실패했습니다.",
})
);
}

return response.json();
}
3 changes: 3 additions & 0 deletions src/lib/store/useAlertStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export type AlertKey =
| "emailDuplicated"
| "signupSuccess"
| "deleteColumn"
| "loginSuccess"
| "userNotFound"
| "wrongPassword"
| null;

type AlertState = {
Expand Down
13 changes: 7 additions & 6 deletions src/lib/utils/validationSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,29 @@ import { z } from "zod";
export const loginSchema = z.object({
email: z
.string()
.nonempty("이메일을 입력해주세요.")
.min(1, "이메일을 입력해주세요.")
.email("이메일 형식으로 작성해 주세요."),
password: z
.string()
.nonempty("비밀번호를 입력해주세요.")
.min(1, "비밀번호를 입력해주세요.")
.min(8, "8자 이상 작성해 주세요."),
});

export const signupSchema = z
.object({
email: z
.string()
.nonempty("이메일을 입력해주세요.")
.min(1, "이메일을 입력해주세요.")
.email("이메일 형식으로 작성해 주세요."),
nickname: z
.string()
.nonempty("닉네임을 입력해주세요.")
.min(1, "닉네임을 입력해주세요.")
.max(10, "열 자 이하로 작성해주세요."),
password: z
.string()
.nonempty("비밀번호를 입력해주세요.")
.min(1, "비밀번호를 입력해주세요.")
.min(8, "8자 이상 입력해주세요."),
confirmPassword: z.string().nonempty("비밀번호 확인을 입력해주세요."),
confirmPassword: z.string().min(1, "비밀번호 확인을 입력해주세요."),
terms: z
.boolean()
.refine((val) => val === true, "이용약관에 동의해 주세요."),
Expand All @@ -34,5 +34,6 @@ export const signupSchema = z
message: "비밀번호가 일치하지 않습니다.",
path: ["confirmPassword"],
});

export type LoginFormData = z.infer<typeof loginSchema>;
export type SignupFormData = z.infer<typeof signupSchema>;