using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.Attendance.GetList; internal sealed class Handler(IAppDbContext db) : IQueryHandler { public async Task Handle(Query request, CancellationToken ct) { // 로컬(서버 KST) 기준 날짜를 UTC 범위로 환산 var targetDateLocal = DateTime.SpecifyKind(request.Date?.Date ?? DateTime.Now.Date, DateTimeKind.Local); var startUtc = targetDateLocal.ToUniversalTime(); var endUtc = startUtc.AddDays(1); var query = db.Attendance.AsNoTracking() .Where(x => x.CreatedAt >= startUtc && x.CreatedAt < endUtc); var total = await query.CountAsync(ct); // 시간순 Rank 사전 계산 (정렬 옵션과 무관하게 항상 시간 기준) var rankList = await query .OrderBy(x => x.CreatedAt) .Select(x => new { x.MemberID, x.CreatedAt }) .ToListAsync(ct); var rankMap = new Dictionary(); for (var i = 0; i < rankList.Count; i++) { rankMap[rankList[i].MemberID] = i + 1; } // 정렬 분기 var sortBy = request.SortBy ?? "time"; IQueryable sortedQuery = sortBy switch { "count" => from a in query join stats in db.MemberStats.AsNoTracking() on a.MemberID equals stats.MemberID orderby stats.AttendanceCount descending, a.CreatedAt select a, "streak" => query.OrderByDescending(x => x.ConsecutiveDays).ThenBy(x => x.CreatedAt), _ => query.OrderBy(x => x.CreatedAt) }; var list = await sortedQuery .Skip((request.Page - 1) * request.PerPage) .Take(request.PerPage) .Select(x => new AttendanceItem( 0, x.MemberID, x.Member.SID, x.Member.Name ?? "", x.Member.Thumb, x.Greeting, x.ConsecutiveDays, x.ExpRewarded, x.PointRewarded, x.CreatedAt )) .ToListAsync(ct); // Rank 주입 + UTC → 로컬 변환 var ranked = list.Select(item => item with { Rank = rankMap.GetValueOrDefault(item.MemberID), CreatedAt = DateTime.SpecifyKind(item.CreatedAt, DateTimeKind.Utc).ToLocalTime() }).ToList(); return new Response(total, ranked); } }