issue # 이메일 인증 코드 발송 로직 구현

This commit is contained in:
geonhee-min
2025-11-21 16:21:07 +09:00
parent 0170421d16
commit 6cd0361375
13 changed files with 118 additions and 6 deletions

2
.env
View File

@@ -19,7 +19,7 @@ GMAIL_USER=bkd.scheduler@gmail.com
GMAIL_PASS= # 앱 비밀번호 또는 OAuth2 토큰
GMAIL_CLIENT_ID=688417162908-iqvnj4ceb8t1dkbjr70dtcafo27m8kqe.apps.googleusercontent.com
GMAIL_CLIENT_SECRET=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
GMAIL_REFRESH_TOKEN=1//04_pSivNoGpPUCgYIARAAGAQSNwF-L9IrO0Kx6jSzq_eQNjdl65f0O2iqKSNpFeZ3gtIGMhOk0oiZsnKrPfWs8jvuEic1NhUoZ0g
GMAIL_REFRESH_TOKEN=1//04P8ekVQmkdtnCgYIARAAGAQSNwF-L9IrqPOyH8oYB-mdjUqw9jGHienVLBTWFdiZgpRnPgFmYnAdbjnstd9RkRVeJErB0NRAwg4
# SMTP 추가 옵션
SMTP_AUTH=true

View File

@@ -28,7 +28,9 @@
"dotenv": "^17.2.3",
"drizzle-kit": "^0.31.7",
"drizzle-orm": "^0.44.7",
"googleapis": "^166.0.0",
"ioredis": "^5.8.2",
"nodemailer": "^7.0.10",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
@@ -43,6 +45,7 @@
"@types/ioredis": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/nodemailer": "^7.0.4",
"@types/pg": "^8.15.6",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",

View File

@@ -3,9 +3,11 @@ import { AppController } from './app.controller';
import { AppService } from './app.service';
import { DbModule } from './db/db.module';
import { RedisModule } from './redis/redis.module';
import { AccountModule } from './modules/account/account.module';
import { MailerModule } from './util/mailer/mailer.module';
@Module({
imports: [DbModule, RedisModule],
imports: [DbModule, RedisModule, MailerModule, AccountModule],
controllers: [AppController],
providers: [AppService],
})

View File

