Handler.cs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Microsoft.EntityFrameworkCore;
  4. namespace Application.Features.Admin.Donation.Rank.Search;
  5. public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
  6. {
  7. public async Task<Response> Handle(Query request, CancellationToken ct)
  8. {
  9. // 탭 → 기간 범위 (롤링 윈도우)
  10. var now = DateTime.UtcNow;
  11. var periodStart = request.Tab switch
  12. {
  13. 0 => now.AddHours(-24), // 오늘
  14. 1 => now.AddHours(-72), // 3일
  15. 2 => now.AddDays(-7), // 1주일
  16. 3 => now.AddDays(-30), // 1개월
  17. 4 => now.AddDays(-90), // 3개월
  18. 5 => now.AddDays(-180), // 6개월
  19. 6 => now.AddDays(-365), // 1년
  20. _ => now.AddHours(-24)
  21. };
  22. var donations = db.Donation.AsNoTracking().Where(d => d.CreatedAt >= periodStart && d.CreatedAt <= now);
  23. // 키워드 검색 (검색 창은 후원 내역과 동일)
  24. if (!string.IsNullOrWhiteSpace(request.Keyword))
  25. {
  26. donations = request.Search switch
  27. {
  28. 1 => donations.Where(x => x.Sponsor != null && x.Sponsor.ID.ToString().Contains(request.Keyword)),
  29. 2 => donations.Where(x => x.Sponsor != null && x.Sponsor.SID.Contains(request.Keyword)),
  30. 3 => donations.Where(x => x.Sponsor != null && x.Sponsor.Name != null && x.Sponsor.Name.Contains(request.Keyword)),
  31. 4 => donations.Where(x => x.Receiver != null && x.Receiver.ID.ToString().Contains(request.Keyword)),
  32. 5 => donations.Where(x => x.Receiver != null && x.Receiver.SID.Contains(request.Keyword)),
  33. 6 => donations.Where(x => x.Receiver != null && x.Receiver.Name != null && x.Receiver.Name.Contains(request.Keyword)),
  34. 7 => donations.Where(x => x.SendName.Contains(request.Keyword)),
  35. _ => donations
  36. };
  37. }
  38. // 기간 추가 필터 (StartAt/EndAt) — 탭 기간 내에서 추가로 좁히기
  39. if (!string.IsNullOrWhiteSpace(request.StartAt) && DateTime.TryParse(request.StartAt, out var startAt))
  40. {
  41. donations = donations.Where(x => x.CreatedAt >= startAt);
  42. }
  43. if (!string.IsNullOrWhiteSpace(request.EndAt) && DateTime.TryParse(request.EndAt, out var endAt))
  44. {
  45. donations = donations.Where(x => x.CreatedAt <= endAt);
  46. }
  47. // 후원자별 집계
  48. var grouped = donations.GroupBy(d => d.SponsorMemberID).Select(g => new
  49. {
  50. SponsorMemberID = g.Key,
  51. TotalAmount = g.Sum(x => (long)x.Amount),
  52. DonationCount = g.Count(),
  53. LastDonatedAt = g.Max(x => (DateTime?)x.CreatedAt)
  54. });
  55. // 정렬 + 순위
  56. grouped = request.Sort switch
  57. {
  58. 1 => grouped.OrderByDescending(x => x.TotalAmount).ThenByDescending(x => x.DonationCount),
  59. 2 => grouped.OrderBy(x => x.TotalAmount).ThenBy(x => x.DonationCount),
  60. 3 => grouped.OrderByDescending(x => x.DonationCount).ThenByDescending(x => x.TotalAmount),
  61. 4 => grouped.OrderBy(x => x.DonationCount).ThenBy(x => x.TotalAmount),
  62. _ => grouped.OrderByDescending(x => x.TotalAmount).ThenByDescending(x => x.DonationCount)
  63. };
  64. var total = await grouped.CountAsync(ct);
  65. var skip = (request.Page - 1) * request.PerPage;
  66. var paged = await grouped.Skip(skip).Take(request.PerPage).ToListAsync(ct);
  67. // 마지막 피후원자 lookup — 후원자별 마지막 Donation의 Receiver
  68. var sponsorIDs = paged.Select(p => p.SponsorMemberID).ToArray();
  69. var lastReceivers = await donations
  70. .Where(d => sponsorIDs.Contains(d.SponsorMemberID))
  71. .GroupBy(d => d.SponsorMemberID)
  72. .Select(g => new
  73. {
  74. SponsorMemberID = g.Key,
  75. LastDonationID = g.OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.ID).First().ID
  76. })
  77. .ToListAsync(ct);
  78. var lastDonationIDs = lastReceivers.Select(r => r.LastDonationID).ToArray();
  79. var lastDonations = await db.Donation.AsNoTracking()
  80. .Where(d => lastDonationIDs.Contains(d.ID))
  81. .Include(d => d.Receiver)
  82. .Select(d => new
  83. {
  84. d.ID,
  85. d.SponsorMemberID,
  86. ReceiverID = d.Receiver != null ? d.Receiver.ID : (int?)null,
  87. ReceiverEmail = d.Receiver != null ? d.Receiver.Email : null,
  88. ReceiverName = d.Receiver != null ? d.Receiver.Name : null
  89. })
  90. .ToListAsync(ct);
  91. var lastBySponsor = lastDonations.ToDictionary(x => x.SponsorMemberID);
  92. // Sponsor 정보 lookup
  93. var sponsors = await db.Member.AsNoTracking()
  94. .Where(m => sponsorIDs.Contains(m.ID))
  95. .Select(m => new { m.ID, m.Email, m.Name })
  96. .ToListAsync(ct);
  97. var sponsorByID = sponsors.ToDictionary(x => x.ID);
  98. // Rank 번호 — 현재 페이지에서 skip+1 부터 순서대로
  99. var rows = paged.Select((x, idx) =>
  100. {
  101. sponsorByID.TryGetValue(x.SponsorMemberID, out var sponsor);
  102. lastBySponsor.TryGetValue(x.SponsorMemberID, out var last);
  103. return new Response.Row
  104. {
  105. Num = total - skip - idx,
  106. Rank = skip + idx + 1,
  107. SponsorMemberID = x.SponsorMemberID,
  108. SponsorEmail = sponsor?.Email ?? string.Empty,
  109. SponsorName = sponsor?.Name,
  110. TotalAmount = x.TotalAmount,
  111. DonationCount = x.DonationCount,
  112. LastReceiverID = last?.ReceiverID,
  113. LastReceiverEmail = last?.ReceiverEmail,
  114. LastReceiverName = last?.ReceiverName,
  115. LastDonatedAt = x.LastDonatedAt
  116. };
  117. }).ToList();
  118. return new Response
  119. {
  120. Total = total,
  121. List = rows
  122. };
  123. }
  124. }