Skip to content

[feature] 동아리 추천 로직 개선#660

Merged
Zepelown merged 1 commit intodevelop/befrom
feature/#655-recommend-club-server-MOA-161
Aug 16, 2025
Merged

[feature] 동아리 추천 로직 개선#660
Zepelown merged 1 commit intodevelop/befrom
feature/#655-recommend-club-server-MOA-161

Conversation

@Zepelown
Copy link
Member

@Zepelown Zepelown commented Aug 16, 2025

#️⃣연관된 이슈

#655

📝작업 내용

동아리 추천 로직 개선

  • 6개 추천
  • 그 중 4개 같은 카테고리에서 랜덤(모집중 우선, 없으면 모집마감)
  • 그 중 2개는 전체 랜덤(모집중 우선, 없으면 모집 마감)
  • 만약 같은 카테고리가 4개보다 적으면 나머지는 전체 랜덤으로 채움

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

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

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

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

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

🫡 참고사항

Summary by CodeRabbit

  • New Features
    • 추천 클럽 결과가 최대 6개로 제한되며, 카테고리 내 모집 중(상시/오픈) 클럽을 우선 제공하고 부족분은 무작위 풀에서 보충합니다.
  • Bug Fixes
    • 추천 결과의 중복을 제거하여 동일 클럽이 반복 노출되지 않습니다.
    • 검색/추천 결과가 없을 때 빈 목록을 안정적으로 반환합니다.
    • 태그 기본값을 빈 리스트로 일관화하고, 로고 기본값은 빈 문자열로 유지합니다.
  • Refactor
    • 단계적 선별 로직으로 추천 결과의 일관성과 품질을 개선했습니다.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 16, 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

추천 동아리 조회(searchRecommendClubs)을 단일 쿼리 샘플링에서 단계적 수집 방식으로 변경했다. 카테고리 내 모집 중 우선(최대 4) → 동일 카테고리 보충 → 랜덤 풀에서 우선순위 선별로 총 6개까지 구성한다. 중복 방지와 비모집 보충 로직, 헬퍼 메서드들이 추가되었다.

Changes

Cohort / File(s) Summary
추천 클럽 검색 로직 단계화
backend/src/main/java/moadong/club/repository/ClubSearchRepository.java
searchRecommendClubs를 3단계(카테고리 내 모집중 → 카테고리 내 보충 → 랜덤 풀 우선선별)로 재구성, 총 6개 제한 및 중복 방지 도입
헬퍼 메서드 추가
.../ClubSearchRepository.java
findClubsByCategoryAndState, addClubs, isRecruiting, selectClubsByStatePriority, findRandomClubs 추가
프로젝션 기본값 조정
.../ClubSearchRepository.java
tags 기본값을 빈 문자열에서 빈 리스트(Collections.emptyList())로 변경; 로고 기본값 빈 문자열 유지
임포트 정리
.../ClubSearchRepository.java
java.util.* 와일드카드 임포트로 변경

Sequence Diagram(s)

sequenceDiagram
  actor Client
  participant Repo as ClubSearchRepository

  Client->>Repo: searchRecommendClubs(category, excludeClubId)
  activate Repo
  Repo->>Repo: excludeIds 초기화(add excludeClubId)
  Repo->>Repo: findClubsByCategoryAndState(category, excludeIds, onlyRecruit=true, limit=4)
  Repo->>Repo: addClubs(result, excludeIds, stage1)
  alt result < 4
    Repo->>Repo: findClubsByCategoryAndState(category, excludeIds, onlyRecruit=false, limit=remaining)
    Repo->>Repo: addClubs(result, excludeIds, stage2)
  end
  alt result < 6
    Repo->>Repo: findRandomClubs(excludeIds, sampleSize=10)
    Repo->>Repo: selectClubsByStatePriority(pool, maxCount=6-result)
    Repo->>Repo: addClubs(result, excludeIds, stage3)
  end
  Repo-->>Client: result(최대 6개, 중복 제거)
  deactivate Repo
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
같은 분과에 모집 중인 동아리 상위 5개를 보여준다 (MOA-161) 최대 6개 반환, 비모집 클럽 포함 가능, “상위” 정렬 기준 부재

Possibly related issues

Possibly related PRs

Suggested labels

✨ Feature, 💾 BE

Suggested reviewers

  • PororoAndFriends
  • oesnuj
  • lepitaaar

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ 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/#655-recommend-club-server-MOA-161

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel
Copy link

vercel bot commented Aug 16, 2025

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

Project Deployment Preview Comments Updated (UTC)
moadong Ready Ready Preview Comment Aug 16, 2025 9:06am

@github-actions
Copy link

Test Results

75 tests   75 ✅  2s ⏱️
12 suites   0 💤
12 files     0 ❌

Results for commit c61dc77.

Copy link
Contributor

@lepitaaar lepitaaar left a comment

Choose a reason for hiding this comment

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

