회원관리 - Part1 회원가입

Project Diary/Next.js + Prisma + MariaDB (KiloFlow)

회원가입 과정에서는 사용자가 이메일, 비밀번호, 닉네임 등의 정보를 입력하고, 이 정보를 데이터베이스에 저장합니다. 


 

데이터베이스 스키마 모델

 

- users 테이블

model users {
  user_id                Int              @id @default(autoincrement())
  email                  String           @unique
  password               String
  nickname               String
  profile_image          String           @default("default_image_url")
  isInitialSetupComplete Boolean          @default(false)
  created_at             DateTime         @default(now())
  userProfile            UserProfile?

  @@index([user_id])
}

 


 

프론트엔드

 

1. 이메일 중복 확인 함수

const checkEmail = async () => {
  // 이메일 중복 확인을 위해 API 호출
  const res = await fetch(`/api/auth/check?email=${email}`);
  // API 응답 데이터를 JSON 형식으로 변환
  const data = await res.json();
  // 응답 메시지를 상태로 설정
  setEmailMessage(data.message);
};
  • fetch 함수로 /api/auth/check 엔드포인트에 이메일 파라미터를 붙여 GET 요청을 보냅니다.
  • 서버의 응답을 JSON으로 변환한 후, 그 데이터를 setEmailMessage 함수를 통해 상태로 설정합니다.

2. 닉네임 중복 확인 함수

const checkNickname = async () => {
  // 닉네임 중복 확인을 위해 API 호출
  const res = await fetch(`/api/auth/check?nickname=${nickname}`);
  // API 응답 데이터를 JSON 형식으로 변환
  const data = await res.json();
  // 응답 메시지를 상태로 설정
  setNicknameMessage(data.message);
};
  • fetch 함수로 /api/auth/check 엔드포인트에 닉네임 파라미터를 붙여 GET 요청을 보냅니다.
  • 서버의 응답을 JSON으로 변환한 후, 그 데이터를 setNicknameMessage 함수를 통해 상태로 설정합니다.

3. 비밀번호 일치 확인 함수

const checkPasswordMatch = () => {
  // 비밀번호와 비밀번호 확인 값이 일치하지 않는 경우
  if (password !== confirmPassword) {
    setPasswordMessage("비밀번호가 일치하지 않습니다.");
  } else {
    // 비밀번호와 비밀번호 확인 값이 일치하는 경우
    setPasswordMessage("비밀번호가 일치합니다.");
  }
};
  • password와 confirmPassword 값이 일치하는지 비교하여 메시지를 설정합니다.

4. 회원가입 폼 데이터 전송 함수

const handleSubmit = async (event: React.FormEvent) => {
  event.preventDefault();
  setError("");

  // 필수 필드 체크
  if (!email) {
    setError("이메일을 입력해주세요.");
    return;
  }
  if (!password) {
    setError("비밀번호를 입력해주세요.");
    return;
  }
  if (!confirmPassword) {
    setError("비밀번호 확인을 입력해주세요.");
    return;
  }
  if (password !== confirmPassword) {
    setError("비밀번호가 일치하지 않습니다.");
    return;
  }
  if (!nickname) {
    setError("닉네임을 입력해주세요.");
    return;
  }

  // 폼 데이터 생성
  const formData = new FormData();
  formData.append("email", email);
  formData.append("password", password);
  formData.append("nickname", nickname);
  formData.append("profile_image", profileImage);

  try {
    // 회원가입 API 호출
    const res = await fetch("/api/auth/join", {
      method: "POST",
      body: formData,
    });

    // 회원가입 성공 시 로그인 페이지로 리다이렉트
    if (res.ok) {
      router.push("/auth/login");
    } else {
      const data = await res.json();
      setError(data.message);
    }
  } catch (err) {
    setError("An unexpected error occurred");
  }
};
  • 폼 제출 시 각 필드의 유효성을 검사하고, 유효하지 않으면 오류 메시지를 설정합니다.
  • 유효한 경우 FormData 객체를 생성하여 fetch 함수를 통해 서버에 POST 요청을 보냅니다.

5. 파일 변경 처리 함수

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  // 파일이 선택된 경우
  if (e.target.files && e.target.files[0]) {
    const file = e.target.files[0];
    setProfileImage(file);
    const fileUrl = URL.createObjectURL(file);
    setProfilePreview(fileUrl);
  }
};
  • 파일 입력 필드에서 파일이 선택되면 profileImage 상태와 미리보기 URL을 설정합니다.

 

