Skip to content

✨ Feat: 동호회 목록 조회 시 유저 관심 종목 기반 우선 정렬 적용#76

Open
angoroa wants to merge 1 commit intodevelopfrom
feat/match-recommendation
Open

✨ Feat: 동호회 목록 조회 시 유저 관심 종목 기반 우선 정렬 적용#76
angoroa wants to merge 1 commit intodevelopfrom
feat/match-recommendation

Conversation

@angoroa
Copy link
Member

@angoroa angoroa commented Mar 12, 2026

#️⃣ Issue Number

📝 요약(Summary)

  • 동호회 목록 조회(GET /api/clubs) 시 로그인한 유저의 관심 종목(interestType)과 일치하는 동호회를 상단에 우선 노출하도록 개선
    기존에는 단순 id 내림차순 정렬이었으나, 관심 종목 기반 정렬을 추가
  • 커서 기반 무한스크롤 구조는 유지

🛠️ PR 유형

어떤 변경 사항이 있나요?

  • 새로운 기능 추가
  • 버그 수정
  • CSS 등 사용자 UI 디자인 변경
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 테스트 추가, 테스트 리팩토링
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

📸스크린샷 (선택)

💬 공유사항 to 리뷰어

  • 추후 데이터가 많아지면 DB 레벨 정렬 + 페이지네이션 재설계로 전환을 고려해볼 수 있을 듯?

✅ PR Checklist

PR이 다음 요구 사항을 충족하는지 확인하세요.

  • 커밋 메시지 컨벤션에 맞게 작성했습니다.
  • 변경 사항에 대한 테스트를 했습니다.(버그 수정/기능에 대한 테스트).

Summary by CodeRabbit

  • 새로운 기능
    • 사용자의 관심사를 기반으로 클럽 목록을 개인화하여 제공하는 기능이 추가되었습니다. 이제 당신의 관심사와 일치하는 클럽들이 우선적으로 표시됩니다.

@angoroa angoroa linked an issue Mar 12, 2026 that may be closed by this pull request
1 task
@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

요약 (Walkthrough)

인증된 사용자의 ID를 컨트롤러에서 추출하여 서비스 계층으로 전달하고, 사용자의 관심 종목과 일치하는 동호회를 우선적으로 정렬하는 로직을 추가하여 동호회 목록 조회 엔드포인트에 사용자 맞춤형 개인화 기능을 구현합니다.

변경 사항 (Changes)

집합 / 파일 요약
컨트롤러 인증 통합
src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java
getClubs 엔드포인트가 @AuthenticationPrincipal UserAuthInfo userAuthInfo 파라미터를 추가받아 인증된 사용자 정보를 컨트롤러에서 처리하고, clubService.getClubsByScroll() 호출 시 사용자 ID를 전달하도록 업데이트됨
서비스 계층 메서드 서명 확장
src/main/java/com/be/sportizebe/domain/club/service/ClubService.java, src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java
getClubsByScroll 메서드의 서명이 (Long cursor, int size)에서 (Long cursor, int size, Long userId)로 변경되며, 구현부에서 사용자의 관심 종목을 조회하여 매칭되는 동호회를 상위에 우선 정렬하는 로직 추가

시퀀스 다이어그램 (Sequence Diagram)

sequenceDiagram
    actor Client
    participant Controller as ClubController
    participant Service as ClubService
    participant UserService as UserService
    participant Repository as ClubRepository
    
    Client->>Controller: getClubs(cursor, size)<br/>with Authentication
    Controller->>Controller: Extract userId from<br/>@AuthenticationPrincipal
    Controller->>Service: getClubsByScroll(cursor, size, userId)
    Service->>UserService: Get user interest types
    UserService-->>Service: Return user interests
    Service->>Repository: Query clubs by cursor & size
    Repository-->>Service: Return club entities
    Service->>Service: Sort clubs by<br/>interest match
    Service-->>Controller: Return ClubScrollResponse
    Controller-->>Client: Return BaseResponse<br/>with sorted clubs
Loading

코드 검토 소요 시간 (Estimated Code Review Effort)

🎯 3 (Moderate) | ⏱️ ~20 분

관련 가능 PR (Possibly Related PRs)

토끼의 시 (Poem)

🐰✨ 관심사로 동호회를 정렬하고,
사용자 맞춤 추천으로 채워진 목록,
userId 하나가 마법을 일으키네!
더 나은 만남을 찾는 여정,
토끼도 응원한다고 깡총깡총 🐇💫

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 PR의 주요 변경사항을 명확하게 요약하고 있으며, 사용자 관심 종목 기반 동호회 목록 우선 정렬이라는 핵심 기능을 구체적으로 표현하고 있습니다.
Linked Issues check ✅ Passed PR에서 구현한 변경사항이 연결된 이슈 #75의 요구사항을 충족합니다. 사용자 관심 종목 기반 동호회 우선 정렬 로직이 서비스 계층에 추가되었으며, 인증된 사용자 정보를 활용하여 목록을 조회합니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 연결된 이슈 #75와 PR 목표(사용자 관심 종목 기반 동호회 우선 정렬)와 직접 관련이 있으며, 범위를 벗어난 변경사항이 없습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/match-recommendation

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.

