using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Security.Cryptography;
using Domain.Entities.Members.ValueObject;
using Domain.Entities.Wallets;
using NanoidDotNet;
///
/// 사용자단 일반 회원 정보
///
namespace Domain.Entities.Members;
public class Member
{
[Key]
public int ID { get; private set; }
// 회원 등급 정보
[ForeignKey(nameof(MemberGradeID))]
public virtual MemberGrade? MemberGrade { get; private set; }
// 알림/약관 동의 여부
public virtual MemberApprove MemberApprove { get; private set; } = null!;
// 회원 집계 정보
public virtual MemberStats MemberStats { get; private set; } = null!;
// 지갑
public virtual Wallet? Wallet { get; private set; }
// 채널
public virtual Channel? Channel { get; private set; }
public int? MemberGradeID { get; private set; }
public string SID { get; private set; } = Nanoid.Generate(size: 8); // 무작위 문자열
public string Email { get; private set; } = default!;
public string? Name { get; private set; }
public string? FullName { get; private set; }
public string? FirstName { get; private set; }
public string? LastName { get; private set; }
public string? Password { get; private set; } = string.Empty; // 관리자 수정 시 필요
public string? PasswordHash { get; private set; }
public string? Intro { get; private set; }
public string? Summary { get; private set; }
public string? Phone { get; private set; }
public DateOnly? Birthday { get; private set; }
public Gender? Gender { get; private set; }
public string? Thumb { get; private set; }
public string? BannerUrl { get; private set; }
public string? Icon { get; private set; }
///
/// PG customerKey (토스페이먼츠 — 예측 가능 값 금지, 회원별 UUID).
/// 결제 주문 생성 시 없으면 발급·저장한다. 마이그레이션은 후속 처리.
///
public Guid? PaymentCustomerKey { get; private set; }
public bool IsEmailVerified { get; private set; } = false;
public bool IsAuthCertified { get; private set; } = false;
public bool IsDenied { get; private set; } = false;
public bool IsAdmin { get; private set; } = false;
public bool IsWithdraw { get; private set; } = false;
public bool IsCreator { get; private set; } = false;
public string? DeviceInfo { get; private set; }
public string? SignupIP { get; private set; }
public string? LastLoginIp { get; private set; }
public string? IpAddress { get; private set; }
public string? UserAgent { get; private set; }
public DateTime? LastLoginAt { get; private set; }
public DateTime? LastEmailChangedAt { get; private set; }
public DateTime? LastNameChangedAt { get; private set; }
public DateTime? LastSummaryChangedAt { get; private set; }
public DateTime? LastIntroChangedAt { get; private set; }
public DateTime? LastThumbChangedAt { get; private set; }
public DateTime? EmailVerifiedAt { get; private set; }
public DateTime? AuthCertifiedAt { get; private set; }
public DateTime PasswordUpdatedAt { get; private set; }
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; private set; }
public DateTime? DeletedAt { get; private set; }
public DateTime? DeniedAt { get; private set; }
private Member() { }
private Member(string email)
{
if (string.IsNullOrWhiteSpace(email))
{
throw new ArgumentException("Email is required.", nameof(email));
}
if (email.Length > 60)
{
throw new ArgumentOutOfRangeException(nameof(email));
}
Email = email;
PasswordUpdatedAt = DateTime.UtcNow;
}
public static Member Create(string email)
{
return new(email);
}
public static Member Create(string email, string password)
{
var member = new Member(email);
member.SetPassword(password);
return member;
}
public void SetPassword(string password)
{
PasswordHash = HashPassword(password);
PasswordUpdatedAt = DateTime.UtcNow;
}
public bool VerifyPassword(string password)
{
if (string.IsNullOrEmpty(PasswordHash))
{
return false;
}
return VerifyHashedPassword(PasswordHash, password);
}
public void SetEmail(string email)
{
Email = email;
IsEmailVerified = false;
LastEmailChangedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
public void SetName(string? name)
{
Name = name;
LastNameChangedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
public void SetSummary(string? summary)
{
Summary = summary;
LastSummaryChangedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
public void SetIntro(string? intro)
{
Intro = intro;
LastIntroChangedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
public void SetThumb(string? thumb)
{
Thumb = thumb;
LastThumbChangedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
public void SetBannerUrl(string? bannerUrl)
{
BannerUrl = bannerUrl;
UpdatedAt = DateTime.UtcNow;
}
public void SetCreator(bool isCreator)
{
IsCreator = isCreator;
UpdatedAt = DateTime.UtcNow;
}
/// 회원 등급 설정 — 보상 엔진 자동 승급(래칫)에서 사용 (d3 M2).
public void SetMemberGrade(int memberGradeID)
{
MemberGradeID = memberGradeID;
UpdatedAt = DateTime.UtcNow;
}
/// PG customerKey 확보 — 없으면 이 시점에 UUID 발급 (호출측이 SaveChanges 로 영속화)
public Guid EnsurePaymentCustomerKey()
{
if (PaymentCustomerKey is null)
{
PaymentCustomerKey = Guid.NewGuid();
UpdatedAt = DateTime.UtcNow;
}
return PaymentCustomerKey.Value;
}
public void MarkEmailVerified()
{
IsEmailVerified = true;
EmailVerifiedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
public void Withdraw()
{
IsWithdraw = true;
DeletedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
public void RecordLogin(string? ipAddress, string? userAgent = null)
{
// DB 컬럼 길이 방어 — Cloudflare 더블 경유 등으로 비정상 길이 값이 와도 throw 대신 truncate.
// (IpAddress/LastLoginIp nvarchar(45), UserAgent nvarchar(255))
if (ipAddress is not null && ipAddress.Length > 45)
{
ipAddress = ipAddress[..45];
}
if (userAgent is not null && userAgent.Length > 255)
{
userAgent = userAgent[..255];
}
LastLoginAt = DateTime.UtcNow;
LastLoginIp = ipAddress;
if (!string.IsNullOrWhiteSpace(ipAddress))
{
IpAddress = ipAddress;
}
if (!string.IsNullOrWhiteSpace(userAgent))
{
UserAgent = userAgent;
}
UpdatedAt = DateTime.UtcNow;
}
// PBKDF2 (RFC 2898) - 순수 C# 비밀번호 해싱
// public: Admin 회원 생성/수정의 bulk ExecuteUpdate 에서 평문 대신 해시를 저장하기 위해 노출
public static string HashPassword(string password)
{
var salt = RandomNumberGenerator.GetBytes(16);
var hash = Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
iterations: 100_000,
HashAlgorithmName.SHA256,
outputLength: 32
);
// salt + hash → Base64
var combined = new byte[salt.Length + hash.Length];
Buffer.BlockCopy(salt, 0, combined, 0, salt.Length);
Buffer.BlockCopy(hash, 0, combined, salt.Length, hash.Length);
return Convert.ToBase64String(combined);
}
// 비밀번호 검증
private static bool VerifyHashedPassword(string hashedPassword, string password)
{
var combined = Convert.FromBase64String(hashedPassword);
if (combined.Length != 48) // 16(salt) + 32(hash)
{
return false;
}
var salt = combined[..16];
var storedHash = combined[16..];
var hash = Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
iterations: 100_000,
HashAlgorithmName.SHA256,
outputLength: 32
);
return CryptographicOperations.FixedTimeEquals(storedHash, hash);
}
}