using Application.Abstractions.Messaging; using Application.Abstractions.Data; using Domain.Entities.Forum.ValueObject; using SharedKernel.Results; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.Forum.Comment.List; public sealed class Handler(IAppDbContext db) : IQueryHandler> { public async Task> Handle(Query request, CancellationToken ct) { // 1. 댓글 총 개수 (표시용: 루트+대댓글) & 루트 댓글 개수 (페이지네이션용) var total = await db.Comment.AsNoTracking().CountAsync(c => c.PostID == request.PostID && !c.IsDeleted, ct); var totalRoots = await db.Comment.AsNoTracking().CountAsync(c => c.PostID == request.PostID && !c.IsDeleted && c.ParentID == null, ct); if (totalRoots == 0) { return new Response(total, 0, []); } // 비밀글 열람 권한 판별을 위한 게시글 정보 조회 var post = await db.Post.AsNoTracking().Where(p => p.ID == request.PostID).Select(p => new { p.MemberID, p.BoardID }).FirstOrDefaultAsync(ct); var canViewAllSecrets = false; if (request.MemberID.HasValue && post is not null) { var reqMember = await db.Member.AsNoTracking().FirstOrDefaultAsync(x => x.ID == request.MemberID.Value, ct); if (reqMember is not null && reqMember.IsAdmin) { canViewAllSecrets = true; } else if (reqMember is not null) { canViewAllSecrets = await db.BoardManager.AsNoTracking().AnyAsync(x => x.BoardID == post.BoardID && x.MemberID == request.MemberID.Value, ct); } } // 2. 루트 댓글 페이지네이션 + 정렬 var rootQuery = db.Comment.AsNoTracking().Include(c => c.Member).ThenInclude(m => m.MemberGrade).Include(c => c.CommentMention).Where(c => c.PostID == request.PostID && !c.IsDeleted && c.ParentID == null); rootQuery = request.Sort switch { // 인기순 1 => rootQuery.OrderByDescending(c => c.Likes).ThenByDescending(c => c.ID), // 최신순 _ => rootQuery.OrderByDescending(c => c.ID) }; var roots = await rootQuery.Skip((request.Page - 1) * request.PerPage).Take(request.PerPage).ToListAsync(ct); // 3. 루트 댓글의 자식 댓글 일괄 로드 var rootIDs = roots.Select(c => c.ID).ToHashSet(); var children = await db.Comment.AsNoTracking() .Include(c => c.Member).ThenInclude(m => m.MemberGrade) .Include(c => c.CommentMention) .Where(c => c.PostID == request.PostID && !c.IsDeleted && c.ParentID != null && rootIDs.Contains(c.ParentID.Value)) .OrderBy(c => c.ID) .ToListAsync(ct); var allComments = roots.Concat(children).ToList(); // 4. 사용자별 상태 벌크 로드 HashSet likeIDs = [], dislikeIDs = [], reportedIDs = []; if (request.MemberID is int memberID) { var allCommentIds = allComments.Select(c => c.ID).ToHashSet(); likeIDs = (await db.CommentReaction.AsNoTracking() .Where(x => x.PostID == request.PostID && x.MemberID == memberID && x.Reaction == Reaction.Like && allCommentIds.Contains(x.CommentID)) .Select(x => x.CommentID) .ToListAsync(ct)).ToHashSet(); dislikeIDs = (await db.CommentReaction.AsNoTracking() .Where(x => x.PostID == request.PostID && x.MemberID == memberID && x.Reaction == Reaction.Dislike && allCommentIds.Contains(x.CommentID)) .Select(x => x.CommentID) .ToListAsync(ct)).ToHashSet(); reportedIDs = (await db.CommentReport.AsNoTracking() .Where(x => x.PostID == request.PostID && x.MemberID == memberID && allCommentIds.Contains(x.CommentID)) .Select(x => x.CommentID) .ToListAsync(ct)).ToHashSet(); } // 5. CommentItem 매핑 var itemMap = new Dictionary(); var rootItems = new List(); foreach (var c in allComments) { var writer = new Response.WriterDto( c.Member.ID, c.Member.SID, c.Member.Name, c.Member.Thumb, c.Member.Icon, c.Member.MemberGrade?.Image, c.Member.CreatedAt ); Response.MentionDto? mention = c.CommentMention is not null ? new Response.MentionDto(c.CommentMention.MemberID, c.CommentMention.RawHandle) : null; // 비밀글: 본인, 게시글 작성자, 관리자/매니저만 열람 가능 var content = c.Content; if (c.IsSecret && !canViewAllSecrets && request.MemberID != c.MemberID && request.MemberID != post?.MemberID) { content = ""; } var item = new Response.CommentItem( c.ID, c.PostID, c.MemberID, c.ParentID, writer, mention, content, c.IsReply, c.IsSecret, c.Likes, c.Dislikes, c.Reports, c.Replies, likeIDs.Contains(c.ID), dislikeIDs.Contains(c.ID), reportedIDs.Contains(c.ID), c.CreatedAt, [] ); itemMap[c.ID] = item; } // 6. 트리 빌드 foreach (var item in itemMap.Values) { if (item.ParentID is int parentID && itemMap.TryGetValue(parentID, out var parent)) { parent.Children.Add(item); } else { rootItems.Add(item); } } return new Response(total, totalRoots, rootItems); } }