using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Domain.Entities.Members; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.TrackRecord.GetLeaderboard; /// /// 예측 적중률 리더보드 조회 (D2 M4). 필터 인덱스(IX_MemberTrackRecord_HitRate WHERE Predictions>=10)를 활용해 /// 채점 표본(Hits+Misses) 10+ 회원을 HitRate 내림차순으로 페이징한다. Redis 캐시는 v1 미사용(필터 인덱스로 충분). /// internal sealed class Handler(IAppDbContext db) : IQueryHandler { private const int PageSize = 50; /// 리더보드 게이트 — 채점 표본(Hits+Misses) 최소치 (d2 §③ 표본 10+). private const int MinSample = TrackRecordTiers.MinSample; public async Task Handle(Query request, CancellationToken ct) { var page = request.Page < 1 ? 1 : request.Page; // 표본(채점 완료) = Hits + Misses. 필터 인덱스는 Predictions>=10 이지만 Void 가 껴 실제 표본이 10 미만일 수 // 있어 Hits+Misses>=10 을 명시적으로 재확인한다(정확성 우선, 인덱스는 후보 축소). var query = db.MemberTrackRecord.AsNoTracking().Where(c => c.Hits + c.Misses >= MinSample); var total = await query.CountAsync(ct); var rows = await query .OrderByDescending(c => c.HitRate).ThenByDescending(c => c.Hits + c.Misses).ThenBy(c => c.MemberID) .Skip((page - 1) * PageSize) .Take(PageSize) .Select(c => new { c.Member.SID, c.Member.Name, c.Tier, c.HitRate, c.Predictions, c.Hits, c.Misses, c.BestStreak }) .ToListAsync(ct); var baseRank = (page - 1) * PageSize; return new Response { Total = total, Period = "all", List = [..rows.Select((c, i) => new Response.Row { Rank = baseRank + i + 1, SID = c.SID, Name = c.Name ?? string.Empty, Tier = c.Tier.ToString(), TierCode = (byte)c.Tier, HitRate = c.HitRate, Predictions = c.Predictions, Hits = c.Hits, Misses = c.Misses, BestStreak = c.BestStreak })] }; } }