react

NextJS 실무편 - #05 회원가입 API

2024년 2월 15일 05:14

이메일 인증을 위해 해당 라이브러리를 설치합니다.

yarn add nodemailer
yarn add @types/nodemailer --dev

 

.env.local 파일에 다음 내용을 추가합니다.

// gmail 기준

SYSTEM_EMAIL_SERVICE=gmail
SYSTEM_EMAIL_PORT=465
SYSTEM_EMAIL_HOST=smtp.gmail.com
SYSTEM_EMAIL_SENDER=이메일 주소
SYSTEM_EMAIL_APPPASS=이메일 앱 비밀번호

 

src/libs 폴더에 nodemailer.ts를 만듭니다.

// nodemailer.ts

import nodemailer from 'nodemailer';

if (
  !process.env.SYSTEM_EMAIL_SERVICE ||
  !process.env.SYSTEM_EMAIL_PORT ||
  !process.env.SYSTEM_EMAIL_HOST ||
  !process.env.SYSTEM_EMAIL_SENDER ||
  !process.env.SYSTEM_EMAIL_APPPASS
) {
  console.error('환경 변수가 설정되지 않았습니다.');
  process.exit(1);
}

interface VerifyEmailProps {
  email: string;
  id: number;
};

let transporter = nodemailer.createTransport({
  service: process.env.SYSTEM_EMAIL_SERVICE,
  host: process.env.SYSTEM_EMAIL_HOST,
  port: parseInt(process.env.SYSTEM_EMAIL_PORT, 10),
  secure: true,
  auth: {
    user: process.env.SYSTEM_EMAIL_SENDER,
    pass: process.env.SYSTEM_EMAIL_APPPASS,
  },
});

export async function verifyEmail({ email, id }: VerifyEmailProps) {
  const mailData = {
    to: email,
    subject: `이메일 인증`,
    from: process.env.SYSTEM_EMAIL_SENDER,
    html: `
    <table style="margin:40px auto 20px;text-align:left;border-collapse:collapse;border:0;width:600px;padding:64px 16px;box-sizing:border-box">
      <tbody>
        <tr>
          <td style="display:flex;flex-direction: column;justify-items: center;align-items: center;border: 1px solid #b1b1b1;padding: 80px 0;border-radius: 20px;">
            <a href="http://localhost:3000" target="_blank">
              <img style="width: 300px;" src="https://rgvzlonuavmjvodmalpd.supabase.co/storage/v1/object/public/images/public/Logo.png" alt="fastcampus" class="CToWUd" data-bit="iit">
            </a>
            <p style="padding-top:20px;font-weight:700;font-size:20px;line-height:1.5;color:#222">
              이메일 주소를 인증해주세요.
            </p>
            <p style="font-size:16px;font-weight:400;line-height:1.5;margin-bottom: 40px;">
              하단 버튼을 누르시면 이메일 인증이 완료됩니다.
            </p>
            <a href="http://localhost:3000/verifyemail/${id}" style="background:#404040;text-decoration:none;padding:10px 24px;font-size:18px;color:#fff;font-weight:400;border-radius:4px;" >이메일 인증하러 가기</a>
          </td>
        </tr>
      </tbody>
    </table>
    `,
  };

  return transporter.sendMail(mailData);
}

 

api/auth/signup/route.ts 파일을 만듭니다.

 

// api/auth/signup/route.ts

import { verifyEmail } from '@/libs/nodemailer';
import prisma from '@/libs/prisma';
import bcrypt from 'bcryptjs';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  try {
    const body = await request.json();
    const { email, name, password } = body;

    const user = await prisma.user.findUnique({
      where: {
        email,
      },
    });

    if (user) {
      return NextResponse.json(
        {
          message: '이미 가입된 이메일입니다.',
        },
        {
          status: 409,
        }
      );
    }

    const hashedPassword = await bcrypt.hash(password, 12);

    const newUser = await prisma.user.create({
      data: {
        email,
        name,
        password: hashedPassword,
      },
    });

    verifyEmail({
      email: newUser.email,
      id: newUser.id,
    });

    return NextResponse.json(
      {
        message: '회원가입이 완료되었습니다.',
      },
      { status: 201 }
    );
  } catch (error) {
    return NextResponse.error();
  }
}

 

src/app/(auth)/verifyemail/[userId]/page.tsx 파일을 생성합니다.

// src/app/(auth)/verifyemail/page.tsx 

'use client'

import axios from 'axios';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { toast } from 'react-toastify';

type VerifyEmailProps = {
  params: {
    userId: string;
  };
};

export default function Page({ params }: VerifyEmailProps) {
  const router = useRouter();

  const userId = params.userId

  useEffect(() => {
    (async()=>{
      const response = await axios.put(`/api/auth/verifyemail/${userId}`)
      const { message } = response.data;

      switch (message) {
        case 'Invalid path':
          toast.error('잘못된 경로입니다.');
          router.push('/');
          break;
        case 'Verified':
          toast.success('이메일 인증이 완료되었습니다.');
          router.push('/login');
          break;
        case 'Already_verified':
          toast.error('이미 인증된 회원입니다.');
          router.push('/');
          break;
        case 'Not_found':
          toast.error('해당 유저 ID가 없습니다.');
          router.push('/');
          break;
        default:
          router.push('/');
      }
    })()
  }, [userId, router]);

  return null;
}

 

src/app/api/verifyemail/[userId]/route.ts 파일을 생성합니다.

// src/app/api/verifyemail/[userId]/route.ts

import prisma from '@/libs/prisma';
import { NextResponse } from 'next/server';

export async function PUT(
  request: Request,
  { params }: { params: { userId: string } }
) {
  try {
    const userId = parseInt(params.userId, 10);

    if (isNaN(userId)) {
      return new NextResponse(JSON.stringify({ message: 'Invalid_user_id' }), {
        status: 400,
      });
    }

    const user = await prisma.user.findUnique({
      where: {
        id: userId,
      },
    });
    console.log('user : ', user);

    if (!user) {
      return new NextResponse(JSON.stringify({ message: 'Not_found' }), {
        status: 404,
      });
    }

    if (user.emailVerified) {
      return new NextResponse(JSON.stringify({ message: 'Already_verified' }), {
        status: 200,
      });
    }

    await prisma.user.update({
      where: {
        id: userId,
      },
      data: {
        emailVerified: new Date(),
      },
    });

    return new NextResponse(JSON.stringify({ message: 'Verified' }), {
      status: 201,
    });
  } catch (error) {
    console.error(error);
    return new NextResponse(
      JSON.stringify({ error: 'Internal Server Error' }),
      { status: 500 }
    );
  }
}


Copyright © 2024 White Mouse Dev