Handler.cs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Domain.Entities.Donations.ValueObject;
  4. using Microsoft.EntityFrameworkCore;
  5. namespace Application.Features.Api.Ranking.GetRanking;
  6. internal sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
  7. {
  8. private const string AnonymousName = "비공개 사용자";
  9. public async Task<Response> Handle(Query request, CancellationToken ct)
  10. {
  11. var category = string.IsNullOrWhiteSpace(request.Category) || request.Category == "all" ? null : request.Category;
  12. var page = request.Page < 1 ? 1 : request.Page;
  13. var perPage = request.PerPage is < 1 or > 200 ? 50 : request.PerPage;
  14. var baseQuery = db.RankingSnapshot.AsNoTracking()
  15. .Where(r => r.Type == request.Type && r.Period == request.Period);
  16. baseQuery = category == null
  17. ? baseQuery.Where(r => r.Category == null)
  18. : baseQuery.Where(r => r.Category == category);
  19. var latestSnapshotAt = await baseQuery.MaxAsync(r => (DateTime?)r.SnapshotAt, ct);
  20. if (latestSnapshotAt == null)
  21. {
  22. return new Response([], [], null, 0, DateTime.MinValue, DateTime.MinValue, null);
  23. }
  24. var snapshotStart = latestSnapshotAt.Value.AddMinutes(-1);
  25. var snapshotQuery = baseQuery.Where(r => r.SnapshotAt >= snapshotStart && r.SnapshotAt <= latestSnapshotAt.Value);
  26. var total = await snapshotQuery.CountAsync(ct);
  27. if (total == 0)
  28. {
  29. return new Response([], [], null, 0, DateTime.MinValue, DateTime.MinValue, latestSnapshotAt);
  30. }
  31. var first = await snapshotQuery.OrderBy(r => r.Rank).FirstAsync(ct);
  32. var periodStart = first.PeriodStart;
  33. var periodEnd = first.PeriodEnd;
  34. var shouldAnonymize = request.Type == RankingType.Donor;
  35. var takeCount = Math.Max(3 + (page * perPage), perPage * 2);
  36. var rawEntries = await snapshotQuery.OrderBy(r => r.Rank).Take(takeCount)
  37. .Select(r => new RawEntry(
  38. r.Rank,
  39. r.PreviousRank,
  40. r.MemberID,
  41. r.Member != null ? r.Member.Name : null,
  42. r.Member != null ? r.Member.Thumb : null,
  43. r.Category,
  44. r.Score,
  45. r.MoneyAmount,
  46. r.DonorCount,
  47. r.BroadcastSec,
  48. r.ViewCount,
  49. r.LikeCount,
  50. r.Member != null && r.Member.MemberApprove != null && r.Member.MemberApprove.IsRankingPublic,
  51. r.Member != null && r.Member.MemberGrade != null ? new MemberGradeDto(
  52. r.Member.MemberGrade.ID,
  53. r.Member.MemberGrade.KorName,
  54. r.Member.MemberGrade.Image,
  55. r.Member.MemberGrade.TextColor,
  56. r.Member.MemberGrade.Order
  57. ) : null
  58. ))
  59. .ToListAsync(ct);
  60. var memberIds = rawEntries.Select(e => e.MemberID).ToList();
  61. // 채널명 + 채널 썸네일 묶음 — Donor 타입에서도 채널이 있을 수 있으므로 무조건 조회.
  62. // 익명 처리는 Map 안에서 따로 막음.
  63. var channelDict = await db.Channel.AsNoTracking()
  64. .Where(c => memberIds.Contains(c.MemberID))
  65. .GroupBy(c => c.MemberID)
  66. .Select(g => new {
  67. MemberID = g.Key,
  68. Name = g.First().Name,
  69. Thumbnail = g.First().ThumbnailUrl
  70. })
  71. .ToDictionaryAsync(x => x.MemberID, x => new ChannelInfo(x.Name, x.Thumbnail), ct);
  72. RankingEntry Map(RawEntry e) {
  73. var anonymize = shouldAnonymize && !e.IsRankingPublic;
  74. int? rankChange = e.PreviousRank.HasValue ? e.PreviousRank.Value - e.Rank : null;
  75. var channel = channelDict.GetValueOrDefault(e.MemberID);
  76. return new RankingEntry(
  77. e.Rank,
  78. e.PreviousRank,
  79. rankChange,
  80. e.MemberID,
  81. anonymize ? AnonymousName : (e.MemberName ?? ""),
  82. anonymize ? null : e.MemberThumb,
  83. anonymize ? null : channel?.Name,
  84. anonymize ? null : channel?.Thumbnail,
  85. e.Category,
  86. e.Score,
  87. e.MoneyAmount,
  88. e.DonorCount,
  89. e.BroadcastSec,
  90. e.ViewCount,
  91. e.LikeCount,
  92. anonymize,
  93. e.MemberGrade
  94. );
  95. }
  96. var top3 = rawEntries.Where(e => e.Rank <= 3).Select(Map).ToList();
  97. var skip = (page - 1) * perPage;
  98. var list = rawEntries.Where(e => e.Rank > 3).Skip(skip).Take(perPage).Select(Map).ToList();
  99. MyRankInfo? myRank = null;
  100. if (request.ViewerMemberID.HasValue)
  101. {
  102. var mine = await snapshotQuery.FirstOrDefaultAsync(r => r.MemberID == request.ViewerMemberID.Value, ct);
  103. if (mine != null)
  104. {
  105. int? change = mine.PreviousRank.HasValue ? mine.PreviousRank.Value - mine.Rank : null;
  106. myRank = new MyRankInfo(mine.Rank, mine.MoneyAmount, mine.Score, mine.PreviousRank, change);
  107. }
  108. }
  109. return new Response(top3, list, myRank, total, periodStart, periodEnd, latestSnapshotAt);
  110. }
  111. private sealed record ChannelInfo(string Name, string? Thumbnail);
  112. private sealed record RawEntry(
  113. int Rank,
  114. int? PreviousRank,
  115. int MemberID,
  116. string? MemberName,
  117. string? MemberThumb,
  118. string? Category,
  119. long Score,
  120. long MoneyAmount,
  121. int DonorCount,
  122. long BroadcastSec,
  123. long ViewCount,
  124. long LikeCount,
  125. bool IsRankingPublic,
  126. MemberGradeDto? MemberGrade
  127. );
  128. }