Skip to content

Commit 78b0fe9

Browse files
KoSeonJeclaude
andcommitted
[DDING-000] 피드 집계 쿼리 COALESCE 누락 수정 및 테스트 추가
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0fd363b commit 78b0fe9

File tree

5 files changed

+323
-2
lines changed

5 files changed

+323
-2
lines changed

src/main/java/ddingdong/ddingdongBE/domain/feed/repository/FeedRepository.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ List<MonthlyFeedRankingDto> findMonthlyRankingByClub(
103103
@Query(value = """
104104
SELECT COUNT(f.id) AS feedCount,
105105
COALESCE(SUM(f.view_count), 0) AS totalViewCount,
106-
SUM(CASE WHEN f.feed_type = 'IMAGE' THEN 1 ELSE 0 END) AS imageCount,
107-
SUM(CASE WHEN f.feed_type = 'VIDEO' THEN 1 ELSE 0 END) AS videoCount
106+
COALESCE(SUM(CASE WHEN f.feed_type = 'IMAGE' THEN 1 ELSE 0 END), 0) AS imageCount,
107+
COALESCE(SUM(CASE WHEN f.feed_type = 'VIDEO' THEN 1 ELSE 0 END), 0) AS videoCount
108108
FROM feed f
109109
WHERE f.deleted_at IS NULL
110110
AND f.club_id = :clubId
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package ddingdong.ddingdongBE.domain.feed.repository;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.SoftAssertions.assertSoftly;
5+
6+
import ddingdong.ddingdongBE.common.config.JpaAuditingConfig;
7+
import ddingdong.ddingdongBE.common.fixture.ClubFixture;
8+
import ddingdong.ddingdongBE.common.fixture.FeedFixture;
9+
import ddingdong.ddingdongBE.common.support.DataJpaTestSupport;
10+
import ddingdong.ddingdongBE.domain.club.entity.Club;
11+
import ddingdong.ddingdongBE.domain.club.repository.ClubRepository;
12+
import ddingdong.ddingdongBE.domain.feed.entity.Feed;
13+
import ddingdong.ddingdongBE.domain.feed.entity.FeedComment;
14+
import ddingdong.ddingdongBE.domain.feed.repository.dto.FeedCountDto;
15+
import jakarta.persistence.EntityManager;
16+
import jakarta.persistence.PersistenceContext;
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.stream.Collectors;
20+
import org.junit.jupiter.api.DisplayName;
21+
import org.junit.jupiter.api.Test;
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.context.annotation.Import;
24+
25+
@Import(JpaAuditingConfig.class)
26+
class FeedCommentRepositoryTest extends DataJpaTestSupport {
27+
28+
@Autowired
29+
private FeedCommentRepository feedCommentRepository;
30+
31+
@Autowired
32+
private FeedRepository feedRepository;
33+
34+
@Autowired
35+
private ClubRepository clubRepository;
36+
37+
@PersistenceContext
38+
private EntityManager entityManager;
39+
40+
@DisplayName("countsByFeedIds로 피드별 댓글 수를 벌크 조회한다")
41+
@Test
42+
void countsByFeedIds_ReturnsCorrectCounts() {
43+
// given
44+
Club club = clubRepository.save(ClubFixture.createClub());
45+
Feed feed1 = feedRepository.save(FeedFixture.createImageFeed(club, "피드 1"));
46+
Feed feed2 = feedRepository.save(FeedFixture.createImageFeed(club, "피드 2"));
47+
48+
feedCommentRepository.save(FeedFixture.createFeedComment(feed1, "uuid-1", 1, "댓글 1"));
49+
feedCommentRepository.save(FeedFixture.createFeedComment(feed1, "uuid-2", 2, "댓글 2"));
50+
feedCommentRepository.save(FeedFixture.createFeedComment(feed2, "uuid-3", 1, "댓글 3"));
51+
52+
List<Long> feedIds = List.of(feed1.getId(), feed2.getId());
53+
54+
// when
55+
List<FeedCountDto> result = feedCommentRepository.countsByFeedIds(feedIds);
56+
57+
// then
58+
Map<Long, Long> countMap = result.stream()
59+
.collect(Collectors.toMap(FeedCountDto::getFeedId, FeedCountDto::getCnt));
60+
61+
assertSoftly(softly -> {
62+
softly.assertThat(countMap.get(feed1.getId())).isEqualTo(2);
63+
softly.assertThat(countMap.get(feed2.getId())).isEqualTo(1);
64+
});
65+
}
66+
67+
@DisplayName("countsByFeedIds에서 soft delete된 댓글은 제외된다")
68+
@Test
69+
void countsByFeedIds_ExcludesSoftDeletedComments() {
70+
// given
71+
Club club = clubRepository.save(ClubFixture.createClub());
72+
Feed feed = feedRepository.save(FeedFixture.createImageFeed(club, "피드"));
73+
74+
feedCommentRepository.save(FeedFixture.createFeedComment(feed, "uuid-1", 1, "활성 댓글"));
75+
FeedComment deletedComment = feedCommentRepository.save(
76+
FeedFixture.createFeedComment(feed, "uuid-2", 2, "삭제 댓글"));
77+
78+
feedCommentRepository.delete(deletedComment);
79+
entityManager.flush();
80+
entityManager.clear();
81+
82+
// when
83+
List<FeedCountDto> result = feedCommentRepository.countsByFeedIds(List.of(feed.getId()));
84+
85+
// then
86+
assertThat(result).hasSize(1);
87+
assertThat(result.get(0).getCnt()).isEqualTo(1);
88+
}
89+
90+
@DisplayName("countsByFeedIds에 댓글 없는 피드만 있으면 빈 리스트를 반환한다")
91+
@Test
92+
void countsByFeedIds_ReturnsEmptyWhenNoComments() {
93+
// given
94+
Club club = clubRepository.save(ClubFixture.createClub());
95+
Feed feed = feedRepository.save(FeedFixture.createImageFeed(club, "피드"));
96+
97+
// when
98+
List<FeedCountDto> result = feedCommentRepository.countsByFeedIds(List.of(feed.getId()));
99+
100+
// then
101+
assertThat(result).isEmpty();
102+
}
103+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package ddingdong.ddingdongBE.domain.feed.repository;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.SoftAssertions.assertSoftly;
5+
6+
import ddingdong.ddingdongBE.common.config.JpaAuditingConfig;
7+
import ddingdong.ddingdongBE.common.fixture.ClubFixture;
8+
import ddingdong.ddingdongBE.common.fixture.FeedFixture;
9+
import ddingdong.ddingdongBE.common.support.DataJpaTestSupport;
10+
import ddingdong.ddingdongBE.domain.club.entity.Club;
11+
import ddingdong.ddingdongBE.domain.club.repository.ClubRepository;
12+
import ddingdong.ddingdongBE.domain.feed.entity.Feed;
13+
import ddingdong.ddingdongBE.domain.feed.repository.dto.FeedCountDto;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.stream.Collectors;
17+
import org.junit.jupiter.api.DisplayName;
18+
import org.junit.jupiter.api.Test;
19+
import org.springframework.beans.factory.annotation.Autowired;
20+
import org.springframework.context.annotation.Import;
21+
22+
@Import(JpaAuditingConfig.class)
23+
class FeedLikeRepositoryTest extends DataJpaTestSupport {
24+
25+
@Autowired
26+
private FeedLikeRepository feedLikeRepository;
27+
28+
@Autowired
29+
private FeedRepository feedRepository;
30+
31+
@Autowired
32+
private ClubRepository clubRepository;
33+
34+
@DisplayName("countsByFeedIds로 피드별 좋아요 수를 벌크 조회한다")
35+
@Test
36+
void countsByFeedIds_ReturnsCorrectCounts() {
37+
// given
38+
Club club = clubRepository.save(ClubFixture.createClub());
39+
Feed feed1 = feedRepository.save(FeedFixture.createImageFeed(club, "피드 1"));
40+
Feed feed2 = feedRepository.save(FeedFixture.createImageFeed(club, "피드 2"));
41+
Feed feed3 = feedRepository.save(FeedFixture.createImageFeed(club, "피드 3"));
42+
43+
feedLikeRepository.save(FeedFixture.createFeedLike(feed1, "uuid-1"));
44+
feedLikeRepository.save(FeedFixture.createFeedLike(feed1, "uuid-2"));
45+
feedLikeRepository.save(FeedFixture.createFeedLike(feed1, "uuid-3"));
46+
feedLikeRepository.save(FeedFixture.createFeedLike(feed2, "uuid-4"));
47+
48+
List<Long> feedIds = List.of(feed1.getId(), feed2.getId(), feed3.getId());
49+
50+
// when
51+
List<FeedCountDto> result = feedLikeRepository.countsByFeedIds(feedIds);
52+
53+
// then
54+
Map<Long, Long> countMap = result.stream()
55+
.collect(Collectors.toMap(FeedCountDto::getFeedId, FeedCountDto::getCnt));
56+
57+
assertSoftly(softly -> {
58+
softly.assertThat(countMap.getOrDefault(feed1.getId(), 0L)).isEqualTo(3);
59+
softly.assertThat(countMap.getOrDefault(feed2.getId(), 0L)).isEqualTo(1);
60+
softly.assertThat(countMap.containsKey(feed3.getId())).isFalse();
61+
});
62+
}
63+
64+
@DisplayName("countsByFeedIds에 좋아요 없는 피드만 있으면 빈 리스트를 반환한다")
65+
@Test
66+
void countsByFeedIds_ReturnsEmptyWhenNoLikes() {
67+
// given
68+
Club club = clubRepository.save(ClubFixture.createClub());
69+
Feed feed = feedRepository.save(FeedFixture.createImageFeed(club, "피드"));
70+
71+
// when
72+
List<FeedCountDto> result = feedLikeRepository.countsByFeedIds(List.of(feed.getId()));
73+
74+
// then
75+
assertThat(result).isEmpty();
76+
}
77+
}

src/test/java/ddingdong/ddingdongBE/domain/feed/repository/FeedRepositoryTest.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import ddingdong.ddingdongBE.domain.club.repository.ClubRepository;
1313
import ddingdong.ddingdongBE.domain.feed.entity.Feed;
1414
import ddingdong.ddingdongBE.domain.feed.repository.dto.MonthlyFeedRankingDto;
15+
import ddingdong.ddingdongBE.domain.feed.repository.dto.MyFeedStatDto;
1516
import ddingdong.ddingdongBE.domain.filemetadata.entity.FileMetaData;
1617
import ddingdong.ddingdongBE.domain.filemetadata.repository.FileMetaDataRepository;
1718
import ddingdong.ddingdongBE.domain.vodprocessing.repository.VodProcessingJobRepository;
@@ -353,6 +354,70 @@ void findMonthlyRankingByClub_ExcludesDeletedFeeds() {
353354
assertThat(result.get(0).getFeedCount()).isEqualTo(1);
354355
}
355356

357+
@DisplayName("findMyFeedStat으로 클럽의 피드 집계를 조회한다")
358+
@Test
359+
void findMyFeedStat_ReturnsCorrectStats() {
360+
// given
361+
Club club = clubRepository.save(ClubFixture.createClub());
362+
Feed imageFeed1 = feedRepository.save(FeedFixture.createImageFeed(club, "이미지 1"));
363+
Feed imageFeed2 = feedRepository.save(FeedFixture.createImageFeed(club, "이미지 2"));
364+
Feed videoFeed = feedRepository.save(FeedFixture.createVideoFeed(club, "비디오 1"));
365+
366+
feedRepository.incrementViewCount(imageFeed1.getId());
367+
feedRepository.incrementViewCount(imageFeed1.getId());
368+
feedRepository.incrementViewCount(videoFeed.getId());
369+
entityManager.flush();
370+
entityManager.clear();
371+
372+
// when
373+
MyFeedStatDto stat = feedRepository.findMyFeedStat(club.getId());
374+
375+
// then
376+
assertSoftly(softly -> {
377+
softly.assertThat(stat.getFeedCount()).isEqualTo(3);
378+
softly.assertThat(stat.getTotalViewCount()).isEqualTo(3);
379+
softly.assertThat(stat.getImageCount()).isEqualTo(2);
380+
softly.assertThat(stat.getVideoCount()).isEqualTo(1);
381+
});
382+
}
383+
384+
@DisplayName("findMyFeedStat에서 삭제된 피드는 제외된다")
385+
@Test
386+
void findMyFeedStat_ExcludesDeletedFeeds() {
387+
// given
388+
Club club = clubRepository.save(ClubFixture.createClub());
389+
feedRepository.save(FeedFixture.createImageFeed(club, "활성 피드"));
390+
Feed deletedFeed = feedRepository.save(FeedFixture.createImageFeed(club, "삭제 피드"));
391+
392+
feedRepository.delete(deletedFeed);
393+
entityManager.flush();
394+
entityManager.clear();
395+
396+
// when
397+
MyFeedStatDto stat = feedRepository.findMyFeedStat(club.getId());
398+
399+
// then
400+
assertThat(stat.getFeedCount()).isEqualTo(1);
401+
}
402+
403+
@DisplayName("findMyFeedStat에서 피드가 없으면 모든 집계가 0이다")
404+
@Test
405+
void findMyFeedStat_ReturnsZerosWhenNoFeeds() {
406+
// given
407+
Club club = clubRepository.save(ClubFixture.createClub());
408+
409+
// when
410+
MyFeedStatDto stat = feedRepository.findMyFeedStat(club.getId());
411+
412+
// then
413+
assertSoftly(softly -> {
414+
softly.assertThat(stat.getFeedCount()).isEqualTo(0);
415+
softly.assertThat(stat.getTotalViewCount()).isEqualTo(0);
416+
softly.assertThat(stat.getImageCount()).isEqualTo(0);
417+
softly.assertThat(stat.getVideoCount()).isEqualTo(0);
418+
});
419+
}
420+
356421
@DisplayName("findMonthlyRankingByClub에서 다른 월 피드는 제외된다")
357422
@Test
358423
void findMonthlyRankingByClub_ExcludesDifferentMonthFeeds() {

src/test/java/ddingdong/ddingdongBE/domain/feed/service/FacadeFeedServiceTest.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
import ddingdong.ddingdongBE.domain.club.entity.Club;
1111
import ddingdong.ddingdongBE.domain.club.repository.ClubRepository;
1212
import ddingdong.ddingdongBE.domain.feed.entity.Feed;
13+
import ddingdong.ddingdongBE.domain.feed.entity.FeedComment;
14+
import ddingdong.ddingdongBE.domain.feed.entity.FeedLike;
1315
import ddingdong.ddingdongBE.domain.feed.entity.FeedType;
16+
import ddingdong.ddingdongBE.domain.feed.repository.FeedCommentRepository;
17+
import ddingdong.ddingdongBE.domain.feed.repository.FeedLikeRepository;
1418
import ddingdong.ddingdongBE.domain.feed.repository.FeedRepository;
1519
import ddingdong.ddingdongBE.domain.feed.service.dto.query.FeedQuery;
1620
import ddingdong.ddingdongBE.domain.filemetadata.entity.DomainType;
@@ -49,6 +53,12 @@ class FacadeFeedServiceTest extends TestContainerSupport {
4953
@Autowired
5054
private FileMetaDataRepository fileMetaDataRepository;
5155

56+
@Autowired
57+
private FeedLikeRepository feedLikeRepository;
58+
59+
@Autowired
60+
private FeedCommentRepository feedCommentRepository;
61+
5262
@MockitoBean
5363
private S3FileService s3FileService;
5464

@@ -126,5 +136,71 @@ void getFeedById() {
126136
assertThat(info.activityContent()).isEqualTo(savedFeed.getActivityContent());
127137
assertThat(info.feedType()).isEqualTo(savedFeed.getFeedType().toString());
128138
assertThat(info.createdDate()).isEqualTo(LocalDate.from(now));
139+
assertThat(info.likeCount()).isZero();
140+
assertThat(info.commentCount()).isZero();
141+
assertThat(info.comments()).isEmpty();
142+
}
143+
144+
@DisplayName("피드 상세 조회 시 좋아요, 댓글 수와 댓글 목록이 포함된다.")
145+
@Test
146+
void getFeedById_WithLikesAndComments() {
147+
// given
148+
Club club = fixture.giveMeBuilder(Club.class)
149+
.setNull("id")
150+
.set("name", "카우")
151+
.set("user", null)
152+
.set("score", Score.from(BigDecimal.ZERO))
153+
.set("clubMembers", null)
154+
.set("deletedAt", null)
155+
.sample();
156+
Club savedClub = clubRepository.save(club);
157+
158+
UUID clubFileId = UuidCreator.getTimeOrderedEpoch();
159+
fileMetaDataRepository.save(
160+
fixture.giveMeBuilder(FileMetaData.class)
161+
.set("id", clubFileId)
162+
.set("domainType", DomainType.CLUB_PROFILE)
163+
.set("entityId", savedClub.getId())
164+
.set("fileStatus", FileStatus.COUPLED)
165+
.sample()
166+
);
167+
168+
Feed feed = fixture.giveMeBuilder(Feed.class)
169+
.setNull("id")
170+
.set("club", savedClub)
171+
.set("activityContent", "활동 내역")
172+
.set("feedType", FeedType.IMAGE)
173+
.set("createdAt", LocalDateTime.now())
174+
.sample();
175+
Feed savedFeed = feedRepository.save(feed);
176+
177+
UUID feedFileId = UuidCreator.getTimeOrderedEpoch();
178+
fileMetaDataRepository.save(
179+
fixture.giveMeBuilder(FileMetaData.class)
180+
.set("id", feedFileId)
181+
.set("domainType", DomainType.FEED_IMAGE)
182+
.set("entityId", savedFeed.getId())
183+
.set("fileStatus", FileStatus.COUPLED)
184+
.sample()
185+
);
186+
187+
feedLikeRepository.save(FeedLike.builder().feed(savedFeed).uuid("uuid-1").build());
188+
feedLikeRepository.save(FeedLike.builder().feed(savedFeed).uuid("uuid-2").build());
189+
feedCommentRepository.save(FeedComment.builder().feed(savedFeed).uuid("uuid-3").anonymousNumber(1).content("댓글 1").build());
190+
191+
BDDMockito.given(s3FileService.getUploadedFileUrl(any()))
192+
.willReturn(new UploadedFileUrlQuery(null, null, null));
193+
BDDMockito.given(s3FileService.getUploadedFileUrlAndName(any(), any()))
194+
.willReturn(new UploadedFileUrlAndNameQuery(null, null, null, null));
195+
196+
// when
197+
FeedQuery info = facadeFeedService.getById(savedFeed.getId());
198+
199+
// then
200+
assertThat(info.likeCount()).isEqualTo(2);
201+
assertThat(info.commentCount()).isEqualTo(1);
202+
assertThat(info.comments()).hasSize(1);
203+
assertThat(info.comments().get(0).content()).isEqualTo("댓글 1");
204+
assertThat(info.comments().get(0).anonymousName()).isEqualTo("익명1");
129205
}
130206
}

0 commit comments

Comments
 (0)