MemberAddress.cs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. using System.ComponentModel.DataAnnotations;
  2. using System.ComponentModel.DataAnnotations.Schema;
  3. using Domain.Entities.Members;
  4. namespace Domain.Entities.Store;
  5. /// <summary>
  6. /// 회원 배송 주소. AES-GCM 으로 PII 필드 암호화 (수령인명/연락처/주소). ZipCode 만 평문.
  7. /// 키 회전 지원: 각 row 의 KeyVersion 컬럼으로 사용한 키 추적.
  8. /// 수령인명 검색은 RecipientName_Hash (SHA-256 hex) 로 수행.
  9. /// 1차 정책: 회원당 최대 5개 주소, IsDefault=true 는 정확히 1개.
  10. /// </summary>
  11. public class MemberAddress
  12. {
  13. [ForeignKey(nameof(MemberID))]
  14. public virtual Member? Member { get; private set; }
  15. [Key]
  16. public int ID { get; private set; }
  17. public int MemberID { get; private set; }
  18. public bool IsDefault { get; private set; }
  19. // AES-GCM 암호화 컬럼 (Base64)
  20. public string RecipientName_Encrypted { get; private set; } = default!;
  21. /// <summary>수령인명 SHA-256 hex (검색용, 64자 대문자). 키 회전과 무관.</summary>
  22. public string RecipientName_Hash { get; private set; } = default!;
  23. public string Phone_Encrypted { get; private set; } = default!;
  24. public string ZipCode { get; private set; } = default!;
  25. public string Address1_Encrypted { get; private set; } = default!;
  26. public string Address2_Encrypted { get; private set; } = default!;
  27. /// <summary>암호화 시 사용한 키 버전 (IFieldEncryptor.CurrentKeyVersion 스냅샷).</summary>
  28. public int KeyVersion { get; private set; }
  29. public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
  30. public DateTime? UpdatedAt { get; private set; }
  31. private MemberAddress() { }
  32. public static MemberAddress Create(
  33. int memberID,
  34. string recipientNameEncrypted,
  35. string recipientNameHash,
  36. string phoneEncrypted,
  37. string zipCode,
  38. string address1Encrypted,
  39. string address2Encrypted,
  40. int keyVersion,
  41. bool isDefault = false
  42. ) {
  43. if (memberID <= 0)
  44. {
  45. throw new ArgumentOutOfRangeException(nameof(memberID));
  46. }
  47. ValidateEncryptedFields(recipientNameEncrypted, recipientNameHash, phoneEncrypted, zipCode, address1Encrypted, address2Encrypted, keyVersion);
  48. return new MemberAddress
  49. {
  50. MemberID = memberID,
  51. RecipientName_Encrypted = recipientNameEncrypted,
  52. RecipientName_Hash = recipientNameHash,
  53. Phone_Encrypted = phoneEncrypted,
  54. ZipCode = zipCode,
  55. Address1_Encrypted = address1Encrypted,
  56. Address2_Encrypted = address2Encrypted,
  57. KeyVersion = keyVersion,
  58. IsDefault = isDefault
  59. };
  60. }
  61. public void Update(
  62. string recipientNameEncrypted,
  63. string recipientNameHash,
  64. string phoneEncrypted,
  65. string zipCode,
  66. string address1Encrypted,
  67. string address2Encrypted,
  68. int keyVersion
  69. ) {
  70. ValidateEncryptedFields(recipientNameEncrypted, recipientNameHash, phoneEncrypted, zipCode, address1Encrypted, address2Encrypted, keyVersion);
  71. RecipientName_Encrypted = recipientNameEncrypted;
  72. RecipientName_Hash = recipientNameHash;
  73. Phone_Encrypted = phoneEncrypted;
  74. ZipCode = zipCode;
  75. Address1_Encrypted = address1Encrypted;
  76. Address2_Encrypted = address2Encrypted;
  77. KeyVersion = keyVersion;
  78. UpdatedAt = DateTime.UtcNow;
  79. }
  80. public void SetAsDefault()
  81. {
  82. IsDefault = true;
  83. UpdatedAt = DateTime.UtcNow;
  84. }
  85. public void UnsetDefault()
  86. {
  87. IsDefault = false;
  88. UpdatedAt = DateTime.UtcNow;
  89. }
  90. private static void ValidateEncryptedFields(
  91. string recipientNameEncrypted,
  92. string recipientNameHash,
  93. string phoneEncrypted,
  94. string zipCode,
  95. string address1Encrypted,
  96. string address2Encrypted,
  97. int keyVersion
  98. ) {
  99. if (string.IsNullOrWhiteSpace(recipientNameEncrypted))
  100. {
  101. throw new ArgumentException("RecipientName_Encrypted is required.", nameof(recipientNameEncrypted));
  102. }
  103. if (string.IsNullOrWhiteSpace(recipientNameHash))
  104. {
  105. throw new ArgumentException("RecipientName_Hash is required.", nameof(recipientNameHash));
  106. }
  107. if (recipientNameHash.Length != 64)
  108. {
  109. throw new ArgumentOutOfRangeException(nameof(recipientNameHash), "Hash must be 64 hex chars (SHA-256).");
  110. }
  111. if (string.IsNullOrWhiteSpace(phoneEncrypted))
  112. {
  113. throw new ArgumentException("Phone_Encrypted is required.", nameof(phoneEncrypted));
  114. }
  115. if (string.IsNullOrWhiteSpace(zipCode))
  116. {
  117. throw new ArgumentException("ZipCode is required.", nameof(zipCode));
  118. }
  119. if (zipCode.Length > 10)
  120. {
  121. throw new ArgumentOutOfRangeException(nameof(zipCode));
  122. }
  123. if (string.IsNullOrWhiteSpace(address1Encrypted))
  124. {
  125. throw new ArgumentException("Address1_Encrypted is required.", nameof(address1Encrypted));
  126. }
  127. if (string.IsNullOrWhiteSpace(address2Encrypted))
  128. {
  129. throw new ArgumentException("Address2_Encrypted is required.", nameof(address2Encrypted));
  130. }
  131. if (keyVersion <= 0)
  132. {
  133. throw new ArgumentOutOfRangeException(nameof(keyVersion));
  134. }
  135. }
  136. }