Handler.cs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. using SharedKernel.Results;
  2. using Application.Abstractions.Data;
  3. using Application.Abstractions.Cache;
  4. using Application.Abstractions.Messaging.Email;
  5. using Application.Helpers;
  6. using Domain.Entities.EmailVerification;
  7. using Domain.Entities.EmailVerification.ValueObject;
  8. using MediatR;
  9. using Microsoft.EntityFrameworkCore;
  10. namespace Application.Features.Api.Auth.Register;
  11. internal sealed class Handler(
  12. IAppDbContext db,
  13. ICacheService cache,
  14. IMailService mailService
  15. ) : IRequestHandler<Command, Result<RegisterResponse>> {
  16. public async Task<Result<RegisterResponse>> Handle(Command request, CancellationToken ct)
  17. {
  18. // 유효성 검사
  19. if (string.IsNullOrWhiteSpace(request.Email))
  20. {
  21. return Result.Failure<RegisterResponse>(Error.Problem("Auth.EmailRequired", "이메일은 필수입니다."));
  22. }
  23. if (string.IsNullOrWhiteSpace(request.Password))
  24. {
  25. return Result.Failure<RegisterResponse>(Error.Problem("Auth.PasswordRequired", "비밀번호는 필수입니다."));
  26. }
  27. // Config 로드
  28. var accountConfig = await AccountConfigLoader.GetAccountConfigAsync(cache, db, ct);
  29. // 회원가입 차단 확인
  30. if (accountConfig.IsRegisterBlock)
  31. {
  32. return Result.Failure<RegisterResponse>(Error.Problem("Auth.RegisterBlocked", "현재 회원가입이 차단되어 있습니다."));
  33. }
  34. // 비밀번호 복잡도 검증
  35. var passwordResult = PasswordPolicyValidator.Validate(request.Password, accountConfig);
  36. if (!passwordResult.IsSuccess)
  37. {
  38. return Result.Failure<RegisterResponse>(passwordResult.Error);
  39. }
  40. // 이메일 중복 체크
  41. var email = request.Email.Trim().ToLower();
  42. var exists = await db.Member.AnyAsync(m => m.Email == email, ct);
  43. if (exists)
  44. {
  45. return Result.Failure<RegisterResponse>(Error.Conflict("Auth.EmailExists", "이미 사용 중인 이메일입니다."));
  46. }
  47. // 금지 이메일 체크
  48. if (!string.IsNullOrWhiteSpace(accountConfig.DeniedEmailList))
  49. {
  50. var deniedEmails = accountConfig.DeniedEmailList
  51. .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
  52. .Select(e => e.ToLower());
  53. foreach (var denied in deniedEmails)
  54. {
  55. // 도메인 매칭 (@domain.com) 또는 전체 이메일 매칭
  56. if (denied.StartsWith('@') && email.EndsWith(denied))
  57. {
  58. return Result.Failure<RegisterResponse>(Error.Problem("Auth.DeniedEmail", "사용할 수 없는 이메일입니다."));
  59. }
  60. else if (email == denied)
  61. {
  62. return Result.Failure<RegisterResponse>(Error.Problem("Auth.DeniedEmail", "사용할 수 없는 이메일입니다."));
  63. }
  64. }
  65. }
  66. // 금지 별명 체크
  67. if (!string.IsNullOrWhiteSpace(request.Name) && !string.IsNullOrWhiteSpace(accountConfig.DeniedNameList))
  68. {
  69. var name = request.Name.Trim().ToLower();
  70. var deniedNames = accountConfig.DeniedNameList
  71. .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
  72. .Select(n => n.ToLower());
  73. if (deniedNames.Contains(name))
  74. {
  75. return Result.Failure<RegisterResponse>(Error.Problem("Auth.DeniedName", "사용할 수 없는 별명입니다."));
  76. }
  77. }
  78. // Member 생성 (비밀번호 해싱 포함)
  79. var member = Domain.Entities.Members.Member.Create(email, request.Password);
  80. await db.Member.AddAsync(member, ct);
  81. await db.SaveChangesAsync(ct);
  82. // Member ID 확보 후 연관 엔티티 생성
  83. await db.MemberApprove.AddAsync(
  84. Domain.Entities.Members.MemberApprove.Create(member.ID), ct
  85. );
  86. await db.MemberStats.AddAsync(
  87. Domain.Entities.Members.MemberStats.Create(member.ID), ct
  88. );
  89. await db.Wallet.AddAsync(
  90. Domain.Entities.Wallets.Wallet.Create(member.ID), ct
  91. );
  92. await db.SaveChangesAsync(ct);
  93. // 이메일 인증이 필요한 경우 인증번호 발송
  94. if (accountConfig.IsRegisterEmailAuth)
  95. {
  96. // 기존 미인증 코드 삭제
  97. var existing = await db.EmailVerifyNumber
  98. .Where(e => e.Email == email && e.Type == VerificationType.Registration && !e.IsVerified)
  99. .ToListAsync(ct);
  100. db.EmailVerifyNumber.RemoveRange(existing);
  101. // 6자리 랜덤 숫자 코드 생성
  102. var code = Random.Shared.Next(100000, 999999).ToString();
  103. var verifyNumber = EmailVerifyNumber.Create(
  104. VerificationType.Registration,
  105. email,
  106. code,
  107. DateTime.UtcNow.AddMinutes(5)
  108. );
  109. await db.EmailVerifyNumber.AddAsync(verifyNumber, ct);
  110. await db.SaveChangesAsync(ct);
  111. // 인증번호 이메일 발송
  112. await mailService.SendAsync(new SendData(
  113. email,
  114. "[bitforum] 회원가입 이메일 인증번호",
  115. $"<p>회원가입 이메일 인증번호입니다.</p><p><strong>{code}</strong></p><p>이 코드는 5분간 유효합니다.</p>"
  116. ), ct);
  117. }
  118. return Result.Success(new RegisterResponse(member.ID, accountConfig.IsRegisterEmailAuth));
  119. }
  120. }