Skip to content

feat: 동아리 이달의 피드 현황 조회 API 구현#387

Merged
KoSeonJe merged 7 commits intodevelopfrom
feat/DDING-000-feed-club-monthly-best
Feb 23, 2026
Merged

feat: 동아리 이달의 피드 현황 조회 API 구현#387
KoSeonJe merged 7 commits intodevelopfrom
feat/DDING-000-feed-club-monthly-best

Conversation

@KoSeonJe
Copy link
Copy Markdown
Collaborator

@KoSeonJe KoSeonJe commented Feb 22, 2026

🚀 작업 내용

동아리 회장이 자신의 동아리의 해당 월 피드 활동 집계와 전체 동아리 중 순위를 조회하는 API 구현

엔드포인트

역할 메서드 경로 식별
Club GET /server/central/feeds/status?year=&month= PrincipalDetails → userId

응답 필드

year, month, rank, feedCount, viewCount, likeCount, commentCount, score

핵심 로직

  • 기존 getClubFeedRanking() 재사용하여 전체 랭킹 계산 후 내 동아리 필터링
  • score = feedCount×10 + viewCount×1 + likeCount×3 + commentCount×5
  • score=0 (미활동)인 동아리는 rank=0 반환

변경 파일

  • 신규: ClubMonthlyStatusQuery, ClubMonthlyStatusResponse
  • 수정: FeedRankingService, GeneralFeedRankingService, ClubFeedApi, ClubFeedController

🤔 고민했던 내용

  • score=0인 동아리에 대해 rank=0을 반환하여 "미활동 = 미랭킹"을 명시적으로 구분

💬 리뷰 중점사항

  • score=0 → rank=0 정책이 프론트 요구사항과 맞는지 확인

✅ 테스트

  • E2E 테스트 2개 (Club 성공 200 + 미인증 401)
  • 단위 테스트 3개 (GeneralFeedRankingServiceTest)
  • 전체 테스트 통과 확인

🤖 Generated with Claude Code

Summary by CodeRabbit

새로운 기능

  • 동아리 월간 피드 통계 조회 기능 추가: 인증된 동아리 사용자가 특정 연월의 피드 순위, 피드 수, 조회 수, 좋아요 수, 댓글 수 및 점수를 조회할 수 있는 새로운 엔드포인트가 추가되었습니다.

KoSeonJe and others added 5 commits February 22, 2026 23:33
- ClubMonthlyStatusQuery, ClubMonthlyStatusResponse DTO 생성
- FeedRankingService에 getClubMonthlyStatus, getClubMonthlyStatusByClubId 추가
- GeneralFeedRankingService에 구현 (기존 getClubFeedRanking 재사용)
- score=0인 동아리는 rank=0으로 반환 (미활동 동아리 구분)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ClubFeedApi에 GET /server/central/feeds/status 엔드포인트 추가
- ClubFeedController에 getFeedStatus() 구현 (PrincipalDetails → userId)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- AdminFeedApi base path를 /server/admin으로 변경
- GET /server/admin/clubs/{clubId}/feeds/ranking/status 엔드포인트 추가
- AdminFeedController에 getClubMonthlyStatus() 구현 (clubId path variable)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ClubFeedStatusE2ETest: E2E 5개 시나리오 (성공/rank/동점/피드없음/미인증)
- GeneralFeedRankingServiceTest: 단위 테스트 3개 추가 (피드있음/없음/rank 정확성)
- UserFixture에 createClubUser(authId, encode) 오버로드 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ClubFeedController에 @validated 추가 (@Min/@max 검증 활성화)
- AdminFeedControllerE2ETest에 동아리 이달의 현황 조회 E2E 테스트 2개 추가
  - Admin 성공 조회 / CLUB 권한 403 검증

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 22, 2026

Walkthrough

클럽의 월별 피드 상태 조회 기능을 추가합니다. 새로운 API 엔드포인트를 통해 특정 년도와 월의 클럽 피드 통계(순위, 피드 수, 조회 수, 좋아요, 댓글, 점수)를 반환하며, Controller, Service, DTO 계층 전반에 걸쳐 구현됩니다.

Changes