Copy link

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java (1)

149-161: ⚠️ Potential issue | 🔴 Critical

정렬 후 nextCursor 를 계산하면 무한스크롤이 깨집니다.

여기서는 관심사 우선 정렬이 끝난 뒤 마지막 항목의 clubId 를 다음 커서로 사용하고 있습니다. 그러면 현재 페이지 안에 더 작은 id 의 클럽이 앞쪽으로 이동한 경우, 다음 요청에서 중복 조회되거나 일부 항목이 건너뛰어집니다. 커서는 정렬 전 id DESC 기준 페이지 슬라이스의 마지막 Club.id 로 계산하고, 화면 노출용 순서만 별도 리스트에서 재정렬해야 합니다.

수정 예시
-        List<Club> clubs = new java.util.ArrayList<>(clubRepository.findClubsByCursor(cursor, pageable));
+        List<Club> clubs = new java.util.ArrayList<>(clubRepository.findClubsByCursor(cursor, pageable));

         boolean hasNext = clubs.size() > size;

         if (hasNext) {
             clubs = clubs.subList(0, size);
         }
+
+        Long nextCursor = clubs.isEmpty()
+                ? null
+                : clubs.get(clubs.size() - 1).getId();

         // 유저 관심 종목 조회 후 일치하는 동호회 상단 정렬
         List<com.be.sportizebe.common.enums.SportType> interestTypes = userRepository.findById(userId)
                 .map(user -> user.getInterestType())
                 .orElse(java.util.Collections.emptyList());

         clubs.sort(java.util.Comparator.comparingInt(club ->
                 interestTypes.contains(club.getClubType()) ? 0 : 1));

         List<ClubListItemResponse> items = clubs.stream()
                 .map(club -> {
                     int memberCount = clubMemberRepository.countByClubId(club.getId());
                     return ClubListItemResponse.from(club, memberCount);
                 })
                 .toList();
-
-        Long nextCursor = items.isEmpty()
-                ? null
-                : items.get(items.size() - 1).clubId();

         return new ClubScrollResponse(items, nextCursor, hasNext);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java`
around lines 149 - 161, Compute nextCursor from the original page slice before
reordering by interest priority: capture the last Club.id from the unsorted
clubs list (the slice returned by the repository which is ordered by id DESC)
and assign that to nextCursor, then perform the interestTypes-based sort and map
to ClubListItemResponse; update references in ClubServiceImpl (variables: clubs,
nextCursor, items) so nextCursor uses clubs.get(...).getId() before the
Comparator.sort is applied.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java`:
- Around line 67-72: The public GET endpoint ClubController.getClubs currently
calls userAuthInfo.getId() which can be null for unauthenticated requests; make
this null-safe by checking userAuthInfo before calling getId() and pass a
nullable Long (e.g., userId = userAuthInfo != null ? userAuthInfo.getId() :
null) into clubService.getClubsByScroll, and update the service method
signature/handling (clubService.getClubsByScroll) to accept and treat a null
userId as an anonymous request (use non-personalized sorting/behavior).
Alternatively, if you intend to require authentication, lock down the endpoint
in SecurityConfig instead (but do not leave the direct getId() call unchanged).

In `@src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java`:
- Around line 134-150: Current implementation in ClubServiceImpl sorts
interestTypes in-memory after calling findClubsByCursor, so DB paging
(findClubsByCursor in ClubRepository) still slices by id DESC and can omit
interest-matching clubs from the page; fix by pushing the interest-priority
ordering into the repository query or by changing the cursor to include the
match flag: modify ClubRepository.findClubsByCursor (or add a new method like
findClubsByCursorWithInterest) to accept the user's interestTypes or userId and
apply ORDER BY (CASE WHEN club_type IN (:interestTypes) THEN 0 ELSE 1 END), id
DESC (or equivalent JPQL/SQL), or redesign the cursor to be (isMatch, id) so
paging respects interest matching; update ClubServiceImpl to call the new
repository method instead of sorting post-fetch.

---

Outside diff comments:
In `@src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java`:
- Around line 149-161: Compute nextCursor from the original page slice before
reordering by interest priority: capture the last Club.id from the unsorted
clubs list (the slice returned by the repository which is ordered by id DESC)
and assign that to nextCursor, then perform the interestTypes-based sort and map
to ClubListItemResponse; update references in ClubServiceImpl (variables: clubs,
nextCursor, items) so nextCursor uses clubs.get(...).getId() before the
Comparator.sort is applied.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 56faf46b-ad1a-4b6a-8991-407929491b24

