-
Notifications
You must be signed in to change notification settings - Fork 3
feat: 동아리 이달의 피드 현황 조회 API 구현 #387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
d2e3a10
feat: [DDING-000] 동아리 이달의 현황 조회 서비스 계층 구현
KoSeonJe 2aaff7d
feat: [DDING-000] 동아리 이달의 현황 조회 Club API 구현
KoSeonJe 7935494
feat: [DDING-000] 동아리 이달의 현황 조회 Admin API 구현
KoSeonJe 13701f2
test: [DDING-000] 동아리 이달의 현황 조회 테스트 작성
KoSeonJe b39a10d
fix: [DDING-000] 코드 리뷰 피드백 반영
KoSeonJe 6b23786
refactor: [DDING-000] E2E 테스트 정리 — 핵심 시나리오만 유지
KoSeonJe 619537e
refactor: [DDING-000] Admin 동아리 현황 조회 API 제거
KoSeonJe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
.../ddingdong/ddingdongBE/domain/feed/controller/dto/response/ClubMonthlyStatusResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package ddingdong.ddingdongBE.domain.feed.controller.dto.response; | ||
|
|
||
| import ddingdong.ddingdongBE.domain.feed.service.dto.query.ClubMonthlyStatusQuery; | ||
| import lombok.Builder; | ||
|
|
||
| @Builder | ||
| public record ClubMonthlyStatusResponse( | ||
| int year, | ||
| int month, | ||
| int rank, | ||
| long feedCount, | ||
| long viewCount, | ||
| long likeCount, | ||
| long commentCount, | ||
| long score | ||
| ) { | ||
|
|
||
| public static ClubMonthlyStatusResponse from(ClubMonthlyStatusQuery query) { | ||
| return ClubMonthlyStatusResponse.builder() | ||
| .year(query.year()) | ||
| .month(query.month()) | ||
| .rank(query.rank()) | ||
| .feedCount(query.feedCount()) | ||
| .viewCount(query.viewCount()) | ||
| .likeCount(query.likeCount()) | ||
| .commentCount(query.commentCount()) | ||
| .score(query.score()) | ||
| .build(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
...main/java/ddingdong/ddingdongBE/domain/feed/service/dto/query/ClubMonthlyStatusQuery.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package ddingdong.ddingdongBE.domain.feed.service.dto.query; | ||
|
|
||
| import lombok.Builder; | ||
|
|
||
| @Builder | ||
| public record ClubMonthlyStatusQuery( | ||
| int year, | ||
| int month, | ||
| int rank, | ||
| long feedCount, | ||
| long viewCount, | ||
| long likeCount, | ||
| long commentCount, | ||
| long score | ||
| ) { | ||
|
|
||
| public static ClubMonthlyStatusQuery from(int year, int month, ClubFeedRankingQuery ranking) { | ||
| return ClubMonthlyStatusQuery.builder() | ||
| .year(year) | ||
| .month(month) | ||
| .rank(ranking.rank()) | ||
| .feedCount(ranking.feedCount()) | ||
| .viewCount(ranking.viewCount()) | ||
| .likeCount(ranking.likeCount()) | ||
| .commentCount(ranking.commentCount()) | ||
| .score(ranking.score()) | ||
| .build(); | ||
| } | ||
|
|
||
| public static ClubMonthlyStatusQuery createEmpty(int year, int month) { | ||
| return ClubMonthlyStatusQuery.builder() | ||
| .year(year) | ||
| .month(month) | ||
| .rank(0) | ||
| .feedCount(0) | ||
| .viewCount(0) | ||
| .likeCount(0) | ||
| .commentCount(0) | ||
| .score(0) | ||
| .build(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
130 changes: 130 additions & 0 deletions
130
src/test/java/ddingdong/ddingdongBE/domain/feed/controller/ClubFeedStatusE2ETest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| package ddingdong.ddingdongBE.domain.feed.controller; | ||
|
|
||
| import static io.restassured.RestAssured.given; | ||
| import static org.assertj.core.api.SoftAssertions.assertSoftly; | ||
|
|
||
| import ddingdong.ddingdongBE.auth.controller.dto.request.SignInRequest; | ||
| import ddingdong.ddingdongBE.auth.controller.dto.response.SignInResponse; | ||
| import ddingdong.ddingdongBE.common.fixture.ClubFixture; | ||
| import ddingdong.ddingdongBE.common.fixture.FeedFixture; | ||
| import ddingdong.ddingdongBE.common.fixture.UserFixture; | ||
| import ddingdong.ddingdongBE.common.support.NonTxTestContainerSupport; | ||
| import ddingdong.ddingdongBE.domain.club.entity.Club; | ||
| import ddingdong.ddingdongBE.domain.club.repository.ClubRepository; | ||
| import ddingdong.ddingdongBE.domain.feed.entity.Feed; | ||
| import ddingdong.ddingdongBE.domain.feed.repository.FeedCommentRepository; | ||
| import ddingdong.ddingdongBE.domain.feed.repository.FeedLikeRepository; | ||
| import ddingdong.ddingdongBE.domain.feed.repository.FeedRepository; | ||
| import ddingdong.ddingdongBE.domain.user.entity.User; | ||
| import ddingdong.ddingdongBE.domain.user.repository.UserRepository; | ||
| import io.restassured.RestAssured; | ||
| import io.restassured.http.ContentType; | ||
| import java.time.LocalDate; | ||
| import java.util.Map; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
| import org.springframework.boot.test.context.SpringBootTest; | ||
| import org.springframework.boot.test.web.server.LocalServerPort; | ||
| import org.springframework.security.crypto.password.PasswordEncoder; | ||
|
|
||
| @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) | ||
| class ClubFeedStatusE2ETest extends NonTxTestContainerSupport { | ||
|
|
||
| @LocalServerPort | ||
| private int port; | ||
|
|
||
| @Autowired | ||
| private UserRepository userRepository; | ||
|
|
||
| @Autowired | ||
| private ClubRepository clubRepository; | ||
|
|
||
| @Autowired | ||
| private FeedRepository feedRepository; | ||
|
|
||
| @Autowired | ||
| private FeedLikeRepository feedLikeRepository; | ||
|
|
||
| @Autowired | ||
| private FeedCommentRepository feedCommentRepository; | ||
|
|
||
| @Autowired | ||
| private PasswordEncoder passwordEncoder; | ||
|
|
||
| private int year; | ||
| private int month; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| RestAssured.port = port; | ||
| year = LocalDate.now().getYear(); | ||
| month = LocalDate.now().getMonthValue(); | ||
| } | ||
|
|
||
| @DisplayName("동아리 이달의 현황 조회 성공 - 피드 2개, 좋아요 1, 댓글 1") | ||
| @Test | ||
| void getFeedStatus_success() { | ||
| // given | ||
| User clubUser = userRepository.save(UserFixture.createClubUser(passwordEncoder.encode("1234"))); | ||
| Club club = clubRepository.save(ClubFixture.createClub(clubUser)); | ||
|
|
||
| Feed feed1 = feedRepository.save(FeedFixture.createImageFeed(club, "활동 내용 1")); | ||
| Feed feed2 = feedRepository.save(FeedFixture.createImageFeed(club, "활동 내용 2")); | ||
| feedLikeRepository.save(FeedFixture.createFeedLike(feed1, "uuid-1")); | ||
| feedCommentRepository.save(FeedFixture.createFeedComment(feed2, "uuid-2", 1, "댓글")); | ||
|
|
||
| String token = signIn("club123", "1234"); | ||
|
|
||
| // when & then | ||
| // score = feedCount(2)*10 + viewCount(0)*1 + likeCount(1)*3 + commentCount(1)*5 = 28 | ||
| Map<?, ?> response = given() | ||
| .contentType(ContentType.JSON) | ||
| .header("Authorization", "Bearer " + token) | ||
| .queryParam("year", year) | ||
| .queryParam("month", month) | ||
| .when() | ||
| .get("/server/central/feeds/status") | ||
| .then() | ||
| .statusCode(200) | ||
| .extract() | ||
| .as(Map.class); | ||
|
|
||
| assertSoftly(softly -> { | ||
| softly.assertThat(response.get("year")).isEqualTo(year); | ||
| softly.assertThat(response.get("month")).isEqualTo(month); | ||
| softly.assertThat(((Number) response.get("feedCount")).longValue()).isEqualTo(2L); | ||
| softly.assertThat(((Number) response.get("likeCount")).longValue()).isEqualTo(1L); | ||
| softly.assertThat(((Number) response.get("commentCount")).longValue()).isEqualTo(1L); | ||
| softly.assertThat(((Number) response.get("score")).longValue()).isEqualTo(28L); | ||
| softly.assertThat(((Number) response.get("rank")).intValue()).isEqualTo(1); | ||
| }); | ||
| } | ||
|
|
||
| @DisplayName("동아리 이달의 현황 조회 - 미인증 접근 시 401") | ||
| @Test | ||
| void getFeedStatus_unauthorized() { | ||
| given() | ||
| .contentType(ContentType.JSON) | ||
| .queryParam("year", year) | ||
| .queryParam("month", month) | ||
| .when() | ||
| .get("/server/central/feeds/status") | ||
| .then() | ||
| .statusCode(401); | ||
| } | ||
|
|
||
| private String signIn(String authId, String password) { | ||
| SignInResponse response = given() | ||
| .contentType(ContentType.JSON) | ||
| .body(new SignInRequest(authId, password)) | ||
| .when() | ||
| .post("/server/auth/sign-in") | ||
| .then() | ||
| .statusCode(200) | ||
| .extract() | ||
| .as(SignInResponse.class); | ||
| return response.getToken(); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
전체 랭킹 조회를 통한 단일 동아리 조회 — 성능 문제
getClubMonthlyStatus가 내부적으로getClubFeedRanking(year, month)를 호출함으로써, 단 하나의 동아리 상태를 반환하기 위해 해당 월의 모든 동아리 피드 데이터를 DB에서 fetch하고, 전체를 인메모리에서 정렬(O(n log n))한 뒤, 순위를 재계산합니다. 동아리 수(n)가 증가할수록 이 API의 응답 시간이 선형 이상으로 증가합니다.feedRepository에 특정 동아리의 월별 피드 카운트를 직접 조회하는 쿼리를 추가하고, 순위는 해당 점수보다 높은 동아리 수를 COUNT하는 서브쿼리(또는 별도 쿼리)로 계산하는 방식으로 DB 레벨에서 처리하는 것을 권장합니다.♻️ 개선 방향 예시 (Repository 쿼리 기반)
`@Override` public ClubMonthlyStatusQuery getClubMonthlyStatus(Long userId, int year, int month) { Club club = clubService.getByUserId(userId); - List<ClubFeedRankingQuery> rankings = getClubFeedRanking(year, month); - - return rankings.stream() - .filter(rankingQuery -> rankingQuery.clubId().equals(club.getId())) - .findFirst() - .filter(rankingQuery -> rankingQuery.score() > 0) - .map(rankingQuery -> ClubMonthlyStatusQuery.from(year, month, rankingQuery)) - .orElse(ClubMonthlyStatusQuery.createEmpty(year, month)); + // 1. 해당 동아리의 월별 피드 통계를 단일 쿼리로 조회 + Optional<MonthlyFeedRankingDto> statOpt = + feedRepository.findMonthlyRankingByClubId(club.getId(), year, month); + if (statOpt.isEmpty()) { + return ClubMonthlyStatusQuery.createEmpty(year, month); + } + MonthlyFeedRankingDto stat = statOpt.get(); + long score = calculateScore(stat); + if (score == 0) { + return ClubMonthlyStatusQuery.createEmpty(year, month); + } + // 2. 점수보다 높은 동아리 수를 COUNT하여 순위 계산 + int rank = feedRepository.countClubsWithHigherScore(year, month, score) + 1; + return ClubMonthlyStatusQuery.from(year, month, + ClubFeedRankingQuery.of(rank, stat, score)); }🤖 Prompt for AI Agents