백엔드

 

1. 이메일 및 닉네임 중복 확인 (check.ts)

 

1-1 이메일 중복 확인

import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "../../../lib/prisma";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { email, nickname } = req.query;

  // 이메일 중복 확인
  if (email) {
    const userByEmail = await prisma.users.findUnique({
      where: { email: email as string },
    });

    // 이미 존재하는 이메일인 경우
    if (userByEmail) {
      return res.status(200).json({ message: "이미 존재하는 이메일입니다.", available: false });
    } else {
      // 사용 가능한 이메일인 경우
      return res.status(200).json({ message: "사용 가능한 이메일입니다.", available: true });
    }
  }
  • email이 쿼리 파라미터로 전달된 경우 prisma.users.findUnique를 통해 데이터베이스에서 해당 이메일을 가진 사용자가 존재하는지 확인합니다.
  • 존재하면 중복 메시지와 함께 응답을 보내고, 존재하지 않으면 사용 가능 메시지와 함께 응답을 보냅니다.

1-2 닉네임 중복 확인

  // 닉네임 중복 확인
  if (nickname) {
    const userByNickname = await prisma.users.findFirst({
      where: { nickname: nickname as string },
    });

    // 이미 존재하는 닉네임인 경우
    if (userByNickname) {
      return res.status(200).json({ message: "이미 존재하는 닉네임입니다.", available: false });
    } else {
      // 사용 가능한 닉네임인 경우
      return res.status(200).json({ message: "사용 가능한 닉네임입니다.", available: true });
    }
  }
}

 

 

2. 회원가입 요청 처리 (join.ts)

 

2-1 파일 업로드 설정

import multer from "multer";
import path from "path";
import fs from "fs";

// 업로드 폴더 설정
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const uploadDir = path.join(process.cwd(), "public/uploads");
    if (!fs.existsSync(uploadDir)) {
      fs.mkdirSync(uploadDir, { recursive: true });
    }
    cb(null, uploadDir);
  },
  filename: (req, file, cb) => {
    cb(null, `${Date.now()}-${file.originalname}`);
  },
});

const upload = multer({ storage });
export default upload;
  • Multer를 사용하여 파일 업로드 폴더와 파일 이름을 설정합니다.
  • 업로드 폴더가 존재하지 않으면 폴더를 생성합니다.

2-2 NextApiRequest 확장

// NextApiRequest를 확장하여 file 속성을 추가한 인터페이스 정의
interface ExtendedRequest extends NextApiRequest {
  file: Express.Multer.File;
}
  • Multer로 파일을 처리하기 위해 NextApiRequest를 확장하여 file 속성을 추가합니다.

2-3  Multer 미들웨어 비동기 처리 함수

const runMiddleware = (
  req: NextApiRequest,
  res: NextApiResponse,
  fn: Function
) => {
  return new Promise((resolve, reject) => {
    fn(req, res, (result: any) => {
      if (result instanceof Error) {
        return reject(result);
      }
      return resolve(result);
    });
  });
};
  • Multer 미들웨어를 비동기적으로 실행하기 위해 runMiddleware 함수를 정의합니다.

2-4 회원가입 요청 처리

export default async function handler(
  req: ExtendedRequest,
  res: NextApiResponse
) {
  if (req.method !== "POST") {
    return res.status(405).json({ error: `Method ${req.method} Not Allowed` });
  }

  try {
    // Multer 미들웨어 실행
    await runMiddleware(req, res, upload.single("profile_image"));

    const { email, password, nickname, profile_image } = req.body;

    // 필수 필드 체크
    if (!email || !password || !nickname) {
      return res.status(400).json({ message: "모든 필드를 채워주세요." });
    }

    // 프로필 이미지 URL 설정
    const profileImageUrl = req.file
      ? `/uploads/${req.file.filename}`
      : profile_image;

    // 새 사용자 생성
    const newUser = await prisma.users.create({
      data: {
        email,
        password,
        nickname,
        profile_image: profileImageUrl,
      },
    });

    return res.status(201).json(newUser);
  } catch (error) {
    console.log(error);
    return res.status(500).json({ message: "서버 오류가 발생했습니다." });
  }
}