Skip to content

Commit 9f60b4a

Browse files
authored
Merge pull request #23 from Leets-Official/feat/#21/게시글-좋아요-게시글-댓글-기능-구현
[feat] 게시글 좋아요 / 게시글 댓글 기능 구현
2 parents 844a6e1 + f1a6d5e commit 9f60b4a

29 files changed

+1002
-78
lines changed

src/main/java/com/leets/xcellentbe/domain/article/domain/Article.java

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import java.util.List;
55
import java.util.UUID;
66

7+
import com.leets.xcellentbe.domain.articleLike.domain.ArticleLike;
78
import com.leets.xcellentbe.domain.articleMedia.domain.ArticleMedia;
9+
import com.leets.xcellentbe.domain.comment.domain.Comment;
810
import com.leets.xcellentbe.domain.hashtag.domain.Hashtag;
911
import com.leets.xcellentbe.domain.shared.BaseTimeEntity;
1012
import com.leets.xcellentbe.domain.shared.DeletedStatus;
@@ -61,10 +63,17 @@ public class Article extends BaseTimeEntity {
6163
@OneToMany(mappedBy = "article")
6264
private List<ArticleMedia> mediaList;
6365

64-
private int viewCnt, repostCnt, likeCnt, commentCnt;
66+
@OneToMany(mappedBy = "article")
67+
private List<Comment> comments;
68+
69+
@OneToMany(mappedBy = "article")
70+
private List<ArticleLike> articleLikes;
71+
72+
@Column
73+
private int viewCnt;
6574

6675
@Builder
67-
private Article(User writer, String content, DeletedStatus deletedStatus) {
76+
private Article(User writer, String content) {
6877
this.writer = writer;
6978
this.content = content;
7079
this.deletedStatus = DeletedStatus.NOT_DELETED;
@@ -83,10 +92,23 @@ public static Article createArticle(User writer, String content) {
8392
return Article.builder()
8493
.writer(writer)
8594
.content(content)
86-
.deletedStatus(DeletedStatus.NOT_DELETED)
8795
.build();
8896
}
8997

98+
public void addComments(List<Comment> comments) {
99+
if(this.comments == null){
100+
this.comments = new ArrayList<>();
101+
}
102+
this.comments.addAll(comments);
103+
}
104+
105+
public void addArticleLike(List<ArticleLike> articleLikes) {
106+
if(this.articleLikes == null){
107+
this.articleLikes = new ArrayList<>();
108+
}
109+
this.articleLikes.addAll(articleLikes);
110+
}
111+
90112
public void addRepost(Article rePost) {
91113
this.rePost = rePost;
92114
}
@@ -112,29 +134,4 @@ public void addMedia(List<ArticleMedia> mediaList) {
112134
public void updateViewCount() {
113135
this.viewCnt++;
114136
}
115-
116-
public void plusRepostCount() {
117-
this.repostCnt++;
118-
}
119-
120-
public void minusRepostCount() {
121-
this.repostCnt--;
122-
}
123-
124-
public void plusLikeCount() {
125-
this.likeCnt++;
126-
}
127-
128-
public void minusLikeCount() {
129-
this.likeCnt--;
130-
}
131-
132-
public void plusCommentCount() {
133-
this.commentCnt++;
134-
}
135-
136-
public void minusCommentCount() {
137-
this.commentCnt--;
138-
139-
}
140137
}

src/main/java/com/leets/xcellentbe/domain/article/domain/repository/ArticleRepository.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,22 @@
77
import org.springframework.data.domain.Pageable;
88
import org.springframework.data.jpa.repository.JpaRepository;
99
import org.springframework.data.jpa.repository.Query;
10+
import org.springframework.data.repository.query.Param;
1011

1112
import com.leets.xcellentbe.domain.article.domain.Article;
1213
import com.leets.xcellentbe.domain.article.dto.ArticlesWithMediaDto;
1314
import com.leets.xcellentbe.domain.user.domain.User;
1415

15-
import io.lettuce.core.dynamic.annotation.Param;
16-
1716
public interface ArticleRepository extends JpaRepository<Article, UUID> {
1817
@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")
1918
List<ArticlesWithMediaDto[]> findPostsByWriter(User user);
2019

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

24-
@Query("SELECT a FROM Article a WHERE a.createdAt < :cursor ORDER BY a.createdAt DESC")
23+
@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")
2524
List<Article> findRecentArticles(@Param("cursor") LocalDateTime cursor, Pageable pageable);
2625

26+
@Query("SELECT COUNT(a) FROM Article a WHERE a.rePost = :article AND a.deletedStatus = com.leets.xcellentbe.domain.shared.DeletedStatus.NOT_DELETED")
27+
long countReposts(@Param("article") Article article);
2728
}

src/main/java/com/leets/xcellentbe/domain/article/dto/ArticleResponseDto.java

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package com.leets.xcellentbe.domain.article.dto;
22

33
import java.util.List;
4+
import java.util.Map;
45
import java.util.UUID;
56
import java.util.stream.Collectors;
67

78
import com.leets.xcellentbe.domain.article.domain.Article;
89
import com.leets.xcellentbe.domain.articleMedia.domain.ArticleMedia;
10+
import com.leets.xcellentbe.domain.comment.dto.CommentResponseDto;
11+
import com.leets.xcellentbe.domain.comment.dto.CommentStatsDto;
912
import com.leets.xcellentbe.domain.hashtag.domain.Hashtag;
1013
import com.leets.xcellentbe.domain.shared.DeletedStatus;
1114

@@ -23,16 +26,17 @@ public class ArticleResponseDto {
2326
private List<String> hashtags;
2427
private UUID rePostId;
2528
private List<String> mediaUrls;
29+
private List<CommentResponseDto> comments;
2630
private int viewCnt;
27-
private int rePostCnt;
28-
private int likeCnt;
29-
private int commentCnt;
31+
private long rePostCnt;
32+
private long likeCnt;
33+
private long commentCnt;
3034
private boolean owner;
3135

3236
@Builder
3337
private ArticleResponseDto(UUID articleId, Long writerId, String content, DeletedStatus deletedStatus,
34-
List<String> hashtags, UUID rePostId, List<String> mediaUrls, int viewCnt,
35-
int rePostCnt, int likeCnt, int commentCnt, boolean owner) {
38+
List<String> hashtags, UUID rePostId, List<String> mediaUrls, List<CommentResponseDto> comments,
39+
int viewCnt, long rePostCnt, long likeCnt, long commentCnt, boolean owner) {
3640
this.articleId = articleId;
3741
this.writerId = writerId;
3842
this.content = content;
@@ -45,9 +49,10 @@ private ArticleResponseDto(UUID articleId, Long writerId, String content, Delete
4549
this.likeCnt = likeCnt;
4650
this.commentCnt = commentCnt;
4751
this.owner = owner;
52+
this.comments = comments;
4853
}
4954

50-
public static ArticleResponseDto from(Article article, boolean isOwner) {
55+
public static ArticleResponseDto from(Article article, boolean isOwner, ArticleStatsDto stats, Map<UUID, CommentStatsDto> replyStatsMap) {
5156
return ArticleResponseDto.builder()
5257
.articleId(article.getArticleId())
5358
.content(article.getContent())
@@ -62,10 +67,43 @@ public static ArticleResponseDto from(Article article, boolean isOwner) {
6267
.stream()
6368
.map(ArticleMedia::getFilePath) // 이미지 URL로 매핑
6469
.collect(Collectors.toList()) : null)
70+
.comments(article.getComments() != null ? article.getComments()
71+
.stream()
72+
.filter(comment -> comment != null && comment.getDeletedStatus() == DeletedStatus.NOT_DELETED) // null 및 삭제된 댓글 필터링
73+
.map(comment -> {
74+
CommentStatsDto commentStats = replyStatsMap.getOrDefault(comment.getCommentId(), CommentStatsDto.from(0, 0));
75+
boolean isCommentOwner = comment.getWriter().getUserId().equals(article.getWriter().getUserId());
76+
return CommentResponseDto.from(comment, isCommentOwner, commentStats, replyStatsMap, 1); // 깊이 1로 제한
77+
})
78+
.collect(Collectors.toList()) : null)
79+
.viewCnt(article.getViewCnt())
80+
.rePostCnt(stats.getRepostCnt())
81+
.likeCnt(stats.getLikeCnt())
82+
.commentCnt(stats.getCommentCnt())
83+
.owner(isOwner)
84+
.build();
85+
}
86+
87+
public static ArticleResponseDto fromWithoutComments(Article article, boolean isOwner, ArticleStatsDto stats) {
88+
return ArticleResponseDto.builder()
89+
.articleId(article.getArticleId())
90+
.content(article.getContent())
91+
.deletedStatus(article.getDeletedStatus())
92+
.writerId(article.getWriter().getUserId())
93+
.hashtags(article.getHashtags() != null ? article.getHashtags()
94+
.stream()
95+
.map(Hashtag::getContent)
96+
.collect(Collectors.toList()) : null)
97+
.rePostId(article.getRePost() != null ? article.getRePost().getArticleId() : null)
98+
.mediaUrls(article.getMediaList() != null ? article.getMediaList()
99+
.stream()
100+
.map(ArticleMedia::getFilePath)
101+
.collect(Collectors.toList()) : null)
102+
.comments(null) // 전체 조회 시 댓글 정보 제외
65103
.viewCnt(article.getViewCnt())
66-
.rePostCnt(article.getRepostCnt())
67-
.likeCnt(article.getLikeCnt())
68-
.commentCnt(article.getCommentCnt())
104+
.rePostCnt(stats.getRepostCnt())
105+
.likeCnt(stats.getLikeCnt())
106+
.commentCnt(stats.getCommentCnt())
69107
.owner(isOwner)
70108
.build();
71109
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.leets.xcellentbe.domain.article.dto;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
7+
@Getter
8+
@NoArgsConstructor
9+
public class ArticleStatsDto {
10+
private long likeCnt;
11+
private long commentCnt;
12+
private long repostCnt;
13+
14+
@Builder
15+
private ArticleStatsDto(long likeCnt, long commentCnt, long repostCnt) {
16+
this.likeCnt = likeCnt;
17+
this.commentCnt = commentCnt;
18+
this.repostCnt = repostCnt;
19+
}
20+
21+
public static ArticleStatsDto from(long likeCnt, long commentCnt, long repostCnt) {
22+
return ArticleStatsDto.builder()
23+
.likeCnt(likeCnt)
24+
.commentCnt(commentCnt)
25+
.repostCnt(repostCnt)
26+
.build();
27+
}
28+
}

src/main/java/com/leets/xcellentbe/domain/article/service/ArticleService.java

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,18 @@
1919
import com.leets.xcellentbe.domain.article.dto.ArticleCreateRequestDto;
2020
import com.leets.xcellentbe.domain.article.dto.ArticleCreateResponseDto;
2121
import com.leets.xcellentbe.domain.article.dto.ArticleResponseDto;
22+
import com.leets.xcellentbe.domain.article.dto.ArticleStatsDto;
2223
import com.leets.xcellentbe.domain.article.dto.ArticlesResponseDto;
2324
import com.leets.xcellentbe.domain.article.dto.ArticlesWithMediaDto;
2425
import com.leets.xcellentbe.domain.article.exception.ArticleNotFoundException;
25-
import com.leets.xcellentbe.domain.article.exception.DeleteForbiddenException;
26+
import com.leets.xcellentbe.domain.articleLike.domain.repository.ArticleLikeRepository;
27+
import com.leets.xcellentbe.domain.comment.domain.Comment;
28+
import com.leets.xcellentbe.domain.comment.dto.CommentStatsDto;
29+
import com.leets.xcellentbe.domain.commentLike.domain.repository.CommentLikeRepository;
30+
import com.leets.xcellentbe.global.error.exception.custom.DeleteForbiddenException;
2631
import com.leets.xcellentbe.domain.articleMedia.domain.ArticleMedia;
2732
import com.leets.xcellentbe.domain.articleMedia.domain.repository.ArticleMediaRepository;
28-
import com.leets.xcellentbe.domain.articleMedia.exception.ArticleMediaNotFoundException;
33+
import com.leets.xcellentbe.domain.comment.domain.repository.CommentRepository;
2934
import com.leets.xcellentbe.domain.hashtag.HashtagService.HashtagService;
3035
import com.leets.xcellentbe.domain.hashtag.domain.Hashtag;
3136
import com.leets.xcellentbe.domain.user.domain.User;
@@ -43,6 +48,9 @@ public class ArticleService {
4348
private final ArticleRepository articleRepository;
4449
private final ArticleMediaRepository articleMediaRepository;
4550
private final UserRepository userRepository;
51+
private final CommentRepository commentRepository;
52+
private final ArticleLikeRepository articleLikeRepository;
53+
private final CommentLikeRepository commentLikeRepository;
4654
private final HashtagService hashtagService;
4755
private final S3UploadMediaService s3UploadMediaService;
4856
private final JwtService jwtService;
@@ -149,15 +157,21 @@ public ArticleResponseDto getArticle(HttpServletRequest request, UUID articleId)
149157

150158
Article targetArticle = articleRepository.findById(articleId)
151159
.orElseThrow(ArticleNotFoundException::new);
152-
153-
List<ArticleMedia> mediaList = articleMediaRepository.findByArticle_ArticleId(targetArticle.getArticleId());
154-
if (mediaList.isEmpty()) {
155-
throw new ArticleMediaNotFoundException();
156-
}
160+
ArticleStatsDto stats = findArticleStats(targetArticle);
157161
targetArticle.updateViewCount();
158162
boolean isOwner = targetArticle.getWriter().getUserId().equals(user.getUserId());
159163

160-
return ArticleResponseDto.from(targetArticle, isOwner);
164+
List<Comment> comments = commentRepository.findAllByArticleAndNotDeleted(targetArticle);
165+
Map<UUID, CommentStatsDto> replyStatsMap = comments.stream()
166+
.collect(Collectors.toMap(
167+
Comment::getCommentId,
168+
reply -> {
169+
long likeCount = commentLikeRepository.countLikesByComment(reply);
170+
long replyCount = commentRepository.countRepliesByComment(reply);
171+
return CommentStatsDto.from(likeCount, replyCount);
172+
}
173+
));
174+
return ArticleResponseDto.from(targetArticle, isOwner, stats, replyStatsMap);
161175
}
162176

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

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

170-
List<Article> articles = cursor == null ?
184+
List<Article> articles = (cursor == null) ?
171185
articleRepository.findRecentArticles(pageable) : // 처음 로드 시
172186
articleRepository.findRecentArticles(cursor, pageable);
173187

174188
return articles
175189
.stream()
176-
.map(article -> ArticleResponseDto.from(article, article.getWriter().getUserId().equals(user.getUserId())))
190+
.map(article -> {
191+
boolean isOwner = article.getWriter().getUserId().equals(user.getUserId());
192+
ArticleStatsDto stats = findArticleStats(article);
193+
return ArticleResponseDto.fromWithoutComments(article, isOwner, stats);
194+
})
177195
.collect(Collectors.toList());
178196
}
179197

@@ -185,8 +203,7 @@ public ArticleCreateResponseDto rePostArticle(HttpServletRequest request, UUID a
185203
Article repostedArticle = articleRepository.findById(articleId)
186204
.orElseThrow(ArticleNotFoundException::new);
187205
Article newArticle = Article.createArticle(writer, repostedArticle.getContent());
188-
repostedArticle.addRepost(newArticle);
189-
repostedArticle.plusRepostCount();
206+
newArticle.addRepost(repostedArticle);
190207

191208
return ArticleCreateResponseDto.from(articleRepository.save(newArticle));
192209
}
@@ -197,12 +214,20 @@ public void deleteRepost(HttpServletRequest request, UUID articleId) {
197214
User user = getUser(request);
198215
Article targetArticle = articleRepository.findById(articleId)
199216
.orElseThrow(ArticleNotFoundException::new);
200-
if (!(targetArticle.getWriter().getUserId().equals(user.getUserId()))) {
217+
// 게시글 작성자와 현재 사용자 일치 여부 확인, 리포스트 ID가 있는 경우에만 삭제 가능
218+
if ((!targetArticle.getWriter().getUserId().equals(user.getUserId()))||(targetArticle.getRePost() == null)) {
201219
throw new DeleteForbiddenException();
202-
} else {
203-
targetArticle.deleteArticle();
204-
targetArticle.getRePost().minusRepostCount();
205220
}
221+
// 리포스트 삭제 처리
222+
targetArticle.deleteArticle();
223+
articleRepository.save(targetArticle);
224+
}
225+
226+
public ArticleStatsDto findArticleStats(Article article) {
227+
long likeCount = articleLikeRepository.countLikesByArticleId(article.getArticleId());
228+
long commentCount = commentRepository.countCommentsByArticle(article);
229+
long repostCount = articleRepository.countReposts(article);
230+
return ArticleStatsDto.from(likeCount, commentCount, repostCount);
206231
}
207232

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

216241
return user;
217-
218242
}
219243
}

0 commit comments

Comments
 (0)