| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- using Application.Abstractions.Data;
- using Application.Abstractions.Messaging;
- using Domain.Entities.Donations.ValueObject;
- using Microsoft.EntityFrameworkCore;
- namespace Application.Features.Api.Ranking.GetRanking;
- internal sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
- {
- private const string AnonymousName = "비공개 사용자";
- public async Task<Response> Handle(Query request, CancellationToken ct)
- {
- var category = string.IsNullOrWhiteSpace(request.Category) || request.Category == "all" ? null : request.Category;
- var page = request.Page < 1 ? 1 : request.Page;
- var perPage = request.PerPage is < 1 or > 200 ? 50 : request.PerPage;
- var baseQuery = db.RankingSnapshot.AsNoTracking()
- .Where(r => r.Type == request.Type && r.Period == request.Period);
- baseQuery = category == null
- ? baseQuery.Where(r => r.Category == null)
- : baseQuery.Where(r => r.Category == category);
- var latestSnapshotAt = await baseQuery.MaxAsync(r => (DateTime?)r.SnapshotAt, ct);
- if (latestSnapshotAt == null)
- {
- return new Response([], [], null, 0, DateTime.MinValue, DateTime.MinValue, null);
- }
- var snapshotStart = latestSnapshotAt.Value.AddMinutes(-1);
- var snapshotQuery = baseQuery.Where(r => r.SnapshotAt >= snapshotStart && r.SnapshotAt <= latestSnapshotAt.Value);
- var total = await snapshotQuery.CountAsync(ct);
- if (total == 0)
- {
- return new Response([], [], null, 0, DateTime.MinValue, DateTime.MinValue, latestSnapshotAt);
- }
- var first = await snapshotQuery.OrderBy(r => r.Rank).FirstAsync(ct);
- var periodStart = first.PeriodStart;
- var periodEnd = first.PeriodEnd;
- var shouldAnonymize = request.Type == RankingType.Donor;
- var takeCount = Math.Max(3 + (page * perPage), perPage * 2);
- var rawEntries = await snapshotQuery.OrderBy(r => r.Rank).Take(takeCount)
- .Select(r => new RawEntry(
- r.Rank,
- r.PreviousRank,
- r.MemberID,
- r.Member != null ? r.Member.Name : null,
- r.Member != null ? r.Member.Thumb : null,
- r.Category,
- r.Score,
- r.MoneyAmount,
- r.DonorCount,
- r.BroadcastSec,
- r.ViewCount,
- r.LikeCount,
- r.Member != null && r.Member.MemberApprove != null && r.Member.MemberApprove.IsRankingPublic,
- r.Member != null && r.Member.MemberGrade != null ? new MemberGradeDto(
- r.Member.MemberGrade.ID,
- r.Member.MemberGrade.KorName,
- r.Member.MemberGrade.Image,
- r.Member.MemberGrade.TextColor,
- r.Member.MemberGrade.Order
- ) : null
- ))
- .ToListAsync(ct);
- var memberIds = rawEntries.Select(e => e.MemberID).ToList();
- // 채널명 + 채널 썸네일 묶음 — Donor 타입에서도 채널이 있을 수 있으므로 무조건 조회.
- // 익명 처리는 Map 안에서 따로 막음.
- var channelDict = await db.Channel.AsNoTracking()
- .Where(c => memberIds.Contains(c.MemberID))
- .GroupBy(c => c.MemberID)
- .Select(g => new {
- MemberID = g.Key,
- Name = g.First().Name,
- Thumbnail = g.First().ThumbnailUrl
- })
- .ToDictionaryAsync(x => x.MemberID, x => new ChannelInfo(x.Name, x.Thumbnail), ct);
- RankingEntry Map(RawEntry e) {
- var anonymize = shouldAnonymize && !e.IsRankingPublic;
- int? rankChange = e.PreviousRank.HasValue ? e.PreviousRank.Value - e.Rank : null;
- var channel = channelDict.GetValueOrDefault(e.MemberID);
- return new RankingEntry(
- e.Rank,
- e.PreviousRank,
- rankChange,
- e.MemberID,
- anonymize ? AnonymousName : (e.MemberName ?? ""),
- anonymize ? null : e.MemberThumb,
- anonymize ? null : channel?.Name,
- anonymize ? null : channel?.Thumbnail,
- e.Category,
- e.Score,
- e.MoneyAmount,
- e.DonorCount,
- e.BroadcastSec,
- e.ViewCount,
- e.LikeCount,
- anonymize,
- e.MemberGrade
- );
- }
- var top3 = rawEntries.Where(e => e.Rank <= 3).Select(Map).ToList();
- var skip = (page - 1) * perPage;
- var list = rawEntries.Where(e => e.Rank > 3).Skip(skip).Take(perPage).Select(Map).ToList();
- MyRankInfo? myRank = null;
- if (request.ViewerMemberID.HasValue)
- {
- var mine = await snapshotQuery.FirstOrDefaultAsync(r => r.MemberID == request.ViewerMemberID.Value, ct);
- if (mine != null)
- {
- int? change = mine.PreviousRank.HasValue ? mine.PreviousRank.Value - mine.Rank : null;
- myRank = new MyRankInfo(mine.Rank, mine.MoneyAmount, mine.Score, mine.PreviousRank, change);
- }
- }
- return new Response(top3, list, myRank, total, periodStart, periodEnd, latestSnapshotAt);
- }
- private sealed record ChannelInfo(string Name, string? Thumbnail);
- private sealed record RawEntry(
- int Rank,
- int? PreviousRank,
- int MemberID,
- string? MemberName,
- string? MemberThumb,
- string? Category,
- long Score,
- long MoneyAmount,
- int DonorCount,
- long BroadcastSec,
- long ViewCount,
- long LikeCount,
- bool IsRankingPublic,
- MemberGradeDto? MemberGrade
- );
- }
|