@@ -1,5 +1,8 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import dotenv from 'dotenv';
dotenv.config();
async function bootstrap() {
const app = await NestFactory.create(AppModule);

View File

@@ -1,14 +1,27 @@
import { Controller, Get, Post, Query } from "@nestjs/common";
import { Body, Controller, Get, Post, Query } from "@nestjs/common";
import { CheckDuplicationRequestDto } from "./dto/checkDuplication/check-duplication-request.dto";
import { CheckDuplicationResponseDto } from "./dto/checkDuplication/check-duplication-response.dto";
import { AccountService } from "./account.service";
import { SendVerificationCodeRequestDto } from "./dto/checkDuplication/send-verification-code-request.dto";
import { SendVerificationCodeResponseDto } from "./dto/checkDuplication/send-verification-code-response.dto";
@Controller('account')
export class AccountController {
constructor(private readonly accountService: AccountService) {}
@Get('/')
async test() {
return "Test"
}
@Get('check-duplication')
async checkDuplication(@Query() query: CheckDuplicationRequestDto): Promise<CheckDuplicationResponseDto> {
return this.accountService.checkDuplication(query);
return await this.accountService.checkDuplication(query);
}
@Post('send-verification-code')
async sendVerificationCode(@Body() body: SendVerificationCodeRequestDto): Promise<SendVerificationCodeResponseDto> {
const result = await this.accountService.sendVerificationCode(body);
return result;
}
}

View File

@@ -5,6 +5,7 @@ import { AccountService } from "./account.service";
@Module({
controllers: [AccountController],
providers: [AccountService, AccountRepo]
providers: [AccountService, AccountRepo],
exports: [AccountService, AccountRepo]
})
export class AccountModule {}

View File

@@ -2,14 +2,33 @@ import { Injectable } from "@nestjs/common";
import { AccountRepo } from "./account.repo";
import { CheckDuplicationRequestDto } from "./dto/checkDuplication/check-duplication-request.dto";
import { CheckDuplicationResponseDto } from "./dto/checkDuplication/check-duplication-response.dto";
import { SendVerificationCodeRequestDto } from "./dto/checkDuplication/send-verification-code-request.dto";
import { MailerService } from "src/util/mailer/mailer.service";
import { Generator } from "src/util/generator";
import { SendVerificationCodeResponseDto } from "./dto/checkDuplication/send-verification-code-response.dto";
@Injectable()
export class AccountService {
constructor(private readonly accountRepo: AccountRepo) {}
constructor(
private readonly accountRepo: AccountRepo
, private readonly mailerService: MailerService
) {}
async checkDuplication(data: CheckDuplicationRequestDto): Promise<CheckDuplicationResponseDto> {
const count = await this.accountRepo.checkDuplication(data.type, data.value);
return { isDuplicated: count > 0 };
}
async sendVerificationCode(data: SendVerificationCodeRequestDto): Promise<SendVerificationCodeResponseDto> {
const code = Generator.getVerificationCode();
const html = `<p>Your verification code is: <strong style="font-size:16px;">${code}</strong></p>`;
const result = await this.mailerService.sendMail(data.email, "<Scheduler> 이메일 인증 코드", html);
if (result.rejected.length > 0) {
return { success: false, error: result.response }
} else {
return { success: true, message: "이메일 발송 완료" };
}
}
}

View File

@@ -0,0 +1,6 @@
import { IsEmail } from "@nestjs/class-validator";
export class SendVerificationCodeRequestDto {
@IsEmail()
email: string;
}

View File

@@ -0,0 +1,5 @@
export class SendVerificationCodeResponseDto {
success: boolean;
message?: string;
error?: string;
}

0
src/util/converter.ts Normal file
View File

5
src/util/generator.ts Normal file
View File

@@ -0,0 +1,5 @@
export class Generator {
static getVerificationCode() {
return Math.random().toString().slice(2, 8);
}
}

View File

@@ -0,0 +1,9 @@
import { Module, Global } from '@nestjs/common';
import { MailerService } from './mailer.service';
@Global()
@Module({
providers: [MailerService],
exports: [MailerService]
})
export class MailerModule {}

View File

@@ -0,0 +1,46 @@
import { Injectable } from '@nestjs/common';
import nodemailer from 'nodemailer';
import { google } from 'googleapis';
import SMTPTransport from 'nodemailer/lib/smtp-transport';
@Injectable()
export class MailerService {
private oauth2Client = new google.auth.OAuth2(
process.env.GMAIL_CLIENT_ID,
process.env.GMAIL_CLIENT_SECRET,
'https://developers.google.com/oauthplayground'
);
constructor() {
this.oauth2Client.setCredentials({
refresh_token: process.env.GMAIL_REFRESH_TOKEN
});
}
async sendMail(to: string, subject: string, html: string) {
const accessToken = await this.oauth2Client.getAccessToken();
console.log(accessToken);
const options: SMTPTransport.Options = {
host: "smtp.gmail.com",
port: 465,
secure: true,
auth: {
type: "OAuth2",
user: process.env.GMAIL_USER,
clientId: process.env.GMAIL_CLIENT_ID,
clientSecret: process.env.GMAIL_CLIENT_SECRET,
refreshToken: process.env.GMAIL_REFRESH_TOKEN,
accessToken: accessToken?.token || ''
}
}
const transporter = nodemailer.createTransport(options);
return transporter.sendMail({
from: `Scheduler ${process.env.GMAIL_USER}>`,
to,
subject,
html
})
}
}