Cohort / File(s) Summary
API 엔드포인트
src/main/java/ddingdong/ddingdongBE/domain/feed/api/ClubFeedApi.java, src/main/java/ddingdong/ddingdongBE/domain/feed/controller/ClubFeedController.java
월별 피드 상태 조회 엔드포인트 추가. 매개변수 검증(연도, 월) 및 보안 처리 포함. FeedRankingService 의존성 추가.
Service 계층
src/main/java/ddingdong/ddingdongBE/domain/feed/service/FeedRankingService.java, src/main/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedRankingService.java
getClubMonthlyStatus 메서드 추가. 사용자의 클럽 정보 조회 및 월별 순위 데이터 반환. ClubService 의존성 주입.
DTO 응답 및 쿼리
src/main/java/ddingdong/ddingdongBE/domain/feed/controller/dto/response/ClubMonthlyStatusResponse.java, src/main/java/ddingdong/ddingdongBE/domain/feed/service/dto/query/ClubMonthlyStatusQuery.java, src/main/java/ddingdong/ddingdongBE/domain/feed/controller/dto/response/AdminFeedRankingWinnerResponse.java, src/main/java/ddingdong/ddingdongBE/domain/feed/service/dto/query/FeedRankingWinnerQuery.java
월별 상태 응답/쿼리 DTO 신규 생성. Builder 패턴으로 데이터 매핑. 기존 WinnerResponse/Query에 clubId 필드 추가.
테스트
src/test/java/ddingdong/ddingdongBE/domain/feed/controller/ClubFeedStatusE2ETest.java, src/test/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedRankingServiceTest.java, src/test/java/ddingdong/ddingdongBE/common/fixture/UserFixture.java
E2E 테스트 신규 추가 (인증 포함). Service 단위 테스트 확장. 커스텀 authId 지원 테스트 픽스처 메서드 추가.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Controller as ClubFeedController
    participant Service as FeedRankingService
    participant ClubSvc as ClubService
    participant Repo as Repository
    participant DB as Database

    Client->>Controller: GET /feeds/status?year=2025&month=2
    activate Controller
    Controller->>Service: getClubMonthlyStatus(userId, year, month)
    activate Service
    Service->>ClubSvc: getUserClub(userId)
    activate ClubSvc
    ClubSvc->>Repo: findClubByUser(userId)
    activate Repo
    Repo->>DB: Query club
    DB-->>Repo: Club entity
    deactivate Repo
    deactivate ClubSvc
    ClubSvc-->>Service: Club
    Service->>Service: getClubFeedRanking(year, month)
    Service->>Repo: Query feed ranking data
    activate Repo
    Repo->>DB: Query rankings, feeds, stats
    DB-->>Repo: Results
    deactivate Repo
    Service->>Service: Filter for user's club with score
    Service-->>Controller: ClubMonthlyStatusQuery
    deactivate Service
    Controller->>Controller: ClubMonthlyStatusResponse.from(query)
    Controller-->>Client: HTTP 200 + JSON response
    deactivate Controller
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

✨기능, D-3

Suggested reviewers

  • wonjunYou
  • 5uhwann
  • Seooooo24
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 '동아리 이달의 피드 현황 조회 API 구현'으로, 변경사항의 핵심(동아리의 월간 피드 현황 조회 API 추가)을 명확하고 간결하게 설명합니다.
Description check ✅ Passed PR 설명은 저장소 템플릿의 모든 필수 섹션(작업 내용, 고민했던 내용, 리뷰 중점사항)을 포함하고 있으며, 엔드포인트, 응답 필드, 핵심 로직, 변경 파일, 테스트 결과를 상세히 기술하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/DDING-000-feed-club-monthly-best

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@KoSeonJe KoSeonJe changed the title feat: [DDING-000] 동아리 이달의 현황 조회 API 구현 feat: 동아리 이달의 현황 조회 API 구현 Feb 22, 2026
- ClubFeedStatusE2ETest: rank/동점/피드없음 테스트 제거 (단위 테스트에서 커버)
- 남은 E2E: 성공 조회(200) + 미인증(401) 2개

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@KoSeonJe KoSeonJe changed the title feat: 동아리 이달의 현황 조회 API 구현 feat: 동아리 이달의 피드 현황 조회 API 구현 Feb 22, 2026
@KoSeonJe KoSeonJe assigned KoSeonJe and unassigned KoSeonJe Feb 22, 2026
@KoSeonJe
Copy link
Copy Markdown
Collaborator Author

@coderabbitai

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 22, 2026

Tip

For best results, initiate chat on the files or code changes.

