Skip to content

Commit

Permalink
Merge pull request #23 from Leets-Official/feat/#21/게시글-좋아요-게시글-댓글-기능-구현
Browse files Browse the repository at this point in the history
[feat] 게시글 좋아요 / 게시글 댓글 기능 구현
  • Loading branch information
ehs208 authored Nov 12, 2024
2 parents 844a6e1 + f1a6d5e commit 9f60b4a
Show file tree
Hide file tree
Showing 29 changed files with 1,002 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import java.util.List;
import java.util.UUID;

import com.leets.xcellentbe.domain.articleLike.domain.ArticleLike;
import com.leets.xcellentbe.domain.articleMedia.domain.ArticleMedia;
import com.leets.xcellentbe.domain.comment.domain.Comment;
import com.leets.xcellentbe.domain.hashtag.domain.Hashtag;
import com.leets.xcellentbe.domain.shared.BaseTimeEntity;
import com.leets.xcellentbe.domain.shared.DeletedStatus;
Expand Down Expand Up @@ -61,10 +63,17 @@ public class Article extends BaseTimeEntity {
@OneToMany(mappedBy = "article")
private List<ArticleMedia> mediaList;

private int viewCnt, repostCnt, likeCnt, commentCnt;
@OneToMany(mappedBy = "article")
private List<Comment> comments;

@OneToMany(mappedBy = "article")
private List<ArticleLike> articleLikes;

@Column
private int viewCnt;

@Builder
private Article(User writer, String content, DeletedStatus deletedStatus) {
private Article(User writer, String content) {
this.writer = writer;
this.content = content;
this.deletedStatus = DeletedStatus.NOT_DELETED;
Expand All @@ -83,10 +92,23 @@ public static Article createArticle(User writer, String content) {
return Article.builder()
.writer(writer)
.content(content)
.deletedStatus(DeletedStatus.NOT_DELETED)
.build();
}

public void addComments(List<Comment> comments) {
if(this.comments == null){
this.comments = new ArrayList<>();
}
this.comments.addAll(comments);
}

public void addArticleLike(List<ArticleLike> articleLikes) {
if(this.articleLikes == null){
this.articleLikes = new ArrayList<>();
}
this.articleLikes.addAll(articleLikes);
}

public void addRepost(Article rePost) {
this.rePost = rePost;
}
Expand All @@ -112,29 +134,4 @@ public void addMedia(List<ArticleMedia> mediaList) {
public void updateViewCount() {
this.viewCnt++;
}

public void plusRepostCount() {
this.repostCnt++;
}

public void minusRepostCount() {
this.repostCnt--;
}

public void plusLikeCount() {
this.likeCnt++;
}

public void minusLikeCount() {
this.likeCnt--;
}

public void plusCommentCount() {
this.commentCnt++;
}

public void minusCommentCount() {
this.commentCnt--;

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.leets.xcellentbe.domain.article.domain.Article;
import com.leets.xcellentbe.domain.article.dto.ArticlesWithMediaDto;
import com.leets.xcellentbe.domain.user.domain.User;

import io.lettuce.core.dynamic.annotation.Param;

public interface ArticleRepository extends JpaRepository<Article, UUID> {
@Query("SELECT new com.leets.xcellentbe.domain.article.dto.ArticlesWithMediaDto(p, pm.filePath) FROM Article p LEFT JOIN PostMedia pm ON p.articleId = pm.article.articleId WHERE p.writer = :user")
List<ArticlesWithMediaDto[]> findPostsByWriter(User user);

@Query("SELECT a FROM Article a ORDER BY a.createdAt DESC")
@Query("SELECT a FROM Article a WHERE a.deletedStatus = com.leets.xcellentbe.domain.shared.DeletedStatus.NOT_DELETED ORDER BY a.createdAt DESC")
List<Article> findRecentArticles(Pageable pageable);

@Query("SELECT a FROM Article a WHERE a.createdAt < :cursor ORDER BY a.createdAt DESC")
@Query("SELECT a FROM Article a WHERE a.createdAt < :cursor AND a.deletedStatus = com.leets.xcellentbe.domain.shared.DeletedStatus.NOT_DELETED ORDER BY a.createdAt DESC")
List<Article> findRecentArticles(@Param("cursor") LocalDateTime cursor, Pageable pageable);

@Query("SELECT COUNT(a) FROM Article a WHERE a.rePost = :article AND a.deletedStatus = com.leets.xcellentbe.domain.shared.DeletedStatus.NOT_DELETED")
long countReposts(@Param("article") Article article);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.leets.xcellentbe.domain.article.dto;

import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

import com.leets.xcellentbe.domain.article.domain.Article;
import com.leets.xcellentbe.domain.articleMedia.domain.ArticleMedia;
import com.leets.xcellentbe.domain.comment.dto.CommentResponseDto;
import com.leets.xcellentbe.domain.comment.dto.CommentStatsDto;
import com.leets.xcellentbe.domain.hashtag.domain.Hashtag;
import com.leets.xcellentbe.domain.shared.DeletedStatus;

Expand All @@ -23,16 +26,17 @@ public class ArticleResponseDto {
private List<String> hashtags;
private UUID rePostId;
private List<String> mediaUrls;
private List<CommentResponseDto> comments;
private int viewCnt;
private int rePostCnt;
private int likeCnt;
private int commentCnt;
private long rePostCnt;
private long likeCnt;
private long commentCnt;
private boolean owner;

@Builder
private ArticleResponseDto(UUID articleId, Long writerId, String content, DeletedStatus deletedStatus,
List<String> hashtags, UUID rePostId, List<String> mediaUrls, int viewCnt,
int rePostCnt, int likeCnt, int commentCnt, boolean owner) {
List<String> hashtags, UUID rePostId, List<String> mediaUrls, List<CommentResponseDto> comments,
int viewCnt, long rePostCnt, long likeCnt, long commentCnt, boolean owner) {
this.articleId = articleId;
this.writerId = writerId;
this.content = content;
Expand All @@ -45,9 +49,10 @@ private ArticleResponseDto(UUID articleId, Long writerId, String content, Delete
this.likeCnt = likeCnt;
this.commentCnt = commentCnt;
this.owner = owner;
this.comments = comments;
}

public static ArticleResponseDto from(Article article, boolean isOwner) {
public static ArticleResponseDto from(Article article, boolean isOwner, ArticleStatsDto stats, Map<UUID, CommentStatsDto> replyStatsMap) {
return ArticleResponseDto.builder()
.articleId(article.getArticleId())
.content(article.getContent())
Expand All @@ -62,10 +67,43 @@ public static ArticleResponseDto from(Article article, boolean isOwner) {
.stream()
.map(ArticleMedia::getFilePath) // 이미지 URL로 매핑
.collect(Collectors.toList()) : null)
.comments(article.getComments() != null ? article.getComments()
.stream()
.filter(comment -> comment != null && comment.getDeletedStatus() == DeletedStatus.NOT_DELETED) // null 및 삭제된 댓글 필터링
.map(comment -> {
CommentStatsDto commentStats = replyStatsMap.getOrDefault(comment.getCommentId(), CommentStatsDto.from(0, 0));
boolean isCommentOwner = comment.getWriter().getUserId().equals(article.getWriter().getUserId());
return CommentResponseDto.from(comment, isCommentOwner, commentStats, replyStatsMap, 1); // 깊이 1로 제한
})
.collect(Collectors.toList()) : null)
.viewCnt(article.getViewCnt())
.rePostCnt(stats.getRepostCnt())
.likeCnt(stats.getLikeCnt())
.commentCnt(stats.getCommentCnt())
.owner(isOwner)
.build();
}

public static ArticleResponseDto fromWithoutComments(Article article, boolean isOwner, ArticleStatsDto stats) {
return ArticleResponseDto.builder()
.articleId(article.getArticleId())
.content(article.getContent())
.deletedStatus(article.getDeletedStatus())
.writerId(article.getWriter().getUserId())
.hashtags(article.getHashtags() != null ? article.getHashtags()
.stream()
.map(Hashtag::getContent)
.collect(Collectors.toList()) : null)
.rePostId(article.getRePost() != null ? article.getRePost().getArticleId() : null)
.mediaUrls(article.getMediaList() != null ? article.getMediaList()
.stream()
.map(ArticleMedia::getFilePath)
.collect(Collectors.toList()) : null)
.comments(null) // 전체 조회 시 댓글 정보 제외
.viewCnt(article.getViewCnt())
.rePostCnt(article.getRepostCnt())
.likeCnt(article.getLikeCnt())
.commentCnt(article.getCommentCnt())
.rePostCnt(stats.getRepostCnt())
.likeCnt(stats.getLikeCnt())
.commentCnt(stats.getCommentCnt())
.owner(isOwner)
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.leets.xcellentbe.domain.article.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class ArticleStatsDto {
private long likeCnt;
private long commentCnt;
private long repostCnt;

@Builder
private ArticleStatsDto(long likeCnt, long commentCnt, long repostCnt) {
this.likeCnt = likeCnt;
this.commentCnt = commentCnt;
this.repostCnt = repostCnt;
}

public static ArticleStatsDto from(long likeCnt, long commentCnt, long repostCnt) {
return ArticleStatsDto.builder()
.likeCnt(likeCnt)
.commentCnt(commentCnt)
.repostCnt(repostCnt)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,18 @@
import com.leets.xcellentbe.domain.article.dto.ArticleCreateRequestDto;
import com.leets.xcellentbe.domain.article.dto.ArticleCreateResponseDto;
import com.leets.xcellentbe.domain.article.dto.ArticleResponseDto;
import com.leets.xcellentbe.domain.article.dto.ArticleStatsDto;
import com.leets.xcellentbe.domain.article.dto.ArticlesResponseDto;
import com.leets.xcellentbe.domain.article.dto.ArticlesWithMediaDto;
import com.leets.xcellentbe.domain.article.exception.ArticleNotFoundException;
import com.leets.xcellentbe.domain.article.exception.DeleteForbiddenException;
import com.leets.xcellentbe.domain.articleLike.domain.repository.ArticleLikeRepository;
import com.leets.xcellentbe.domain.comment.domain.Comment;
import com.leets.xcellentbe.domain.comment.dto.CommentStatsDto;
import com.leets.xcellentbe.domain.commentLike.domain.repository.CommentLikeRepository;
import com.leets.xcellentbe.global.error.exception.custom.DeleteForbiddenException;
import com.leets.xcellentbe.domain.articleMedia.domain.ArticleMedia;
import com.leets.xcellentbe.domain.articleMedia.domain.repository.ArticleMediaRepository;
import com.leets.xcellentbe.domain.articleMedia.exception.ArticleMediaNotFoundException;
import com.leets.xcellentbe.domain.comment.domain.repository.CommentRepository;
import com.leets.xcellentbe.domain.hashtag.HashtagService.HashtagService;
import com.leets.xcellentbe.domain.hashtag.domain.Hashtag;
import com.leets.xcellentbe.domain.user.domain.User;
Expand All @@ -43,6 +48,9 @@ public class ArticleService {
private final ArticleRepository articleRepository;
private final ArticleMediaRepository articleMediaRepository;
private final UserRepository userRepository;
private final CommentRepository commentRepository;
private final ArticleLikeRepository articleLikeRepository;
private final CommentLikeRepository commentLikeRepository;
private final HashtagService hashtagService;
private final S3UploadMediaService s3UploadMediaService;
private final JwtService jwtService;
Expand Down Expand Up @@ -149,15 +157,21 @@ public ArticleResponseDto getArticle(HttpServletRequest request, UUID articleId)

Article targetArticle = articleRepository.findById(articleId)
.orElseThrow(ArticleNotFoundException::new);

List<ArticleMedia> mediaList = articleMediaRepository.findByArticle_ArticleId(targetArticle.getArticleId());
if (mediaList.isEmpty()) {
throw new ArticleMediaNotFoundException();
}
ArticleStatsDto stats = findArticleStats(targetArticle);
targetArticle.updateViewCount();
boolean isOwner = targetArticle.getWriter().getUserId().equals(user.getUserId());

return ArticleResponseDto.from(targetArticle, isOwner);
List<Comment> comments = commentRepository.findAllByArticleAndNotDeleted(targetArticle);
Map<UUID, CommentStatsDto> replyStatsMap = comments.stream()
.collect(Collectors.toMap(
Comment::getCommentId,
reply -> {
long likeCount = commentLikeRepository.countLikesByComment(reply);
long replyCount = commentRepository.countRepliesByComment(reply);
return CommentStatsDto.from(likeCount, replyCount);
}
));
return ArticleResponseDto.from(targetArticle, isOwner, stats, replyStatsMap);
}

//게시글 전체 조회
Expand All @@ -167,13 +181,17 @@ public List<ArticleResponseDto> getArticles(HttpServletRequest request, LocalDat

Pageable pageable = PageRequest.of(0, size);

List<Article> articles = cursor == null ?
List<Article> articles = (cursor == null) ?
articleRepository.findRecentArticles(pageable) : // 처음 로드 시
articleRepository.findRecentArticles(cursor, pageable);

return articles
.stream()
.map(article -> ArticleResponseDto.from(article, article.getWriter().getUserId().equals(user.getUserId())))
.map(article -> {
boolean isOwner = article.getWriter().getUserId().equals(user.getUserId());
ArticleStatsDto stats = findArticleStats(article);
return ArticleResponseDto.fromWithoutComments(article, isOwner, stats);
})
.collect(Collectors.toList());
}

Expand All @@ -185,8 +203,7 @@ public ArticleCreateResponseDto rePostArticle(HttpServletRequest request, UUID a
Article repostedArticle = articleRepository.findById(articleId)
.orElseThrow(ArticleNotFoundException::new);
Article newArticle = Article.createArticle(writer, repostedArticle.getContent());
repostedArticle.addRepost(newArticle);
repostedArticle.plusRepostCount();
newArticle.addRepost(repostedArticle);

return ArticleCreateResponseDto.from(articleRepository.save(newArticle));
}
Expand All @@ -197,12 +214,20 @@ public void deleteRepost(HttpServletRequest request, UUID articleId) {
User user = getUser(request);
Article targetArticle = articleRepository.findById(articleId)
.orElseThrow(ArticleNotFoundException::new);
if (!(targetArticle.getWriter().getUserId().equals(user.getUserId()))) {
// 게시글 작성자와 현재 사용자 일치 여부 확인, 리포스트 ID가 있는 경우에만 삭제 가능
if ((!targetArticle.getWriter().getUserId().equals(user.getUserId()))||(targetArticle.getRePost() == null)) {
throw new DeleteForbiddenException();
} else {
targetArticle.deleteArticle();
targetArticle.getRePost().minusRepostCount();
}
// 리포스트 삭제 처리
targetArticle.deleteArticle();
articleRepository.save(targetArticle);
}

public ArticleStatsDto findArticleStats(Article article) {
long likeCount = articleLikeRepository.countLikesByArticleId(article.getArticleId());
long commentCount = commentRepository.countCommentsByArticle(article);
long repostCount = articleRepository.countReposts(article);
return ArticleStatsDto.from(likeCount, commentCount, repostCount);
}

//JWT 토큰 기반 사용자 정보 반환 메소드
Expand All @@ -214,6 +239,5 @@ private User getUser(HttpServletRequest request) {
.orElseThrow(UserNotFoundException::new);

return user;

}
}
Loading

0 comments on commit 9f60b4a

Please sign in to comment.