Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import ddingdong.ddingdongBE.domain.feed.controller.dto.response.AdminClubFeedRankingResponse;
import ddingdong.ddingdongBE.domain.feed.controller.dto.response.AdminFeedRankingWinnerResponse;
import ddingdong.ddingdongBE.domain.feed.controller.dto.response.ClubMonthlyStatusResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
Expand All @@ -14,20 +15,21 @@
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;

@Tag(name = "Feed - Admin", description = "Feed Admin API")
@RequestMapping("/server/admin/feeds")
@RequestMapping("/server/admin")
public interface AdminFeedApi {

@Operation(summary = "총동연 월별 1위 동아리 목록 조회 API")
@ApiResponse(responseCode = "200", description = "월별 1위 동아리 목록 조회 성공",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AdminFeedRankingWinnerResponse.class))))
@ResponseStatus(HttpStatus.OK)
@SecurityRequirement(name = "AccessToken")
@GetMapping("/ranking/last")
@GetMapping("/feeds/ranking/last")
List<AdminFeedRankingWinnerResponse> getMonthlyWinners(
@RequestParam("year") @Min(value = 2000, message = "year는 2000 이상이어야 합니다.") @Max(value = 2100, message = "year는 2100 이하여야 합니다.") int year
);
Expand All @@ -37,9 +39,21 @@ List<AdminFeedRankingWinnerResponse> getMonthlyWinners(
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AdminClubFeedRankingResponse.class))))
@ResponseStatus(HttpStatus.OK)
@SecurityRequirement(name = "AccessToken")
@GetMapping("/ranking")
@GetMapping("/feeds/ranking")
List<AdminClubFeedRankingResponse> getClubFeedRanking(
@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
);

