[feature] 동아리 검색 결과에서 분과 순서가 고정된 것에서 분과 순서를 랜덤으로 바꾼다#749
[feature] 동아리 검색 결과에서 분과 순서가 고정된 것에서 분과 순서를 랜덤으로 바꾼다#749Zepelown merged 2 commits intodevelop/befrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Cohort / File(s) | Change Summary |
|---|---|
검색 정렬 로직 업데이트backend/src/main/java/moadong/club/service/ClubSearchService.java |
- 기존의 고정된 분과 순서 대신 실행 시점에 ClubCategory 목록을 섞어 분과 → 우선순위 맵을 생성하고 이를 2차 정렬 키로 사용하도록 변경- 정렬 우선순위: 모집 상태 → 무작위 분과 우선순위 → 이름 - 일부 import 정리 및 로컬 변수( categories, randomCategoryPriorities) 추가 |
테스트 비활성화(주석 처리)backend/src/test/java/moadong/club/service/ClubSearchServiceTest.java |
- 두 개의 테스트 메서드를 주석 처리하여 실행 대상에서 제외(주석으로 정의 유지) - 나머지 테스트는 변경 없음 |
Sequence Diagram(s)
sequenceDiagram
autonumber
actor Client as 클라이언트
participant Service as ClubSearchService
participant Repo as ClubRepository
note over Service: 검색 요청 처리
Client->>Service: searchClubs(query, filters)
Service->>Repo: findClubs(query, filters)
Repo-->>Service: List<Club>
rect rgba(230,240,255,0.6)
note right of Service: 분과 우선순위 생성
Service->>Service: ClubCategory.values() 호출
Service->>Service: 리스트 섞기(shuffle)
Service->>Service: 카테고리→우선순위 Map 생성
end
rect rgba(220,255,230,0.6)
note right of Service: 정렬 적용 (우선순위)
Service->>Service: Comparator 적용:\n1) 모집 상태 우선\n2) 랜덤 카테고리 우선순위\n3) 이름
end
Service-->>Client: 정렬된 결과 반환
Estimated code review effort
🎯 2 (Simple) | ⏱️ ~10 minutes
Possibly related issues
- MOA-238: 동아리 검색 결과에서 분과 순서를 랜덤으로 바꾸는 요구사항 — 본 PR이 해당 요구사항(모집 우선 유지, 분과 랜덤화)을 직접 구현함.
- [feature] MOA-238 동아리 검색 결과에서 분과 순서가 고정된 것에서 분과 순서를 랜덤으로 바꾼다 #747: 분과 순서를 랜덤화하라는 목표과 일치 — 구현 내용과 목적이 동일.
Possibly related PRs
- [fix] 동아리 검색 API #586: 클럽 검색 정렬 관련 변경 이력 — 정렬 로직 및 테스트 변경과 코드 수준 연관성 있음.
Suggested reviewers
- lepitaaar
- PororoAndFriends
Pre-merge checks and finishing touches
❌ Failed checks (2 warnings)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Out of Scope Changes Check | PR에는 목표 기능 구현 외에 ClubSearchServiceTest에서 두 개의 테스트 메서드를 주석 처리해 비활성화한 변경사항이 포함되어 있어 이는 명시된 이슈 목표와 직접적인 관련이 없고 테스트 커버리지 저하를 초래합니다. 또한 와일드카드 import와 static import 도입 등 스타일·포맷 변경이 섞여 있어 기능 변경과 분리되어야 할 성격의 변경입니다. 따라서 일부 변경은 범위를 벗어나며 별도 수정이 필요합니다. | 주석 처리된 테스트는 복원하거나 명시적 @disabled와 이유를 기재해 의도를 분명히 하고 스타일 관련 import 정리는 별도 커밋으로 분리하세요; 또한 무작위 동작 검증을 위해 고정 시드 기반 테스트나 통계적 검증을 추가해 안정성을 확보하시기 바랍니다. | |
| Docstring Coverage | Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. | You can run @coderabbitai generate docstrings to improve docstring coverage. |
✅ Passed checks (3 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title Check | ✅ Passed | PR 제목 '[feature] 동아리 검색 결과에서 분과 순서가 고정된 것에서 분과 순서를 랜덤으로 바꾼다'은 변경의 핵심인 "분과 순서의 랜덤화"와 "모집 중 항목 우선 유지"를 간결하게 반영합니다. 제목은 한 문장으로 명확하며 과도한 잡음이나 모호한 표현이 없습니다. 따라서 제목 체크는 통과로 판단합니다. |
| Linked Issues Check | ✅ Passed | raw_summary와 PR 목적(MOA-238)을 비교한 결과 ClubSearchService에서 분과 목록을 셔플하고 분과별 랜덤 우선순위 맵을 사용하며 정렬 우선순위는 recruitmentStatus(모집중) → 랜덤 분과 → 이름 순으로 구현되어 있어 링크된 이슈의 기술적 요구사항을 충족합니다. 변경은 주로 ClubSearchService.java에 한정되어 있어 목표와 일치합니다. 다만 테스트 파일에서 일부 테스트가 주석 처리된 점은 품질보증 측면에서 보완이 권장됩니다. |
✨ Finishing touches
- 📝 Generate Docstrings
🧪 Generate unit tests
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
feature/#747-random-search-MOA-238
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.
Comment @coderabbitai help to get the list of available commands and usage tips.
Test Results70 tests 67 ✅ 7s ⏱️ Results for commit d1a5cf3. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (5)
backend/src/main/java/moadong/club/service/ClubSearchService.java (5)
3-3: 와일드카드 import 지양 (명시적 import 권장)네임스페이스 충돌/불필요 노출을 줄이기 위해 명시적 import로 교체를 권장합니다.
다음과 같이 변경 제안:
-import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map;
13-14: static 와일드카드 import 제거불필요한 범위 확장을 막기 위해 제거를 권장합니다. (아래
asList호출은Arrays.asList로 바꾸는 제안과 함께 적용)-import static java.util.Arrays.*;
33-36: 분과 무작위화 구현은 의도 충족 (LGTM). asList 호출만 정리 필요기능 요구사항(분과 랜덤화) 충족합니다.
Arrays.asList로 명시 호출해 주세요.
- 페이징/캐시가 있는 경우, 요청마다 순서가 달라지는 것이 UX/통계에 문제 없는지 확인 부탁드립니다.
-List<ClubCategory> categories = new ArrayList<>(asList(ClubCategory.values())); +List<ClubCategory> categories = new ArrayList<>(Arrays.asList(ClubCategory.values()));
38-41: Map 초기 용량 지정 (리사이즈 방지) 및 대안 제안카테고리 개수만큼 초기 용량을 주면 리사이즈를 피할 수 있습니다. (대안:
EnumMap<ClubCategory, Integer>사용)-Map<String, Integer> randomCategoryPriorities = new HashMap<>(); +Map<String, Integer> randomCategoryPriorities = new HashMap<>(categories.size());대안(선호): 키를
String대신ClubCategory로-Map<String, Integer> randomCategoryPriorities = new HashMap<>(); +Map<ClubCategory, Integer> randomCategoryPriorities = new EnumMap<>(ClubCategory.class); - randomCategoryPriorities.put(categories.get(i).name(), i); + randomCategoryPriorities.put(categories.get(i), i);(이 경우 비교 로직에서
String→Enum변환이 필요)
49-53: Locale 의존성 제거 및 공백 처리대소문자 변환 시 시스템 Locale 영향(예: Turkish I) 회피를 위해
Locale.ROOT사용을 권장합니다. 입력 공백도 정리해 두면 안전합니다.- club.category() != null ? club.category().toUpperCase() : null, + club.category() != null ? club.category().trim().toUpperCase(Locale.ROOT) : null,
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
backend/src/main/java/moadong/club/service/ClubSearchService.java(3 hunks)
🔇 Additional comments (1)
backend/src/main/java/moadong/club/service/ClubSearchService.java (1)
46-49: 확인 필요 — 모집상태 우선순위 매핑 및 널 처리
- 확인: enum은 OPEN("모집중")=1, ALWAYS("상시모집")=2, CLOSED("모집마감")=4이고, getPriorityFromString(status)는 fromString(status)를 호출하여 일치하는 enum이 없으면 Integer.MAX_VALUE를 반환합니다.
- 중요한 점: fromString(...)는 rs.name().equalsIgnoreCase(status)로 enum 이름(예: "OPEN")만 비교합니다. recruitmentStatus()가 한국어 라벨("모집중" 등)을 반환하면 매칭 실패 후 Integer.MAX_VALUE로 처리되어 의도와 다른 정렬이 될 수 있습니다 — recruitmentStatus() 반환값이 enum 이름인지 라벨인지 확인하거나 fromString에 라벨 비교를 추가하세요. (참고: backend/src/main/java/moadong/club/enums/ClubRecruitmentStatus.java, backend/src/main/java/moadong/club/service/ClubSearchService.java)
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
backend/src/test/java/moadong/club/service/ClubSearchServiceTest.java(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-01T08:32:09.398Z
Learnt from: Due-IT
PR: Moadong/moadong#329
File: backend/src/main/java/moadong/club/controller/ClubSearchController.java:0-0
Timestamp: 2025-05-01T08:32:09.398Z
Learning: ClubSearchController의 searchClubsByKeyword 메서드와 ClubSearchService의 searchClubsByKeyword 메서드 사이에 파라미터 순서 일치가 중요하다. 현재 컨트롤러의 매개변수 순서는 'keyword, recruitmentStatus, category, division'인 반면, 서비스 메서드의 순서는 'keyword, recruitmentStatus, division, category'이다. 컨트롤러에서 서비스 호출 시 'keyword, recruitmentStatus, division, category' 순서로 전달하면 category와 division 값이 바뀌어 검색 결과가 잘못될 수 있다.
Applied to files:
backend/src/test/java/moadong/club/service/ClubSearchServiceTest.java
🔇 Additional comments (1)
backend/src/test/java/moadong/club/service/ClubSearchServiceTest.java (1)
57-74: 주석 처리된 테스트 재활성화하세요 — 파라미터 순서 일관성 확인됨backend/src/test/java/moadong/club/service/ClubSearchServiceTest.java (57–74) 에 주석 처리된 테스트는 랜덤 정렬과 무관하므로 주석을 해제해 재활성화하세요.
검증 결과: Controller(backend/src/main/java/moadong/club/controller/ClubSearchController.java), Service(backend/src/main/java/moadong/club/service/ClubSearchService.java), Repository(backend/src/main/java/moadong/club/repository/ClubSearchRepository.java) 모두 searchClubsByKeyword 파라미터 순서가 (keyword, recruitmentStatus, division, category)로 일치합니다.
| // @Test | ||
| // void 모집상태가_같다면_카테고리순으로_정렬하고_카테고리도_같다면_이름순으로_반환한다() { | ||
| // // given | ||
| // String keyword = "동아리"; | ||
| // String recruitmentStatus = "all"; | ||
| // String division = "all"; | ||
| // String category = "all"; // 전체 카테고리 | ||
| // | ||
| // ClubSearchResult club1 = ClubSearchResult.builder().name("club1").recruitmentStatus("OPEN") | ||
| // .division("중동").category("봉사").build(); | ||
| // ClubSearchResult club2 = ClubSearchResult.builder().name("club2").recruitmentStatus("OPEN") | ||
| // .division("중동").category("종교").build(); | ||
| // ClubSearchResult club3 = ClubSearchResult.builder().name("club3").recruitmentStatus("OPEN") | ||
| // .division("중동").category("종교").build(); | ||
| // | ||
| // List<ClubSearchResult> unsorted = List.of(club3, club2, club1); | ||
| // | ||
| // when(clubSearchRepository.searchClubsByKeyword(keyword, recruitmentStatus, division, category)) | ||
| // .thenReturn(unsorted); | ||
| // | ||
| // // when | ||
| // ClubSearchResponse response = clubSearchService.searchClubsByKeyword(keyword, recruitmentStatus, division, category); | ||
| // | ||
| // // then | ||
| // List<ClubSearchResult> sorted = response.clubs(); | ||
| // assertEquals("club1", sorted.get(0).name()); | ||
| // assertEquals("club2", sorted.get(1).name()); | ||
| // assertEquals("club3", sorted.get(2).name()); | ||
| // } | ||
|
|
There was a problem hiding this comment.
랜덤 카테고리 정렬로 기존 기준(카테고리→이름) 검증은 부적합 — 랜덤성에 독립적인 불변 규칙으로 테스트 교체 권장
카테고리 우선순위가 매호출 랜덤이면 카테고리 간 상대 순서를 단정할 수 없습니다. 대신 “같은 모집상태·같은 카테고리 내부에서는 이름 오름차순” 같은 불변 규칙을 검증하세요. 아래처럼 테스트를 교체하면 랜덤성에 영향받지 않으면서 3차 정렬(이름)을 보장합니다.
-// @Test
-// void 모집상태가_같다면_카테고리순으로_정렬하고_카테고리도_같다면_이름순으로_반환한다() {
-// // given
-// String keyword = "동아리";
-// String recruitmentStatus = "all";
-// String division = "all";
-// String category = "all"; // 전체 카테고리
-//
-// ClubSearchResult club1 = ClubSearchResult.builder().name("club1").recruitmentStatus("OPEN")
-// .division("중동").category("봉사").build();
-// ClubSearchResult club2 = ClubSearchResult.builder().name("club2").recruitmentStatus("OPEN")
-// .division("중동").category("종교").build();
-// ClubSearchResult club3 = ClubSearchResult.builder().name("club3").recruitmentStatus("OPEN")
-// .division("중동").category("종교").build();
-//
-// List<ClubSearchResult> unsorted = List.of(club3, club2, club1);
-//
-// when(clubSearchRepository.searchClubsByKeyword(keyword, recruitmentStatus, division, category))
-// .thenReturn(unsorted);
-//
-// // when
-// ClubSearchResponse response = clubSearchService.searchClubsByKeyword(keyword, recruitmentStatus, division, category);
-//
-// // then
-// List<ClubSearchResult> sorted = response.clubs();
-// assertEquals("club1", sorted.get(0).name());
-// assertEquals("club2", sorted.get(1).name());
-// assertEquals("club3", sorted.get(2).name());
-// }
+ @Test
+ void 같은_모집상태이고_같은_카테고리라면_이름순으로_반환한다() {
+ // given
+ String keyword = "동아리";
+ String recruitmentStatus = "all";
+ String division = "all";
+ String category = "all";
+
+ ClubSearchResult b = ClubSearchResult.builder().name("clubB").recruitmentStatus("OPEN")
+ .division("중동").category("종교").build();
+ ClubSearchResult a = ClubSearchResult.builder().name("clubA").recruitmentStatus("OPEN")
+ .division("중동").category("종교").build();
+ ClubSearchResult c = ClubSearchResult.builder().name("clubC").recruitmentStatus("OPEN")
+ .division("중동").category("종교").build();
+
+ List<ClubSearchResult> unsorted = List.of(b, a, c);
+ when(clubSearchRepository.searchClubsByKeyword(keyword, recruitmentStatus, division, category))
+ .thenReturn(unsorted);
+
+ // when
+ ClubSearchResponse response = clubSearchService.searchClubsByKeyword(keyword, recruitmentStatus, division, category);
+
+ // then
+ List<ClubSearchResult> sorted = response.clubs();
+ assertEquals("clubA", sorted.get(0).name());
+ assertEquals("clubB", sorted.get(1).name());
+ assertEquals("clubC", sorted.get(2).name());
+ }추가 권장사항:
- 서비스에서 랜덤 소스를 주입(예: Supplier<Map<Category,Integer>> 또는 Random) 가능하게 하면 테스트에서 고정 seed로 검증이 가능합니다(비플래키). 원하시면 DI-friendly 구조로 제안 드리겠습니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // @Test | |
| // void 모집상태가_같다면_카테고리순으로_정렬하고_카테고리도_같다면_이름순으로_반환한다() { | |
| // // given | |
| // String keyword = "동아리"; | |
| // String recruitmentStatus = "all"; | |
| // String division = "all"; | |
| // String category = "all"; // 전체 카테고리 | |
| // | |
| // ClubSearchResult club1 = ClubSearchResult.builder().name("club1").recruitmentStatus("OPEN") | |
| // .division("중동").category("봉사").build(); | |
| // ClubSearchResult club2 = ClubSearchResult.builder().name("club2").recruitmentStatus("OPEN") | |
| // .division("중동").category("종교").build(); | |
| // ClubSearchResult club3 = ClubSearchResult.builder().name("club3").recruitmentStatus("OPEN") | |
| // .division("중동").category("종교").build(); | |
| // | |
| // List<ClubSearchResult> unsorted = List.of(club3, club2, club1); | |
| // | |
| // when(clubSearchRepository.searchClubsByKeyword(keyword, recruitmentStatus, division, category)) | |
| // .thenReturn(unsorted); | |
| // | |
| // // when | |
| // ClubSearchResponse response = clubSearchService.searchClubsByKeyword(keyword, recruitmentStatus, division, category); | |
| // | |
| // // then | |
| // List<ClubSearchResult> sorted = response.clubs(); | |
| // assertEquals("club1", sorted.get(0).name()); | |
| // assertEquals("club2", sorted.get(1).name()); | |
| // assertEquals("club3", sorted.get(2).name()); | |
| // } | |
| @Test | |
| void 같은_모집상태이고_같은_카테고리라면_이름순으로_반환한다() { | |
| // given | |
| String keyword = "동아리"; | |
| String recruitmentStatus = "all"; | |
| String division = "all"; | |
| String category = "all"; | |
| ClubSearchResult b = ClubSearchResult.builder().name("clubB").recruitmentStatus("OPEN") | |
| .division("중동").category("종교").build(); | |
| ClubSearchResult a = ClubSearchResult.builder().name("clubA").recruitmentStatus("OPEN") | |
| .division("중동").category("종교").build(); | |
| ClubSearchResult c = ClubSearchResult.builder().name("clubC").recruitmentStatus("OPEN") | |
| .division("중동").category("종교").build(); | |
| List<ClubSearchResult> unsorted = List.of(b, a, c); | |
| when(clubSearchRepository.searchClubsByKeyword(keyword, recruitmentStatus, division, category)) | |
| .thenReturn(unsorted); | |
| // when | |
| ClubSearchResponse response = clubSearchService.searchClubsByKeyword(keyword, recruitmentStatus, division, category); | |
| // then | |
| List<ClubSearchResult> sorted = response.clubs(); | |
| assertEquals("clubA", sorted.get(0).name()); | |
| assertEquals("clubB", sorted.get(1).name()); | |
| assertEquals("clubC", sorted.get(2).name()); | |
| } |
🤖 Prompt for AI Agents
In backend/src/test/java/moadong/club/service/ClubSearchServiceTest.java around
lines 75 to 104, the commented test tries to assert global category ordering but
the service applies a random category priority so the test is flaky; replace it
with a deterministic assertion that when recruitmentStatus and category are the
same, results are ordered by name ascending (i.e., build test data with
identical recruitmentStatus and category but different names, mock the
repository to return them in an unsorted order, call searchClubsByKeyword and
assert the returned list is sorted by name), and optionally make randomness
testable by injecting a deterministic random/source into the service if you need
to assert category ordering in future.
alsdddk
left a comment
There was a problem hiding this comment.
수고하셨습니다! 너무 늦게 확인했네요ㅠ 다음에는 더 빨리 확인하겠습니다
#️⃣연관된 이슈
#747
📝작업 내용
기존에 고정된 분과 순서대로 정렬되는 현상을 랜덤으로 적용하여 기존에 하위 정렬이었던 분과가 앞에 올 수 있도록 하였습니다.
중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit
New Features
Tests