LGTM

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: 2

🧹 Nitpick comments (6)
backend/src/main/java/moadong/club/repository/ClubSearchRepository.java (6)

3-3: 와일드카드 import 대신 명시적 import 권장

팀 컨벤션에 따라 다르지만, java.util.* 대신 사용하는 타입만 명시하는 것이 가독성/충돌 방지에 유리합니다. 큰 이슈는 아니므로 필요 시에만 반영하세요.


76-87: remainCount 계산은 result.size() 기준이 더 견고합니다

현재는 remainCount = 4 - categoryClubs.size()로 계산하는데, 일반적으로는 이미 반영된 결과 리스트(result) 기준으로 남은 수량을 계산하는 편이 견고합니다. 향후 중복 필터링이나 쿼리 변경 시에도 안전합니다.

다음과 같이 수정 제안드립니다:

-        int remainCount = maxCategoryCount - categoryClubs.size();
+        int remainCount = maxCategoryCount - result.size();

139-145: 방어 로직: id 누락 시 스킵

예외적인 매핑/프로젝션 이슈로 club.id()가 null이 될 가능성에 대비해, null인 경우를 건너뛰면 안정성이 높아집니다.

         for (ClubSearchResult club : clubs) {
-            if (!excludeIds.contains(club.id())) {
+            if (club.id() == null) {
+                continue;
+            }
+            if (!excludeIds.contains(club.id())) {
                 result.add(club);
                 excludeIds.add(club.id());
             }
         }

93-98: (선택) 랜덤 풀 샘플 크기를 요청 수량에 맞춰 가변화

현재 샘플을 고정 10개로 가져와 우선순위 선별합니다. 모집 중인 대상이 적은 데이터셋에서는 randomNeeded를 충분히 채우지 못할 확률이 커집니다. 요청 수량에 비례해 샘플을 키우면 완성도 향상에 도움이 됩니다.

-            List<ClubSearchResult> randomPool = findRandomClubs(excludeIds, 10);
+            int sampleSize = Math.max(10, randomNeeded * 3);
+            List<ClubSearchResult> randomPool = findRandomClubs(excludeIds, sampleSize);

127-131: tags 기본값 타입 일관성 확인(빈 문자열 vs 빈 리스트)

본 변경에서는 tags의 기본값을 Collections.emptyList()로 투영했는데, searchClubsByKeyword에서는 빈 문자열("")을 사용 중입니다. DTO의 tags 타입이 List<String>이라면 빈 문자열 투영은 매핑 오류를 유발할 수 있습니다. 전역적으로 빈 리스트로 맞추는 것을 권장합니다.

searchClubsByKeyword의 project 단계(참고: 현재 코드는 빈 문자열)도 다음과 같이 맞추는 것을 제안드립니다. 이 수정은 본 코멘트 범위를 벗어난 위치에 있으므로 참고용 예시입니다.

// searchClubsByKeyword 내 project 단계 예시
Aggregation.project("name", "state", "category", "division")
    .and("recruitmentInformation.introduction").as("introduction")
    .and("recruitmentInformation.clubRecruitmentStatus").as("recruitmentStatus")
    .and(ConditionalOperators.ifNull("$recruitmentInformation.logo").then("")).as("logo")
    .and(ConditionalOperators.ifNull("$recruitmentInformation.tags").then(Collections.emptyList())).as("tags");

Also applies to: 192-194


68-76: (선택) category 미지정 시 전체 랜덤만으로 구성하는 빠른 가드 추가

category == null/공백/“all”과 같은 값이 들어올 가능성이 있다면, 1,2단계를 건너뛰고 바로 3단계(전체 랜덤)로 가는 가드를 두면 불필요한 쿼리를 줄일 수 있습니다. 필요 시 말씀 주시면 패치 드리겠습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 ffbae3a and c61dc77.

📒 Files selected for processing (1)
  • backend/src/main/java/moadong/club/repository/ClubSearchRepository.java (2 hunks)
🔇 Additional comments (4)
backend/src/main/java/moadong/club/repository/ClubSearchRepository.java (4)

69-74: excludeIds 세팅 방식 LGTM

중복 방지를 위한 excludeIds 초기화 및 단건 제외 처리 흐름이 명확합니다.


76-78: 요건 정합성 확인: 4+2 구성과 MOA-161(동일 카테고리 5개) 상이

본 로직은 동일 카테고리 최대 4개 + 전체 랜덤 2개(총 6개)인데, 연결 이슈(MOA-161) 체크리스트에는 “동일 카테고리의 모집 중 상위 5개” 노출이 요구사항으로 적혀 있습니다. 최신 기획 기준이 4+2인지, 아니면 5+1 등의 변형이 필요한지 확인 부탁드립니다. 필요 시 maxCategoryCount 또는 totalNeeded 조정으로 쉽게 반영 가능합니다.

Also applies to: 90-91


148-151: 모집 상태에 UPCOMING 포함 여부 확인 필요

현재 우선순위는 ALWAYS/OPEN만 모집 중으로 간주합니다. 검색 조건(getMatchedCriteria)에서는 OPEN 선택 시 ALWAYS/OPEN/UPCOMING까지 포함시키고 있어, 추천에서도 UPCOMING을 모집 중으로 볼지 여부를 기획과 일치시킬 필요가 있습니다.


153-178: 상태 우선 선별 로직 LGTM

풀에서 모집 중 → 비모집 순 선별이 명확합니다. 중복 방지용 ids도 안전 장치로 적절합니다.

Comment on lines +108 to +111
Criteria criteria = Criteria.where("category").is(category)
.and("_id").nin(excludeIds);

if (onlyRecruitAvailable) {
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

중요: 사용 불가 상태(state != AVAILABLE) 클럽이 추천에 포함될 수 있음 + id 별칭을 명시적으로 투영 권장

  • 현재 카테고리 조회에서 state == AVAILABLE 필터가 없습니다. 이렇게 되면 HIDDEN/DELETED 등 비노출 상태의 클럽이 추천에 섞일 수 있습니다. 검색(keyword) 파이프라인에서는 state == AVAILABLE를 적용하고 있어 일관성도 깨집니다.
  • 또한 dedup에 club.id()를 사용하므로 _id 필드가 DTO의 id로 매핑되지 않는 경우를 방지하기 위해 프로젝트 단계에서 명시적으로 id를 투영하는 편이 안전합니다.

다음과 같이 보완을 제안드립니다.

-        Criteria criteria = Criteria.where("category").is(category)
-                .and("_id").nin(excludeIds);
+        Criteria criteria = Criteria.where("state").is(ClubState.AVAILABLE.getName())
+                .and("category").is(category)
+                .and("_id").nin(excludeIds);
         ops.add(
                 Aggregation.project("name", "state", "category", "division")
                         .and("recruitmentInformation.introduction").as("introduction")
                         .and("recruitmentInformation.clubRecruitmentStatus").as("recruitmentStatus")
                         .and(ConditionalOperators.ifNull("$recruitmentInformation.logo").then(""))
                         .as("logo")
                         .and(ConditionalOperators.ifNull("$recruitmentInformation.tags").then(Collections.emptyList()))
                         .as("tags")
+                        .and("_id").as("id")
         );

Also applies to: 119-121, 124-131

Comment on lines +181 to +189
private List<ClubSearchResult> findRandomClubs(Set<String> excludeIds, int sampleSize) {
List<AggregationOperation> ops = new ArrayList<>();
ops.add(Aggregation.match(Criteria.where("_id").nin(excludeIds)));
ops.add(Aggregation.sample((long) sampleSize));

ops.add(
Aggregation.project("name", "state", "category", "division")
.and("recruitmentInformation.introduction").as("introduction")
.and("recruitmentInformation.clubRecruitmentStatus").as("recruitmentStatus")
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

중요: 랜덤 풀에서도 AVAILABLE 상태 필터 누락 + id 투영 명시

findRandomClubs 역시 state == AVAILABLE 조건이 없어 비노출 클럽이 추천될 수 있습니다. 또한 dedup 신뢰성을 위해 id 투영을 명시하는 것이 좋습니다.

-        ops.add(Aggregation.match(Criteria.where("_id").nin(excludeIds)));
+        ops.add(Aggregation.match(new Criteria().andOperator(
+                Criteria.where("state").is(ClubState.AVAILABLE.getName()),
+                Criteria.where("_id").nin(excludeIds)
+        )));
         ops.add(Aggregation.sample((long) sampleSize));
@@
         ops.add(
                 Aggregation.project("name", "state", "category", "division")
                         .and("recruitmentInformation.introduction").as("introduction")
                         .and("recruitmentInformation.clubRecruitmentStatus").as("recruitmentStatus")
                         .and(ConditionalOperators.ifNull("$recruitmentInformation.logo").then(""))
                         .as("logo")
                         .and(ConditionalOperators.ifNull("$recruitmentInformation.tags").then(Collections.emptyList()))
-                        .as("tags")
+                        .as("tags")
+                        .and("_id").as("id")
         );

Also applies to: 187-194

🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/repository/ClubSearchRepository.java
around lines 181-189 (and similarly at 187-194), the aggregation for
findRandomClubs is missing a filter to include only clubs with state ==
AVAILABLE and does not explicitly project the id; add a
Criteria.where("state").is(ClubState.AVAILABLE) (or the appropriate enum/value)
to the Aggregation.match stage and add "_id" to the Aggregation.project call so
the pipeline filters out non-available clubs and returns the id for reliable
deduplication.

@Zepelown Zepelown merged commit f1c6f2d into develop/be Aug 16, 2025
5 checks passed
@Zepelown Zepelown deleted the feature/#655-recommend-club-server-MOA-161 branch October 12, 2025 12:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments