using SharedKernel.Results; using Application.Abstractions.Data; using Application.Abstractions.Cache; using Application.Abstractions.Messaging.Email; using Application.Helpers; using Domain.Entities.EmailVerification; using Domain.Entities.EmailVerification.ValueObject; using MediatR; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.Auth.Register; internal sealed class Handler( IAppDbContext db, ICacheService cache, IMailService mailService ) : IRequestHandler> { public async Task> Handle(Command request, CancellationToken ct) { // 유효성 검사 if (string.IsNullOrWhiteSpace(request.Email)) { return Result.Failure(Error.Problem("Auth.EmailRequired", "이메일은 필수입니다.")); } if (string.IsNullOrWhiteSpace(request.Password)) { return Result.Failure(Error.Problem("Auth.PasswordRequired", "비밀번호는 필수입니다.")); } // Config 로드 var accountConfig = await AccountConfigLoader.GetAccountConfigAsync(cache, db, ct); // 회원가입 차단 확인 if (accountConfig.IsRegisterBlock) { return Result.Failure(Error.Problem("Auth.RegisterBlocked", "현재 회원가입이 차단되어 있습니다.")); } // 비밀번호 복잡도 검증 var passwordResult = PasswordPolicyValidator.Validate(request.Password, accountConfig); if (!passwordResult.IsSuccess) { return Result.Failure(passwordResult.Error); } // 이메일 중복 체크 var email = request.Email.Trim().ToLower(); var exists = await db.Member.AnyAsync(m => m.Email == email, ct); if (exists) { return Result.Failure(Error.Conflict("Auth.EmailExists", "이미 사용 중인 이메일입니다.")); } // 금지 이메일 체크 if (!string.IsNullOrWhiteSpace(accountConfig.DeniedEmailList)) { var deniedEmails = accountConfig.DeniedEmailList .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select(e => e.ToLower()); foreach (var denied in deniedEmails) { // 도메인 매칭 (@domain.com) 또는 전체 이메일 매칭 if (denied.StartsWith('@') && email.EndsWith(denied)) { return Result.Failure(Error.Problem("Auth.DeniedEmail", "사용할 수 없는 이메일입니다.")); } else if (email == denied) { return Result.Failure(Error.Problem("Auth.DeniedEmail", "사용할 수 없는 이메일입니다.")); } } } // 금지 별명 체크 if (!string.IsNullOrWhiteSpace(request.Name) && !string.IsNullOrWhiteSpace(accountConfig.DeniedNameList)) { var name = request.Name.Trim().ToLower(); var deniedNames = accountConfig.DeniedNameList .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select(n => n.ToLower()); if (deniedNames.Contains(name)) { return Result.Failure(Error.Problem("Auth.DeniedName", "사용할 수 없는 별명입니다.")); } } // Member 생성 (비밀번호 해싱 포함) var member = Domain.Entities.Members.Member.Create(email, request.Password); await db.Member.AddAsync(member, ct); await db.SaveChangesAsync(ct); // Member ID 확보 후 연관 엔티티 생성 await db.MemberApprove.AddAsync( Domain.Entities.Members.MemberApprove.Create(member.ID), ct ); await db.MemberStats.AddAsync( Domain.Entities.Members.MemberStats.Create(member.ID), ct ); await db.Wallet.AddAsync( Domain.Entities.Wallets.Wallet.Create(member.ID), ct ); await db.SaveChangesAsync(ct); // 이메일 인증이 필요한 경우 인증번호 발송 if (accountConfig.IsRegisterEmailAuth) { // 기존 미인증 코드 삭제 var existing = await db.EmailVerifyNumber .Where(e => e.Email == email && e.Type == VerificationType.Registration && !e.IsVerified) .ToListAsync(ct); db.EmailVerifyNumber.RemoveRange(existing); // 6자리 랜덤 숫자 코드 생성 var code = Random.Shared.Next(100000, 999999).ToString(); var verifyNumber = EmailVerifyNumber.Create( VerificationType.Registration, email, code, DateTime.UtcNow.AddMinutes(5) ); await db.EmailVerifyNumber.AddAsync(verifyNumber, ct); await db.SaveChangesAsync(ct); // 인증번호 이메일 발송 await mailService.SendAsync(new SendData( email, "[bitforum] 회원가입 이메일 인증번호", $"

회원가입 이메일 인증번호입니다.

{code}

이 코드는 5분간 유효합니다.

" ), ct); } return Result.Success(new RegisterResponse(member.ID, accountConfig.IsRegisterEmailAuth)); } }