@KoSeonJe: 안녕하세요! 어떻게 도와드릴까요?

이 PR과 관련하여 다음과 같은 도움을 드릴 수 있습니다:

  • 코드 리뷰나 특정 부분에 대한 분석
  • 테스트 케이스 생성
  • 리팩토링 제안
  • 문서화 관련 도움
  • 특정 질문에 대한 답변

무엇이 필요하신지 말씀해 주세요! 😊

@KoSeonJe
Copy link
Copy Markdown
Collaborator Author

@coderabbitai

리뷰해줘

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/main/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedRankingService.java (1)

73-83: 전체 랭킹을 재계산하여 단일 클럽 조회 — 성능 개선 여지 있음

getClubMonthlyStatusByClubId는 단일 클럽의 현황을 구하기 위해 getClubFeedRanking(year, month)를 호출합니다. 이 메서드는 DB에서 전체 클럽 목록을 조회하고 O(N log N) 정렬 후 랭킹을 계산하는데, 최종적으로는 그 중 하나만 사용됩니다.

클럽 수가 증가할 경우 불필요한 DB 부하가 생길 수 있습니다. 장기적으로는 특정 클럽의 피드 통계와 해당 클럽보다 높은 점수를 가진 클럽 수만 조회하는 전용 쿼리로 대체하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedRankingService.java`
around lines 73 - 83, getClubMonthlyStatusByClubId currently calls
getClubFeedRanking(year, month) which computes and sorts all clubs; replace that
heavy path with targeted DB queries: in
GeneralFeedRankingService.getClubMonthlyStatusByClubId, stop calling
getClubFeedRanking and instead fetch only the requested club's aggregated
monthly score (e.g., repository method like findClubMonthlyScoreByClubId(year,
month, clubId)) and then fetch the number of clubs with a strictly higher score
(e.g., repository method like countClubsWithScoreGreaterThan(year, month,
score)) to compute rank; if no score exists return
ClubMonthlyStatusQuery.createEmpty(year, month) and otherwise build
ClubMonthlyStatusQuery.from(year, month, clubId, score, rank).
src/test/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedRankingServiceTest.java (1)

332-356: getClubMonthlyStatus_withFeedsLocalDate.now() 의존성

getClubMonthlyStatus_noFeeds는 고정된 과거 날짜(year=2000, month=1)를 사용해 다른 테스트 데이터와 격리가 잘 됩니다. 반면 getClubMonthlyStatus_withFeeds도 현재 연/월 데이터를 사용하므로, 다른 테스트에서 저장한 동점(score=20) 클럽들이 존재하더라도 동점이면 rank=1이라 어서션은 통과합니다. 그러나 noFeeds처럼 안전한 고정 날짜를 사용하는 방식이 더 견고하며, rankAccuracy와 동일한 전략으로 일관성을 유지할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedRankingServiceTest.java`
around lines 332 - 356, The test getClubMonthlyStatus_withFeeds depends on
LocalDate.now(), making it non-deterministic; change it to use the same fixed
year/month as the other test (e.g., year=2000, month=1) and ensure the saved
feeds are created with timestamps in that month by using the
FeedFixture.createImageFeed overload that accepts a createdAt (or add one) when
calling feedRepository.save(FeedFixture.createImageFeed(club, "피드1", createdAt))
and for "피드2", so the query for
feedRankingService.getClubMonthlyStatus(user.getId(), year, month) reliably
returns rank 1 and feedCount 2.
src/main/java/ddingdong/ddingdongBE/domain/feed/api/AdminFeedApi.java (1)

54-58: clubId 경로 변수에 양수 제약 조건 추가 고려

yearmonth에는 @Min/@Max가 붙어 있지만 clubId에는 아무 제약이 없어 0 또는 음수 값이 서비스 레이어까지 통과합니다.

✨ 제안: `@Positive` 추가
-    `@PathVariable`("clubId") Long clubId,
+    `@PathVariable`("clubId") `@Positive`(message = "clubId는 1 이상이어야 합니다.") Long clubId,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/ddingdong/ddingdongBE/domain/feed/api/AdminFeedApi.java` around
lines 54 - 58, The clubId path variable in AdminFeedApi.getClubMonthlyStatus
lacks validation and can pass zero/negative values; add `@Positive` (e.g.,
`@Positive`(message = "clubId는 양수여야 합니다.")) to the Long clubId parameter and
import the corresponding javax.validation.constraints.Positive so invalid IDs
are rejected before reaching the service layer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/java/ddingdong/ddingdongBE/domain/feed/api/AdminFeedApi.java`:
- Around line 54-58: The clubId path variable in
AdminFeedApi.getClubMonthlyStatus lacks validation and can pass zero/negative
values; add `@Positive` (e.g., `@Positive`(message = "clubId는 양수여야 합니다.")) to the
Long clubId parameter and import the corresponding
javax.validation.constraints.Positive so invalid IDs are rejected before
reaching the service layer.

