using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Domain.Entities.Members; namespace Domain.Entities.Store; /// /// 회원 배송 주소. AES-GCM 으로 PII 필드 암호화 (수령인명/연락처/주소). ZipCode 만 평문. /// 키 회전 지원: 각 row 의 KeyVersion 컬럼으로 사용한 키 추적. /// 수령인명 검색은 RecipientName_Hash (SHA-256 hex) 로 수행. /// 1차 정책: 회원당 최대 5개 주소, IsDefault=true 는 정확히 1개. /// public class MemberAddress { [ForeignKey(nameof(MemberID))] public virtual Member? Member { get; private set; } [Key] public int ID { get; private set; } public int MemberID { get; private set; } public bool IsDefault { get; private set; } // AES-GCM 암호화 컬럼 (Base64) public string RecipientName_Encrypted { get; private set; } = default!; /// 수령인명 SHA-256 hex (검색용, 64자 대문자). 키 회전과 무관. public string RecipientName_Hash { get; private set; } = default!; public string Phone_Encrypted { get; private set; } = default!; public string ZipCode { get; private set; } = default!; public string Address1_Encrypted { get; private set; } = default!; public string Address2_Encrypted { get; private set; } = default!; /// 암호화 시 사용한 키 버전 (IFieldEncryptor.CurrentKeyVersion 스냅샷). public int KeyVersion { get; private set; } public DateTime CreatedAt { get; private set; } = DateTime.UtcNow; public DateTime? UpdatedAt { get; private set; } private MemberAddress() { } public static MemberAddress Create( int memberID, string recipientNameEncrypted, string recipientNameHash, string phoneEncrypted, string zipCode, string address1Encrypted, string address2Encrypted, int keyVersion, bool isDefault = false ) { if (memberID <= 0) { throw new ArgumentOutOfRangeException(nameof(memberID)); } ValidateEncryptedFields(recipientNameEncrypted, recipientNameHash, phoneEncrypted, zipCode, address1Encrypted, address2Encrypted, keyVersion); return new MemberAddress { MemberID = memberID, RecipientName_Encrypted = recipientNameEncrypted, RecipientName_Hash = recipientNameHash, Phone_Encrypted = phoneEncrypted, ZipCode = zipCode, Address1_Encrypted = address1Encrypted, Address2_Encrypted = address2Encrypted, KeyVersion = keyVersion, IsDefault = isDefault }; } public void Update( string recipientNameEncrypted, string recipientNameHash, string phoneEncrypted, string zipCode, string address1Encrypted, string address2Encrypted, int keyVersion ) { ValidateEncryptedFields(recipientNameEncrypted, recipientNameHash, phoneEncrypted, zipCode, address1Encrypted, address2Encrypted, keyVersion); RecipientName_Encrypted = recipientNameEncrypted; RecipientName_Hash = recipientNameHash; Phone_Encrypted = phoneEncrypted; ZipCode = zipCode; Address1_Encrypted = address1Encrypted; Address2_Encrypted = address2Encrypted; KeyVersion = keyVersion; UpdatedAt = DateTime.UtcNow; } public void SetAsDefault() { IsDefault = true; UpdatedAt = DateTime.UtcNow; } public void UnsetDefault() { IsDefault = false; UpdatedAt = DateTime.UtcNow; } private static void ValidateEncryptedFields( string recipientNameEncrypted, string recipientNameHash, string phoneEncrypted, string zipCode, string address1Encrypted, string address2Encrypted, int keyVersion ) { if (string.IsNullOrWhiteSpace(recipientNameEncrypted)) { throw new ArgumentException("RecipientName_Encrypted is required.", nameof(recipientNameEncrypted)); } if (string.IsNullOrWhiteSpace(recipientNameHash)) { throw new ArgumentException("RecipientName_Hash is required.", nameof(recipientNameHash)); } if (recipientNameHash.Length != 64) { throw new ArgumentOutOfRangeException(nameof(recipientNameHash), "Hash must be 64 hex chars (SHA-256)."); } if (string.IsNullOrWhiteSpace(phoneEncrypted)) { throw new ArgumentException("Phone_Encrypted is required.", nameof(phoneEncrypted)); } if (string.IsNullOrWhiteSpace(zipCode)) { throw new ArgumentException("ZipCode is required.", nameof(zipCode)); } if (zipCode.Length > 10) { throw new ArgumentOutOfRangeException(nameof(zipCode)); } if (string.IsNullOrWhiteSpace(address1Encrypted)) { throw new ArgumentException("Address1_Encrypted is required.", nameof(address1Encrypted)); } if (string.IsNullOrWhiteSpace(address2Encrypted)) { throw new ArgumentException("Address2_Encrypted is required.", nameof(address2Encrypted)); } if (keyVersion <= 0) { throw new ArgumentOutOfRangeException(nameof(keyVersion)); } } }