Handler.cs 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Microsoft.EntityFrameworkCore;
  4. namespace Application.Features.Api.Note.SearchRecipient;
  5. /// <summary>
  6. /// 쪽지 수신자 검색 — 보안 정책상 본인이 팔로우한 회원/채널만 검색됨 (enumeration 차단).
  7. /// 직접 진입 (프로필/채널 페이지 "쪽지 보내기" 버튼)은 별도 — 검색은 빠른 발송용.
  8. /// </summary>
  9. internal sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
  10. {
  11. public async Task<Response> Handle(Query r, CancellationToken ct)
  12. {
  13. var kw = r.Keyword?.Trim() ?? string.Empty;
  14. if (kw.Length < 2)
  15. {
  16. return new Response([], false);
  17. }
  18. var offset = r.Offset < 0 ? 0 : r.Offset;
  19. var limit = r.Limit is < 1 or > 50 ? 20 : r.Limit;
  20. // 본인이 팔로우 중인 회원 IDs (보안 필터)
  21. var followingIDs = db.MemberFollow.AsNoTracking()
  22. .Where(f => f.FollowerMemberID == r.CurrentMemberID)
  23. .Select(f => f.FolloweeMemberID);
  24. // Member LEFT JOIN Channel — 닉네임/SID/채널명/핸들 OR 검색
  25. var rows = await (
  26. from m in db.Member.AsNoTracking()
  27. where !m.IsWithdraw && !m.IsDenied
  28. && m.ID != r.CurrentMemberID
  29. && followingIDs.Contains(m.ID)
  30. from c in db.Channel.AsNoTracking().Where(c => c.MemberID == m.ID).DefaultIfEmpty()
  31. where (m.Name != null && m.Name.Contains(kw))
  32. || m.SID.Contains(kw)
  33. || (c != null && c.Name.Contains(kw))
  34. || (c != null && c.Handle != null && c.Handle.Contains(kw))
  35. orderby m.ID descending
  36. select new
  37. {
  38. m.ID,
  39. MemberName = m.Name,
  40. m.SID,
  41. MemberThumb = m.Thumb,
  42. ChannelName = c != null ? c.Name : null,
  43. ChannelHandle = c != null ? c.Handle : null,
  44. ChannelThumbnail = c != null ? c.ThumbnailUrl : null
  45. }
  46. )
  47. .Skip(offset)
  48. .Take(limit + 1)
  49. .ToListAsync(ct);
  50. var hasMore = rows.Count > limit;
  51. var page = rows.Take(limit).ToList();
  52. // 채널 보유 여부와 무관하게 일관된 UI 데이터 — (썸네일, 이름, 부제)
  53. var list = page.Select(x => new RecipientItem(
  54. x.ID,
  55. x.ChannelName ?? x.MemberName ?? x.SID,
  56. x.ChannelHandle != null ? "@" + x.ChannelHandle : "@" + x.SID,
  57. x.ChannelThumbnail ?? x.MemberThumb
  58. )).ToList();
  59. return new Response(list, hasMore);
  60. }
  61. }