In
`@src/main/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedRankingService.java`:
- Around line 73-83: getClubMonthlyStatusByClubId currently calls
getClubFeedRanking(year, month) which computes and sorts all clubs; replace that
heavy path with targeted DB queries: in
GeneralFeedRankingService.getClubMonthlyStatusByClubId, stop calling
getClubFeedRanking and instead fetch only the requested club's aggregated
monthly score (e.g., repository method like findClubMonthlyScoreByClubId(year,
month, clubId)) and then fetch the number of clubs with a strictly higher score
(e.g., repository method like countClubsWithScoreGreaterThan(year, month,
score)) to compute rank; if no score exists return
ClubMonthlyStatusQuery.createEmpty(year, month) and otherwise build
ClubMonthlyStatusQuery.from(year, month, clubId, score, rank).

In
`@src/test/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedRankingServiceTest.java`:
- Around line 332-356: The test getClubMonthlyStatus_withFeeds depends on
LocalDate.now(), making it non-deterministic; change it to use the same fixed
year/month as the other test (e.g., year=2000, month=1) and ensure the saved
feeds are created with timestamps in that month by using the
FeedFixture.createImageFeed overload that accepts a createdAt (or add one) when
calling feedRepository.save(FeedFixture.createImageFeed(club, "피드1", createdAt))
and for "피드2", so the query for
feedRankingService.getClubMonthlyStatus(user.getId(), year, month) reliably
returns rank 1 and feedCount 2.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 22, 2026

@KoSeonJe: 네, PR을 리뷰하겠습니다! 동아리 이달의 피드 현황 조회 API 구현에 대해 전체적으로 검토해드리겠습니다.

잠시만 기다려주세요. 🔍

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
src/main/java/ddingdong/ddingdongBE/domain/feed/api/AdminFeedApi.java (1)

54-58: clubId@Min(1) 유효성 검증 추가를 고려하세요.

year, month에는 @Min/@Max 제약이 적용되어 있지만, clubId에는 없습니다. 0 이하 값이 들어올 경우 서비스 계층까지 전달되어 의미없는 DB 조회가 발생합니다. @Min(1) 추가로 컨트롤러 레벨에서 400 Bad Request를 빠르게 반환할 수 있습니다.

