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