using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.Forum.StockBoard.ListPosts; /// /// 종목 가상보드 글 목록 (d2 §④ M2) — Post.StockCode 파티션 필터(최신순), StockBoardStats + 종목명 헤더 포함. /// 삭제글 제외. 익명 열람. /// internal sealed class Handler(IAppDbContext db) : IQueryHandler { public async Task Handle(Query request, CancellationToken ct) { var stockCode = request.StockCode?.Trim() ?? string.Empty; var page = request.Page < 1 ? 1 : request.Page; var perPage = request.PerPage == 0 ? (ushort)20 : request.PerPage; var stockName = await db.Stock.AsNoTracking().Where(c => c.Code == stockCode).Select(c => c.Name).FirstOrDefaultAsync(ct); var stats = await db.StockBoardStats.AsNoTracking().Where(c => c.StockCode == stockCode) .Select(c => new { c.Posts, c.Comments, c.LastPostAt }) .FirstOrDefaultAsync(ct); var header = new Response.StatsHeader( stockCode, stockName, stats?.Posts ?? 0, stats?.Comments ?? 0, stats?.LastPostAt); var baseQuery = db.Post.AsNoTracking().Where(c => c.StockCode == stockCode && !c.IsDeleted); var total = await baseQuery.CountAsync(ct); var predictionPostIDs = await db.PostPrediction.AsNoTracking() .Where(c => c.StockCode == stockCode) .Select(c => c.PostID) .ToListAsync(ct); var predictionSet = predictionPostIDs.ToHashSet(); var list = await baseQuery .OrderByDescending(c => c.ID) .Skip((page - 1) * perPage) .Take(perPage) .Select(c => new { c.ID, c.StockCode, c.Subject, c.Thumbnail, c.Name, c.SID, c.Views, c.Likes, c.Comments, c.Images, c.UpdatedAt, c.CreatedAt }) .ToListAsync(ct); var startNum = total - ((page - 1) * perPage); return new Response( total, header, [..list.Select((c, i) => new Response.Row( Num: startNum - i, c.ID, c.StockCode ?? stockCode, c.Subject, c.Thumbnail, c.Name, c.SID, predictionSet.Contains(c.ID), c.Views, c.Likes, c.Comments, c.Images, c.UpdatedAt, c.CreatedAt ))] ); } }