✨ clubId 유효성 검증 추가 제안
     ClubMonthlyStatusResponse getClubMonthlyStatus(
-            `@PathVariable`("clubId") Long clubId,
+            `@PathVariable`("clubId") `@Min`(value = 1, message = "clubId는 1 이상이어야 합니다.") Long clubId,
             `@RequestParam`("year") `@Min`(value = 2000, message = "year는 2000 이상이어야 합니다.") `@Max`(value = 2100, message = "year는 2100 이하여야 합니다.") int year,
             `@RequestParam`("month") `@Min`(value = 1, message = "month는 1 이상이어야 합니다.") `@Max`(value = 12, message = "month는 12 이하여야 합니다.") int month
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/ddingdong/ddingdongBE/domain/feed/api/AdminFeedApi.java` around
lines 54 - 58, AdminFeedApi의 getClubMonthlyStatus 메서드에서 PathVariable인 clubId에
유효성 검증이 빠져 있어 0 이하 값이 서비스로 넘어가는 문제가 있습니다; clubId 파라미터에 `@Min`(value = 1, message =
"clubId는 1 이상이어야 합니다.") 어노테이션을 추가하고 필요한 경우 javax.validation.constraints.Min를
임포트해 컨트롤러 레벨에서 400 Bad Request가 반환되도록 수정하세요.
src/test/java/ddingdong/ddingdongBE/domain/feed/controller/AdminFeedControllerE2ETest.java (1)

206-208: 순위 단언이 지나치게 느슨합니다.

isGreaterThan(0)은 score > 0임을 보장하지만, 다른 테스트에서 현재 월에 더 높은 score를 가진 클럽이 남아있어 rank가 2 이상이 되더라도 테스트가 통과됩니다. NonTxTestContainerSupport가 테스트 간 데이터를 정리한다면 isEqualTo(1)로 강화하여 랭킹 로직 오류를 더 정확히 검출할 수 있습니다.

✨ 순위 단언 개선 제안
-        assertThat(((Number) response.get("rank")).intValue()).isGreaterThan(0);
+        assertThat(((Number) response.get("rank")).intValue()).isEqualTo(1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/ddingdong/ddingdongBE/domain/feed/controller/AdminFeedControllerE2ETest.java`
around lines 206 - 208, The rank assertion in AdminFeedControllerE2ETest is too
loose: replace the assertion using response.get("rank") from isGreaterThan(0) to
isEqualTo(1) so the test expects the created club to be top-ranked; update the
assertion line that currently reads something like assertThat(((Number)
response.get("rank")).intValue()).isGreaterThan(0) to assert equality to 1 and
keep using the same response extraction logic to locate the change.
src/test/java/ddingdong/ddingdongBE/domain/feed/controller/ClubFeedStatusE2ETest.java (1)

94-102: viewCount 필드에 대한 검증이 누락되었습니다.

응답의 다른 모든 필드(year, month, feedCount, likeCount, commentCount, score, rank)는 검증하고 있으나, viewCount만 빠져 있습니다.

🧪 제안: viewCount 검증 추가
         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("viewCount")).longValue()).isEqualTo(0L);
             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);
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/ddingdong/ddingdongBE/domain/feed/controller/ClubFeedStatusE2ETest.java`
around lines 94 - 102, Add an assertion for the missing viewCount in the
ClubFeedStatusE2ETest soft assertions: inside the assertSoftly block that
currently asserts year, month, feedCount, likeCount, commentCount, score, and
rank (using the response map), add a softly.assertThat for
response.get("viewCount") converting to Number and comparing to the expected
long value (same style as feedCount/likeCount assertions) so viewCount is
validated alongside the other fields.
src/main/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedRankingService.java (1)

73-83: 전체 랭킹 조회 후 필터링하는 방식의 성능 참고 사항

getClubMonthlyStatusByClubId가 단일 동아리 조회를 위해 getClubFeedRanking()으로 전체 동아리 랭킹을 매번 계산합니다. 현재 동아리 수 규모에서는 문제 없겠지만, 향후 동아리 수가 크게 증가하거나 해당 API 호출 빈도가 높아지면 개별 동아리 전용 쿼리 최적화를 고려해 볼 수 있습니다.

또한 현재 로직에서 .filter(rankingQuery -> rankingQuery.score() > 0) 조건은, 랭킹 리스트에 포함된 동아리는 최소 feedCount >= 1 (score >= 10)이므로 실질적으로 방어 코드 역할입니다. 의도가 명확하다면 현재 구조로 충분합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedRankingService.java`
around lines 73 - 83, Currently getClubMonthlyStatusByClubId calls
getClubFeedRanking(year, month) and then filters the full ranking list to find
one club, which can be inefficient as club count grows; modify the service to
use a dedicated query that fetches only the target club's monthly ranking
instead of computing the whole ranking each time—either add a repository/service
method like findClubFeedRankingByClubId(Long clubId, int year, int month) or
overload getClubFeedRanking to accept an optional clubId and return a single
ClubFeedRankingQuery; keep the existing score>0 guard and map to
ClubMonthlyStatusQuery.from(...) or ClubMonthlyStatusQuery.createEmpty(...) as
now.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/java/ddingdong/ddingdongBE/domain/feed/api/AdminFeedApi.java`:
- Around line 54-58: AdminFeedApi의 getClubMonthlyStatus 메서드에서 PathVariable인
clubId에 유효성 검증이 빠져 있어 0 이하 값이 서비스로 넘어가는 문제가 있습니다; clubId 파라미터에 `@Min`(value = 1,
message = "clubId는 1 이상이어야 합니다.") 어노테이션을 추가하고 필요한 경우
javax.validation.constraints.Min를 임포트해 컨트롤러 레벨에서 400 Bad Request가 반환되도록 수정하세요.

In
`@src/main/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedRankingService.java`:
- Around line 73-83: Currently getClubMonthlyStatusByClubId calls
getClubFeedRanking(year, month) and then filters the full ranking list to find
one club, which can be inefficient as club count grows; modify the service to
use a dedicated query that fetches only the target club's monthly ranking
instead of computing the whole ranking each time—either add a repository/service
method like findClubFeedRankingByClubId(Long clubId, int year, int month) or
overload getClubFeedRanking to accept an optional clubId and return a single
ClubFeedRankingQuery; keep the existing score>0 guard and map to
ClubMonthlyStatusQuery.from(...) or ClubMonthlyStatusQuery.createEmpty(...) as
now.

In
`@src/test/java/ddingdong/ddingdongBE/domain/feed/controller/AdminFeedControllerE2ETest.java`:
- Around line 206-208: The rank assertion in AdminFeedControllerE2ETest is too
loose: replace the assertion using response.get("rank") from isGreaterThan(0) to
isEqualTo(1) so the test expects the created club to be top-ranked; update the
assertion line that currently reads something like assertThat(((Number)
response.get("rank")).intValue()).isGreaterThan(0) to assert equality to 1 and
keep using the same response extraction logic to locate the change.

In
`@src/test/java/ddingdong/ddingdongBE/domain/feed/controller/ClubFeedStatusE2ETest.java`:
- Around line 94-102: Add an assertion for the missing viewCount in the
ClubFeedStatusE2ETest soft assertions: inside the assertSoftly block that
currently asserts year, month, feedCount, likeCount, commentCount, score, and
rank (using the response map), add a softly.assertThat for
response.get("viewCount") converting to Number and comparing to the expected
long value (same style as feedCount/likeCount assertions) so viewCount is
validated alongside the other fields.

- AdminFeedApi에서 /clubs/{clubId}/feeds/ranking/status 엔드포인트 제거
- AdminFeedController에서 getClubMonthlyStatus 구현 제거
- FeedRankingService에서 getClubMonthlyStatusByClubId 인터페이스 메서드 제거
- GeneralFeedRankingService에서 로직을 getClubMonthlyStatus에 인라인
- AdminFeedApi 베이스 경로 /server/admin/feeds로 복원
- AdminFeedControllerE2ETest에서 관련 테스트 2개 제거

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedRankingService.java`:
- Around line 67-78: getClubMonthlyStatus currently calls
getClubFeedRanking(year, month) which loads and sorts all clubs; instead, add
repository-level queries to fetch only the target club's monthly score and its
rank. Replace the in-memory approach in getClubMonthlyStatus (and keep using
clubService.getByUserId and ClubMonthlyStatusQuery.from/createEmpty) by calling
new feedRepository methods such as findMonthlyScoreByClubId(year, month, clubId)
to get the club's score and countClubsWithHigherScore(year, month, score) (or a
single query that returns both score and rank); then construct and return
ClubMonthlyStatusQuery.from(year, month, /* build from score+rank */) or
createEmpty when no score, avoiding getClubFeedRanking entirely. Ensure
repository/query method names match the feedRepository interface used by
GeneralFeedRankingService.

