using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.Note.SearchRecipient; /// /// 쪽지 수신자 검색 — 보안 정책상 본인이 팔로우한 회원/채널만 검색됨 (enumeration 차단). /// 직접 진입 (프로필/채널 페이지 "쪽지 보내기" 버튼)은 별도 — 검색은 빠른 발송용. /// internal sealed class Handler(IAppDbContext db) : IQueryHandler { public async Task Handle(Query r, CancellationToken ct) { var kw = r.Keyword?.Trim() ?? string.Empty; if (kw.Length < 2) { return new Response([], false); } var offset = r.Offset < 0 ? 0 : r.Offset; var limit = r.Limit is < 1 or > 50 ? 20 : r.Limit; // 본인이 팔로우 중인 회원 IDs (보안 필터) var followingIDs = db.MemberFollow.AsNoTracking() .Where(f => f.FollowerMemberID == r.CurrentMemberID) .Select(f => f.FolloweeMemberID); // Member LEFT JOIN Channel — 닉네임/SID/채널명/핸들 OR 검색 var rows = await ( from m in db.Member.AsNoTracking() where !m.IsWithdraw && !m.IsDenied && m.ID != r.CurrentMemberID && followingIDs.Contains(m.ID) from c in db.Channel.AsNoTracking().Where(c => c.MemberID == m.ID).DefaultIfEmpty() where (m.Name != null && m.Name.Contains(kw)) || m.SID.Contains(kw) || (c != null && c.Name.Contains(kw)) || (c != null && c.Handle != null && c.Handle.Contains(kw)) orderby m.ID descending select new { m.ID, MemberName = m.Name, m.SID, MemberThumb = m.Thumb, ChannelName = c != null ? c.Name : null, ChannelHandle = c != null ? c.Handle : null, ChannelThumbnail = c != null ? c.ThumbnailUrl : null } ) .Skip(offset) .Take(limit + 1) .ToListAsync(ct); var hasMore = rows.Count > limit; var page = rows.Take(limit).ToList(); // 채널 보유 여부와 무관하게 일관된 UI 데이터 — (썸네일, 이름, 부제) var list = page.Select(x => new RecipientItem( x.ID, x.ChannelName ?? x.MemberName ?? x.SID, x.ChannelHandle != null ? "@" + x.ChannelHandle : "@" + x.SID, x.ChannelThumbnail ?? x.MemberThumb )).ToList(); return new Response(list, hasMore); } }