Handler.cs 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Application.Abstractions.YouTube;
  4. using Domain.Entities.Donations.ValueObject;
  5. using Microsoft.EntityFrameworkCore;
  6. namespace Application.Features.Api.Crew.SearchMember;
  7. internal sealed class Handler(
  8. IAppDbContext db,
  9. IYouTubeChannelCache channelCache
  10. ) : IQueryHandler<Query, Response>
  11. {
  12. public async Task<Response> Handle(Query request, CancellationToken ct)
  13. {
  14. if (string.IsNullOrWhiteSpace(request.Keyword) || request.Keyword.Trim().Length < 2)
  15. {
  16. return new Response([]);
  17. }
  18. var kw = request.Keyword.Trim();
  19. // 이미 등록된 크루원의 MemberID 목록
  20. var existingMemberIDs = await db.CrewMember.AsNoTracking()
  21. .Where(m => m.CrewID == request.CrewID)
  22. .Select(m => m.MemberID)
  23. .ToListAsync(ct);
  24. // 진행 중인 초대의 TargetMemberID 목록 (중복 초대 방지)
  25. var pendingInviteMemberIDs = await db.CrewInvitation.AsNoTracking()
  26. .Where(i => i.CrewID == request.CrewID && i.Status == CrewInviteStatus.Pending)
  27. .Select(i => i.TargetMemberID)
  28. .ToListAsync(ct);
  29. var excludeMemberIDs = existingMemberIDs.Concat(pendingInviteMemberIDs).Distinct().ToList();
  30. var list = await db.Member.AsNoTracking()
  31. .Where(m => !m.IsWithdraw && !m.IsDenied)
  32. .Where(m => !excludeMemberIDs.Contains(m.ID))
  33. .Where(m => db.Channel.Any(c => c.MemberID == m.ID))
  34. .Where(m =>
  35. m.Email.Contains(kw) ||
  36. db.Channel.Any(c => c.MemberID == m.ID && (
  37. c.Name.Contains(kw) ||
  38. (c.Handle != null && c.Handle.Contains(kw))
  39. ))
  40. )
  41. .Take(20)
  42. .Select(m => new
  43. {
  44. m.ID,
  45. m.Name,
  46. m.Email,
  47. m.Thumb,
  48. Channel = db.Channel.AsNoTracking()
  49. .Where(c => c.MemberID == m.ID)
  50. .Select(c => new { c.SID, c.Name, c.Handle, c.ThumbnailUrl })
  51. .FirstOrDefault()
  52. })
  53. .ToListAsync(ct);
  54. // YouTube 채널 정보 캐시 일괄 조회 (Redis 우선, 미스 시 DB fallback)
  55. var channelSIDs = list.Where(x => x.Channel != null).Select(x => x.Channel!.SID).ToList();
  56. var ytCache = channelSIDs.Count > 0
  57. ? await channelCache.GetManyAsync(channelSIDs)
  58. : [];
  59. var result = list.Select(x =>
  60. {
  61. string? channelThumb = null;
  62. if (x.Channel != null)
  63. {
  64. channelThumb = ytCache.TryGetValue(x.Channel.SID, out var ytInfo)
  65. ? ytInfo.ThumbnailUrl
  66. : x.Channel.ThumbnailUrl;
  67. }
  68. return new SearchResultItem(
  69. x.ID,
  70. x.Name,
  71. x.Email,
  72. x.Thumb,
  73. x.Channel?.Name,
  74. channelThumb,
  75. x.Channel?.Handle
  76. );
  77. }).ToList();
  78. return new Response(result);
  79. }
  80. }