Comment on lines +67 to +78
@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));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

전체 랭킹 조회를 통한 단일 동아리 조회 — 성능 문제

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
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedRankingService.java`
around lines 67 - 78, getClubMonthlyStatus currently calls
getClubFeedRanking(year, month) which loads and sorts all clubs; instead, add
repository-level queries to fetch only the target club's monthly score and its
rank. Replace the in-memory approach in getClubMonthlyStatus (and keep using
clubService.getByUserId and ClubMonthlyStatusQuery.from/createEmpty) by calling
new feedRepository methods such as findMonthlyScoreByClubId(year, month, clubId)
to get the club's score and countClubsWithHigherScore(year, month, score) (or a
single query that returns both score and rank); then construct and return
ClubMonthlyStatusQuery.from(year, month, /* build from score+rank */) or
createEmpty when no score, avoiding getClubFeedRanking entirely. Ensure
repository/query method names match the feedRepository interface used by
GeneralFeedRankingService.

@KoSeonJe KoSeonJe merged commit 0cf16f4 into develop Feb 23, 2026
2 checks passed
@KoSeonJe KoSeonJe deleted the feat/DDING-000-feed-club-monthly-best branch February 23, 2026 06:22
KoSeonJe added a commit that referenced this pull request Feb 28, 2026
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit 0cf16f4)
@KoSeonJe KoSeonJe mentioned this pull request Feb 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant