Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,13 @@ public ResponseEntity<BaseResponse<ClubImageResponse>> updateClubImage(

@GetMapping
@Operation(summary = "모든 동호회 조회 (무한스크롤)",
description = "커서 기반 무한스크롤 방식으로 동호회 목록을 조회합니다.")
description = "커서 기반 무한스크롤 방식으로 동호회 목록을 조회합니다. 유저의 관심 종목과 일치하는 동호회가 우선 노출됩니다.")
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());
Comment on lines 67 to +72
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).

return ResponseEntity.ok(
BaseResponse.success("동호회 목록 조회 성공", response)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public interface ClubService {

ClubDetailResponse getClub(Long clubId); // 동호회 개별 조회

ClubScrollResponse getClubsByScroll(Long cursor, int size); // 동호회 전체 조회 (무한 스크롤)
ClubScrollResponse getClubsByScroll(Long cursor, int size, Long userId); // 동호회 전체 조회 (무한 스크롤)

ClubScrollResponse getMyClubsByScroll(Long cursor, int size, User user); // 내가 가입한 동호회 조회 (무한 스크롤)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,19 +129,26 @@ public ClubDetailResponse getClub(Long clubId) {
}
@Override
@Transactional(readOnly = true)
public ClubScrollResponse getClubsByScroll(Long cursor, int size) {
public ClubScrollResponse getClubsByScroll(Long cursor, int size, Long userId) {

// +1 조회해서 다음 페이지 존재 여부 판단
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));
Comment on lines 134 to +150
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.


List<ClubListItemResponse> items = clubs.stream()
.map(club -> {
int memberCount = clubMemberRepository.countByClubId(club.getId());
Expand Down