using Application.Abstractions.Cache; using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Application.Abstractions.YouTube; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; namespace Application.Features.Api.Channel.List; public sealed class Handler( IAppDbContext db, IYouTubeLiveStateStore liveStateStore, IYouTubeChannelCache channelCache, ICacheService cache ) : IQueryHandler> { private const string CacheKey = "channel:list:main"; private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(30); public async Task> Handle(Query request, CancellationToken ct) { var cached = await cache.GetAsync>(CacheKey, ct); if (cached is not null) { return new Response(cached); } var items = await BuildAsync(ct); await cache.SetAsync(CacheKey, items, CacheTtl, ct); return new Response(items); } private async Task> BuildAsync(CancellationToken ct) { // 1. 활성 채널 목록 조회 var channels = await db.Channel.AsNoTracking().Where(c => c.IsActive).Select(c => new { c.SID, c.Name, c.Handle }).ToListAsync(ct); if (channels.Count == 0) { return []; } // 2. YouTube 채널 정보 캐시 일괄 조회 (Redis — API 호출 0) var channelIds = channels.Select(c => c.SID).ToList(); var ytCache = await channelCache.GetManyAsync(channelIds); // 3. 현재 라이브 중인 채널 상태 조회 (Redis — 0 unit) var allLive = await liveStateStore.GetAllLiveAsync(); var liveMap = allLive.ToDictionary(l => l.ChannelId); // 4. 채널별 정보 조합 — ViewerCount 는 poller 가 30초 주기로 갱신한 Redis 값 사용 var items = new List(channels.Count); foreach (var ch in channels) { var isLive = liveMap.TryGetValue(ch.SID, out var liveInfo); var hasYtInfo = ytCache.TryGetValue(ch.SID, out var ytInfo); items.Add(new ChannelItem( ch.SID, ch.Name, ch.Handle, hasYtInfo ? ytInfo!.ThumbnailUrl : null, hasYtInfo ? ytInfo!.SubscriberCount : 0, isLive, isLive ? liveInfo!.ViewerCount : 0, isLive ? liveInfo!.VideoId : null )); } // 5. 정렬: 라이브(시청자 desc) → 비라이브(Fisher-Yates random shuffle) var live = items.Where(i => i.IsLive).OrderByDescending(i => i.ViewerCount).ToList(); var offline = items.Where(i => !i.IsLive).ToList(); Shuffle(offline); return [.. live, .. offline]; } private static void Shuffle(List list) { var rnd = Random.Shared; for (int i = list.Count - 1; i > 0; i--) { int j = rnd.Next(i + 1); (list[i], list[j]) = (list[j], list[i]); } } }