@Operation(summary = "총동연 동아리 이달의 현황 조회 API")
@ApiResponse(responseCode = "200", description = "동아리 이달의 현황 조회 성공",
content = @Content(schema = @Schema(implementation = ClubMonthlyStatusResponse.class)))
@ResponseStatus(HttpStatus.OK)
@SecurityRequirement(name = "AccessToken")
@GetMapping("/clubs/{clubId}/feeds/ranking/status")
ClubMonthlyStatusResponse getClubMonthlyStatus(
@PathVariable("clubId") 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
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import ddingdong.ddingdongBE.auth.PrincipalDetails;
import ddingdong.ddingdongBE.domain.feed.controller.dto.request.CreateFeedRequest;
import ddingdong.ddingdongBE.domain.feed.controller.dto.request.UpdateFeedRequest;
import ddingdong.ddingdongBE.domain.feed.controller.dto.response.ClubMonthlyStatusResponse;
import ddingdong.ddingdongBE.domain.feed.controller.dto.response.MyFeedPageResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
Expand All @@ -11,6 +12,8 @@
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand Down Expand Up @@ -67,4 +70,16 @@ MyFeedPageResponse getMyFeedPage(
@RequestParam(value = "size", defaultValue = "9") int size,
@RequestParam(value = "currentCursorId", defaultValue = "-1") Long currentCursorId
);

@Operation(summary = "동아리 이달의 현황 조회 API")
@ApiResponse(responseCode = "200", description = "동아리 이달의 현황 조회 성공",
content = @Content(schema = @Schema(implementation = ClubMonthlyStatusResponse.class)))
@ResponseStatus(HttpStatus.OK)
@SecurityRequirement(name = "AccessToken")
@GetMapping("/feeds/status")
ClubMonthlyStatusResponse getFeedStatus(
@AuthenticationPrincipal PrincipalDetails principalDetails,
@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
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import ddingdong.ddingdongBE.domain.feed.api.AdminFeedApi;
import ddingdong.ddingdongBE.domain.feed.controller.dto.response.AdminClubFeedRankingResponse;
import ddingdong.ddingdongBE.domain.feed.controller.dto.response.AdminFeedRankingWinnerResponse;
import ddingdong.ddingdongBE.domain.feed.controller.dto.response.ClubMonthlyStatusResponse;
import ddingdong.ddingdongBE.domain.feed.service.FeedRankingService;
import ddingdong.ddingdongBE.domain.feed.service.dto.query.ClubMonthlyStatusQuery;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
Expand All @@ -29,4 +31,10 @@ public List<AdminClubFeedRankingResponse> getClubFeedRanking(int year, int month
feedRankingService.getClubFeedRanking(year, month)
);
}

@Override
public ClubMonthlyStatusResponse getClubMonthlyStatus(Long clubId, int year, int month) {
ClubMonthlyStatusQuery query = feedRankingService.getClubMonthlyStatusByClubId(clubId, year, month);
return ClubMonthlyStatusResponse.from(query);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@
import ddingdong.ddingdongBE.domain.feed.api.ClubFeedApi;
import ddingdong.ddingdongBE.domain.feed.controller.dto.request.CreateFeedRequest;
import ddingdong.ddingdongBE.domain.feed.controller.dto.request.UpdateFeedRequest;
import ddingdong.ddingdongBE.domain.feed.controller.dto.response.ClubMonthlyStatusResponse;
import ddingdong.ddingdongBE.domain.feed.controller.dto.response.MyFeedPageResponse;
import ddingdong.ddingdongBE.domain.feed.service.FacadeClubFeedService;
import ddingdong.ddingdongBE.domain.feed.service.FeedRankingService;
import ddingdong.ddingdongBE.domain.feed.service.dto.query.ClubMonthlyStatusQuery;
import ddingdong.ddingdongBE.domain.feed.service.dto.query.MyFeedPageQuery;
import ddingdong.ddingdongBE.domain.user.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@Validated
public class ClubFeedController implements ClubFeedApi {

private final FacadeClubFeedService facadeClubFeedService;
private final FeedRankingService feedRankingService;

@Override
public void createFeed(
Expand Down Expand Up @@ -45,4 +51,11 @@ public MyFeedPageResponse getMyFeedPage(PrincipalDetails principalDetails, int s
MyFeedPageQuery query = facadeClubFeedService.getMyFeedPage(user, size, currentCursorId);
return MyFeedPageResponse.from(query);
}

@Override
public ClubMonthlyStatusResponse getFeedStatus(PrincipalDetails principalDetails, int year, int month) {
Long userId = principalDetails.getUser().getId();
ClubMonthlyStatusQuery query = feedRankingService.getClubMonthlyStatus(userId, year, month);
return ClubMonthlyStatusResponse.from(query);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

@Builder
public record AdminFeedRankingWinnerResponse(
Long clubId,
String clubName,
long feedCount,
long viewCount,
Expand All @@ -17,6 +18,7 @@ public record AdminFeedRankingWinnerResponse(

public static AdminFeedRankingWinnerResponse from(FeedRankingWinnerQuery query) {
return AdminFeedRankingWinnerResponse.builder()
.clubId(query.clubId())
.clubName(query.clubName())
.feedCount(query.feedCount())
.viewCount(query.viewCount())
Expand Down
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();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ddingdong.ddingdongBE.domain.feed.service;

import ddingdong.ddingdongBE.domain.feed.service.dto.query.ClubFeedRankingQuery;
import ddingdong.ddingdongBE.domain.feed.service.dto.query.ClubMonthlyStatusQuery;
import ddingdong.ddingdongBE.domain.feed.service.dto.query.FeedRankingWinnerQuery;
import java.util.List;

Expand All @@ -9,4 +10,8 @@ public interface FeedRankingService {
List<FeedRankingWinnerQuery> getMonthlyWinners(int year);

List<ClubFeedRankingQuery> getClubFeedRanking(int year, int month);

ClubMonthlyStatusQuery getClubMonthlyStatus(Long userId, int year, int month);

ClubMonthlyStatusQuery getClubMonthlyStatusByClubId(Long clubId, int year, int month);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package ddingdong.ddingdongBE.domain.feed.service;

import ddingdong.ddingdongBE.domain.club.entity.Club;
import ddingdong.ddingdongBE.domain.club.service.ClubService;
import ddingdong.ddingdongBE.domain.feed.entity.FeedMonthlyRanking;
import ddingdong.ddingdongBE.domain.feed.repository.FeedMonthlyRankingRepository;
import ddingdong.ddingdongBE.domain.feed.repository.FeedRepository;
import ddingdong.ddingdongBE.domain.feed.repository.dto.MonthlyFeedRankingDto;
import ddingdong.ddingdongBE.domain.feed.service.dto.query.ClubFeedRankingQuery;
import ddingdong.ddingdongBE.domain.feed.service.dto.query.ClubMonthlyStatusQuery;
import ddingdong.ddingdongBE.domain.feed.service.dto.query.FeedRankingWinnerQuery;
import java.util.ArrayList;
import java.util.Comparator;
Expand All @@ -26,6 +29,7 @@ public class GeneralFeedRankingService implements FeedRankingService {

private final FeedMonthlyRankingRepository feedMonthlyRankingRepository;
private final FeedRepository feedRepository;
private final ClubService clubService;

@Override
public List<FeedRankingWinnerQuery> getMonthlyWinners(int year) {
Expand Down Expand Up @@ -60,6 +64,24 @@ public List<ClubFeedRankingQuery> getClubFeedRanking(int year, int month) {
return result;
}

@Override
public ClubMonthlyStatusQuery getClubMonthlyStatus(Long userId, int year, int month) {
Club club = clubService.getByUserId(userId);
return getClubMonthlyStatusByClubId(club.getId(), year, month);
}

@Override
public ClubMonthlyStatusQuery getClubMonthlyStatusByClubId(Long clubId, int year, int month) {
List<ClubFeedRankingQuery> rankings = getClubFeedRanking(year, month);

return rankings.stream()
.filter(rankingQuery -> rankingQuery.clubId().equals(clubId))
.findFirst()
.filter(rankingQuery -> rankingQuery.score() > 0)
.map(rankingQuery -> ClubMonthlyStatusQuery.from(year, month, rankingQuery))
.orElse(ClubMonthlyStatusQuery.createEmpty(year, month));
}
Comment on lines +67 to +78
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.


private long calculateScore(MonthlyFeedRankingDto dto) {
return dto.getFeedCount() * FEED_WEIGHT
+ dto.getViewCount() * VIEW_WEIGHT
Expand Down
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

@Builder
public record FeedRankingWinnerQuery(
Long clubId,
String clubName,
long feedCount,
long viewCount,
Expand All @@ -17,6 +18,7 @@ public record FeedRankingWinnerQuery(

public static FeedRankingWinnerQuery from(FeedMonthlyRanking entity) {
return FeedRankingWinnerQuery.builder()
.clubId(entity.getClubId())
.clubName(entity.getClubName())
.feedCount(entity.getFeedCount())
.viewCount(entity.getViewCount())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ public static User createClubUser(String encode) {
.build();
}

public static User createClubUser(String authId, String encode) {
return User.builder()
.authId(authId)
.password(encode)
.name("동아리 사용자")
.role(Role.CLUB)
.build();
}

public static User createClubUser() {
return User.builder()
.authId("club123")
Expand Down
Loading