using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Application.Abstractions.Stocks; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.Forum.StockBoard.Trending; /// /// 종목 트렌딩 랭킹 (d2 §④·§⑥ M2) — Redis Sorted Set 캐시(TTL 10분)에서 상위 종목 코드/점수를 가져와 /// Stock(종목명·denorm 시세)·StockBoardStats(총 글 수)를 조인해 반환한다. 익명 열람. /// internal sealed class Handler(IAppDbContext db, IStockBoardTrendingCache cache) : IQueryHandler { private const int DefaultCount = 20; private const int MaxCount = 50; public async Task Handle(Query request, CancellationToken ct) { var count = request.Count <= 0 ? DefaultCount : Math.Min(request.Count, MaxCount); var ranked = await cache.GetTopAsync(count, db, ct); if (ranked.Count == 0) { return new Response([]); } var codes = ranked.Select(c => c.StockCode).ToList(); var stocks = await db.Stock.AsNoTracking() .Where(c => codes.Contains(c.Code)) .Select(c => new { c.Code, c.Name, c.LastClosePrice, c.LastChangeRate }) .ToDictionaryAsync(c => c.Code, ct); var postCounts = await db.StockBoardStats.AsNoTracking() .Where(c => codes.Contains(c.StockCode)) .Select(c => new { c.StockCode, c.Posts }) .ToDictionaryAsync(c => c.StockCode, c => c.Posts, ct); var rows = new List(ranked.Count); for (var i = 0; i < ranked.Count; i++) { var entry = ranked[i]; stocks.TryGetValue(entry.StockCode, out var stock); postCounts.TryGetValue(entry.StockCode, out var posts); rows.Add(new Response.Row( Rank: i + 1, StockCode: entry.StockCode, StockName: stock?.Name, ClosePrice: stock?.LastClosePrice, ChangeRate: stock?.LastChangeRate, Posts: posts, Score: entry.Score)); } return new Response(rows); } }