| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- using Application.Abstractions.Data;
- using Application.Abstractions.Hub;
- using Application.Abstractions.Messaging;
- using Application.Abstractions.YouTube;
- using SharedKernel.Results;
- using Domain.Entities.Donations.ValueObject;
- using Microsoft.EntityFrameworkCore;
- namespace Application.Features.Api.Crew.GetCrewMembers;
- internal sealed class Handler(
- IAppDbContext db,
- IPresenceTracker presence,
- IYouTubeChannelCache channelCache
- ) : IQueryHandler<Query, Result<Response>>
- {
- public async Task<Result<Response>> Handle(Query request, CancellationToken ct)
- {
- var crew = await db.Crew.AsNoTracking()
- .FirstOrDefaultAsync(c => c.ID == request.CrewID, ct);
- if (crew is null)
- {
- return Result.Failure<Response>(Error.NotFound("Crew.NotFound", "크루를 찾을 수 없습니다."));
- }
- // 1) 확정 멤버 (Accepted)
- var members = await db.CrewMember.AsNoTracking()
- .Where(m => m.CrewID == request.CrewID)
- .OrderBy(m => m.SortOrder)
- .ThenBy(m => m.JoinedAt)
- .Select(m => new
- {
- m.ID,
- m.MemberID,
- m.ChannelID,
- m.Nickname,
- m.Role,
- m.SortOrder,
- m.IsActive,
- m.JoinedAt,
- Thumb = m.Member != null ? m.Member.Thumb : null,
- ChannelName = m.Channel != null ? m.Channel.Name : null,
- ChannelSID = m.Channel != null ? m.Channel.SID : null,
- ChannelDbThumb = m.Channel != null ? m.Channel.ThumbnailUrl : null
- })
- .ToListAsync(ct);
- // 2) 미응답/거절/만료 초대
- var invitations = await db.CrewInvitation.AsNoTracking()
- .Where(i => i.CrewID == request.CrewID && i.Status != CrewInviteStatus.Accepted)
- .OrderByDescending(i => i.InvitedAt)
- .Select(i => new
- {
- ID = -i.ID, // 음수: CrewMember.ID와 충돌 방지
- MemberID = i.TargetMemberID,
- ChannelID = (int?)null,
- i.Nickname,
- i.Role,
- i.SortOrder,
- IsActive = false,
- JoinedAt = i.InvitedAt,
- Thumb = i.TargetMember != null ? i.TargetMember.Thumb : null,
- ChannelInfo = db.Channel.AsNoTracking()
- .Where(c => c.MemberID == i.TargetMemberID)
- .Select(c => new { c.Name, c.SID, c.ThumbnailUrl })
- .FirstOrDefault(),
- i.Status,
- i.ExpiresAt
- })
- .ToListAsync(ct);
- // 3) 모든 MemberID 일괄 online 체크 (Redis 1회)
- var allMemberIDs = members.Select(m => m.MemberID)
- .Concat(invitations.Select(i => i.MemberID))
- .Distinct()
- .ToList();
- var onlineSet = await presence.GetOnlineMembersAsync(allMemberIDs);
- // 4) 채널 SID 일괄 수집 → YouTube 캐시 조회 (Redis MGET 1회, 미스 시 DB ThumbnailUrl fallback)
- var channelSIDs = members.Where(m => m.ChannelSID != null).Select(m => m.ChannelSID!)
- .Concat(invitations.Where(i => i.ChannelInfo != null).Select(i => i.ChannelInfo!.SID))
- .Distinct()
- .ToList();
- var ytCache = channelSIDs.Count > 0
- ? await channelCache.GetManyAsync(channelSIDs)
- : [];
- string? ResolveChannelThumb(string? sid, string? dbThumb)
- {
- if (sid == null) { return null; }
- return ytCache.TryGetValue(sid, out var info) ? info.ThumbnailUrl : dbThumb;
- }
- // 5) 통합 응답
- var combined = new List<CrewMemberItem>(members.Count + invitations.Count);
- foreach (var m in members)
- {
- combined.Add(new CrewMemberItem(
- m.ID, m.MemberID, m.ChannelID, m.Nickname, m.Role,
- m.SortOrder, m.IsActive, m.JoinedAt,
- m.Thumb, m.ChannelName,
- ResolveChannelThumb(m.ChannelSID, m.ChannelDbThumb),
- "Accepted", null,
- onlineSet.Contains(m.MemberID)
- ));
- }
- var now = DateTime.UtcNow;
- foreach (var i in invitations)
- {
- // lazy expiration: Pending인데 ExpiresAt 지난 것은 Expired 처리
- var isExpired = i.Status == CrewInviteStatus.Pending && i.ExpiresAt < now;
- var statusLabel = i.Status == CrewInviteStatus.Declined ? "Declined"
- : isExpired || i.Status == CrewInviteStatus.Expired ? "Expired"
- : "Pending";
- combined.Add(new CrewMemberItem(
- i.ID, i.MemberID, i.ChannelID, i.Nickname, i.Role,
- i.SortOrder, i.IsActive, i.JoinedAt,
- i.Thumb, i.ChannelInfo?.Name,
- ResolveChannelThumb(i.ChannelInfo?.SID, i.ChannelInfo?.ThumbnailUrl),
- statusLabel,
- statusLabel == "Pending" ? i.ExpiresAt : null,
- onlineSet.Contains(i.MemberID)
- ));
- }
- return Result.Success(new Response(combined));
- }
- }
|