| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- 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<Query, Result<Response>>
- {
- public async Task<Result<Response>> 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<int> 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<int, Response.CommentItem>();
- var rootItems = new List<Response.CommentItem>();
- 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);
- }
- }
|