로그로그

This commit is contained in:
2025-12-15 09:55:43 +09:00
parent ef5f921e21
commit 6b8ea7e74c
7 changed files with 429 additions and 352 deletions

View File

@@ -1,55 +1,88 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as nodemailer from 'nodemailer';
/**
* 이메일 발송 서비스
*
* @export
* @class EmailService
*/
@Injectable()
export class EmailService {
private transporter; // nodemailer 전송 객체
private readonly logger = new Logger(EmailService.name);
private transporter;
constructor(private configService: ConfigService) {
// SMTP 서버 설정 (AWS SES, Gmail 등)
this.transporter = nodemailer.createTransport({ // .env 파일 EMAIL CONFIGURATION
host: this.configService.get('SMTP_HOST'),
port: parseInt(this.configService.get('SMTP_PORT')),
secure: this.configService.get('SMTP_PORT') === '465',
const smtpHost = this.configService.get('SMTP_HOST');
const smtpPort = this.configService.get('SMTP_PORT');
const smtpUser = this.configService.get('SMTP_USER');
const smtpPass = this.configService.get('SMTP_PASS');
this.logger.log(`[EMAIL] SMTP 설정 초기화`);
this.logger.log(`[EMAIL] Host: ${smtpHost}`);
this.logger.log(`[EMAIL] Port: ${smtpPort}`);
this.logger.log(`[EMAIL] User: ${smtpUser}`);
this.logger.log(`[EMAIL] Pass: ${smtpPass ? '****설정됨' : '미설정!!'}`);
process.stdout.write(`[EMAIL] SMTP Config - Host: ${smtpHost}, Port: ${smtpPort}, User: ${smtpUser}\n`);
if (!smtpHost || !smtpPort || !smtpUser || !smtpPass) {
this.logger.error(`[EMAIL] SMTP 설정 누락! 환경변수를 확인하세요.`);
process.stdout.write(`[EMAIL] ERROR: SMTP 설정 누락!\n`);
}
this.transporter = nodemailer.createTransport({
host: smtpHost,
port: parseInt(smtpPort || '587'),
secure: smtpPort === '465',
auth: {
user: this.configService.get('SMTP_USER'),
pass: this.configService.get('SMTP_PASS'),
user: smtpUser,
pass: smtpPass,
},
});
}
/**
* 인증번호 이메일 발송
*
* @async
* @param {string} email - 수신자 이메일
* @param {string} code - 6자리 인증번호
* @returns {Promise<void>}
*/
async sendVerificationCode(email: string, code: string): Promise<void> {
await this.transporter.sendMail({
from: this.configService.get('FROM_EMAIL'),
to: email,
subject: '[한우 유전능력 시스템] 인증번호 안내',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #333;">인증번호 안내</h2>
<p>아래 인증번호를 입력해주세요.</p>
<div style="background-color: #f5f5f5; padding: 20px; text-align: center; margin: 20px 0;">
<h1 style="color: #4CAF50; font-size: 32px; margin: 0;">${code}</h1>
const fromEmail = this.configService.get('FROM_EMAIL');
this.logger.log(`[EMAIL] ========== 이메일 발송 시작 ==========`);
this.logger.log(`[EMAIL] From: ${fromEmail}`);
this.logger.log(`[EMAIL] To: ${email}`);
this.logger.log(`[EMAIL] Code: ${code}`);
process.stdout.write(`[EMAIL] Sending to: ${email}, code: ${code}\n`);
try {
const result = await this.transporter.sendMail({
from: fromEmail,
to: email,
subject: '[한우 유전능력 시스템] 인증번호 안내',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #333;">인증번호 안내</h2>
<p>아래 인증번호를 입력해주세요.</p>
<div style="background-color: #f5f5f5; padding: 20px; text-align: center; margin: 20px 0;">
<h1 style="color: #4CAF50; font-size: 32px; margin: 0;">${code}</h1>
</div>
<p style="color: #666;">인증번호는 3분간 유효합니다.</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="color: #999; font-size: 12px;">본 메일은 발신 전용입니다.</p>
</div>
<p style="color: #666;">인증번호는 3분간 유효합니다.</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="color: #999; font-size: 12px;">본 메일은 발신 전용입니다.</p>
</div>
`,
});
`,
});
this.logger.log(`[EMAIL] 발송 성공 - MessageId: ${result.messageId}`);
this.logger.log(`[EMAIL] Response: ${JSON.stringify(result)}`);
process.stdout.write(`[EMAIL] SUCCESS - MessageId: ${result.messageId}\n`);
} catch (error) {
this.logger.error(`[EMAIL] ========== 발송 실패 ==========`);
this.logger.error(`[EMAIL] Error Name: ${error.name}`);
this.logger.error(`[EMAIL] Error Message: ${error.message}`);
this.logger.error(`[EMAIL] Error Code: ${error.code}`);
this.logger.error(`[EMAIL] Stack: ${error.stack}`);
process.stdout.write(`[EMAIL] ERROR: ${error.message}\n`);
process.stdout.write(`[EMAIL] STACK: ${error.stack}\n`);
throw error;
}
}
}

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';
import * as crypto from 'crypto';
@@ -6,92 +6,138 @@ import { VERIFICATION_CONFIG } from 'src/common/config/VerificationConfig';
/**
* 인증번호 생성 및 검증 서비스 (Redis 기반)
*
* @export
* @class VerificationService
*/
@Injectable()
export class VerificationService {
constructor(@InjectRedis() private readonly redis: Redis) {}
private readonly logger = new Logger(VerificationService.name);
/**
* 6자리 인증번호 생성
*
* @returns {string} 6자리 숫자
*/
generateCode(): string {
return Math.floor(100000 + Math.random() * 900000).toString();
}
constructor(@InjectRedis() private readonly redis: Redis) {
this.logger.log(`[REDIS] VerificationService 초기화`);
process.stdout.write(`[REDIS] VerificationService initialized\n`);
// Redis 연결 상태 로깅
this.checkRedisConnection();
}
/**
* 인증번호 저장 (Redis 3분 후 자동 삭제)
*
* @async
* @param {string} key - Redis 키 (예: find-id:test@example.com)
* @param {string} code - 인증번호
* @returns {Promise<void>}
*/
async saveCode(key: string, code: string): Promise<void> {
await this.redis.set(key, code, 'EX', VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS);
}
private async checkRedisConnection(): Promise<void> {
try {
const pong = await this.redis.ping();
this.logger.log(`[REDIS] 연결 상태: ${pong}`);
process.stdout.write(`[REDIS] Connection status: ${pong}\n`);
} catch (error) {
this.logger.error(`[REDIS] 연결 실패: ${error.message}`);
process.stdout.write(`[REDIS] Connection FAILED: ${error.message}\n`);
}
}
/**
* 인증번호 검증
*
* @async
* @param {string} key - Redis 키
* @param {string} code - 사용자가 입력한 인증번호
* @returns {Promise<boolean>} 검증 성공 여부
*/
async verifyCode(key: string, code: string): Promise<boolean> {
const savedCode = await this.redis.get(key);
console.log(`[DEBUG VerificationService] Key: ${key}, Input code: ${code}, Saved code: ${savedCode}`);
/**
* 6자리 인증번호 생성
*/
generateCode(): string {
const code = Math.floor(100000 + Math.random() * 900000).toString();
this.logger.log(`[REDIS] 인증번호 생성: ${code}`);
return code;
}
if (!savedCode) {
console.log(`[DEBUG VerificationService] No saved code found for key: ${key}`);
return false; // 인증번호 없음 (만료 또는 미발급)
}
/**
* 인증번호 저장 (Redis)
*/
async saveCode(key: string, code: string): Promise<void> {
this.logger.log(`[REDIS] ========== 코드 저장 시작 ==========`);
this.logger.log(`[REDIS] Key: ${key}`);
this.logger.log(`[REDIS] Code: ${code}`);
this.logger.log(`[REDIS] TTL: ${VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS}`);
process.stdout.write(`[REDIS] Saving - Key: ${key}, Code: ${code}\n`);
if (savedCode !== code) {
console.log(`[DEBUG VerificationService] Code mismatch - Saved: ${savedCode}, Input: ${code}`);
return false; // 인증번호 불일치
}
try {
const result = await this.redis.set(key, code, 'EX', VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS);
this.logger.log(`[REDIS] 저장 결과: ${result}`);
process.stdout.write(`[REDIS] Save result: ${result}\n`);
} catch (error) {
this.logger.error(`[REDIS] ========== 저장 실패 ==========`);
this.logger.error(`[REDIS] Error: ${error.message}`);
this.logger.error(`[REDIS] Stack: ${error.stack}`);
process.stdout.write(`[REDIS] SAVE ERROR: ${error.message}\n`);
process.stdout.write(`[REDIS] STACK: ${error.stack}\n`);
throw error;
}
}
// 검증 성공 시 Redis에서 삭제 (1회용)
await this.redis.del(key);
console.log(`[DEBUG VerificationService] Code verified successfully!`);
return true;
}
/**
* 인증번호 검증
*/
async verifyCode(key: string, code: string): Promise<boolean> {
this.logger.log(`[REDIS] ========== 코드 검증 시작 ==========`);
this.logger.log(`[REDIS] Key: ${key}`);
this.logger.log(`[REDIS] Input Code: ${code}`);
process.stdout.write(`[REDIS] Verifying - Key: ${key}, Input: ${code}\n`);
/**
* 비밀번호 재설정 토큰 생성 및 저장
*
* @async
* @param {string} userId - 사용자 ID
* @returns {Promise<string>} 재설정 토큰
*/
async generateResetToken(userId: string): Promise<string> {
const token = crypto.randomBytes(VERIFICATION_CONFIG.TOKEN_BYTES_LENGTH).toString('hex');
await this.redis.set(`reset:${token}`, userId, 'EX', VERIFICATION_CONFIG.RESET_TOKEN_EXPIRY_SECONDS);
return token;
}
try {
const savedCode = await this.redis.get(key);
this.logger.log(`[REDIS] Saved Code: ${savedCode}`);
process.stdout.write(`[REDIS] Saved code: ${savedCode}\n`);
/**
* 비밀번호 재설정 토큰 검증
*
* @async
* @param {string} token - 재설정 토큰
* @returns {Promise<string | null>} 사용자 ID 또는 null
*/
async verifyResetToken(token: string): Promise<string | null> {
const userId = await this.redis.get(`reset:${token}`);
if (!savedCode) {
this.logger.warn(`[REDIS] 저장된 코드 없음 (만료 또는 미발급)`);
return false;
}
if (!userId) {
return null; // 토큰 없음 (만료 또는 미발급)
}
if (savedCode !== code) {
this.logger.warn(`[REDIS] 코드 불일치 - Saved: ${savedCode}, Input: ${code}`);
return false;
}
// 검증 성공 시 토큰 삭제 (1회용)
await this.redis.del(`reset:${token}`);
return userId;
}
await this.redis.del(key);
this.logger.log(`[REDIS] 검증 성공, 코드 삭제됨`);
return true;
} catch (error) {
this.logger.error(`[REDIS] ========== 검증 실패 ==========`);
this.logger.error(`[REDIS] Error: ${error.message}`);
this.logger.error(`[REDIS] Stack: ${error.stack}`);
process.stdout.write(`[REDIS] VERIFY ERROR: ${error.message}\n`);
throw error;
}
}
/**
* 비밀번호 재설정 토큰 생성 및 저장
*/
async generateResetToken(userId: string): Promise<string> {
this.logger.log(`[REDIS] 리셋 토큰 생성 - userId: ${userId}`);
try {
const token = crypto.randomBytes(VERIFICATION_CONFIG.TOKEN_BYTES_LENGTH).toString('hex');
await this.redis.set(`reset:${token}`, userId, 'EX', VERIFICATION_CONFIG.RESET_TOKEN_EXPIRY_SECONDS);
this.logger.log(`[REDIS] 리셋 토큰 저장 완료`);
return token;
} catch (error) {
this.logger.error(`[REDIS] 리셋 토큰 생성 실패: ${error.message}`);
throw error;
}
}
/**
* 비밀번호 재설정 토큰 검증
*/
async verifyResetToken(token: string): Promise<string | null> {
this.logger.log(`[REDIS] 리셋 토큰 검증`);
try {
const userId = await this.redis.get(`reset:${token}`);
if (!userId) {
this.logger.warn(`[REDIS] 리셋 토큰 없음 또는 만료`);
return null;
}
await this.redis.del(`reset:${token}`);
this.logger.log(`[REDIS] 리셋 토큰 검증 성공 - userId: ${userId}`);
return userId;
} catch (error) {
this.logger.error(`[REDIS] 리셋 토큰 검증 실패: ${error.message}`);
throw error;
}
}
}