| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788 |
- 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<Query, Result<Response>>
- {
- private const string CacheKey = "channel:list:main";
- private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(30);
- public async Task<Result<Response>> Handle(Query request, CancellationToken ct)
- {
- var cached = await cache.GetAsync<List<ChannelItem>>(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<List<ChannelItem>> 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<ChannelItem>(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<T>(List<T> 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]);
- }
- }
- }
|