using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.Attendance.GetCalendar; internal sealed class Handler(IAppDbContext db) : IQueryHandler { public async Task Handle(Query request, CancellationToken ct) { // 로컬(서버 KST) 기준 월 범위를 UTC 로 환산 var monthStartLocal = new DateTime(request.Year, request.Month, 1, 0, 0, 0, DateTimeKind.Local); var startUtc = monthStartLocal.ToUniversalTime(); var endUtc = monthStartLocal.AddMonths(1).ToUniversalTime(); // 해당 월 출석 레코드 조회 var records = await db.Attendance.AsNoTracking() .Where(x => x.CreatedAt >= startUtc && x.CreatedAt < endUtc) .Select(x => new { x.MemberID, x.CreatedAt, x.ConsecutiveDays }) .ToListAsync(ct); // EF Core 는 Unspecified 로 반환하므로 UTC 명시 후 로컬 변환 var localRecords = records.Select(r => new { r.MemberID, CreatedAt = DateTime.SpecifyKind(r.CreatedAt, DateTimeKind.Utc).ToLocalTime(), r.ConsecutiveDays }).ToList(); // KST 달력일(yyyy-MM-dd) 문자열 기준으로 집계 (UTC 직렬화에 의한 하루 밀림 방지) var dailyCounts = localRecords .GroupBy(x => x.CreatedAt.ToString("yyyy-MM-dd")) .ToDictionary(g => g.Key, g => g.Count()); var myRecords = request.MemberID is > 0 ? localRecords.Where(x => x.MemberID == request.MemberID.Value).OrderBy(x => x.CreatedAt).ToList() : []; var daysInMonth = DateTime.DaysInMonth(request.Year, request.Month); var days = new List(daysInMonth); for (var day = 1; day <= daysInMonth; day++) { var dateStr = $"{request.Year:D4}-{request.Month:D2}-{day:D2}"; var myRecord = myRecords.FirstOrDefault(r => r.CreatedAt.ToString("yyyy-MM-dd") == dateStr); var totalCount = dailyCounts.GetValueOrDefault(dateStr); days.Add(new CalendarDay( dateStr, myRecord?.CreatedAt, myRecord?.ConsecutiveDays ?? 0, totalCount )); } return new Response(days); } }