using Application.Abstractions.Crypto; using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; using MemberAddressEntity = Domain.Entities.Store.MemberAddress; namespace Application.Features.Api.MyPage.Address.Save; public sealed class Handler(IAppDbContext db, IFieldEncryptor encryptor) : ICommandHandler> { private const int MaxAddressesPerMember = 5; public async Task> Handle(Command request, CancellationToken ct) { // 입력 검증 if (string.IsNullOrWhiteSpace(request.RecipientName)) { return Result.Failure(Error.Problem("Address.RecipientRequired", "수령인명은 필수입니다.")); } if (string.IsNullOrWhiteSpace(request.Phone)) { return Result.Failure(Error.Problem("Address.PhoneRequired", "연락처는 필수입니다.")); } if (string.IsNullOrWhiteSpace(request.ZipCode) || request.ZipCode.Length > 10) { return Result.Failure(Error.Problem("Address.InvalidZipCode", "우편번호가 잘못되었습니다.")); } if (string.IsNullOrWhiteSpace(request.Address1) || string.IsNullOrWhiteSpace(request.Address2)) { return Result.Failure(Error.Problem("Address.RequiredFields", "주소를 정확히 입력해주세요.")); } // 암호화 var recipientEnc = encryptor.Encrypt(request.RecipientName); var recipientHash = encryptor.Hash(request.RecipientName); var phoneEnc = encryptor.Encrypt(request.Phone); var addr1Enc = encryptor.Encrypt(request.Address1); var addr2Enc = encryptor.Encrypt(request.Address2); var keyVersion = encryptor.CurrentKeyVersion; // IsDefault 처리: true 라면 다른 주소의 IsDefault 모두 false 로 if (request.IsDefault) { var others = await db.MemberAddress .Where(a => a.MemberID == request.MemberID && a.IsDefault) .ToListAsync(ct); foreach (var o in others) { if (request.ID.HasValue && o.ID == request.ID.Value) { continue; } o.UnsetDefault(); } } if (!request.ID.HasValue || request.ID.Value == 0) { // 신규 — 회원당 최대 개수 검증 var count = await db.MemberAddress.CountAsync(a => a.MemberID == request.MemberID, ct); if (count >= MaxAddressesPerMember) { return Result.Failure(Error.Problem("Address.MaxExceeded", $"주소는 회원당 최대 {MaxAddressesPerMember}개까지 등록 가능합니다.")); } // 첫 주소는 자동 default var isDefault = request.IsDefault || count == 0; var newAddress = MemberAddressEntity.Create( memberID: request.MemberID, recipientNameEncrypted: recipientEnc, recipientNameHash: recipientHash, phoneEncrypted: phoneEnc, zipCode: request.ZipCode, address1Encrypted: addr1Enc, address2Encrypted: addr2Enc, keyVersion: keyVersion, isDefault: isDefault ); db.MemberAddress.Add(newAddress); await db.SaveChangesAsync(ct); return Result.Success(newAddress.ID); } // 수정 var existing = await db.MemberAddress .FirstOrDefaultAsync(a => a.ID == request.ID.Value && a.MemberID == request.MemberID, ct); if (existing is null) { return Result.Failure(Error.NotFound("Address.NotFound", "수정할 주소를 찾을 수 없습니다.")); } existing.Update( recipientNameEncrypted: recipientEnc, recipientNameHash: recipientHash, phoneEncrypted: phoneEnc, zipCode: request.ZipCode, address1Encrypted: addr1Enc, address2Encrypted: addr2Enc, keyVersion: keyVersion ); if (request.IsDefault && !existing.IsDefault) { existing.SetAsDefault(); } else if (!request.IsDefault && existing.IsDefault) { existing.UnsetDefault(); } await db.SaveChangesAsync(ct); return Result.Success(existing.ID); } }