using Application.Abstractions.Data; using Domain.Entities.Donations.ValueObject; using Web.Api.Common; using Microsoft.EntityFrameworkCore; namespace Web.Api.Endpoints.Widget; /// WidgetToken + configID 기반 후원 순위 (위젯 초기 로드용, 인증 불필요). /// configID 지정 시 해당 RankConfig의 Period + MaxRankCount 적용. 미지정 시 활성 첫 번째 또는 Daily/Top5 default. /// Donation 테이블 실시간 GROUP BY (DonationRanking 사전 집계 무시). 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() }); } // 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(); } }