Skip to content

Comments

[feature] 동아리 검색 결과에서 분과 순서가 고정된 것에서 분과 순서를 랜덤으로 바꾼다#749

Merged
Zepelown merged 2 commits intodevelop/befrom
feature/#747-random-search-MOA-238
Sep 23, 2025
Merged

[feature] 동아리 검색 결과에서 분과 순서가 고정된 것에서 분과 순서를 랜덤으로 바꾼다#749
Zepelown merged 2 commits intodevelop/befrom
feature/#747-random-search-MOA-238

Conversation

@Zepelown
Copy link
Member

@Zepelown Zepelown commented Sep 20, 2025

#️⃣연관된 이슈

#747

📝작업 내용

기존에 고정된 분과 순서대로 정렬되는 현상을 랜덤으로 적용하여 기존에 하위 정렬이었던 분과가 앞에 올 수 있도록 하였습니다.

  • 봉사,종교 .. 고정된 순서에서 랜덤 순서로 변경합니다.
  • 단, 모집 중이 앞인 건 여전히 적용합니다.

중점적으로 리뷰받고 싶은 부분(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

논의하고 싶은 부분(선택)

논의하고 싶은 부분이 있다면 작성해주세요.

🫡 참고사항

Summary by CodeRabbit

  • New Features

    • 클럽 검색 결과 정렬을 개선했습니다: 모집 상태 우선 정렬은 유지하면서, 동일 상태 내에서는 카테고리 우선순위를 무작위로 적용하고 마지막으로 이름순 정렬하여 노출 편중을 완화하고 결과 다양성을 높였습니다.
  • Tests

    • 일부 정렬 관련 테스트가 실행 대상에서 제외되어 테스트 범위가 축소되었습니다.

@vercel
Copy link

vercel bot commented Sep 20, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
moadong Ready Ready Preview Comment Sep 20, 2025 1:16pm

@Zepelown Zepelown added 📬 API 서버 API 통신 작업 💾 BE Backend labels Sep 20, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 20, 2025

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

동아리 검색 정렬 로직을 변경했다: 모집 상태 우선 정렬은 유지하면서 분과(category) 정렬을 실행 시 무작위 우선순위를 부여해 2차 정렬로 사용하고, 마지막으로 이름으로 정렬한다. 테스트 몇 건이 주석 처리되었다.

Changes

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: 정렬된 결과 반환
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • lepitaaar
  • PororoAndFriends

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning PR에는 목표 기능 구현 외에 ClubSearchServiceTest에서 두 개의 테스트 메서드를 주석 처리해 비활성화한 변경사항이 포함되어 있어 이는 명시된 이슈 목표와 직접적인 관련이 없고 테스트 커버리지 저하를 초래합니다. 또한 와일드카드 import와 static import 도입 등 스타일·포맷 변경이 섞여 있어 기능 변경과 분리되어야 할 성격의 변경입니다. 따라서 일부 변경은 범위를 벗어나며 별도 수정이 필요합니다. 주석 처리된 테스트는 복원하거나 명시적 @disabled와 이유를 기재해 의도를 분명히 하고 스타일 관련 import 정리는 별도 커밋으로 분리하세요; 또한 무작위 동작 검증을 위해 고정 시드 기반 테스트나 통계적 검증을 추가해 안정성을 확보하시기 바랍니다.
Docstring Coverage ⚠️ Warning 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.

❤️ Share

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

@github-actions
Copy link

github-actions bot commented Sep 20, 2025

Test Results

70 tests   67 ✅  7s ⏱️
11 suites   3 💤
11 files     0 ❌

Results for commit d1a5cf3.

♻️ This comment has been updated with latest results.

Copy link
Contributor

@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: 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);

(이 경우 비교 로직에서 StringEnum 변환이 필요)


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.

📥 Commits

Reviewing files that changed from the base of the PR and between b310982 and 926e763.

📒 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)

@PororoAndFriends PororoAndFriends self-requested a review September 20, 2025 13:03
Copy link
Collaborator

@PororoAndFriends PororoAndFriends left a comment

Choose a reason for hiding this comment

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

고생하셨습니당

Copy link
Contributor

@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

📜 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 926e763 and d1a5cf3.

📒 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)로 일치합니다.

Comment on lines +75 to 104
// @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());
// }

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

랜덤 카테고리 정렬로 기존 기준(카테고리→이름) 검증은 부적합 — 랜덤성에 독립적인 불변 규칙으로 테스트 교체 권장

카테고리 우선순위가 매호출 랜덤이면 카테고리 간 상대 순서를 단정할 수 없습니다. 대신 “같은 모집상태·같은 카테고리 내부에서는 이름 오름차순” 같은 불변 규칙을 검증하세요. 아래처럼 테스트를 교체하면 랜덤성에 영향받지 않으면서 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.

Suggested change
// @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.

Copy link
Collaborator

@alsdddk alsdddk left a comment

Choose a reason for hiding this comment

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

수고하셨습니다! 너무 늦게 확인했네요ㅠ 다음에는 더 빨리 확인하겠습니다

@Zepelown Zepelown merged commit 1f9f275 into develop/be Sep 23, 2025
5 checks passed
@lepitaaar lepitaaar deleted the feature/#747-random-search-MOA-238 branch October 21, 2025 08:08
@coderabbitai coderabbitai bot mentioned this pull request Jan 25, 2026
12 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📬 API 서버 API 통신 작업 💾 BE Backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants