Handler.cs 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Microsoft.EntityFrameworkCore;
  4. namespace Application.Features.Api.Ranking.GetRising;
  5. /// <summary>
  6. /// 최근 7일 vs 이전 7일 후원 증가율 기준 급상승 크리에이터.
  7. /// 집계는 실시간 Donation 테이블로 계산 (최근성이 중요하므로 스냅샷 아님).
  8. /// </summary>
  9. internal sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
  10. {
  11. public async Task<Response> Handle(Query request, CancellationToken ct)
  12. {
  13. var limit = request.Limit is < 1 or > 100 ? 20 : request.Limit;
  14. var now = DateTime.UtcNow;
  15. var recent7End = now;
  16. var recent7Start = now.AddDays(-7);
  17. var prev7Start = now.AddDays(-14);
  18. // 최근 7일 집계
  19. var recent = await db.Donation.AsNoTracking()
  20. .Where(d => d.CreatedAt >= recent7Start && d.CreatedAt < recent7End && !d.IsTest)
  21. .GroupBy(d => d.ReceiverMemberID)
  22. .Select(g => new
  23. {
  24. MemberID = g.Key,
  25. Amount = g.Sum(d => (long)d.NetAmount),
  26. DonorCount = g.Select(d => d.SponsorMemberID).Distinct().Count()
  27. })
  28. .ToListAsync(ct);
  29. if (recent.Count == 0)
  30. {
  31. return new Response([], now);
  32. }
  33. // 이전 7일 집계
  34. var memberIds = recent.Select(r => r.MemberID).ToList();
  35. var previous = await db.Donation.AsNoTracking()
  36. .Where(d => d.CreatedAt >= prev7Start && d.CreatedAt < recent7Start && !d.IsTest && memberIds.Contains(d.ReceiverMemberID))
  37. .GroupBy(d => d.ReceiverMemberID)
  38. .Select(g => new { MemberID = g.Key, Amount = g.Sum(d => (long)d.NetAmount) })
  39. .ToDictionaryAsync(g => g.MemberID, g => g.Amount, ct);
  40. var rising = recent.Select(r => {
  41. var prevAmount = previous.GetValueOrDefault(r.MemberID);
  42. double rate;
  43. if (prevAmount <= 0)
  44. {
  45. rate = r.Amount > 0 ? 1000.0 : 0.0; // 신규 유입 — 1000% 취급
  46. }
  47. else
  48. {
  49. rate = ((double)(r.Amount - prevAmount) / prevAmount) * 100.0;
  50. }
  51. return new { r.MemberID, r.Amount, r.DonorCount, PreviousAmount = prevAmount, Rate = rate };
  52. })
  53. .Where(r => r.Rate > 0)
  54. .OrderByDescending(r => r.Rate).ThenByDescending(r => r.Amount)
  55. .Take(limit)
  56. .ToList();
  57. var memberInfo = await db.Member.AsNoTracking()
  58. .Where(m => rising.Select(r => r.MemberID).Contains(m.ID))
  59. .Select(m => new { m.ID, m.Name, m.Thumb })
  60. .ToListAsync(ct);
  61. var memberDict = memberInfo.ToDictionary(m => m.ID, m => m);
  62. var channels = await db.Channel.AsNoTracking()
  63. .Where(c => rising.Select(r => r.MemberID).Contains(c.MemberID))
  64. .Select(c => new
  65. {
  66. c.MemberID,
  67. c.Name,
  68. c.ThumbnailUrl
  69. })
  70. .ToListAsync(ct);
  71. var channelDict = channels.GroupBy(c => c.MemberID).ToDictionary(g => g.Key, g => g.First());
  72. var entries = rising.Select(r => {
  73. var m = memberDict.GetValueOrDefault(r.MemberID);
  74. var ch = channelDict.GetValueOrDefault(r.MemberID);
  75. // Member.Thumb 우선, 없으면 Channel.ThumbnailUrl(YouTube 채널 썸네일) 폴백
  76. return new RisingEntry(
  77. r.MemberID,
  78. m?.Name ?? "",
  79. m?.Thumb ?? ch?.ThumbnailUrl,
  80. ch?.Name,
  81. r.Amount,
  82. r.PreviousAmount,
  83. r.Rate,
  84. r.DonorCount
  85. );
  86. }).ToList();
  87. return new Response(entries, now);
  88. }
  89. }