using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Domain.Entities.Members; /// /// 회원 예측 트랙레코드 집계 (D2 M4 예측 채점+트랙레코드, d2 §③). /// PredictionSettlementService(M4)가 채점 후 회원별로 재집계한다 — 매 실행마다 해당 회원의 /// 전체 non-Pending 예측으로 처음부터 다시 계산하므로 멱등하다. /// MemberID = PK(FK Member). Predictions=채점 완료 총계(Hits+Misses+Voids), HitRate=Hits/(Hits+Misses)*100(Void 제외). /// 리더보드는 채점 표본(Hits+Misses) 10+ 회원만 HitRate 내림차순 필터 인덱스로 조회한다. /// public class MemberTrackRecord { [ForeignKey(nameof(MemberID))] public virtual Member Member { get; private set; } = null!; /// 회원 ID (PK, FK Member) [Key] public int MemberID { get; private set; } /// 채점 완료 예측 총계 (Hits + Misses + Voids) public int Predictions { get; private set; } /// 적중 수 public int Hits { get; private set; } /// 실패 수 public int Misses { get; private set; } /// 무효 수 (거래정지·상폐·시세결측 — 적중률 표본 제외) public int Voids { get; private set; } /// 적중률 % = Hits/(Hits+Misses)*100 (Void 제외). 표본 0 이면 0. public decimal HitRate { get; private set; } /// 현재 연속 적중 수 (가장 최근 채점부터의 Hit 연속, Void 무시) public int CurrentStreak { get; private set; } /// 역대 최고 연속 적중 수 public int BestStreak { get; private set; } /// 배지 등급 (0=None/1=Bronze/2=Silver/3=Gold/4=GoldAnt) public TrackRecordTier Tier { get; private set; } public DateTime UpdatedAt { get; private set; } = DateTime.UtcNow; private MemberTrackRecord() { } /// 빈 트랙레코드 생성 (최초 채점 전). 이후 로 채운다. public static MemberTrackRecord Create(int memberID) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(memberID); return new MemberTrackRecord { MemberID = memberID }; } /// /// 집계값 일괄 갱신 (M4 채점 배치가 회원 예측 전체를 다시 계산해 호출 — 멱등 재집계). /// hitRate 는 백분율(0~100), tier 는 (hits+misses, hitRate) 로 산정한 값. /// public void Recompute(int predictions, int hits, int misses, int voids, decimal hitRate, int currentStreak, int bestStreak, TrackRecordTier tier) { Predictions = predictions; Hits = hits; Misses = misses; Voids = voids; HitRate = hitRate; CurrentStreak = currentStreak; BestStreak = bestStreak; Tier = tier; UpdatedAt = DateTime.UtcNow; } }