using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Application.Abstractions.YouTube; using Domain.Entities.Donations.ValueObject; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.Crew.SearchMember; internal sealed class Handler( IAppDbContext db, IYouTubeChannelCache channelCache ) : IQueryHandler { public async Task Handle(Query request, CancellationToken ct) { if (string.IsNullOrWhiteSpace(request.Keyword) || request.Keyword.Trim().Length < 2) { return new Response([]); } var kw = request.Keyword.Trim(); // 이미 등록된 크루원의 MemberID 목록 var existingMemberIDs = await db.CrewMember.AsNoTracking() .Where(m => m.CrewID == request.CrewID) .Select(m => m.MemberID) .ToListAsync(ct); // 진행 중인 초대의 TargetMemberID 목록 (중복 초대 방지) var pendingInviteMemberIDs = await db.CrewInvitation.AsNoTracking() .Where(i => i.CrewID == request.CrewID && i.Status == CrewInviteStatus.Pending) .Select(i => i.TargetMemberID) .ToListAsync(ct); var excludeMemberIDs = existingMemberIDs.Concat(pendingInviteMemberIDs).Distinct().ToList(); var list = await db.Member.AsNoTracking() .Where(m => !m.IsWithdraw && !m.IsDenied) .Where(m => !excludeMemberIDs.Contains(m.ID)) .Where(m => db.Channel.Any(c => c.MemberID == m.ID)) .Where(m => m.Email.Contains(kw) || db.Channel.Any(c => c.MemberID == m.ID && ( c.Name.Contains(kw) || (c.Handle != null && c.Handle.Contains(kw)) )) ) .Take(20) .Select(m => new { m.ID, m.Name, m.Email, m.Thumb, Channel = db.Channel.AsNoTracking() .Where(c => c.MemberID == m.ID) .Select(c => new { c.SID, c.Name, c.Handle, c.ThumbnailUrl }) .FirstOrDefault() }) .ToListAsync(ct); // YouTube 채널 정보 캐시 일괄 조회 (Redis 우선, 미스 시 DB fallback) var channelSIDs = list.Where(x => x.Channel != null).Select(x => x.Channel!.SID).ToList(); var ytCache = channelSIDs.Count > 0 ? await channelCache.GetManyAsync(channelSIDs) : []; var result = list.Select(x => { string? channelThumb = null; if (x.Channel != null) { channelThumb = ytCache.TryGetValue(x.Channel.SID, out var ytInfo) ? ytInfo.ThumbnailUrl : x.Channel.ThumbnailUrl; } return new SearchResultItem( x.ID, x.Name, x.Email, x.Thumb, x.Channel?.Name, channelThumb, x.Channel?.Handle ); }).ToList(); return new Response(result); } }