📥 Commits

Reviewing files that changed from the base of the PR and between 6fbceb5 and 4558d5c.

📒 Files selected for processing (3)
  • src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java
  • src/main/java/com/be/sportizebe/domain/club/service/ClubService.java
  • src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java

Comment on lines 67 to +72
public ResponseEntity<BaseResponse<ClubScrollResponse>> getClubs(
@RequestParam(required = false) Long cursor,
@RequestParam(defaultValue = "20") int size
@RequestParam(defaultValue = "20") int size,
@AuthenticationPrincipal UserAuthInfo userAuthInfo
) {
ClubScrollResponse response = clubService.getClubsByScroll(cursor, size);
ClubScrollResponse response = clubService.getClubsByScroll(cursor, size, userAuthInfo.getId());
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

공개 조회 엔드포인트에서 바로 NPE 가 날 수 있습니다.

GET /api/**src/main/java/com/be/sportizebe/global/security/SecurityConfig.java:36-44 에서 permitAll() 이고, src/main/java/com/be/sportizebe/global/jwt/JwtAuthenticationFilter.java:32-72 도 토큰이 없으면 principal 을 채우지 않습니다. 그래서 여기의 userAuthInfo 는 null 일 수 있는데, 바로 getId() 를 호출해서 비인증 조회가 500 으로 끝납니다. 이 엔드포인트를 인증 필수로 바꾸거나, principal 이 없으면 비개인화 정렬로 내려가도록 controller/service 를 함께 null-safe 하게 처리해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java`
around lines 67 - 72, The public GET endpoint ClubController.getClubs currently
calls userAuthInfo.getId() which can be null for unauthenticated requests; make
this null-safe by checking userAuthInfo before calling getId() and pass a
nullable Long (e.g., userId = userAuthInfo != null ? userAuthInfo.getId() :
null) into clubService.getClubsByScroll, and update the service method
signature/handling (clubService.getClubsByScroll) to accept and treat a null
userId as an anonymous request (use non-personalized sorting/behavior).
Alternatively, if you intend to require authentication, lock down the endpoint
in SecurityConfig instead (but do not leave the direct getId() call unchanged).

Comment on lines 134 to +150
Pageable pageable = PageRequest.of(0, size + 1);

List<Club> clubs = clubRepository.findClubsByCursor(cursor, pageable);
List<Club> clubs = new java.util.ArrayList<>(clubRepository.findClubsByCursor(cursor, pageable));

boolean hasNext = clubs.size() > size;

if (hasNext) {
clubs = clubs.subList(0, size);
}

// 유저 관심 종목 조회 후 일치하는 동호회 상단 정렬
List<com.be.sportizebe.common.enums.SportType> interestTypes = userRepository.findById(userId)
.map(user -> user.getInterestType())
.orElse(java.util.Collections.emptyList());

clubs.sort(java.util.Comparator.comparingInt(club ->
interestTypes.contains(club.getClubType()) ? 0 : 1));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

현재 로직은 “관심 종목 우선 정렬”을 전체 목록 기준으로 보장하지 못합니다.

findClubsByCursorsrc/main/java/com/be/sportizebe/domain/club/repository/ClubRepository.java:14-19 에서 먼저 id DESC 로 페이지를 잘라 오고, 그 결과 집합 안에서만 다시 정렬합니다. 그래서 관심 종목 클럽이 더 오래된 데이터에 있으면 첫 페이지로 끌어올릴 수 없습니다. 요구사항이 실제 정렬 기준이라면 정렬을 DB 쿼리 단계로 내리거나, 커서 자체를 (관심사 매치 여부, id) 기준으로 다시 설계해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java`
around lines 134 - 150, Current implementation in ClubServiceImpl sorts
interestTypes in-memory after calling findClubsByCursor, so DB paging
(findClubsByCursor in ClubRepository) still slices by id DESC and can omit
interest-matching clubs from the page; fix by pushing the interest-priority
ordering into the repository query or by changing the cursor to include the
match flag: modify ClubRepository.findClubsByCursor (or add a new method like
findClubsByCursorWithInterest) to accept the user's interestTypes or userId and
apply ORDER BY (CASE WHEN club_type IN (:interestTypes) THEN 0 ELSE 1 END), id
DESC (or equivalent JPQL/SQL), or redesign the cursor to be (isMatch, id) so
paging respects interest matching; update ClubServiceImpl to call the new
repository method instead of sorting post-fetch.

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.

✨Feat: 사용자 관심 종목 기반 동호회 목록 조회 개선

1 participant