| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107 |
- using Application.Abstractions.Data;
- using Domain.Entities.Donations.ValueObject;
- using Web.Api.Common;
- using Microsoft.EntityFrameworkCore;
- namespace Web.Api.Endpoints.Widget;
- /// <summary>WidgetToken + configID 기반 후원 순위 (위젯 초기 로드용, 인증 불필요).
- /// configID 지정 시 해당 RankConfig의 Period + MaxRankCount 적용. 미지정 시 활성 첫 번째 또는 Daily/Top5 default.
- /// Donation 테이블 실시간 GROUP BY (DonationRanking 사전 집계 무시).</summary>
- internal sealed class DonationRankingByToken : IEndpoint
- {
- public void MapEndpoint(IEndpointRouteBuilder app)
- {
- app.MapGet("api/widget/rank/by-token/{widgetToken}", async (
- string widgetToken,
- int? configID,
- IAppDbContext db,
- CancellationToken ct
- ) => {
- var channelID = await db.Channel.AsNoTracking()
- .Where(c => c.WidgetToken == widgetToken && c.IsActive)
- .Select(c => c.ID)
- .FirstOrDefaultAsync(ct);
- if (channelID == 0)
- {
- return ApiResponse.Ok(new { list = Array.Empty<object>() });
- }
- // RankConfig 조회 (configID 지정 또는 활성 첫 번째)
- var rankCfgQuery = db.DonationRankConfig.AsNoTracking()
- .Where(r => r.ChannelID == channelID);
- if (configID.HasValue)
- {
- rankCfgQuery = rankCfgQuery.Where(r => r.ID == configID.Value);
- }
- else
- {
- rankCfgQuery = rankCfgQuery.Where(r => r.IsActive);
- }
- var rankCfg = await rankCfgQuery
- .OrderByDescending(r => r.ID)
- .Select(r => new { r.Period, r.MaxRankCount, r.StartAt, r.EndAt })
- .FirstOrDefaultAsync(ct);
- // config 없으면 default (Daily, Top5)
- var period = rankCfg?.Period ?? RankPeriodType.Daily;
- var limit = rankCfg?.MaxRankCount ?? 5;
- // Period에 따른 시작 시각
- var now = DateTime.UtcNow;
- DateTime? rangeStart = period switch
- {
- RankPeriodType.Daily => now.Date,
- RankPeriodType.Weekly => now.Date.AddDays(-(int)now.DayOfWeek),
- RankPeriodType.Monthly => new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc),
- _ => null // Total 등
- };
- // 추가 StartAt/EndAt 필터 (config가 명시한 기간)
- var startAt = rankCfg?.StartAt ?? rangeStart;
- var endAt = rankCfg?.EndAt;
- var donationQuery = db.Donation.AsNoTracking()
- .Where(d => d.ChannelID == channelID && !d.IsTest);
- if (startAt.HasValue)
- {
- donationQuery = donationQuery.Where(d => d.CreatedAt >= startAt.Value);
- }
- if (endAt.HasValue)
- {
- donationQuery = donationQuery.Where(d => d.CreatedAt <= endAt.Value);
- }
- // SponsorMemberID로 통합 (같은 sponsor가 다른 별명으로 후원해도 합산). SendName은 가장 최근 것.
- var raw = await donationQuery
- .GroupBy(d => d.SponsorMemberID)
- .Select(g => new
- {
- SponsorMemberID = g.Key,
- SendName = g.OrderByDescending(d => d.CreatedAt).Select(d => d.SendName).FirstOrDefault() ?? "",
- TotalAmount = g.Sum(d => d.NetAmount),
- DonationCount = g.Count()
- })
- .OrderByDescending(g => g.TotalAmount)
- .Take(limit)
- .ToListAsync(ct);
- var list = raw.Select((r, i) => new
- {
- rank = i + 1,
- sponsorMemberID = r.SponsorMemberID,
- sponsorName = r.SendName,
- totalAmount = r.TotalAmount,
- donationCount = r.DonationCount
- }).ToList();
- return ApiResponse.Ok(new { list });
- })
- .WithTags("Widget")
- .AllowAnonymous();
- }
- }
|