DiscussionCommentService.java

package qwerty.chaekit.service.group;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import qwerty.chaekit.domain.group.activity.discussion.Discussion;
import qwerty.chaekit.domain.group.activity.discussion.DiscussionStance;
import qwerty.chaekit.domain.group.activity.discussion.comment.DiscussionComment;
import qwerty.chaekit.domain.group.activity.discussion.comment.repository.DiscussionCommentRepository;
import qwerty.chaekit.domain.member.user.UserProfile;
import qwerty.chaekit.dto.group.activity.discussion.DiscussionCommentFetchResponse;
import qwerty.chaekit.dto.group.activity.discussion.DiscussionCommentPatchRequest;
import qwerty.chaekit.dto.group.activity.discussion.DiscussionCommentPostRequest;
import qwerty.chaekit.global.enums.ErrorCode;
import qwerty.chaekit.global.exception.BadRequestException;
import qwerty.chaekit.global.exception.NotFoundException;
import qwerty.chaekit.global.security.resolver.UserToken;
import qwerty.chaekit.mapper.DiscussionMapper;
import qwerty.chaekit.service.notification.NotificationService;
import qwerty.chaekit.service.util.EntityFinder;

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class DiscussionCommentService {
    private final DiscussionCommentRepository discussionCommentRepository;
    private final ActivityPolicy activityPolicy;
    private final DiscussionMapper discussionMapper;
    private final NotificationService notificationService;
    private final EntityFinder entityFinder;
    
    public DiscussionCommentFetchResponse getComment(UserToken userToken, Long commentId) {
        DiscussionComment comment = discussionCommentRepository.findByIdWithAuthor(commentId)
                .orElseThrow(() -> new NotFoundException(ErrorCode.DISCUSSION_COMMENT_NOT_FOUND));
        activityPolicy.assertJoined(userToken.userId(), comment.getDiscussion().getActivity().getId());
        return discussionMapper.toCommentFetchResponse(comment);
    }

    public DiscussionCommentFetchResponse addComment(Long discussionId, DiscussionCommentPostRequest request, UserToken userToken) {
        UserProfile user = entityFinder.findUser(userToken.userId());
        Discussion discussion = entityFinder.findDiscussion(discussionId);
        String content = request.content();
        DiscussionStance stance = request.stance();
        
        activityPolicy.assertJoined(user, discussion.getActivity());

        DiscussionComment parentComment;
        boolean isReply = request.parentId() != null;
        if (isReply) {
            parentComment = entityFinder.findDiscussionComment(request.parentId());
            if (parentComment.isReply()){
                throw new BadRequestException(ErrorCode.REPLY_CANNOT_HAVE_CHILD);
            }
            if (parentComment.isDeleted()){
                throw new BadRequestException(ErrorCode.DISCUSSION_COMMENT_DELETED);
            }
        } else {
            parentComment = null;
        }

        DiscussionComment comment = DiscussionComment.builder()
                .author(user)
                .discussion(discussion)
                .content(content)
                .stance(stance)
                .parent(parentComment)
                .build();
        discussionCommentRepository.save(comment);

        if (parentComment != null && !parentComment.isAuthor(user)) {
            notificationService.createCommentReplyNotification(parentComment.getAuthor(), user,parentComment);
        } else {
            if (!discussion.isAuthor(user)) {
                notificationService.createDiscussionCommentNotification(discussion.getAuthor(), user, discussion);
            }
            // TODO: 이 루프에서 c.getAuthor().getNickname() 접근 시 N+1 쿼리 발생 가능성 있음.
            // 필요 시 discussion.getComments() 조회 시 fetch join 적용 고려
            discussion.getComments().stream()
                    .filter(c -> !c.isDeleted() && c.isRootComment() && !c.isAuthor(user))
                    .forEach(c -> notificationService.createDiscussionCommentNotification(c.getAuthor(), user, discussion));
        }

        return discussionMapper.toCommentFetchResponse(comment);
    }

    public DiscussionCommentFetchResponse updateComment(Long commentId, DiscussionCommentPatchRequest request, UserToken userToken) {
        UserProfile user = entityFinder.findUser(userToken.userId());
        DiscussionComment comment = entityFinder.findDiscussionComment(commentId);

        if (comment.isDeleted()) {
            throw new BadRequestException(ErrorCode.DISCUSSION_COMMENT_DELETED);
        }
        if (!comment.isAuthor(user)) {
            throw new BadRequestException(ErrorCode.DISCUSSION_COMMENT_NOT_YOURS);
        }
        comment.updateContent(request.content());
        return discussionMapper.toCommentFetchResponse(comment);
    }

    public void deleteComment(Long commentId, UserToken userToken) {
        UserProfile user = entityFinder.findUser(userToken.userId());
        DiscussionComment comment = entityFinder.findDiscussionComment(commentId);

        if (!comment.isAuthor(user)) {
            throw new BadRequestException(ErrorCode.DISCUSSION_COMMENT_NOT_YOURS);
        }

        if(comment.isDeleted()) {
            throw new BadRequestException(ErrorCode.DISCUSSION_COMMENT_DELETED);
        }
        
        if (comment.isRootComment()) {
            if (hasReplies(comment)) {
                comment.softDelete();
            } else {
                removeRootComment(comment);
            }
        } else {
            DiscussionComment parentComment = comment.getParent();
            if (parentComment.isDeleted() && hasLastReply(parentComment)) {
                removeRootComment(parentComment);
            }
            parentComment.removeReply(comment);
        }
    }

    private boolean hasReplies(DiscussionComment comment) {
        return discussionCommentRepository.countByParentId(comment.getId()) > 0;
    }

    private boolean hasLastReply(DiscussionComment comment) {
        return discussionCommentRepository.countByParentId(comment.getId()) == 1;
    }

    private void removeRootComment(DiscussionComment comment) {
        discussionCommentRepository.delete(comment);
    }
}