키모형 2025. 4. 5. 13:43
반응형

회원가입 절차와 거의 동일함.

 

1.  Navigation 컴포넌트에 로그인 링크 추가

/src/components/Navigation.jsx

"use client"

import NavLink from "./NavLink"

export default function Navigation() {
  return (
    <nav>
      <NavLink label="Home" href="/" />
      <div>
        <NavLink label="Register" href="/register" />
        <NavLink label="Login" href="/login" />
        <NavLink label="Dashboard" href="/dashboard" />
      </div>
    </nav>
  )
}

 

2. login 페이지 추가

/src/app/(auth)/login/page.jsx

"use client"

import { useActionState } from "react";
import { login } from "@/actions/auth"; // 해당 action페이지에 login 처리 부분 추가예정
import Link from "next/link";

export default function Login() {

  // useActionState는 서버 액션을 클라이언트에서 사용할 수 있도록 해주는 훅입니다.
  // 우선 action을 빈함수로 막아 놓고 /actions/auth.jsx 수정함
  //const [state, action, isPending] = useActionState(()=>{}, undefined);
  const [state, action, isPending] = useActionState(login, undefined);

  return (
    <div className="container w-1/2">
      <h1 className="title">Login</h1>     
      <form action={action} className="space-y-4">
        <div>
          <label htmlFor="email">Email</label>
          <input type="text" name="email" defaultValue={state?.email} required />
          {state?.errors?.email && (
            <p className="error">{state.errors.email}</p>
          )}
        </div>
        <div>
          <label htmlFor="password">Password</label>
          <input type="password" name="password" required />
          {state?.errors?.password && (
            <p className="error">{state.errors.password}</p>
          )}
        </div>
        <div className="flex items-end gap-4">
          <button disabled={isPending} className="btn-primary w-full">
            {isPending ? "Loading..." : "Login"}
          </button>          
        </div>
        <div className="flex items-center justify-center gap-4">
          <Link href="/register" className="text-link">
            or register here
          </Link>
          </div>
      </form>
    </div>
  );
}

 

3. Action에 login 처리 함수 추가

/src/actions/auth.jsx 에 함수 추가

// 기존내용아래에 login 액션 함수 추가

// 로그인 요청을 처리하는 서버 액션입니다.
export async function login(state, formData) {

  // 유효성 검사 규칙은 src/lib/rules.js에 정의되어 있습니다.
  const validatedFields = LoginFormSchema.safeParse({
    email: formData.get("email"),
    password: formData.get("password"),
  });

  // 유효성 검사에 실패한 경우, 에러 메시지를 반환합니다.
  // 에러 메시지는 src/lib/rules.js에 정의된 규칙에 따라 다르게 표시됩니다.
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      email: formData.get("email"),
    };
  }

  // 유효성 검사 통과 후 DB에 회원가입 정보를 저장합니다.
  const {email, password} = validatedFields.data;  
  // console.log(email, password);

  // next_blog_db DB에서 users collection(테이블)을 가져옵니다.
  // users collection이 존재하지 않는 경우, 에러 메시지를 반환합니다.
  const userCollection = await getCollection("users");
  if (!userCollection) {
    return { errors: { email: "Server error!" } };
  }

  // user email이 존재하지 않는 경우, 에러 메시지를 반환합니다.
  const exitingUser = await userCollection.findOne({ email });
  if (!exitingUser) {
    return { errors: { email: "User not found." } };
  }

  // 비밀번호를 비교하여 일치하지 않는 경우, 에러 메시지를 반환합니다.
  const isPasswordValid = await bcrypt.compare(password, exitingUser.password);
  if (!isPasswordValid) {
    return { 
      errors: { password: "Invalid password." },
      email: formData.get("email")
    };
  }

  // Create Session
  await createSession(exitingUser._id.toString());

  // Rediect to login page after successful registration.
  redirect("/dashboard");
}

 

위 액션에 필요한 로그인 폼데이터에 대한  Validate 함수 추가

/src/lib/rules.js

// 로그인시 이메일, 비밀번호를 검사합니다.
export const LoginFormSchema = z
.object({
  email: z.string().email({ message: "Please enter a valid email." }).trim(),
  password: z
    .string()
    .min(1, { message: "Not be empty" })
    .trim()
});

 

반응형