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);
}
}