Handler.cs 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Microsoft.EntityFrameworkCore;
  4. namespace Application.Features.Api.Store.Inventory.Groups;
  5. internal sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
  6. {
  7. public async Task<Response> Handle(Query request, CancellationToken ct)
  8. {
  9. // 1단계: ProductID 단위 그룹 + aggregations (EF Core 가 SQL 로 깔끔히 번역).
  10. // anonymous key + 다중 join + Response 생성자 projection 을 한 번에 시도하면
  11. // translation 실패하므로, 그룹 aggregations 만 먼저 가져온다.
  12. var groupedQuery = db.MemberInventory.AsNoTracking()
  13. .Where(i => i.MemberID == request.MemberID && i.DeletedAt == null)
  14. .GroupBy(i => i.OrderItem!.ProductID)
  15. .Select(g => new
  16. {
  17. ProductID = g.Key,
  18. TotalCount = g.Count(),
  19. UnusedCount = g.Count(x => x.UsedAt == null),
  20. EarliestAcquiredAt = g.Min(x => x.AcquiredAt)
  21. });
  22. var total = await groupedQuery.CountAsync(ct);
  23. var pagedGroups = await groupedQuery
  24. .OrderByDescending(g => g.EarliestAcquiredAt)
  25. .Skip((request.Page - 1) * request.PerPage)
  26. .Take(request.PerPage)
  27. .ToListAsync(ct);
  28. if (pagedGroups.Count == 0)
  29. {
  30. return new Response(total, []);
  31. }
  32. // 2단계: 표시 페이지의 Product + Game 메타데이터 한 번에 fetch.
  33. var productIDs = pagedGroups.Select(g => g.ProductID).ToList();
  34. var products = await db.Product.AsNoTracking()
  35. .Where(p => productIDs.Contains(p.ID))
  36. .Select(p => new
  37. {
  38. p.ID,
  39. p.Name,
  40. p.Thumbnail,
  41. GameName = p.Game!.KorName
  42. })
  43. .ToListAsync(ct);
  44. var productMap = products.ToDictionary(p => p.ID);
  45. // 3단계: 정렬 순서 유지하면서 stitch.
  46. var list = pagedGroups.Select(g =>
  47. {
  48. var meta = productMap.GetValueOrDefault(g.ProductID);
  49. return new Response.GroupRow(
  50. g.ProductID,
  51. meta?.Name ?? string.Empty,
  52. meta?.Thumbnail,
  53. meta?.GameName ?? string.Empty,
  54. g.TotalCount,
  55. g.UnusedCount,
  56. g.EarliestAcquiredAt
  57. );
  58. }).ToList();
  59. return new Response(total, list);
  60. }
  61. }