| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667 |
- using Application.Abstractions.Data;
- using Application.Abstractions.Messaging;
- using Domain.Entities.Members;
- using Microsoft.EntityFrameworkCore;
- namespace Application.Features.Api.TrackRecord.GetLeaderboard;
- /// <summary>
- /// 예측 적중률 리더보드 조회 (D2 M4). 필터 인덱스(IX_MemberTrackRecord_HitRate WHERE Predictions>=10)를 활용해
- /// 채점 표본(Hits+Misses) 10+ 회원을 HitRate 내림차순으로 페이징한다. Redis 캐시는 v1 미사용(필터 인덱스로 충분).
- /// </summary>
- internal sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
- {
- private const int PageSize = 50;
- /// <summary>리더보드 게이트 — 채점 표본(Hits+Misses) 최소치 (d2 §③ 표본 10+).</summary>
- private const int MinSample = TrackRecordTiers.MinSample;
- public async Task<Response> 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
- })]
- };
- }
- }
|