Skip to content

Commit b298669

Browse files
KoSeonJeclaude
andcommitted
refactor: 이달의 현황 및 피드 랭킹 API 응답 필드 "개수 -> 가중치 점수"로 변경 (#390)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> (cherry picked from commit c1fd293)
1 parent dbbbf2e commit b298669

File tree

10 files changed

+356
-100
lines changed

10 files changed

+356
-100
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
name: review-fix-plan
3+
description: |
4+
코드 리뷰 후 수정 계획 문서를 생성하는 스킬.
5+
코드 리뷰 결과를 구조화된 마크다운 문서로 docs/local/ 하위에 저장한다.
6+
"리뷰 문서 만들어줘", "코드리뷰 정리해줘", "/review-fix-plan" 요청 시 사용.
7+
---
8+
9+
# Review Fix Plan
10+
11+
코드 리뷰 결과를 **문제상황 → 문제원인 → 해결 방법 → 결과** 4단계 구조로 정리하여
12+
`docs/local/` 하위에 마크다운 문서로 저장한다.
13+
14+
## When to Use
15+
16+
- 코드 리뷰 수행 후 이슈를 문서로 정리하고 싶을 때
17+
- "리뷰 문서 만들어줘", "코드리뷰 정리해줘" 요청 시
18+
- "/review-fix-plan" 슬래시 커맨드 입력 시
19+
- PR 리뷰 후 수정 사항을 추적하고 싶을 때
20+
21+
## Process
22+
23+
1. **리뷰 결과 수집**: 현재 대화에서 발견된 코드 리뷰 이슈 파악
24+
- 각 이슈의 심각도 판단 (CRITICAL / WARNING / LOW)
25+
- 관련 파일과 코드 위치 식별
26+
27+
2. **문서 작성**: 아래 템플릿에 맞춰 각 이슈를 정리
28+
29+
3. **파일 저장**: `docs/local/{도메인}-review-fixes.md` 경로에 저장
30+
- 기존 파일이 있으면 덮어쓰기 (같은 PR/작업 단위)
31+
32+
4. **완료 보고**: 저장된 파일 경로를 사용자에게 알림
33+
34+
## Template
35+
36+
```markdown
37+
# {작업명} 코드 리뷰 이슈 분석 ({날짜})
38+
39+
## 수정 파일
40+
41+
- `{파일 경로 1}`
42+
- `{파일 경로 2}`
43+
44+
---
45+
46+
## [{심각도 코드}] {위치} — {이슈 한줄 요약}
47+
48+
**심각도**: {이모지} {CRITICAL | WARNING | LOW}
49+
50+
**문제 상황**: {어떤 동작이 문제인지 사용자 관점에서 서술}
51+
52+
**문제 원인**: {왜 이 문제가 발생했는지 코드 레벨에서 분석}
53+
54+
```java
55+
// AS-IS — 문제 코드
56+
{기존 코드}
57+
```
58+
59+
**해결 방법**: {구체적인 수정 방안 서술}
60+
61+
```java
62+
// TO-BE — 개선 코드
63+
{수정 코드}
64+
```
65+
66+
**결과**: {수정 완료 여부, 수정 후 기대 효과}
67+
68+
---
69+
70+
## 패턴 정리
71+
72+
| 상황 | 패턴 |
73+
|------|------|
74+
| {이번 리뷰에서 얻은 교훈} | {재발 방지를 위한 규칙} |
75+
```
76+
77+
## Severity Guide
78+
79+
| 코드 | 이모지 | 기준 |
80+
|------|--------|------|
81+
| C-N | 🔴 CRITICAL | 잘못된 데이터 반환, 런타임 에러, 보안 취약점 |
82+
| W-N | 🟡 WARNING | 성능 저하, 중복 코드, 유지보수 위험 |
83+
| L-N | 🟢 LOW | dead code, 네이밍, 스타일 |
84+
85+
## Naming Convention
86+
87+
- 파일명: `{도메인}-review-fixes.md` (예: `feed-modify-existing-review-fixes.md`)
88+
- 이슈 ID: `[{심각도 첫글자}-{번호}]` (예: `[C-1]`, `[W-2]`, `[L-1]`)

CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- 완전히 실행 가능한 코드만 제공한다 (의사코드 금지)
1818
- `@Valid`, `@NotNull` 등 DTO 검증 어노테이션 적용
1919
- secrets/환경변수 하드코딩 절대 금지
20+
- **변수명은 의미를 알 수 있도록 작성한다**`dto`, `r`, `p` 같은 축약 금지, 역할이 드러나는 이름 사용 (예: `dto``rawRanking`, `r``ranking`)
2021

2122
### 새 기능 추가 시 필수 순서
2223
1. (DB 변경 시) `resources/db/migration/`에 Flyway 마이그레이션 파일 추가
@@ -31,6 +32,9 @@
3132
- 버그 수정/기능 추가 시 반드시 테스트 추가
3233
- TestContainers 사용 — Docker 실행 상태 필요
3334
- **FixtureMonkey 사용 금지** — 테스트 데이터는 `common/fixture/` 의 static 메서드로만 생성한다
35+
- `@DisplayName`에 메서드명을 넣지 않고, **테스트만 보고 요구사항을 파악할 수 있는 문장**으로 작성한다
36+
- 좋은 예: `"피드가 없으면 모든 집계가 0이다"`, `"삭제된 피드는 조회되지 않는다"`
37+
- 나쁜 예: `"findMyFeedStat - 피드가 없으면 0"`, `"getAllFeeds는 삭제된 피드 제외"`
3438

3539
---
3640

@@ -39,6 +43,7 @@
3943
- 브랜치명: `{type}/{DDING-이슈번호}-{설명}` (예: `feat/DDING-123-club-search`)
4044
- 커밋 메시지: 한국어, `[DDING-000] 작업 내용` 형식
4145
- PR 템플릿: 🚀 작업 내용 / 🤔 고민했던 내용 / 💬 리뷰 중점사항
46+
- PR 본문 작성 시 특정 클래스명, 메서드명을 나열하지 않고 **작업 내용 중심의 자연스러운 글**로 작성한다 (가독성 우선)
4247

4348
### API 단위 브랜치 전략 (PR 크기 관리)
4449

src/main/java/ddingdong/ddingdongBE/domain/feed/controller/dto/response/AdminClubFeedRankingResponse.java

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,24 @@
77
@Builder
88
public record AdminClubFeedRankingResponse(
99
int rank,
10-
Long clubId,
1110
String clubName,
12-
long feedCount,
13-
long viewCount,
14-
long likeCount,
15-
long commentCount,
16-
long score
11+
long feedScore,
12+
long viewScore,
13+
long likeScore,
14+
long commentScore,
15+
long totalScore
1716
) {
1817

1918
public static List<AdminClubFeedRankingResponse> from(List<ClubFeedRankingQuery> queries) {
2019
return queries.stream()
2120
.map(query -> AdminClubFeedRankingResponse.builder()
2221
.rank(query.rank())
23-
.clubId(query.clubId())
2422
.clubName(query.clubName())
25-
.feedCount(query.feedCount())
26-
.viewCount(query.viewCount())
27-
.likeCount(query.likeCount())
28-
.commentCount(query.commentCount())
29-
.score(query.score())
23+
.feedScore(query.feedScore())
24+
.viewScore(query.viewScore())
25+
.likeScore(query.likeScore())
26+
.commentScore(query.commentScore())
27+
.totalScore(query.totalScore())
3028
.build())
3129
.toList();
3230
}

src/main/java/ddingdong/ddingdongBE/domain/feed/controller/dto/response/ClubMonthlyStatusResponse.java

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,25 @@ public record ClubMonthlyStatusResponse(
88
int year,
99
int month,
1010
int rank,
11-
long feedCount,
12-
long viewCount,
13-
long likeCount,
14-
long commentCount,
15-
long score
11+
int lastMonthRank,
12+
long feedScore,
13+
long viewScore,
14+
long likeScore,
15+
long commentScore,
16+
long totalScore
1617
) {
1718

1819
public static ClubMonthlyStatusResponse from(ClubMonthlyStatusQuery query) {
1920
return ClubMonthlyStatusResponse.builder()
2021
.year(query.year())
2122
.month(query.month())
2223
.rank(query.rank())
23-
.feedCount(query.feedCount())
24-
.viewCount(query.viewCount())
25-
.likeCount(query.likeCount())
26-
.commentCount(query.commentCount())
27-
.score(query.score())
24+
.lastMonthRank(query.lastMonthRank())
25+
.feedScore(query.feedScore())
26+
.viewScore(query.viewScore())
27+
.likeScore(query.likeScore())
28+
.commentScore(query.commentScore())
29+
.totalScore(query.totalScore())
2830
.build();
2931
}
3032
}

src/main/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedRankingService.java

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@ public List<ClubFeedRankingQuery> getClubFeedRanking(int year, int month) {
3838
long previousScore = Long.MAX_VALUE;
3939
int rank = 1;
4040
for (int i = 0; i < sorted.size(); i++) {
41-
long score = calculateScore(sorted.get(i));
42-
if (i > 0 && score < previousScore) {
41+
long totalScore = calculateScore(sorted.get(i));
42+
if (i > 0 && totalScore < previousScore) {
4343
rank = i + 1;
4444
}
45-
result.add(ClubFeedRankingQuery.of(rank, sorted.get(i), score));
46-
previousScore = score;
45+
result.add(toClubFeedRankingQuery(rank, sorted.get(i)));
46+
previousScore = totalScore;
4747
}
4848

4949
return result;
@@ -53,19 +53,49 @@ public List<ClubFeedRankingQuery> getClubFeedRanking(int year, int month) {
5353
public ClubMonthlyStatusQuery getClubMonthlyStatus(Long userId, int year, int month) {
5454
Club club = clubService.getByUserId(userId);
5555
List<ClubFeedRankingQuery> rankings = getClubFeedRanking(year, month);
56+
int lastMonthRank = getLastMonthRank(club.getId(), year, month);
5657

5758
return rankings.stream()
5859
.filter(rankingQuery -> rankingQuery.clubId().equals(club.getId()))
5960
.findFirst()
60-
.filter(rankingQuery -> rankingQuery.score() > 0)
61-
.map(rankingQuery -> ClubMonthlyStatusQuery.from(year, month, rankingQuery))
62-
.orElse(ClubMonthlyStatusQuery.createEmpty(year, month));
61+
.filter(rankingQuery -> rankingQuery.totalScore() > 0)
62+
.map(rankingQuery -> toMonthlyStatus(year, month, rankingQuery, lastMonthRank))
63+
.orElse(ClubMonthlyStatusQuery.createEmpty(year, month, lastMonthRank));
6364
}
6465

65-
private long calculateScore(MonthlyFeedRankingDto dto) {
66-
return dto.getFeedCount() * FEED_WEIGHT
67-
+ dto.getViewCount() * VIEW_WEIGHT
68-
+ dto.getLikeCount() * LIKE_WEIGHT
69-
+ dto.getCommentCount() * COMMENT_WEIGHT;
66+
private ClubMonthlyStatusQuery toMonthlyStatus(int year, int month,
67+
ClubFeedRankingQuery ranking, int lastMonthRank) {
68+
return ClubMonthlyStatusQuery.of(year, month, ranking.rank(), lastMonthRank,
69+
ranking.feedScore(), ranking.viewScore(), ranking.likeScore(), ranking.commentScore());
70+
}
71+
72+
private int getLastMonthRank(Long clubId, int year, int month) {
73+
int lastYear = month == 1 ? year - 1 : year;
74+
int lastMonth = month == 1 ? 12 : month - 1;
75+
76+
List<ClubFeedRankingQuery> lastMonthRankings = getClubFeedRanking(lastYear, lastMonth);
77+
return lastMonthRankings.stream()
78+
.filter(ranking -> ranking.clubId().equals(clubId))
79+
.filter(ranking -> ranking.totalScore() > 0)
80+
.findFirst()
81+
.map(ClubFeedRankingQuery::rank)
82+
.orElse(0);
83+
}
84+
85+
private ClubFeedRankingQuery toClubFeedRankingQuery(int rank, MonthlyFeedRankingDto rawRanking) {
86+
long feedScore = rawRanking.getFeedCount() * FEED_WEIGHT;
87+
long viewScore = rawRanking.getViewCount() * VIEW_WEIGHT;
88+
long likeScore = rawRanking.getLikeCount() * LIKE_WEIGHT;
89+
long commentScore = rawRanking.getCommentCount() * COMMENT_WEIGHT;
90+
long totalScore = feedScore + viewScore + likeScore + commentScore;
91+
return ClubFeedRankingQuery.of(rank, rawRanking.getClubId(), rawRanking.getClubName(),
92+
feedScore, viewScore, likeScore, commentScore, totalScore);
93+
}
94+
95+
private long calculateScore(MonthlyFeedRankingDto rawRanking) {
96+
return rawRanking.getFeedCount() * FEED_WEIGHT
97+
+ rawRanking.getViewCount() * VIEW_WEIGHT
98+
+ rawRanking.getLikeCount() * LIKE_WEIGHT
99+
+ rawRanking.getCommentCount() * COMMENT_WEIGHT;
70100
}
71101
}
Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
package ddingdong.ddingdongBE.domain.feed.service.dto.query;
22

3-
import ddingdong.ddingdongBE.domain.feed.repository.dto.MonthlyFeedRankingDto;
43
import lombok.Builder;
54

65
@Builder
76
public record ClubFeedRankingQuery(
87
int rank,
98
Long clubId,
109
String clubName,
11-
long feedCount,
12-
long viewCount,
13-
long likeCount,
14-
long commentCount,
15-
long score
10+
long feedScore,
11+
long viewScore,
12+
long likeScore,
13+
long commentScore,
14+
long totalScore
1615
) {
1716

18-
public static ClubFeedRankingQuery of(int rank, MonthlyFeedRankingDto dto, long score) {
17+
public static ClubFeedRankingQuery of(int rank, Long clubId, String clubName,
18+
long feedScore, long viewScore, long likeScore, long commentScore, long totalScore) {
1919
return ClubFeedRankingQuery.builder()
2020
.rank(rank)
21-
.clubId(dto.getClubId())
22-
.clubName(dto.getClubName())
23-
.feedCount(dto.getFeedCount())
24-
.viewCount(dto.getViewCount())
25-
.likeCount(dto.getLikeCount())
26-
.commentCount(dto.getCommentCount())
27-
.score(score)
21+
.clubId(clubId)
22+
.clubName(clubName)
23+
.feedScore(feedScore)
24+
.viewScore(viewScore)
25+
.likeScore(likeScore)
26+
.commentScore(commentScore)
27+
.totalScore(totalScore)
2828
.build();
2929
}
3030
}

src/main/java/ddingdong/ddingdongBE/domain/feed/service/dto/query/ClubMonthlyStatusQuery.java

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,40 @@ public record ClubMonthlyStatusQuery(
77
int year,
88
int month,
99
int rank,
10-
long feedCount,
11-
long viewCount,
12-
long likeCount,
13-
long commentCount,
14-
long score
10+
int lastMonthRank,
11+
long feedScore,
12+
long viewScore,
13+
long likeScore,
14+
long commentScore,
15+
long totalScore
1516
) {
1617

17-
public static ClubMonthlyStatusQuery from(int year, int month, ClubFeedRankingQuery ranking) {
18+
public static ClubMonthlyStatusQuery of(int year, int month, int rank, int lastMonthRank,
19+
long feedScore, long viewScore, long likeScore, long commentScore) {
1820
return ClubMonthlyStatusQuery.builder()
1921
.year(year)
2022
.month(month)
21-
.rank(ranking.rank())
22-
.feedCount(ranking.feedCount())
23-
.viewCount(ranking.viewCount())
24-
.likeCount(ranking.likeCount())
25-
.commentCount(ranking.commentCount())
26-
.score(ranking.score())
23+
.rank(rank)
24+
.lastMonthRank(lastMonthRank)
25+
.feedScore(feedScore)
26+
.viewScore(viewScore)
27+
.likeScore(likeScore)
28+
.commentScore(commentScore)
29+
.totalScore(feedScore + viewScore + likeScore + commentScore)
2730
.build();
2831
}
2932

30-
public static ClubMonthlyStatusQuery createEmpty(int year, int month) {
33+
public static ClubMonthlyStatusQuery createEmpty(int year, int month, int lastMonthRank) {
3134
return ClubMonthlyStatusQuery.builder()
3235
.year(year)
3336
.month(month)
3437
.rank(0)
35-
.feedCount(0)
36-
.viewCount(0)
37-
.likeCount(0)
38-
.commentCount(0)
39-
.score(0)
38+
.lastMonthRank(lastMonthRank)
39+
.feedScore(0)
40+
.viewScore(0)
41+
.likeScore(0)
42+
.commentScore(0)
43+
.totalScore(0)
4044
.build();
4145
}
4246
}

src/test/java/ddingdong/ddingdongBE/domain/feed/controller/AdminFeedControllerE2ETest.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,21 @@ void getClubFeedRanking_success() {
9797
.getList(".", AdminClubFeedRankingResponse.class);
9898

9999
assertThat(response).hasSize(2);
100-
assertThat(response.get(0).clubName()).isEqualTo("동아리B");
101-
assertThat(response.get(0).rank()).isEqualTo(1);
102-
assertThat(response.get(1).clubName()).isEqualTo("동아리A");
103-
assertThat(response.get(1).rank()).isEqualTo(2);
100+
101+
AdminClubFeedRankingResponse first = response.get(0);
102+
assertThat(first.clubName()).isEqualTo("동아리B");
103+
assertThat(first.rank()).isEqualTo(1);
104+
assertThat(first.feedScore()).isEqualTo(2 * 10);
105+
assertThat(first.viewScore()).isEqualTo(0);
106+
assertThat(first.likeScore()).isEqualTo(1 * 3);
107+
assertThat(first.commentScore()).isEqualTo(0);
108+
assertThat(first.totalScore()).isEqualTo(23);
109+
110+
AdminClubFeedRankingResponse second = response.get(1);
111+
assertThat(second.clubName()).isEqualTo("동아리A");
112+
assertThat(second.rank()).isEqualTo(2);
113+
assertThat(second.feedScore()).isEqualTo(1 * 10);
114+
assertThat(second.totalScore()).isEqualTo(10);
104115
}
105116

106117
@DisplayName("총동연 피드 랭킹 조회 API - 성공: 데이터 없으면 빈 리스트 반환")

0 commit comments

Comments
 (0)