Handler.cs 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. using Application.Abstractions.Cache;
  2. using Application.Abstractions.Data;
  3. using Application.Abstractions.Messaging;
  4. using Application.Abstractions.YouTube;
  5. using Microsoft.EntityFrameworkCore;
  6. using SharedKernel.Results;
  7. namespace Application.Features.Api.Channel.List;
  8. public sealed class Handler(
  9. IAppDbContext db,
  10. IYouTubeLiveStateStore liveStateStore,
  11. IYouTubeChannelCache channelCache,
  12. ICacheService cache
  13. ) : IQueryHandler<Query, Result<Response>>
  14. {
  15. private const string CacheKey = "channel:list:main";
  16. private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(30);
  17. public async Task<Result<Response>> Handle(Query request, CancellationToken ct)
  18. {
  19. var cached = await cache.GetAsync<List<ChannelItem>>(CacheKey, ct);
  20. if (cached is not null)
  21. {
  22. return new Response(cached);
  23. }
  24. var items = await BuildAsync(ct);
  25. await cache.SetAsync(CacheKey, items, CacheTtl, ct);
  26. return new Response(items);
  27. }
  28. private async Task<List<ChannelItem>> BuildAsync(CancellationToken ct)
  29. {
  30. // 1. 활성 채널 목록 조회
  31. var channels = await db.Channel.AsNoTracking().Where(c => c.IsActive).Select(c => new { c.SID, c.Name, c.Handle }).ToListAsync(ct);
  32. if (channels.Count == 0)
  33. {
  34. return [];
  35. }
  36. // 2. YouTube 채널 정보 캐시 일괄 조회 (Redis — API 호출 0)
  37. var channelIds = channels.Select(c => c.SID).ToList();
  38. var ytCache = await channelCache.GetManyAsync(channelIds);
  39. // 3. 현재 라이브 중인 채널 상태 조회 (Redis — 0 unit)
  40. var allLive = await liveStateStore.GetAllLiveAsync();
  41. var liveMap = allLive.ToDictionary(l => l.ChannelId);
  42. // 4. 채널별 정보 조합 — ViewerCount 는 poller 가 30초 주기로 갱신한 Redis 값 사용
  43. var items = new List<ChannelItem>(channels.Count);
  44. foreach (var ch in channels)
  45. {
  46. var isLive = liveMap.TryGetValue(ch.SID, out var liveInfo);
  47. var hasYtInfo = ytCache.TryGetValue(ch.SID, out var ytInfo);
  48. items.Add(new ChannelItem(
  49. ch.SID,
  50. ch.Name,
  51. ch.Handle,
  52. hasYtInfo ? ytInfo!.ThumbnailUrl : null,
  53. hasYtInfo ? ytInfo!.SubscriberCount : 0,
  54. isLive,
  55. isLive ? liveInfo!.ViewerCount : 0,
  56. isLive ? liveInfo!.VideoId : null
  57. ));
  58. }
  59. // 5. 정렬: 라이브(시청자 desc) → 비라이브(Fisher-Yates random shuffle)
  60. var live = items.Where(i => i.IsLive).OrderByDescending(i => i.ViewerCount).ToList();
  61. var offline = items.Where(i => !i.IsLive).ToList();
  62. Shuffle(offline);
  63. return [.. live, .. offline];
  64. }
  65. private static void Shuffle<T>(List<T> list)
  66. {
  67. var rnd = Random.Shared;
  68. for (int i = list.Count - 1; i > 0; i--)
  69. {
  70. int j = rnd.Next(i + 1);
  71. (list[i], list[j]) = (list[j], list[i]);
  72. }
  73. }
  74. }