feat(ranking): Phase 5 개인/학교/우리학교 랭킹 API#5
Conversation
- GET /api/rankings/individual|school|school/mine?period=... (기본 this_week) - 랭킹 캡 적용(하루16h/주84h §3.6e) — 마이페이지 실제시간과 대비, StudyTimeAggregator 재사용 - 이름 마스킹 김O현(학교축약명)/무소속/동명이인 접미 (NameMasker) - 개인: 포디움/리스트/내순위(top10밖도, 기록없으면 null), 동점=마지막체크아웃 이른순 - 학교: 활동인원 평균, 최소 5명 필터, 인원수, 동점=인원desc→학교명asc, 무소속 제외 - 우리학교: 내 school_id 필터, 무소속이면 available=false - period converter→400, N+1 방지 프로젝션 쿼리 - 테스트: NameMasker/RankingService/컨트롤러 MockMvc 실 Postgres 검증: 23h→16h캡·마스킹·내순위, 학교평균 14400(5명), 우리학교 필터, period 400. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthrough개인·학교 랭킹 조회 기능을 추가했다. 기간별 공부 시간과 체크아웃 정보를 집계하고, 이름·학교 정보를 포함한 응답을 구성하며, 인증 기반 REST 엔드포인트와 입력 변환 및 통합 테스트를 제공한다. Changes랭킹 기능
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant RankingController
participant RankingService
participant DailyStudyRecordRepository
participant CheckInSessionRepository
Client->>RankingController: 랭킹 API 요청과 period 전달
RankingController->>RankingService: 개인·학교 랭킹 조회 호출
RankingService->>DailyStudyRecordRepository: 기간별 사용자 공부 시간 조회
RankingService->>CheckInSessionRepository: 사용자별 마지막 체크아웃 조회
RankingService-->>RankingController: 랭킹 응답 DTO 반환
RankingController-->>Client: JSON 응답 반환
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/src/main/java/com/studycafe/ranking/ranking/NameMasker.java`:
- Around line 30-33: Update the schoolPart selection in NameMasker to treat a
blank School.getShortName() the same as null, falling back to School.getName()
so masked names never include empty parentheses.
In `@backend/src/main/java/com/studycafe/ranking/ranking/RankingPeriod.java`:
- Around line 12-46: RankingPeriod의 start()와 end() 경계 계산을 전용 RankingPeriodTest로
검증하세요. THIS_WEEK, LAST_WEEK, THIS_MONTH, LAST_MONTH, THIS_YEAR를 포함하고 연초·연말·월말 및
주 경계 날짜를 고정된 LocalDate로 테스트해 각 기간의 시작일과 종료일이 정확한지 확인하세요. 기존 RankingServiceTest에만
의존하지 말고 04:00 스터디 날짜 분할 규칙과 관련된 경계 회귀를 직접 검증하세요.
In `@backend/src/test/java/com/studycafe/ranking/ranking/RankingServiceTest.java`:
- Around line 73-89: RankingServiceTest lacks coverage for individual ranking
pagination and the weekly 84-hour cap. Add tests targeting
RankingService.buildIndividual or the existing individual ranking flow that
create at least 11 users and verify only the top 3 podium entries plus ranks
4–10 are exposed, then add a THIS_WEEK case verifying accumulated time is capped
at 84 hours while preserving ordering and displayed values.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 79c2dc44-fd34-49d8-8318-e36e1e3a69f1
📒 Files selected for processing (16)
backend/src/main/java/com/studycafe/ranking/ranking/NameMasker.javabackend/src/main/java/com/studycafe/ranking/ranking/RankingController.javabackend/src/main/java/com/studycafe/ranking/ranking/RankingPeriod.javabackend/src/main/java/com/studycafe/ranking/ranking/RankingService.javabackend/src/main/java/com/studycafe/ranking/ranking/StringToRankingPeriodConverter.javabackend/src/main/java/com/studycafe/ranking/ranking/dto/IndividualRankingResponse.javabackend/src/main/java/com/studycafe/ranking/ranking/dto/RankEntry.javabackend/src/main/java/com/studycafe/ranking/ranking/dto/SchoolEntry.javabackend/src/main/java/com/studycafe/ranking/ranking/dto/SchoolMineResponse.javabackend/src/main/java/com/studycafe/ranking/ranking/dto/SchoolRankingResponse.javabackend/src/main/java/com/studycafe/ranking/repository/CheckInSessionRepository.javabackend/src/main/java/com/studycafe/ranking/repository/DailyStudyRecordRepository.javabackend/src/main/java/com/studycafe/ranking/repository/UserRepository.javabackend/src/test/java/com/studycafe/ranking/ranking/NameMaskerTest.javabackend/src/test/java/com/studycafe/ranking/ranking/RankingControllerMockMvcTest.javabackend/src/test/java/com/studycafe/ranking/ranking/RankingServiceTest.java
| School school = user.getSchool(); | ||
| String schoolPart = (school != null) | ||
| ? (school.getShortName() != null ? school.getShortName() : school.getName()) | ||
| : "무소속"; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the target file and inspect its structure and nearby code.
git ls-files 'backend/src/main/java/com/studycafe/ranking/ranking/NameMasker.java'
wc -l backend/src/main/java/com/studycafe/ranking/ranking/NameMasker.java
cat -n backend/src/main/java/com/studycafe/ranking/ranking/NameMasker.javaRepository: ca5tlechan/studycafe-ranking
Length of output: 1728
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the target utility and its callers to understand whether blank short names are possible and observable.
git ls-files 'backend/src/main/java/com/studycafe/ranking/ranking/NameMasker.java' \
'backend/src/main/java/**/School*.java' \
'backend/src/main/java/**/User*.java' \
'backend/src/main/java/**/Ranking*.java' \
'backend/src/main/java/**/NameMasker*.java'
printf '\n== NameMasker ==\n'
cat -n backend/src/main/java/com/studycafe/ranking/ranking/NameMasker.java
printf '\n== References ==\n'
rg -n "shortName|schoolPart|NameMasker|mask" backend/src/main/java -g'*.java'Repository: ca5tlechan/studycafe-ranking
Length of output: 4931
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for School.shortName declarations, validation, and normalization.
rg -n --hidden --glob '*.java' --glob '*.kt' \
'shortName|setShortName|`@NotBlank`|`@NotEmpty`|`@Size`|trim\(|blank|normalize' backend/src/main/java
printf '\n== School-related files ==\n'
fd -i 'School*.java' backend/src/main/java
printf '\n== NameMasker and ranking callers ==\n'
rg -n --hidden --glob '*.java' 'NameMasker|schoolPart|무소속' backend/src/main/javaRepository: ca5tlechan/studycafe-ranking
Length of output: 4259
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the school model and any request/response code that populates shortName.
printf '\n== School.java ==\n'
cat -n backend/src/main/java/com/studycafe/ranking/domain/School.java
printf '\n== SchoolController ==\n'
cat -n backend/src/main/java/com/studycafe/ranking/school/SchoolController.java
printf '\n== SchoolResponse ==\n'
cat -n backend/src/main/java/com/studycafe/ranking/school/dto/SchoolResponse.java
printf '\n== School-related DTOs and services ==\n'
rg -n --hidden --glob '*.java' 'shortName|new School\(|setShortName|SchoolResponse|SchoolController' backend/src/main/java/com/studycafe/rankingRepository: ca5tlechan/studycafe-ranking
Length of output: 6200
shortName이 빈 문자열이면 name으로 폴백하게 바꾸자. School.shortName은 nullable이고 blank 제약도 없어서 null만 체크하면 김O현()처럼 괄호만 남는 표기가 나올 수 있습니다. isBlank()까지 확인해 school.getName()으로 대체하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/main/java/com/studycafe/ranking/ranking/NameMasker.java` around
lines 30 - 33, Update the schoolPart selection in NameMasker to treat a blank
School.getShortName() the same as null, falling back to School.getName() so
masked names never include empty parentheses.
| public enum RankingPeriod { | ||
| THIS_WEEK, | ||
| LAST_WEEK, | ||
| THIS_MONTH, | ||
| LAST_MONTH, | ||
| THIS_YEAR; | ||
|
|
||
| public boolean isWeekly() { | ||
| return this == THIS_WEEK || this == LAST_WEEK; | ||
| } | ||
|
|
||
| public LocalDate start(LocalDate today) { | ||
| return switch (this) { | ||
| case THIS_WEEK -> monday(today); | ||
| case LAST_WEEK -> monday(today).minusWeeks(1); | ||
| case THIS_MONTH -> today.withDayOfMonth(1); | ||
| case LAST_MONTH -> YearMonth.from(today).minusMonths(1).atDay(1); | ||
| case THIS_YEAR -> today.withDayOfYear(1); | ||
| }; | ||
| } | ||
|
|
||
| public LocalDate end(LocalDate today) { | ||
| return switch (this) { | ||
| case THIS_WEEK -> monday(today).plusDays(6); | ||
| case LAST_WEEK -> monday(today).minusWeeks(1).plusDays(6); | ||
| case THIS_MONTH -> YearMonth.from(today).atEndOfMonth(); | ||
| case LAST_MONTH -> YearMonth.from(today).minusMonths(1).atEndOfMonth(); | ||
| case THIS_YEAR -> today.withDayOfYear(1).plusYears(1).minusDays(1); | ||
| }; | ||
| } | ||
|
|
||
| private static LocalDate monday(LocalDate date) { | ||
| return date.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); | ||
| } | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -i rankingperiodtest --type fRepository: ca5tlechan/studycafe-ranking
Length of output: 166
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files matching RankingPeriod or RankingService tests =="
git ls-files | rg 'RankingPeriod|RankingServiceTest|RankingPeriodTest|ranking.*Test|Test\.java$'
echo
echo "== ranking-related test usages =="
rg -n "THIS_YEAR|THIS_MONTH|LAST_MONTH|THIS_WEEK|LAST_WEEK|RankingPeriod|04:00|스터디 날짜|84h|10분" backend/src/test/java backend/src/main/java -SRepository: ca5tlechan/studycafe-ranking
Length of output: 15445
RankingPeriod 경계값 테스트를 보강하세요.
start()/end() 계산은 맞지만, 현재 RankingPeriodTest가 없고 RankingServiceTest도 THIS_YEAR만 사용합니다. LAST_WEEK/THIS_MONTH/LAST_MONTH 포함해서 연초·연말·월말 경계 케이스를 전용 테스트로 추가하면 04:00 스터디 날짜 분할 규칙 회귀를 더 잘 막을 수 있습니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/main/java/com/studycafe/ranking/ranking/RankingPeriod.java`
around lines 12 - 46, RankingPeriod의 start()와 end() 경계 계산을 전용 RankingPeriodTest로
검증하세요. THIS_WEEK, LAST_WEEK, THIS_MONTH, LAST_MONTH, THIS_YEAR를 포함하고 연초·연말·월말 및
주 경계 날짜를 고정된 LocalDate로 테스트해 각 기간의 시작일과 종료일이 정확한지 확인하세요. 기존 RankingServiceTest에만
의존하지 말고 04:00 스터디 날짜 분할 규칙과 관련된 경계 회귀를 직접 검증하세요.
Source: Path instructions
| @Test | ||
| @DisplayName("개인: 하루 16h 캡 + 정렬 + 마스킹 + 내 순위") | ||
| void individual_cap_order_mask() { | ||
| User a = user("a", "김민현", 1, schoolA); | ||
| User b = user("b", "이철수", 1, null); | ||
| record(a, 23 * H); // 23h → 16h 캡 | ||
| record(b, 10 * H); | ||
|
|
||
| IndividualRankingResponse r = rankingService.individual(a.getId(), RankingPeriod.THIS_YEAR); | ||
|
|
||
| assertEquals(2, r.podium().size()); | ||
| assertEquals("김O현(에이대)", r.podium().get(0).displayName()); | ||
| assertEquals(16 * H, r.podium().get(0).seconds()); // 캡 적용 | ||
| assertTrue(r.podium().get(0).isMe()); | ||
| assertEquals("이O수(무소속)", r.podium().get(1).displayName()); | ||
| assertEquals(1, r.myRank().rank()); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
10명 초과 시 podium/list 분할, 주 84h 캡 테스트 보강 권장.
현재 테스트는 THIS_YEAR만 사용해 하루 16h 캡·마스킹·동점 tie-break·최소인원 필터는 잘 검증하지만, RankingService.buildIndividual의 PODIUM=3/LIST_MAX=10 슬라이싱(11명 이상일 때 4~10위만 노출되는지)과 THIS_WEEK 경로에서의 주 84h 캡 적용은 이 파일에서 검증되지 않습니다. 랭킹 캡/표시 규칙이 CLAUDE.md 도메인 규칙의 핵심이므로 회귀 방지를 위해 테스트 추가를 권장합니다.
As per path instructions, 하루 16h/주 84h 랭킹 캡 로직의 정확성을 특히 꼼꼼히 검증해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/test/java/com/studycafe/ranking/ranking/RankingServiceTest.java`
around lines 73 - 89, RankingServiceTest lacks coverage for individual ranking
pagination and the weekly 84-hour cap. Add tests targeting
RankingService.buildIndividual or the existing individual ranking flow that
create at least 11 users and verify only the top 3 podium entries plus ranks
4–10 are exposed, then add a THIS_WEEK case verifying accumulated time is capped
at 84 hours while preserving ordering and displayed values.
Source: Path instructions
Phase 5 — 개인/학교/우리학교 랭킹 API
CLAUDE.md§7 Phase 5 / §5.2·§5.3. 여기서 처음으로 랭킹 캡·이름 마스킹·동점 처리가 적용된다(마이페이지=실제시간과 대비).엔드포인트 (전부 인증 필요,
period기본this_week, 잘못된 값 400)GET /api/rankings/individual?period=this_week|last_week|this_month|last_month|this_year— 포디움(13)/리스트(410)/내 순위GET /api/rankings/school?period=...— 학교 평균 + 활동 인원수(최소 5명 미달 제외)GET /api/rankings/school/mine?period=...— 우리 학교 내부 개인 랭킹(무소속이면available:false)핵심 규칙
StudyTimeAggregator.rankingTotalSeconds재사용. (마이페이지는 캡 미적용이었음.)김O현(학교축약명), 무소속(무소속), 동명이인name_seq>1접미 숫자.NameMasker(순수) + 단위테스트.myRank:null.RankingPeriod).구현 메모
Converter<String,RankingPeriod>→ 잘못된 값이면MethodArgumentTypeMismatchException→ 400 ApiError.검증
김O현(서울대) 57600(23h→16h캡) myRank 1, 학교서울대 · 5명 · 평균 14400, 우리학교 필터,period=nope→ 400.범위 밖(다음)
프론트(Phase 6~9) 또는 Phase 10(04:00 자동마감·경고·Web Push 배치). 인사이트 텍스트·데이터 부족 처리는 이후.
Summary by CodeRabbit
새 기능
개선