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));
}
}
}