diff --git a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java index 071f59c7..d4bea002 100644 --- a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java +++ b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java @@ -87,7 +87,7 @@ public ResponseEntity getArticleDetails(@PathVariable( @GetMapping public ResponseEntity> getArticlePages( Pageable pageable, - @ModelAttribute ArticleSearchParameter param) { // todo: 해당 유저가 좋아요를 눌렀는지 여부까지 표시되도록 수정 + @ModelAttribute ArticleSearchParameter param) { return ResponseEntity.ok(articleService.getArticleListPaging(param, pageable)); } diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java index ec940a48..a695eb61 100644 --- a/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java @@ -26,6 +26,7 @@ public class GetArticleDetailResponse { private String content; private boolean isLiked; private ArticleTag articleTag; + private Long commentCount; // 작성자 관련 private String ownerName; @@ -43,7 +44,8 @@ public static GetArticleDetailResponse of( List imageUrls, String thumbNailImageUrl, Member owner, - List memberLikedArticleList) { + List memberLikedArticleList, + Long commentCount ) { return GetArticleDetailResponse.builder() .articleId(article.getId()) .title(article.getTitle()) @@ -64,8 +66,8 @@ public static GetArticleDetailResponse of( .ownerProfileImageUrl(owner.getProfileImageUrl()) .isLiked(memberLikedArticleList.contains(article.getId())) .articleTag(article.getArticleTag()) + .commentCount(commentCount) .build(); } - } diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleListResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleListResponse.java index cfbaa595..cf86ba21 100644 --- a/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleListResponse.java +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleListResponse.java @@ -32,7 +32,7 @@ public class GetArticleListResponse { private Long thumbNailImageId; private Integer articleLikeCount; - private Integer commentCount; + private Long commentCount; private Boolean isLiked; @@ -48,7 +48,6 @@ public GetArticleListResponse(Article article, Long ownerId, Long thumbNailImage this.articleStatus = article.getArticleStatus(); this.thumbNailImageId = thumbNailImageId; this.articleLikeCount = article.getLikeCount(); - this.commentCount = article.getCommentCount(); } public void setOwnerNickName(String nickName){ @@ -59,7 +58,14 @@ public void setThumbNailImageUrl(String thumbNailImageUrl){ this.thumbNailImageUrl = thumbNailImageUrl; } - public void updateInfo(Optional owner, Optional articleImage, List memberLikedArticleIdList){ + public void setCommentCount(Long commentCount){ + this.commentCount = commentCount; + } + + public void updateInfo(Optional owner, + Optional articleImage, + List memberLikedArticleIdList, + Long commentCount ){ owner.ifPresentOrElse( o -> setOwnerNickName(o.getNickName()), () -> setOwnerNickName("알 수 없는 사용자") @@ -69,6 +75,7 @@ public void updateInfo(Optional owner, Optional articleIma () -> setThumbNailImageUrl("") ); this.isLiked = memberLikedArticleIdList.contains(id); + this.commentCount = commentCount; } } diff --git a/src/main/java/com/numberone/backend/domain/article/entity/Article.java b/src/main/java/com/numberone/backend/domain/article/entity/Article.java index aec9aee7..92f0785f 100644 --- a/src/main/java/com/numberone/backend/domain/article/entity/Article.java +++ b/src/main/java/com/numberone/backend/domain/article/entity/Article.java @@ -58,10 +58,6 @@ public class Article extends BaseTimeEntity { @Comment("게시글 좋아요 개수") private Integer likeCount; - @ColumnDefault("0") - @Comment("게시글에 달린 댓글 개수") - private Integer commentCount; - @Comment("작성자 ID") private Long articleOwnerId; @@ -71,7 +67,6 @@ public Article(String title, String content, Long articleOwnerId, ArticleTag tag this.articleOwnerId = articleOwnerId; this.articleTag = tag; this.articleStatus = ArticleStatus.ACTIVATED; - this.commentCount = 0; this.likeCount = 0; } diff --git a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java index 97ca9ece..52de3c0d 100644 --- a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java +++ b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java @@ -141,6 +141,7 @@ public GetArticleDetailResponse getArticleDetail(Long articleId) { Optional thumbNailImage = articleImageRepository.findById(article.getThumbNailImageUrlId()); + Long commentCount = commentRepository.countAllByArticle(articleId); String thumbNailImageUrl = ""; if (thumbNailImage.isPresent()) { @@ -152,7 +153,7 @@ public GetArticleDetailResponse getArticleDetail(Long articleId) { .stream().map(ArticleLike::getArticleId) .toList(); - return GetArticleDetailResponse.of(article, imageUrls, thumbNailImageUrl, owner, memberLikedArticleIdList); + return GetArticleDetailResponse.of(article, imageUrls, thumbNailImageUrl, owner, memberLikedArticleIdList, commentCount); } public Slice getArticleListPaging(ArticleSearchParameter param, Pageable pageable) { @@ -177,8 +178,9 @@ public void updateArticleInfo(GetArticleListResponse articleInfo, List mem Optional owner = memberRepository.findById(ownerId); Optional articleImage = articleImageRepository.findById(thumbNailImageUrlId); + Long commentCount = commentRepository.countAllByArticle(articleInfo.getId()); - articleInfo.updateInfo(owner, articleImage, memberLikedArticleIdList); + articleInfo.updateInfo(owner, articleImage, memberLikedArticleIdList, commentCount); } @Transactional @@ -193,8 +195,8 @@ public CreateCommentResponse createComment(Long articleId, CreateCommentRequest ); articleParticipantRepository.save(new ArticleParticipant(article, member)); + // 게시글 작성자에게 알림을 보낸다. fcmMessageProvider.sendFcm(member, ARTICLE_COMMENT_FCM_ALARM, NotificationTag.COMMUNITY); - return CreateCommentResponse.of(savedComment); } diff --git a/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java b/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java index 6ccf4158..be571887 100644 --- a/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java +++ b/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java @@ -2,12 +2,14 @@ import com.numberone.backend.domain.comment.dto.request.CreateChildCommentRequest; import com.numberone.backend.domain.comment.dto.response.CreateChildCommentResponse; +import com.numberone.backend.domain.comment.dto.response.DeleteCommentResponse; import com.numberone.backend.domain.comment.dto.response.GetCommentDto; import com.numberone.backend.domain.comment.service.CommentService; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.hibernate.sql.Delete; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -56,10 +58,22 @@ public ResponseEntity createChildComment( """) @GetMapping("{article-id}") public ResponseEntity> getCommentsByArticle(@PathVariable("article-id") Long articleId){ - List response = commentService.getCommentsByArticle(articleId); // todo: 해당 유저가 좋아요를 눌렀는지 여부까지 표시되도록 수정 + List response = commentService.getCommentsByArticle(articleId); return ResponseEntity.ok(response); } - // todo: 댓글 삭제, 가장 많은 좋아요 상단 고정, 대댓글 달리면 푸시 알람 전송, 상단 고정된 작성자에게 푸시알람 전송, 댓글 신고 기능 + @Operation(summary = "댓글 삭제 API 입니다", description = """ + 삭제할 댓글의 id 를 path variable 으로 보내주세요. + + 대댓글이 존재하는 댓글을 삭제 요청하는 경우에는, 대댓글까지 모두 삭제됩니다. + + 대댓글이 없는 댓글을 삭제 요청하는 경우에는 해당 댓글만 삭제됩니다. + """) + @DeleteMapping("{comment-id}") + public ResponseEntity deleteComment(@PathVariable("comment-id") Long commentId){ + return ResponseEntity.ok(commentService.deleteComment(commentId)); + } + + // todo: 가장 많은 좋아요 상단 고정, 대댓글 달리면 푸시 알람 전송, 상단 고정된 작성자에게 푸시알람 전송, 댓글 신고 기능 } diff --git a/src/main/java/com/numberone/backend/domain/comment/dto/response/DeleteCommentResponse.java b/src/main/java/com/numberone/backend/domain/comment/dto/response/DeleteCommentResponse.java new file mode 100644 index 00000000..7c06b632 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/comment/dto/response/DeleteCommentResponse.java @@ -0,0 +1,12 @@ +package com.numberone.backend.domain.comment.dto.response; + +import lombok.*; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class DeleteCommentResponse { + private Long commentId; +} diff --git a/src/main/java/com/numberone/backend/domain/comment/dto/response/GetCommentDto.java b/src/main/java/com/numberone/backend/domain/comment/dto/response/GetCommentDto.java index 4fe99af3..efedf0c3 100644 --- a/src/main/java/com/numberone/backend/domain/comment/dto/response/GetCommentDto.java +++ b/src/main/java/com/numberone/backend/domain/comment/dto/response/GetCommentDto.java @@ -32,6 +32,7 @@ public class GetCommentDto { private Long authorId; private String authorNickName; private String authorProfileImageUrl; + private boolean isLiked; @QueryProjection @@ -47,7 +48,7 @@ public GetCommentDto(CommentEntity comment){ this.likeCount = comment.getLikeCount(); } - public void updateCommentInfo(Optional author){ + public void updateCommentInfo(Optional author, List likedCommentIdList){ author.ifPresentOrElse( a -> { this.authorNickName = a.getNickName(); @@ -57,6 +58,7 @@ public void updateCommentInfo(Optional author){ this.authorNickName = "알 수 없는 사용자"; } ); + this.isLiked = likedCommentIdList.contains(commentId); } } diff --git a/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustom.java b/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustom.java index fe326c39..66adfcbf 100644 --- a/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustom.java +++ b/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustom.java @@ -5,6 +5,7 @@ import java.util.List; public interface CommentRepositoryCustom { - public List findAllByArticle(Long articleId); + List findAllByArticle(Long articleId); + Long countAllByArticle(Long articleId); } diff --git a/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustomImpl.java b/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustomImpl.java index 69331895..ce15479f 100644 --- a/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustomImpl.java +++ b/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustomImpl.java @@ -31,4 +31,15 @@ public List findAllByArticle(Long articleId) { ) .fetch(); } + + + @Override + public Long countAllByArticle(Long articleId) { + return queryFactory.select(commentEntity.count()) + .from(commentEntity) + .innerJoin(article, article) + .where(article.id.eq(articleId)) + .fetchOne(); + + } } diff --git a/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java b/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java index ed8b83d7..825ab152 100644 --- a/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java +++ b/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java @@ -6,9 +6,12 @@ import com.numberone.backend.domain.articleparticipant.repository.ArticleParticipantRepository; import com.numberone.backend.domain.comment.dto.request.CreateChildCommentRequest; import com.numberone.backend.domain.comment.dto.response.CreateChildCommentResponse; +import com.numberone.backend.domain.comment.dto.response.DeleteCommentResponse; import com.numberone.backend.domain.comment.dto.response.GetCommentDto; import com.numberone.backend.domain.comment.entity.CommentEntity; import com.numberone.backend.domain.comment.repository.CommentRepository; +import com.numberone.backend.domain.like.entity.CommentLike; +import com.numberone.backend.domain.like.repository.CommentLikeRepository; import com.numberone.backend.domain.member.entity.Member; import com.numberone.backend.domain.member.repository.MemberRepository; import com.numberone.backend.domain.token.util.SecurityContextProvider; @@ -32,6 +35,7 @@ public class CommentService { private final ArticleRepository articleRepository; private final ArticleParticipantRepository articleParticipantRepository; private final MemberRepository memberRepository; + private final CommentLikeRepository commentLikeRepository; @Transactional public CreateChildCommentResponse createChildComment( @@ -56,10 +60,15 @@ public CreateChildCommentResponse createChildComment( } public List getCommentsByArticle(Long articleId) { + String principal = SecurityContextProvider.getAuthenticatedUserEmail(); + Member member = memberRepository.findByEmail(principal) + .orElseThrow(NotFoundMemberException::new); Article article = articleRepository.findById(articleId) .orElseThrow(NotFoundArticleException::new); List comments = commentRepository.findAllByArticle(article.getId()); - + List likedCommentIdList = commentLikeRepository.findByMember(member) + .stream().map(CommentLike::getCommentId) + .toList(); // 계층 구조로 변환 (추후 리팩토링 필요) List result = new ArrayList<>(); Map map = new HashMap<>(); @@ -68,7 +77,7 @@ public List getCommentsByArticle(Long articleId) { CommentEntity commentEntity = commentRepository.findById(comment.getCommentId()) .orElseThrow(NotFoundCommentException::new); Optional author = memberRepository.findById(commentEntity.getAuthorId()); - comment.updateCommentInfo(author); + comment.updateCommentInfo(author, likedCommentIdList); map.put(comment.getCommentId(), comment); @@ -85,4 +94,14 @@ public List getCommentsByArticle(Long articleId) { return result; } + @Transactional + public DeleteCommentResponse deleteComment(Long commentId){ + CommentEntity commentEntity = commentRepository.findById(commentId) + .orElseThrow(NotFoundCommentException::new); + commentRepository.delete(commentEntity); + return DeleteCommentResponse.builder() + .commentId(commentId) + .build(); + } + } diff --git a/src/main/java/com/numberone/backend/domain/like/service/LikeService.java b/src/main/java/com/numberone/backend/domain/like/service/LikeService.java index c43c90d7..f215355e 100644 --- a/src/main/java/com/numberone/backend/domain/like/service/LikeService.java +++ b/src/main/java/com/numberone/backend/domain/like/service/LikeService.java @@ -10,19 +10,24 @@ import com.numberone.backend.domain.like.repository.CommentLikeRepository; import com.numberone.backend.domain.member.entity.Member; import com.numberone.backend.domain.member.repository.MemberRepository; +import com.numberone.backend.domain.notification.entity.NotificationTag; import com.numberone.backend.domain.token.util.SecurityContextProvider; import com.numberone.backend.exception.conflict.AlreadyLikedException; import com.numberone.backend.exception.conflict.AlreadyUnLikedException; import com.numberone.backend.exception.notfound.NotFoundApiException; import com.numberone.backend.exception.notfound.NotFoundCommentException; import com.numberone.backend.exception.notfound.NotFoundMemberException; +import com.numberone.backend.support.fcm.service.FcmMessageProvider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.hibernate.id.IntegralDataTypeHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import static com.numberone.backend.support.notification.NotificationMessage.BEST_ARTICLE_FCM_ALARM; + @Slf4j @Service @RequiredArgsConstructor @@ -34,7 +39,9 @@ public class LikeService { private final CommentLikeRepository commentLikeRepository; private final CommentRepository commentRepository; private final MemberRepository memberRepository; + private final FcmMessageProvider fcmMessageProvider; + private final Integer BEST_ARTICLE_LIKE_COUNT = 20; @Transactional public Integer increaseArticleLike(Long articleId) { @@ -50,6 +57,13 @@ public Integer increaseArticleLike(Long articleId) { article.increaseLikeCount(); articleLikeRepository.save(new ArticleLike(member, article)); + if (article.getLikeCount() >= BEST_ARTICLE_LIKE_COUNT) { + Long ownerId = article.getArticleOwnerId(); + Member owner = memberRepository.findById(ownerId) + .orElseThrow(NotFoundMemberException::new); + fcmMessageProvider.sendFcm(owner, BEST_ARTICLE_FCM_ALARM, NotificationTag.COMMUNITY); + } + return article.getLikeCount(); } @@ -97,7 +111,7 @@ public Integer decreaseCommentLike(Long commentId) { .orElseThrow(NotFoundMemberException::new); CommentEntity commentEntity = commentRepository.findById(commentId) .orElseThrow(NotFoundCommentException::new); - if (!isAlreadyLikedComment(member, commentId)){ + if (!isAlreadyLikedComment(member, commentId)) { // 좋아요를 누르지 않은 댓글이라 좋아요를 취소할 수 없습니다. throw new AlreadyUnLikedException(); } diff --git a/src/main/java/com/numberone/backend/support/notification/NotificationMessage.java b/src/main/java/com/numberone/backend/support/notification/NotificationMessage.java index 8fdf8316..aa0a387c 100644 --- a/src/main/java/com/numberone/backend/support/notification/NotificationMessage.java +++ b/src/main/java/com/numberone/backend/support/notification/NotificationMessage.java @@ -7,7 +7,9 @@ @RequiredArgsConstructor public enum NotificationMessage implements NotificationMessageSpec { - ARTICLE_COMMENT_FCM_ALARM("[대피로 알림]", "게시글에 댓글이 달렸어요!", null); + ARTICLE_COMMENT_FCM_ALARM("[대피로 알림]", "게시글에 댓글이 달렸어요!", null), + BEST_ARTICLE_FCM_ALARM("[대피로 알림]", "축하드립니다! 베스트 게시글로 선정되었습니다. 🎉", null); + ; private final String title; private final String body;