using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Application.Abstractions.Notification; using Domain.Entities.Feed; using Domain.Entities.Notifications.ValueObject; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; namespace Application.Features.Api.Feed.CreateComment; public sealed class Handler(IAppDbContext db, INotificationService notificationService) : ICommandHandler> { private const int MaxContentLength = 2000; private const int MaxDepth = 10; public async Task> Handle(Command request, CancellationToken ct) { if (request.MemberID <= 0) { return Result.Failure(Error.Problem("Feed.Comment.MemberRequired", "로그인이 필요합니다.")); } if (string.IsNullOrWhiteSpace(request.Content)) { return Result.Failure(Error.Problem("Feed.Comment.ContentRequired", "댓글 내용을 입력해주세요.")); } var content = request.Content.Trim(); if (content.Length > MaxContentLength) { return Result.Failure(Error.Problem("Feed.Comment.ContentTooLong", $"댓글은 최대 {MaxContentLength}자까지 입력 가능합니다.")); } var post = await db.FeedPost.FirstOrDefaultAsync(p => p.ID == request.PostID && !p.IsDeleted, ct); if (post is null) { return Result.Failure(Error.NotFound("Feed.PostNotFound", "게시글을 찾을 수 없습니다.")); } sbyte depth = 0; FeedComment? parent = null; if (request.ParentCommentID is int parentID && parentID > 0) { parent = await db.FeedComment.FirstOrDefaultAsync(c => c.ID == parentID && c.FeedPostID == request.PostID, ct); if (parent is null) { return Result.Failure(Error.NotFound("Feed.ParentCommentNotFound", "부모 댓글을 찾을 수 없습니다.")); } depth = (sbyte)Math.Min(parent.Depth + 1, MaxDepth); } var comment = new FeedComment { FeedPostID = request.PostID, MemberID = request.MemberID, ParentID = parent?.ID, Depth = depth, Content = content }; await db.FeedComment.AddAsync(comment, ct); post.Comments++; if (parent is not null) { parent.Replies++; } await db.SaveChangesAsync(ct); // ── 알림: 부모 댓글 작성자 + 포스트 작성자 (본인 제외, 중복 제외) ── var author = await db.Member.AsNoTracking().Where(m => m.ID == request.MemberID).Select(m => new { m.Name, m.SID }).FirstOrDefaultAsync(ct); var notifyTargets = new HashSet(); if (parent is not null && parent.MemberID != request.MemberID) { notifyTargets.Add(parent.MemberID); } if (post.MemberID != request.MemberID) { notifyTargets.Add(post.MemberID); } foreach (var targetID in notifyTargets) { await notificationService.SendAsync( memberID: targetID, type: NotificationType.NewFeedComment, title: parent is not null ? "새 답글" : "새 댓글", message: $"{author?.Name ?? author?.SID ?? "누군가"}님이 댓글을 남겼습니다.", actionUrl: $"/feed/post/{request.PostID}", relatedType: "FeedComment", relatedID: comment.ID, ct: ct ); } return new Response(comment.ID); } }