Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ src/main/resources/*.yml

# 기타
CLAUDE.md
Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.be.sportizebe.domain.match.dto.request.MatchNearRequest;
import com.be.sportizebe.domain.match.dto.response.MatchDetailResponse;
import com.be.sportizebe.domain.match.dto.response.MatchNearResponse;
import com.be.sportizebe.domain.match.dto.response.MatchParticipantResponse;
import com.be.sportizebe.domain.match.dto.response.MatchResponse;
import com.be.sportizebe.domain.match.dto.response.MyMatchResponse;
import com.be.sportizebe.domain.match.service.MatchService;
import com.be.sportizebe.global.cache.dto.UserAuthInfo;
Expand All @@ -23,6 +25,7 @@
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
Expand Down Expand Up @@ -75,7 +78,25 @@ public ResponseEntity<BaseResponse<List<MyMatchResponse>>> getMyMatches(
return ResponseEntity.ok(BaseResponse.success("내 매칭 목록 조회 성공", response));
}

@Operation(summary = "내 주변 매칭 목록 조회", description = "매칭상태가 OPEN인 매칭들만 보여준다.")
@Operation(summary = "매칭 참가자 목록 조회", description = "JOINED 상태인 참가자만 반환한다.")
@GetMapping("/{matchId}/participants")
public ResponseEntity<BaseResponse<List<MatchParticipantResponse>>> getMatchParticipants(
@PathVariable Long matchId
) {
List<MatchParticipantResponse> response = matchService.getMatchParticipants(matchId);
return ResponseEntity.ok(BaseResponse.success("매칭 참가자 목록 조회 성공", response));
}

@Operation(summary = "구장 기반 매칭 목록 조회", description = "해당 구장의 OPEN/FULL 상태이며 아직 시작 전인 매칭을 반환한다.")
@GetMapping
public ResponseEntity<BaseResponse<List<MatchResponse>>> getMatchesByFacility(
@RequestParam Long facilityId
) {
List<MatchResponse> response = matchService.getMatchesByFacility(facilityId);
return ResponseEntity.ok(BaseResponse.success("구장 매칭 목록 조회 성공", response));
}

@Operation(summary = "내 주변 매칭 목록 조회", description = "매칭상태가 OPEN인 매칭들만 보여준다.")
@GetMapping("/near")
public ResponseEntity<BaseResponse<List<MatchNearResponse>>> getNearMatches(
@ParameterObject @Valid @ModelAttribute MatchNearRequest request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@ public interface MatchParticipantRepository extends JpaRepository<MatchParticipa
@Query("SELECT mp FROM MatchParticipant mp JOIN FETCH mp.matchRoom WHERE mp.user.id = :userId AND mp.status = :status ORDER BY mp.joinedAt DESC")
List<MatchParticipant> findAllByUserIdAndStatusFetch(@Param("userId") Long userId, @Param("status") MatchParticipantStatus status);

// 매칭 참가자 목록 (N+1 방지 fetch join)
@Query("SELECT mp FROM MatchParticipant mp JOIN FETCH mp.user WHERE mp.matchRoom.id = :matchId AND mp.status = :status")
List<MatchParticipant> findAllByMatchRoomIdAndStatusFetch(@Param("matchId") Long matchId, @Param("status") MatchParticipantStatus status);
Comment on lines +35 to +37
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

참가자 목록 정렬 기준을 쿼리에 고정해 주세요.

현재 쿼리는 ORDER BY가 없어 응답 순서가 DB 실행 계획에 따라 바뀔 수 있습니다. 사용자 목록을 그대로 노출하는 API라면 가입 순서 등 의도한 기준을 명시하는 편이 안전합니다.

정렬 기준을 명시하는 예시
-    `@Query`("SELECT mp FROM MatchParticipant mp JOIN FETCH mp.user WHERE mp.matchRoom.id = :matchId AND mp.status = :status")
+    `@Query`("SELECT mp FROM MatchParticipant mp JOIN FETCH mp.user WHERE mp.matchRoom.id = :matchId AND mp.status = :status ORDER BY mp.joinedAt ASC")
     List<MatchParticipant> findAllByMatchRoomIdAndStatusFetch(`@Param`("matchId") Long matchId, `@Param`("status") MatchParticipantStatus status);
🤖 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/match/repository/MatchParticipantRepository.java`
around lines 35 - 37, The query in
MatchParticipantRepository.findAllByMatchRoomIdAndStatusFetch lacks an ORDER BY
so result order can vary; update the JPQL to include a deterministic ordering
(e.g., append "ORDER BY mp.createdAt ASC" or "ORDER BY mp.id ASC" depending on
which field represents join order) so participants are consistently returned in
the intended sequence when selecting MatchParticipant JOIN FETCH mp.user WHERE
mp.matchRoom.id = :matchId AND mp.status = :status.


}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.be.sportizebe.domain.match.repository;

import com.be.sportizebe.domain.match.entity.MatchRoom;
import com.be.sportizebe.domain.match.entity.MatchStatus;
import com.be.sportizebe.domain.match.repository.projection.MatchNearProjection;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
Expand Down Expand Up @@ -46,6 +47,10 @@ List<MatchNearProjection> findNear(
@Param("sportsName") String sportsName
);

// 구장 기반 모집 중 매칭 목록 (OPEN/FULL, 미래 일정만)
List<MatchRoom> findByFacilityIdAndStatusInAndScheduledAtAfterOrderByScheduledAtAsc(
Long facilityId, List<MatchStatus> statuses, java.time.LocalDateTime now);

// scheduledAt이 지난 OPEN/FULL → CLOSED
@Modifying
@Query(value = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.be.sportizebe.domain.match.dto.request.MatchNearRequest;
import com.be.sportizebe.domain.match.dto.response.MatchDetailResponse;
import com.be.sportizebe.domain.match.dto.response.MatchNearResponse;
import com.be.sportizebe.domain.match.dto.response.MatchParticipantResponse;
import com.be.sportizebe.domain.match.dto.response.MatchResponse;
import com.be.sportizebe.domain.match.dto.response.MyMatchResponse;

Expand All @@ -27,4 +28,8 @@ public interface MatchService {

List<MyMatchResponse> getMyMatches(Long userId); // 참여 중인 매칭 목록

List<MatchParticipantResponse> getMatchParticipants(Long matchId); // 매칭 참가자 목록

List<MatchResponse> getMatchesByFacility(Long facilityId); // 구장 기반 매칭 목록

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.be.sportizebe.domain.match.dto.request.MatchNearRequest;
import com.be.sportizebe.domain.match.dto.response.MatchDetailResponse;
import com.be.sportizebe.domain.match.dto.response.MatchNearResponse;
import com.be.sportizebe.domain.match.dto.response.MatchParticipantResponse;
import com.be.sportizebe.domain.match.dto.response.MatchResponse;
import com.be.sportizebe.domain.match.dto.response.MyMatchResponse;
import com.be.sportizebe.domain.match.entity.MatchParticipant;
Expand All @@ -23,6 +24,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.be.sportizebe.domain.match.dto.request.MatchCreateRequest;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -165,6 +167,33 @@ public List<MyMatchResponse> getMyMatches(Long userId) {
}

@Override
@Transactional(readOnly = true)
public List<MatchParticipantResponse> getMatchParticipants(Long matchId) {
if (!matchRoomRepository.existsById(matchId)) {
throw new CustomException(MatchErrorCode.MATCH_NOT_FOUND);
}
return matchParticipantRepository
.findAllByMatchRoomIdAndStatusFetch(matchId, MatchParticipantStatus.JOINED)
.stream()
.map(MatchParticipantResponse::from)
.toList();
}

@Override
@Transactional(readOnly = true)
public List<MatchResponse> getMatchesByFacility(Long facilityId) {
return matchRoomRepository
.findByFacilityIdAndStatusInAndScheduledAtAfterOrderByScheduledAtAsc(
facilityId,
List.of(MatchStatus.OPEN, MatchStatus.FULL),
LocalDateTime.now()
Comment on lines +186 to +189
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

구장 조회 API가 요구사항보다 넓은 상태를 반환합니다.

linked issue #71은 특정 구장의 OPEN 매치 목록 조회를 목표로 하고 있는데, Line 186-189는 FULL도 함께 포함합니다. 요구사항 변경이 아니라면 여기서는 OPEN만 조회해야 합니다.

요구사항에 맞추는 예시
         return matchRoomRepository
                 .findByFacilityIdAndStatusInAndScheduledAtAfterOrderByScheduledAtAsc(
                         facilityId,
-                        List.of(MatchStatus.OPEN, MatchStatus.FULL),
+                        List.of(MatchStatus.OPEN),
                         LocalDateTime.now()
                 )
📝 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
.findByFacilityIdAndStatusInAndScheduledAtAfterOrderByScheduledAtAsc(
facilityId,
List.of(MatchStatus.OPEN, MatchStatus.FULL),
LocalDateTime.now()
.findByFacilityIdAndStatusInAndScheduledAtAfterOrderByScheduledAtAsc(
facilityId,
List.of(MatchStatus.OPEN),
LocalDateTime.now()
🤖 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/match/service/MatchServiceImpl.java`
around lines 186 - 189, The current query in MatchServiceImpl uses
findByFacilityIdAndStatusInAndScheduledAtAfterOrderByScheduledAtAsc with
List.of(MatchStatus.OPEN, MatchStatus.FULL) which returns both OPEN and FULL
matches; update it to only include MatchStatus.OPEN so the API returns only OPEN
matches as per issue `#71` (i.e., change the status list passed to the repository
call to contain only MatchStatus.OPEN).

)
.stream()
.map(MatchResponse::from)
.toList();
Comment on lines +184 to +193
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

존재하지 않는 facilityId를 빈 목록으로 숨기지 마세요.

지금 구현은 잘못된 facilityId와 "해당 구장에 예정된 매치가 없음"을 동일하게 빈 리스트로 반환합니다. 이미 sportsFacilityRepositoryFacilityErrorCode.FACILITY_NOT_FOUND가 있으니, 조회 전에 구장 존재 여부를 검증해서 404로 구분하는 편이 API 계약상 안전합니다.

구장 존재 여부를 먼저 검증하는 예시
     `@Override`
     `@Transactional`(readOnly = true)
     public List<MatchResponse> getMatchesByFacility(Long facilityId) {
+        if (!sportsFacilityRepository.existsById(facilityId)) {
+            throw new CustomException(FacilityErrorCode.FACILITY_NOT_FOUND);
+        }
+
         return matchRoomRepository
                 .findByFacilityIdAndStatusInAndScheduledAtAfterOrderByScheduledAtAsc(
                         facilityId,
                         List.of(MatchStatus.OPEN, MatchStatus.FULL),
                         LocalDateTime.now()
                 )
🤖 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/match/service/MatchServiceImpl.java`
around lines 184 - 193, The current getMatchesByFacility method hides a
non-existent facility by returning an empty list; before querying
matchRoomRepository, verify the facility exists via sportsFacilityRepository
(e.g., existsById or findById) and if not present throw the existing not-found
error using FacilityErrorCode.FACILITY_NOT_FOUND (or the project's
NotFoundException wrapper), then proceed to call
matchRoomRepository.findByFacilityIdAndStatusInAndScheduledAtAfterOrderByScheduledAtAsc
and map to MatchResponse::from; update getMatchesByFacility to perform this
existence check using sportsFacilityRepository and throw the proper error when
missing.

}

@Override
@Transactional(readOnly = true)
public List<MatchNearResponse> getNearMatches(MatchNearRequest request) {
String sportsName = request.getSportsName() == null
Expand Down