using Application.Abstractions.Data; using Application.Abstractions.Cache; using Application.Abstractions.Messaging; using Application.Abstractions.Messaging.Email; using Application.Helpers; using Domain.Entities.EmailVerification; using Domain.Entities.EmailVerification.ValueObject; using SharedKernel; using SharedKernel.Results; using Domain.Entities.Members.Logs; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using System.Security.Cryptography; namespace Application.Features.Api.MyPage.ChangeEmail; internal sealed class Handler( IAppDbContext db, ICacheService cache, IMailService mailService, IOptions options ) : ICommandHandler { public async Task Handle(Command request, CancellationToken ct) { var newEmail = request.NewEmail?.Trim().ToLower(); if (string.IsNullOrWhiteSpace(newEmail)) { return Result.Failure(Error.Problem("MyPage.EmailRequired", "이메일은 필수입니다.")); } if (newEmail.Length > 60) { return Result.Failure(Error.Problem("MyPage.EmailTooLong", "이메일은 60자 이하여야 합니다.")); } var member = await db.Member.FirstOrDefaultAsync(m => m.ID == request.MemberID, ct); if (member is null) { return Result.Failure(Error.NotFound("MyPage.MemberNotFound", "회원 정보를 찾을 수 없습니다.")); } // 이메일 변경 주기 확인 var accountConfig = await AccountConfigLoader.GetAccountConfigAsync(cache, db, ct); if (accountConfig.ChangeEmailDay is > 0 && member.LastEmailChangedAt.HasValue) { var nextChangeDate = member.LastEmailChangedAt.Value.AddDays(accountConfig.ChangeEmailDay.Value); if (DateTime.UtcNow < nextChangeDate) { return Result.Failure(Error.Problem("MyPage.EmailChangeTooSoon", $"이메일은 {accountConfig.ChangeEmailDay}일마다 변경 가능합니다.")); } } if (member.Email == newEmail) { return Result.Failure(Error.Problem("MyPage.SameEmail", "현재 이메일과 동일합니다.")); } var emailExists = await db.Member.AnyAsync(m => m.Email == newEmail, ct); if (emailExists) { return Result.Failure(Error.Conflict("MyPage.EmailExists", "이미 사용 중인 이메일입니다.")); } // 인증 토큰 생성 var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)); var expiration = DateTime.UtcNow.AddHours(24); var verifyToken = EmailVerifyToken.Create( VerificationType.ChangedEmail, newEmail, token, expiration, new AdditionalData { Email = member.Email } ); await db.EmailVerifyToken.AddAsync(verifyToken, ct); // 변경 로그 기록 var log = new MemberEmailChangeLog( request.MemberID, member.Email, newEmail, request.Referrer, request.IpAddress, request.UserAgent ); await db.MemberEmailChangeLog.AddAsync(log, ct); await db.SaveChangesAsync(ct); // 인증 메일 발송 var frontURL = options.Value.App.FrontURL.TrimEnd('/'); var verifyUrl = $"{frontURL}/verify-email?token={Uri.EscapeDataString(token)}"; await mailService.SendAsync(new SendData( newEmail, "[bitforum] 이메일 인증", $"

아래 링크를 클릭하여 이메일 인증을 완료해주세요.

{verifyUrl}

이 링크는 24시간 동안 유효합니다.

" ), ct); return Result.Success(); } }