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);
}
}