이메일 인증을 위해 해당 라이브러리를 설치합니다.
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 } ); } }