Skip to content

✨Feat: 운동 매칭 기능 추가#56

Merged
angoroa merged 7 commits intodevelopfrom
feat/match
Feb 20, 2026
Merged

✨Feat: 운동 매칭 기능 추가#56
angoroa merged 7 commits intodevelopfrom
feat/match

Conversation

@angoroa
Copy link
Copy Markdown
Member

@angoroa angoroa commented Feb 9, 2026

#️⃣ Issue Number

  • closed #

📝 요약(Summary)

•	매칭방 엔티티 및 참여자 엔티티 추가
•	매칭 생성, 참여, 상세 조회  API
•	체육시설 등록 및 조회 API 구현

🛠️ PR 유형

어떤 변경 사항이 있나요?

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

📸스크린샷 (선택)

💬 공유사항 to 리뷰어

✅ PR Checklist

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

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

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 위치 기반 근처 경기 검색 기능 추가 (거리 반경, 스포츠 종류별 필터링 지원)
    • 경기 생성, 참여, 상세 정보 조회 기능 구현
    • 체육시설 등록 및 상세 조회 기능 추가
  • Chores

    • 개발 환경 모니터링 및 관찰성 인프라 구축 (Prometheus, Grafana, Redis Exporter)
    • 개발자 가이드 문서 추가

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

개요

이 PR은 매칭 도메인의 전체 구현을 추가하며, 위치 기반 근처 매칭 조회 기능을 도입합니다. 모니터링 스택(Prometheus, Grafana, Redis Exporter), 매칭 생성/참여/조회 API, 체육시설 도메인 확장, 그리고 개발 문서를 포함합니다.

Walkthrough

이 PR은 매칭 관리 시스템 전체(생성, 참여, 상세 조회, 지리적 검색)를 구현하고, Docker 기반 모니터링 스택(Prometheus/Grafana)을 추가하며, 체육시설 API를 확장합니다.

Changes

Cohort / File(s) Summary
모니터링 및 관찰성
docker/docker-compose.local.yml, docker/prometheus.local.yml
Redis Exporter, Prometheus, Grafana 서비스 추가 및 설정 파일 작성
매칭 도메인 - Entity
src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java, MatchParticipant.java, MatchStatus.java, MatchParticipantStatus.java
매칭방, 참여자, 상태 enum 정의 및 참여자 관리 로직 구현
매칭 도메인 - DTO
src/main/java/com/be/sportizebe/domain/match/dto/request/MatchCreateRequest.java, MatchNearRequest.java, src/main/java/com/be/sportizebe/domain/match/dto/response/MatchResponse.java, MatchDetailResponse.java, MatchNearResponse.java, MatchParticipantResponse.java
요청/응답 데이터 전송 객체 및 팩토리 메서드 정의
매칭 도메인 - Repository
src/main/java/com/be/sportizebe/domain/match/repository/MatchRoomRepository.java, MatchParticipantRepository.java, projection/MatchNearProjection.java
매칭방 및 참여자 데이터 접근, PostGIS 기반 지리적 쿼리 구현
매칭 도메인 - Service
src/main/java/com/be/sportizebe/domain/match/service/MatchService.java, MatchServiceImpl.java
매칭 생성, 참여, 상세 조회, 근처 매칭 검색 비즈니스 로직 구현
매칭 도메인 - Controller
src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java
매칭 관련 REST API 엔드포인트 정의
매칭 도메인 - 예외 처리
src/main/java/com/be/sportizebe/domain/match/exception/MatchErrorCode.java
매칭 도메인 특화 에러 코드(MATCH_NOT_FOUND, MATCH_FULL, ALREADY_JOINED)
체육시설 도메인 확장
src/main/java/com/be/sportizebe/domain/facility/controller/SportsFacilityController.java, service/SportsFacilityService.java, SportsFacilityServiceImpl.java, mapper/FacilityMapper.java, dto/request/FacilityCreateRequest.java, dto/response/FacilityResponse.java, repository/SportsFacilityRepository.java
체육시설 생성 및 조회 API 추가, JpaRepository 업그레이드, Geometry 처리 로직 추가
공통 업데이트
src/main/java/com/be/sportizebe/common/enums/SportType.java
BADMINTON 스포츠 타입 추가
문서 및 설정
.gitignore, CLAUDE.md
개발 환경 설정 무시 규칙 추가, 종합 개발 가이드 문서 작성

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant MatchController
    participant MatchService
    participant UserRepository
    participant MatchRoomRepository
    participant MatchParticipantRepository
    participant Database

    Client->>MatchController: POST /api/matches/create
    MatchController->>MatchService: createMatch(userId, request)
    MatchService->>UserRepository: findById(userId)
    UserRepository->>Database: query User
    Database-->>UserRepository: User
    UserRepository-->>MatchService: User
    MatchService->>MatchRoomRepository: save(MatchRoom)
    MatchRoomRepository->>Database: INSERT match_rooms
    Database-->>MatchRoomRepository: saved MatchRoom
    MatchRoomRepository-->>MatchService: MatchRoom with ID
    MatchService->>MatchParticipantRepository: save(MatchParticipant)
    MatchParticipantRepository->>Database: INSERT match_participants
    Database-->>MatchParticipantRepository: saved MatchParticipant
    MatchParticipantRepository-->>MatchService: MatchParticipant
    MatchService-->>MatchController: MatchResponse
    MatchController-->>Client: HTTP 200 (MatchResponse)
Loading
sequenceDiagram
    participant Client
    participant MatchController
    participant MatchService
    participant MatchRoomRepository
    participant Database
    participant PostGIS

    Client->>MatchController: GET /api/matches/near?lat=37.5&lng=127.0&radiusM=1000
    MatchController->>MatchService: getNearMatches(request)
    MatchService->>MatchRoomRepository: findNear(lat, lng, radiusM, sportsName)
    MatchRoomRepository->>Database: Native Query with ST_DWithin
    Database->>PostGIS: ST_Distance, ST_DWithin spatial operations
    PostGIS-->>Database: filtered results with distances
    Database-->>MatchRoomRepository: MatchNearProjection list
    MatchRoomRepository-->>MatchService: List<MatchNearProjection>
    MatchService->>MatchService: map to MatchNearResponse
    MatchService-->>MatchController: List<MatchNearResponse>
    MatchController-->>Client: HTTP 200 (MatchNearResponse list)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 새로운 매칭의 세계, PostGIS 위에 펼쳐지네,
가까운 경기장을 찾아내고, 참여자들을 모으니
Prometheus가 지켜보고, Grafana가 노래하는
모니터링의 춤이 시작되었네,
야호! 스포츠 커뮤니티의 새로운 시대가 열렸어요! ⚽🏀🏸

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 운동 매칭 기능 추가라는 주요 변경사항을 명확하게 요약하고 있습니다. 포함된 변경사항들(매칭방 엔티티, 참여자 엔티티, 매칭 생성/참여/조회 API, 체육시설 API 등)이 모두 '운동 매칭 기능'이라는 범주에 속하므로 제목이 변경사항의 핵심을 적절히 반영합니다.

✏️ 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

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
Copy Markdown

@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: 12

🤖 Fix all issues with AI agents
In
`@src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java`:
- Around line 31-49: Controller is passing userAuthInfo.getId() (Long) to
matchService.joinMatch and matchService.getMatchDetail while MatchServiceImpl
expects a User; change the controller to pass the User from UserAuthInfo by
calling userAuthInfo.getUser() in both joinMatch and getMatchDetail so the
signatures match (joinMatch(matchId, userAuthInfo.getUser()) and
getMatchDetail(matchId, userAuthInfo.getUser())); ensure necessary imports and
that UserAuthInfo exposes getUser().
- Around line 21-29: The controller's createMatch is missing the authenticated
principal and mismatches the service signature/return type: add an
`@AuthenticationPrincipal` UserAuthInfo userAuthInfo parameter to createMatch,
pass the appropriate User (convert or extract from userAuthInfo) into
matchService.createMatch(request, user) to match
MatchServiceImpl.createMatch(Request, User), capture the returned Long matchId,
and then produce a MatchResponse (e.g., new MatchResponse(matchId) or by calling
a mapper/service that converts id→MatchResponse) before returning
BaseResponse.success; alternatively, change the service to return MatchResponse
and call matchService.createMatch(request, user) accordingly so controller and
service signatures/returns align.
- Line 19: Change MatchService from a concrete class to a Java interface so
MatchServiceImpl can implement it: declare the required method signatures (e.g.,
Long createMatch(MatchCreateRequest request, User host); void joinMatch(Long
matchId, User user); MatchDetailResponse getMatchDetail(Long matchId, User
user);) in the MatchService interface and remove any concrete implementation
details from MatchService; ensure MatchServiceImpl implements MatchService and
that MatchController still depends on the MatchService type (the interface)
rather than a concrete class.
- Around line 1-11: The MatchController has missing imports causing compilation
errors; add the necessary import statements for ResponseEntity and HttpStatus
(from Spring), BaseResponse and MatchResponse (your project's response DTOs),
and UserAuthInfo (authentication principal DTO) used in methods of
MatchController so the compiler can resolve those types; locate usages in the
MatchController class and add corresponding import lines for each referenced
symbol (ResponseEntity, HttpStatus, BaseResponse, MatchResponse, UserAuthInfo).

In
`@src/main/java/com/be/sportizebe/domain/match/dto/request/MatchCreateRequest.java`:
- Around line 7-18: MatchCreateRequest is missing Bean Validation annotations so
sportType, facilityId and maxMembers can be null/invalid; add validation
annotations to the record components (e.g., annotate sportType and facilityId
with `@NotNull`, and maxMembers with `@NotNull` and `@Min`(1) or `@Positive`) to enforce
non-null/positive values, and update MatchController.createMatch to validate the
request by adding `@Valid` to the `@RequestBody` parameter so validation errors are
triggered and returned to the client.

In
`@src/main/java/com/be/sportizebe/domain/match/dto/response/MatchDetailResponse.java`:
- Around line 9-32: Add a public static factory method
MatchDetailResponse.of(MatchRoom matchRoom, User user) that maps all record
fields from the MatchRoom (matchId, sportType, facilityId, maxMembers,
currentMemberCount), builds participantIds (List<Long>) from the matchRoom
participants (or accept a pre-fetched participant list if MatchRoom has no
relationship) and sets joined by checking if the user's id is contained in that
participant list; update MatchServiceImpl call sites to pass any fetched
participant list into this factory if you cannot obtain participants from
MatchRoom directly.

In `@src/main/java/com/be/sportizebe/domain/match/entity/MatchParticipant.java`:
- Around line 12-16: The unique constraint on MatchParticipant (columns
match_room_id,user_id) causes insert conflicts because joinMatch() only checks
existsByMatchRoomAndUserAndStatus(..., JOINED) and always calls save(new
MatchParticipant(...)); change joinMatch() to first query for an existing
MatchParticipant by matchRoom and user (regardless of status) using the
repository method that finds by matchRoom and user, then if found and status ==
LEFT update that entity's status to JOINED and save it, if found and status ==
JOINED return/throw as before, and if not found create and save a new
MatchParticipant; also add or plan a leaveMatch()/updateStatus method to set
status to LEFT so future joins work correctly and avoid unique constraint
violations.

In `@src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java`:
- Around line 4-13: The MatchRoom class imports BaseTimeEntity but incorrectly
extends BaseEntity, causing a compile error; update the class declaration so
MatchRoom extends BaseTimeEntity (or, if BaseEntity is intended, change the
import to the correct BaseEntity)—look for the class header "public class
MatchRoom" and the import line "import
com.be.sportizebe.global.common.BaseTimeEntity;" and make the extends clause and
import refer to the same base class (consistent with how Club.java uses
BaseTimeEntity).

In `@src/main/java/com/be/sportizebe/domain/match/service/MatchService.java`:
- Around line 3-5: MatchService is declared as a class but must be an interface
so MatchServiceImpl can implement it; change the declaration of MatchService
(public class MatchService { ... }) to an interface (public interface
MatchService { ... }), ensure any method signatures required by MatchServiceImpl
are declared in this interface (move/declare abstract method signatures
currently implemented in MatchServiceImpl into MatchService), and keep
MatchServiceImpl as implements MatchService so the project compiles consistently
with other service interfaces like CommentService, ClubService, and
SportsFacilityService.

In `@src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java`:
- Around line 22-32: The MatchServiceImpl.createMatch is passing four args to
the MatchRoom constructor (request.sportType(), request.facilityId(),
request.maxMembers(), host) but MatchRoom only defines a three-arg constructor
(SportType, Long, Integer), causing a compile error; either add a host field and
matching constructor/setter to the MatchRoom entity (update MatchRoom class to
include a User host field and a constructor that accepts host, and persist it
before creating MatchParticipant) or remove the host argument from the
MatchServiceImpl.createMatch call and set host separately on the saved MatchRoom
(use matchRoomRepository.save and then setHost/save), then continue creating
MatchParticipant with the correct MatchRoom instance.
- Around line 56-62: MatchDetailResponse.of(matchRoom, user) doesn't exist and
MatchRoom lacks a participants mapping, causing the build error; update
MatchServiceImpl.getMatchDetail to fetch participants via the appropriate
repository (e.g., matchParticipantRepository.findByMatchRoomId(matchId)) after
loading MatchRoom from matchRoomRepository, then construct/return a
MatchDetailResponse by calling its constructor or a newly added static factory
that accepts explicit fields (matchRoom, participant list, user-related flags)
instead of relying on MatchDetailResponse.of.
- Around line 34-54: Add the missing import for
com.be.sportizebe.global.exception.SportizeException and modify joinMatch to use
a pessimistic lock when loading the MatchRoom to avoid race conditions: add a
repository method like findByIdForUpdate(Long id) annotated with
`@Lock`(LockModeType.PESSIMISTIC_WRITE) (or equivalent JPA query) and call that
from joinMatch (instead of matchRoomRepository.findById) inside the
transactional boundary so count/exists/save happen while the row is locked; keep
the existing unique constraint handling for duplicate users but rely on the
locked read to prevent exceeding matchRoom.getMaxMembers().
🧹 Nitpick comments (4)
docker/docker-compose.local.yml (1)

40-50: REDIS_ADDR에서 서비스 이름 대신 컨테이너 이름 사용

REDIS_ADDR가 컨테이너 이름(sportize-redis-local)을 참조하고 있습니다. Docker Compose 네트워크에서는 서비스 이름(redis)으로 DNS가 확실하게 해석되므로, 서비스 이름을 사용하는 것이 더 안정적입니다.

♻️ 수정 제안
     environment:
-      REDIS_ADDR: redis://sportize-redis-local:6379
+      REDIS_ADDR: redis://redis:6379
       REDIS_PASSWORD: password
src/main/java/com/be/sportizebe/domain/match/dto/response/MatchResponse.java (1)

7-21: 코드베이스 내 다른 DTO 패턴과의 일관성을 위해 from 팩토리 메서드 추가를 권장합니다.

ClubDetailResponse, ClubListItemResponse 등 기존 DTO들은 엔티티에서 변환하는 static from(...) 메서드를 포함하고 있습니다. 매핑 로직을 DTO 내에 캡슐화하면 컨트롤러/서비스 레이어가 깔끔해집니다.

♻️ 팩토리 메서드 추가 제안
-) {}
+) {
+    public static MatchResponse from(MatchRoom matchRoom) {
+        return new MatchResponse(
+                matchRoom.getId(),
+                matchRoom.getSportType(),
+                matchRoom.getFacilityId(),
+                matchRoom.getMaxMembers()
+        );
+    }
+}
src/main/java/com/be/sportizebe/domain/match/entity/MatchParticipant.java (1)

35-41: joinedAt 필드에 @Column(nullable = false) 추가를 권장합니다.

생성자에서 항상 LocalDateTime.now()로 설정되지만, DB 스키마 레벨에서도 NOT NULL 제약조건을 명시하면 데이터 정합성이 보장됩니다.

♻️ 수정 제안
-    private LocalDateTime joinedAt;
+    `@Column`(nullable = false)
+    private LocalDateTime joinedAt;
src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java (1)

10-33: @Table 어노테이션 누락 및 host 필드 부재.

  1. @Table 미지정: 다른 엔티티(Club, User)에서는 @Table(name = "...")을 명시적으로 지정하고 있습니다. 일관성을 위해 @Table(name = "match_rooms")를 추가하는 것을 권장합니다.

  2. Host 필드 부재: MatchServiceImpl.createMatch()에서 4번째 인자로 host를 전달하고 있으나, 이 엔티티에는 해당 필드가 없습니다. 매칭방 생성자 정보는 비즈니스적으로 중요한 데이터이므로 host 필드 추가를 고려해 주세요. (생성자 불일치 이슈는 MatchServiceImpl 리뷰에서 상세히 다루었습니다.)

@Tag(name = "match", description = "운동 매칭 관련 API")
public class MatchController {

private final MatchService matchService;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the actual content of MatchService.java
fd "MatchService.java" --type f -x cat -n {}

Repository: Sportize/be

Length of output: 169


🏁 Script executed:

#!/bin/bash
# Also search for MatchServiceImpl to see how it's defined
fd "MatchServiceImpl.java" --type f -x cat -n {}

Repository: Sportize/be

Length of output: 2846


MatchService는 인터페이스로 변경되어야 합니다.

현재 MatchService가 concrete 클래스로 정의되어 있는데, MatchServiceImpl이 이를 implements하려고 합니다. Java에서는 클래스를 구현(implements)할 수 없으므로, MatchService를 인터페이스로 변경하고 필요한 메서드 시그니처를 선언해야 합니다.

MatchService를 interface로 변경
public interface MatchService {
    Long createMatch(MatchCreateRequest request, User host);
    void joinMatch(Long matchId, User user);
    MatchDetailResponse getMatchDetail(Long matchId, User user);
}
🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java`
at line 19, Change MatchService from a concrete class to a Java interface so
MatchServiceImpl can implement it: declare the required method signatures (e.g.,
Long createMatch(MatchCreateRequest request, User host); void joinMatch(Long
matchId, User user); MatchDetailResponse getMatchDetail(Long matchId, User
user);) in the MatchService interface and remove any concrete implementation
details from MatchService; ensure MatchServiceImpl implements MatchService and
that MatchController still depends on the MatchService type (the interface)
rather than a concrete class.

Comment on lines +21 to +29
@Operation(summary = "매칭방 생성")
@PostMapping
public ResponseEntity<BaseResponse<MatchResponse>> createMatch(
@RequestBody MatchCreateRequest request
) {
MatchResponse response = matchService.createMatch(request);
return ResponseEntity.status(HttpStatus.CREATED)
.body(BaseResponse.success("매칭방 생성 성공", response));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

createMatch — 서비스 메서드 시그니처 불일치 및 인증 누락.

  1. 인증 정보 누락: 다른 엔드포인트(joinMatch, getMatchDetail)에서는 @AuthenticationPrincipal UserAuthInfo를 사용하지만, createMatch에서는 빠져 있습니다. 매칭방 생성자(host) 정보가 서비스로 전달되지 않습니다.

  2. 시그니처 불일치: Controller에서 matchService.createMatch(request)로 1개 인자를 전달하지만, MatchServiceImpl.createMatch(MatchCreateRequest, User) 2개 인자를 받습니다. 또한 서비스는 Long을 반환하지만 controller는 MatchResponse를 기대합니다.

🐛 수정 제안
     `@Operation`(summary = "매칭방 생성")
     `@PostMapping`
     public ResponseEntity<BaseResponse<MatchResponse>> createMatch(
-            `@RequestBody` MatchCreateRequest request
+            `@RequestBody` MatchCreateRequest request,
+            `@AuthenticationPrincipal` UserAuthInfo userAuthInfo
     ) {
-        MatchResponse response = matchService.createMatch(request);
+        Long matchId = matchService.createMatch(request, userAuthInfo.getUser());
+        MatchResponse response = new MatchResponse(matchId);
         return ResponseEntity.status(HttpStatus.CREATED)
                 .body(BaseResponse.success("매칭방 생성 성공", response));
     }
🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java`
around lines 21 - 29, The controller's createMatch is missing the authenticated
principal and mismatches the service signature/return type: add an
`@AuthenticationPrincipal` UserAuthInfo userAuthInfo parameter to createMatch,
pass the appropriate User (convert or extract from userAuthInfo) into
matchService.createMatch(request, user) to match
MatchServiceImpl.createMatch(Request, User), capture the returned Long matchId,
and then produce a MatchResponse (e.g., new MatchResponse(matchId) or by calling
a mapper/service that converts id→MatchResponse) before returning
BaseResponse.success; alternatively, change the service to return MatchResponse
and call matchService.createMatch(request, user) accordingly so controller and
service signatures/returns align.

Comment on lines +31 to +49
@Operation(summary = "매칭 참여")
@PostMapping("/{matchId}/join")
public ResponseEntity<BaseResponse<Void>> joinMatch(
@PathVariable Long matchId,
@AuthenticationPrincipal UserAuthInfo userAuthInfo
) {
matchService.joinMatch(matchId, userAuthInfo.getId());
return ResponseEntity.ok(BaseResponse.success("매칭 참여 성공", null));
}

@Operation(summary = "매칭 상세 조회")
@GetMapping("/{matchId}")
public ResponseEntity<BaseResponse<MatchDetailResponse>> getMatchDetail(
@PathVariable Long matchId,
@AuthenticationPrincipal UserAuthInfo userAuthInfo
) {
MatchDetailResponse response =
matchService.getMatchDetail(matchId, userAuthInfo.getId());
return ResponseEntity.ok(BaseResponse.success("매칭 상세 조회 성공", response));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

joinMatch, getMatchDetail — 서비스에 User 대신 Long을 전달하고 있습니다.

Controller에서 userAuthInfo.getId()Long 타입 ID를 전달하지만, MatchServiceImpljoinMatch(Long, User)getMatchDetail(Long, User)User 객체를 기대합니다. 타입 불일치로 컴파일 에러가 발생합니다.

UserAuthInfo에서 User 객체를 직접 가져올 수 있다면 userAuthInfo.getUser()를 사용하거나, 서비스 메서드 시그니처를 Long userId로 변경하고 서비스 내에서 User를 조회하는 방식으로 통일해 주세요.

🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java`
around lines 31 - 49, Controller is passing userAuthInfo.getId() (Long) to
matchService.joinMatch and matchService.getMatchDetail while MatchServiceImpl
expects a User; change the controller to pass the User from UserAuthInfo by
calling userAuthInfo.getUser() in both joinMatch and getMatchDetail so the
signatures match (joinMatch(matchId, userAuthInfo.getUser()) and
getMatchDetail(matchId, userAuthInfo.getUser())); ensure necessary imports and
that UserAuthInfo exposes getUser().

Comment on lines +7 to +18
public record MatchCreateRequest(

@Schema(description = "스포츠 종류", example = "SOCCER")
SportType sportType,

@Schema(description = "체육시설 ID", example = "123")
Long facilityId,

@Schema(description = "최대 참여 인원 수", example = "10")
Integer maxMembers

) {} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the MatchCreateRequest file and check its content
find . -name "MatchCreateRequest.java" -type f

Repository: Sportize/be

Length of output: 137


🏁 Script executed:

# Search for controller files that might use MatchCreateRequest
rg -l "MatchCreateRequest" --type=java | grep -i controller

Repository: Sportize/be

Length of output: 131


🏁 Script executed:

# Check for `@Valid` annotation usage with MatchCreateRequest
rg -n "@Valid.*MatchCreateRequest|MatchCreateRequest.*@Valid" --type=java -B2 -A2

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Also search for any `@Valid` annotation in match-related controllers
rg -n "@Valid" --type=java -l | grep -i match | head -5

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Read the MatchCreateRequest file
cat -n ./src/main/java/com/be/sportizebe/domain/match/dto/request/MatchCreateRequest.java

Repository: Sportize/be

Length of output: 679


🏁 Script executed:

# Read the MatchController to find the endpoint using MatchCreateRequest
rg -n "MatchCreateRequest" src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java -B5 -A5

Repository: Sportize/be

Length of output: 930


요청 DTO에 Bean Validation 어노테이션이 누락되어 있습니다.

sportType, facilityId, maxMembers 필드에 null, 0, 음수 등 유효하지 않은 값이 들어올 수 있습니다. 엔티티의 @Column(nullable = false) 제약만으로는 사용자에게 친절한 에러 메시지를 제공하기 어렵습니다.

🛡️ 유효성 검증 추가 제안
 package com.be.sportizebe.domain.match.dto.request;
 
 import com.be.sportizebe.domain.user.entity.SportType;
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
 
 `@Schema`(description = "매칭 생성 요청 정보")
 public record MatchCreateRequest(
 
+        `@NotNull`
         `@Schema`(description = "스포츠 종류", example = "SOCCER")
         SportType sportType,
 
+        `@NotNull`
         `@Schema`(description = "체육시설 ID", example = "123")
         Long facilityId,
 
+        `@NotNull`
+        `@Positive`
         `@Schema`(description = "최대 참여 인원 수", example = "10")
         Integer maxMembers
 
 ) {}

MatchController의 createMatch 메서드(라인 23-28)에서 @RequestBody 파라미터에 @Valid 어노테이션을 추가해야 합니다.

🤖 Prompt for AI Agents
In
`@src/main/java/com/be/sportizebe/domain/match/dto/request/MatchCreateRequest.java`
around lines 7 - 18, MatchCreateRequest is missing Bean Validation annotations
so sportType, facilityId and maxMembers can be null/invalid; add validation
annotations to the record components (e.g., annotate sportType and facilityId
with `@NotNull`, and maxMembers with `@NotNull` and `@Min`(1) or `@Positive`) to enforce
non-null/positive values, and update MatchController.createMatch to validate the
request by adding `@Valid` to the `@RequestBody` parameter so validation errors are
triggered and returned to the client.

Comment on lines +34 to +54
@Override
public void joinMatch(Long matchId, User user) {
MatchRoom matchRoom = matchRoomRepository.findById(matchId)
.orElseThrow(() -> new SportizeException(GlobalErrorCode.NOT_FOUND_MATCH));

long count = matchParticipantRepository.countByMatchRoomAndStatus(
matchRoom, MatchParticipantStatus.JOINED
);

if (count >= matchRoom.getMaxMembers()) {
throw new SportizeException(GlobalErrorCode.MATCH_FULL);
}

boolean exists = matchParticipantRepository.existsByMatchRoomAndUserAndStatus(
matchRoom, user, MatchParticipantStatus.JOINED
);

if (exists) throw new SportizeException(GlobalErrorCode.ALREADY_JOINED);

matchParticipantRepository.save(new MatchParticipant(matchRoom, user));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

SportizeException import 누락 및 동시성 이슈.

  1. Import 누락 (컴파일 에러): SportizeException이 사용되고 있지만 import 선언이 없습니다. com.be.sportizebe.global.exception.SportizeException을 import해야 합니다.

  2. 동시성 경쟁 조건 (Race Condition): joinMatch에서 인원 수 확인(count)과 중복 참여 확인(exists) 후 저장(save)까지의 과정이 원자적이지 않습니다. @Transactional만으로는 동시 요청 시 capacity를 초과하는 참여가 발생할 수 있습니다.

    MatchParticipant(match_room_id, user_id) unique constraint가 있어 중복 참여는 DB 레벨에서 방지되지만, 정원 초과는 방지되지 않습니다. 비관적 락(Pessimistic Lock)을 findById 대신 사용하거나, application-level 동기화를 고려해 주세요.

수정 제안: import 추가 및 비관적 락 적용
 import com.be.sportizebe.global.exception.GlobalErrorCode;
+import com.be.sportizebe.global.exception.SportizeException;

Repository에 비관적 락을 적용한 조회 메서드 추가:

`@Lock`(LockModeType.PESSIMISTIC_WRITE)
`@Query`("SELECT m FROM MatchRoom m WHERE m.id = :id")
Optional<MatchRoom> findByIdForUpdate(`@Param`("id") Long id);

,

🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java`
around lines 34 - 54, Add the missing import for
com.be.sportizebe.global.exception.SportizeException and modify joinMatch to use
a pessimistic lock when loading the MatchRoom to avoid race conditions: add a
repository method like findByIdForUpdate(Long id) annotated with
`@Lock`(LockModeType.PESSIMISTIC_WRITE) (or equivalent JPA query) and call that
from joinMatch (instead of matchRoomRepository.findById) inside the
transactional boundary so count/exists/save happen while the row is locked; keep
the existing unique constraint handling for duplicate users but rely on the
locked read to prevent exceeding matchRoom.getMaxMembers().

Comment on lines +56 to +62
@Override
public MatchDetailResponse getMatchDetail(Long matchId, User user) {
MatchRoom matchRoom = matchRoomRepository.findById(matchId)
.orElseThrow(() -> new SportizeException(GlobalErrorCode.NOT_FOUND_MATCH));

return MatchDetailResponse.of(matchRoom, user);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

MatchDetailResponse.of(matchRoom, user) — 관련 빌드 에러.

MatchDetailResponseof 정적 팩토리 메서드가 정의되어 있지 않아 컴파일 에러가 발생합니다. 이 메서드 내에서 참여자 목록을 조회해야 하는데, 현재 MatchRoom 엔티티에 participants 관계 매핑이 없으므로 서비스 레이어에서 repository를 통해 참여자 목록을 조회하고, MatchDetailResponse의 생성자 또는 팩토리 메서드에 필요한 값들을 직접 전달하는 방식이 더 적합할 수 있습니다.

서비스에서 직접 구성하는 방식 제안
     `@Override`
     public MatchDetailResponse getMatchDetail(Long matchId, User user) {
         MatchRoom matchRoom = matchRoomRepository.findById(matchId)
                 .orElseThrow(() -> new SportizeException(GlobalErrorCode.NOT_FOUND_MATCH));

-        return MatchDetailResponse.of(matchRoom, user);
+        List<MatchParticipant> participants = matchParticipantRepository
+                .findByMatchRoomAndStatus(matchRoom, MatchParticipantStatus.JOINED);
+
+        List<Long> participantIds = participants.stream()
+                .map(p -> p.getUser().getId())
+                .toList();
+
+        boolean joined = participantIds.contains(user.getId());
+
+        return new MatchDetailResponse(
+                matchRoom.getId(),
+                matchRoom.getSportType(),
+                matchRoom.getFacilityId(),
+                matchRoom.getMaxMembers(),
+                participantIds.size(),
+                participantIds,
+                joined
+        );
     }
🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java`
around lines 56 - 62, MatchDetailResponse.of(matchRoom, user) doesn't exist and
MatchRoom lacks a participants mapping, causing the build error; update
MatchServiceImpl.getMatchDetail to fetch participants via the appropriate
repository (e.g., matchParticipantRepository.findByMatchRoomId(matchId)) after
loading MatchRoom from matchRoomRepository, then construct/return a
MatchDetailResponse by calling its constructor or a newly added static factory
that accepts explicit fields (matchRoom, participant list, user-related flags)
instead of relying on MatchDetailResponse.of.

Copy link
Copy Markdown

@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: 4

🤖 Fix all issues with AI agents
In `@src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java`:
- Around line 12-18: The MatchRoom entity uses Lombok's `@Builder` while its
superclass BaseTimeEntity uses `@SuperBuilder`, causing builder inheritance
issues; replace `@Builder` with `@SuperBuilder` on the MatchRoom class (and ensure
imports/annotations align) so inherited fields from BaseTimeEntity are included
in the generated builder—this mirrors the fix applied to MatchParticipant and
ensures proper builder inheritance for MatchRoom.
- Around line 32-48: MatchRoom currently has no way to update curMembers after
creation, so implement business methods on MatchRoom to manage capacity: add a
method (e.g., addParticipant(MatchParticipant p)) that appends to participants,
increments curMembers (or rejects when curMembers >= maxMembers), and when
curMembers == maxMembers sets status = MatchStatus.FULL; implement a
removeParticipant(MatchParticipant p) that removes from participants, decrements
curMembers (not going below 0) and if status == MatchStatus.FULL sets status =
MatchStatus.OPEN (or appropriate non-full state); ensure these methods update
both the participants list and curMembers consistently and enforce
bounds/validation rather than exposing a public setter for curMembers.

In
`@src/main/java/com/be/sportizebe/domain/match/repository/MatchParticipantRepository.java`:
- Around line 11-21: Add a repository method to fetch an existing
MatchParticipant regardless of status so re-joins can detect prior records: add
a signature like Optional<MatchParticipant> findByMatchRoomAndUser(MatchRoom
matchRoom, User user) to the MatchParticipantRepository interface (alongside
existsByMatchRoomAndUserAndStatus, countByMatchRoomAndStatus,
findAllByMatchRoomAndStatus) so callers can retrieve any existing participant
record for a given MatchRoom and User without filtering by
MatchParticipantStatus.

In `@src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java`:
- Around line 29-45: createMatch currently creates a MatchRoom with curMembers=0
via MatchRoom.create(request), then saves a MatchParticipant but never updates
curMembers, leaving isFull() wrong; either maintain curMembers or remove the
field. Fix by updating the MatchRoom when adding the creator: after
matchParticipantRepository.save(new MatchParticipant(savedRoom, user)) call,
increment and persist the participant count on the same entity (e.g., call a
MatchRoom.incrementCurMembers() or setCurMembers(savedRoom.getCurMembers()+1)
and save via matchRoomRepository.save(savedRoom)), or alternatively remove
curMembers and change isFull() to compute size from
matchParticipantRepository.countByMatchRoomAndStatus(..., JOINED) consistently
(update createMatch to reflect whichever approach you choose).
🧹 Nitpick comments (6)
src/main/java/com/be/sportizebe/domain/match/exception/MatchErrorCode.java (1)

7-36: 기존 ErrorCode 패턴과 필드 순서 및 생성자 스타일이 다릅니다.

PostErrorCode, ChatErrorCode 등 기존 에러 코드 enum은 @AllArgsConstructor를 사용하고 필드 순서가 (String code, String message, HttpStatus status)입니다. MatchErrorCode(HttpStatus status, String code, String message) 순서로 명시적 생성자를 정의하고 있어 일관성이 떨어집니다.

또한, 에러 코드 문자열(MATCH_404, MATCH_400_FULL)에 HTTP 상태 코드가 포함되어 있는데, 기존 패턴(P001, CHAT_001)은 도메인 접두사 + 일련번호 방식을 사용합니다.

♻️ 기존 패턴에 맞춘 수정 제안
 `@Getter`
+@AllArgsConstructor
 public enum MatchErrorCode implements BaseErrorCode {
 
     MATCH_NOT_FOUND(
-            HttpStatus.NOT_FOUND,
-            "MATCH_404",
-            "매칭방을 찾을 수 없습니다."
+            "MATCH_001",
+            "매칭방을 찾을 수 없습니다.",
+            HttpStatus.NOT_FOUND
     ),
 
     MATCH_FULL(
-            HttpStatus.BAD_REQUEST,
-            "MATCH_400_FULL",
-            "매칭방 정원이 가득 찼습니다."
+            "MATCH_002",
+            "매칭방 정원이 가득 찼습니다.",
+            HttpStatus.BAD_REQUEST
     ),
 
     ALREADY_JOINED(
-            HttpStatus.BAD_REQUEST,
-            "MATCH_400_ALREADY_JOINED",
-            "이미 해당 매칭에 참가 중입니다."
+            "MATCH_003",
+            "이미 해당 매칭에 참가 중입니다.",
+            HttpStatus.BAD_REQUEST
     );
 
-    private final HttpStatus status;
     private final String code;
     private final String message;
-
-    MatchErrorCode(HttpStatus status, String code, String message) {
-        this.status = status;
-        this.code = code;
-        this.message = message;
-    }
+    private final HttpStatus status;
 }
src/main/java/com/be/sportizebe/domain/match/entity/MatchParticipant.java (1)

42-52: joinedAtBaseTimeEntity.createdAt이 중복됩니다.

BaseTimeEntity@CreatedDate로 관리하는 createdAt 필드와 joinedAt은 동일한 시점을 나타냅니다. 별도의 비즈니스 의미(예: 재참여 시 갱신)가 있다면 유지하되, 그렇지 않으면 createdAt을 활용하는 것이 간결합니다.

src/main/java/com/be/sportizebe/domain/match/dto/response/MatchParticipantResponse.java (2)

25-33: from() 메서드 인덴테이션이 record 본문과 일치하지 않습니다.

record 컴포넌트는 8칸 들여쓰기인데, from() 메서드 본문은 4칸으로 되어 있어 가독성이 떨어집니다.


7-33: MatchParticipantResponse는 현재 사용되지 않는 미사용 코드입니다.

MatchDetailResponseList<Long> participantIds만 반환하고 있으며, MatchParticipantResponsefrom() 메서드도 호출되지 않습니다. 실제 사용 시점에 추가하거나, 향후 사용 계획이 있다면 주석으로 명시하는 것이 좋습니다.

src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java (1)

3-3: 엔티티 레이어에서 DTO(MatchCreateRequest)를 직접 참조하고 있습니다.

엔티티가 DTO에 의존하면 레이어 간 결합도가 높아집니다. create() 팩토리 메서드의 파라미터를 개별 필드로 받거나, 서비스 레이어에서 빌더를 호출하는 방식으로 변경하면 의존 방향이 깔끔해집니다.

♻️ 리팩터링 제안
-import com.be.sportizebe.domain.match.dto.request.MatchCreateRequest;
 import com.be.sportizebe.domain.user.entity.SportType;
 ...
-    public static MatchRoom create(MatchCreateRequest request) {
+    public static MatchRoom create(SportType sportsName, Long facilityId, int maxMembers) {
         return MatchRoom.builder()
-                .sportsName(request.sportsName())
-                .facilityId(request.facilityId())
+                .sportsName(sportsName)
+                .facilityId(facilityId)
                 .curMembers(0)
-                .maxMembers(request.maxMembers())
+                .maxMembers(maxMembers)
                 .status(MatchStatus.OPEN)
                 .build();
     }

Also applies to: 50-58

src/main/java/com/be/sportizebe/domain/match/dto/response/MatchDetailResponse.java (1)

36-58: of() 메서드에서 N+1 쿼리 문제가 발생할 수 있습니다.

matchRoom.getParticipants()는 lazy 컬렉션이고, 각 참여자의 getUser()도 lazy-load를 트리거합니다. 참여자 수가 많아지면 쿼리 수가 급증합니다.

서비스 레이어에서 MatchRoom을 조회할 때 participants와 user를 함께 fetch join하거나, @EntityGraph를 사용하는 것을 권장합니다.

Comment on lines +12 to +18
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "match_rooms")
public class MatchRoom extends BaseTimeEntity {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

@Builder vs @SuperBuilder 불일치 — MatchParticipant와 동일한 문제입니다.

BaseTimeEntity@SuperBuilder를 사용하므로 여기서도 @SuperBuilder로 변경해야 합니다.

🐛 수정 제안
 `@Entity`
 `@Getter`
-@Builder
+@SuperBuilder
 `@AllArgsConstructor`
-@NoArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
 `@Table`(name = "match_rooms")
 public class MatchRoom extends BaseTimeEntity {
📝 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
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "match_rooms")
public class MatchRoom extends BaseTimeEntity {
`@Entity`
`@Getter`
`@SuperBuilder`
`@AllArgsConstructor`
`@NoArgsConstructor`(access = AccessLevel.PROTECTED)
`@Table`(name = "match_rooms")
public class MatchRoom extends BaseTimeEntity {
🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java` around
lines 12 - 18, The MatchRoom entity uses Lombok's `@Builder` while its superclass
BaseTimeEntity uses `@SuperBuilder`, causing builder inheritance issues; replace
`@Builder` with `@SuperBuilder` on the MatchRoom class (and ensure
imports/annotations align) so inherited fields from BaseTimeEntity are included
in the generated builder—this mirrors the fix applied to MatchParticipant and
ensures proper builder inheritance for MatchRoom.

Comment on lines +32 to +48
@Column(nullable = false)
private int curMembers;

@Column(nullable = false)
private int maxMembers;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private MatchStatus status;

@OneToMany(mappedBy = "matchRoom", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<MatchParticipant> participants = new ArrayList<>();

public boolean isFull() {
return this.curMembers >= this.maxMembers;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

curMembers를 변경할 수 있는 메서드가 없어 정원 관리가 불가능합니다.

@Getter만 있고 @Setter가 없으며, curMembers를 증가/감소시키는 비즈니스 메서드가 없습니다. create()에서 curMembers=0으로 생성된 이후 값이 변경되지 않으므로, isFull()은 항상 false를 반환하고 MATCH_FULL 에러가 절대 발생하지 않습니다.

참여자 추가/제거 시 카운터를 관리하는 메서드와, 정원 도달 시 상태를 FULL로 변경하는 메서드가 필요합니다.

🐛 비즈니스 메서드 추가 제안
     public boolean isFull() {
         return this.curMembers >= this.maxMembers;
     }
 
+    public void addParticipant(MatchParticipant participant) {
+        this.participants.add(participant);
+        this.curMembers++;
+        if (isFull()) {
+            this.status = MatchStatus.FULL;
+        }
+    }
+
+    public void removeParticipant(MatchParticipant participant) {
+        this.participants.remove(participant);
+        this.curMembers--;
+        if (this.status == MatchStatus.FULL) {
+            this.status = MatchStatus.OPEN;
+        }
+    }
+
     public static MatchRoom create(MatchCreateRequest request) {
📝 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
@Column(nullable = false)
private int curMembers;
@Column(nullable = false)
private int maxMembers;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private MatchStatus status;
@OneToMany(mappedBy = "matchRoom", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<MatchParticipant> participants = new ArrayList<>();
public boolean isFull() {
return this.curMembers >= this.maxMembers;
}
`@Column`(nullable = false)
private int curMembers;
`@Column`(nullable = false)
private int maxMembers;
`@Enumerated`(EnumType.STRING)
`@Column`(nullable = false)
private MatchStatus status;
`@OneToMany`(mappedBy = "matchRoom", cascade = CascadeType.ALL, orphanRemoval = true)
`@Builder.Default`
private List<MatchParticipant> participants = new ArrayList<>();
public boolean isFull() {
return this.curMembers >= this.maxMembers;
}
public void addParticipant(MatchParticipant participant) {
this.participants.add(participant);
this.curMembers++;
if (isFull()) {
this.status = MatchStatus.FULL;
}
}
public void removeParticipant(MatchParticipant participant) {
this.participants.remove(participant);
this.curMembers--;
if (this.status == MatchStatus.FULL) {
this.status = MatchStatus.OPEN;
}
}
public static MatchRoom create(MatchCreateRequest request) {
🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java` around
lines 32 - 48, MatchRoom currently has no way to update curMembers after
creation, so implement business methods on MatchRoom to manage capacity: add a
method (e.g., addParticipant(MatchParticipant p)) that appends to participants,
increments curMembers (or rejects when curMembers >= maxMembers), and when
curMembers == maxMembers sets status = MatchStatus.FULL; implement a
removeParticipant(MatchParticipant p) that removes from participants, decrements
curMembers (not going below 0) and if status == MatchStatus.FULL sets status =
MatchStatus.OPEN (or appropriate non-full state); ensure these methods update
both the participants list and curMembers consistently and enforce
bounds/validation rather than exposing a public setter for curMembers.

Comment on lines +11 to +21
public interface MatchParticipantRepository extends JpaRepository<MatchParticipant, Long> {

// 해당 매칭 + 유저 + 상태가 이미 존재하는지 확인
boolean existsByMatchRoomAndUserAndStatus(MatchRoom matchRoom, User user, MatchParticipantStatus status);

// 매칭방에 특정 상태인 참가자 수 카운트
long countByMatchRoomAndStatus(MatchRoom matchRoom, MatchParticipantStatus status);

// 주어진 방에 참여중인 참가자 리스트 전체 조회
List<MatchParticipant> findAllByMatchRoomAndStatus(MatchRoom matchRoom, MatchParticipantStatus status);
} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

재참여 시나리오를 위한 findByMatchRoomAndUser 메서드가 필요합니다.

MatchParticipant 리뷰에서 언급한 재참여 버그를 해결하려면, 상태와 무관하게 기존 참여 레코드를 조회할 수 있는 메서드가 필요합니다.

♻️ 메서드 추가 제안
 public interface MatchParticipantRepository extends JpaRepository<MatchParticipant, Long> {
 
+    // 상태와 무관하게 해당 매칭방 + 유저 레코드 조회 (재참여 처리용)
+    Optional<MatchParticipant> findByMatchRoomAndUser(MatchRoom matchRoom, User user);
+
     // 해당 매칭 + 유저 + 상태가 이미 존재하는지 확인
     boolean existsByMatchRoomAndUserAndStatus(MatchRoom matchRoom, User user, MatchParticipantStatus status);
🤖 Prompt for AI Agents
In
`@src/main/java/com/be/sportizebe/domain/match/repository/MatchParticipantRepository.java`
around lines 11 - 21, Add a repository method to fetch an existing
MatchParticipant regardless of status so re-joins can detect prior records: add
a signature like Optional<MatchParticipant> findByMatchRoomAndUser(MatchRoom
matchRoom, User user) to the MatchParticipantRepository interface (alongside
existsByMatchRoomAndUserAndStatus, countByMatchRoomAndStatus,
findAllByMatchRoomAndStatus) so callers can retrieve any existing participant
record for a given MatchRoom and User without filtering by
MatchParticipantStatus.

Comment on lines +29 to +45
@Override
// 실제로는 관리자용 메서드인데 더미 넣으려고 만듦
public MatchResponse createMatch(Long userId, MatchCreateRequest request) {

// 1) 유저 존재 확인
User user = userRepository.findById(userId)
.orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND));

// 2) 매칭방 생성 (엔티티 팩토리 메서드 사용)
MatchRoom matchRoom = MatchRoom.create(request);
MatchRoom savedRoom = matchRoomRepository.save(matchRoom);

// 3) 생성자 자동 참여 처리
matchParticipantRepository.save(new MatchParticipant(savedRoom, user));

return MatchResponse.from(savedRoom);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# MatchRoom 엔티티 파일 찾기
fd -t f -n "MatchRoom.java" | grep -v test

Repository: Sportize/be

Length of output: 285


🏁 Script executed:

# curMembers 필드 사용 현황 확인
rg -n 'curMembers' --type java -B 2 -A 2

Repository: Sportize/be

Length of output: 2051


🏁 Script executed:

# MatchRoom 엔티티 클래스 정의 확인
rg -n 'class MatchRoom' --type java -A 30

Repository: Sportize/be

Length of output: 3020


🏁 Script executed:

# curMembers를 수정하는 메서드 찾기
rg -n 'curMembers\s*[=+]' --type java -B 2 -A 2

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# MatchParticipant 엔티티와 관련 메서드 확인
rg -n 'class MatchParticipant' --type java -A 20

Repository: Sportize/be

Length of output: 2282


🏁 Script executed:

# createMatch 메서드 전체 코드 확인
rg -n 'public MatchResponse createMatch' --type java -A 20

Repository: Sportize/be

Length of output: 2422


🏁 Script executed:

# `@PostPersist`, `@PostLoad`, listener 등 찾기
rg -n '@PostPersist|@PostLoad|@PreRemove|listener' --type java -B 2 -A 5

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# joinMatch 메서드 확인 (참여 시 curMembers 업데이트 확인)
rg -n 'public void joinMatch' --type java -A 25

Repository: Sportize/be

Length of output: 3128


🏁 Script executed:

# MatchRoom에서 curMembers를 증가시키는 메서드 확인
rg -n 'curMembers' --type java -B 1 -A 1

Repository: Sportize/be

Length of output: 1288


createMatchcurMembers 필드 동기화 누락으로 인한 데이터 불일치 발생.

MatchRoom.create(request)curMembers=0으로 초기화하는 것이 확인되었으며, 42번 줄에서 생성자를 매칭방에 참여자로 추가하고 있습니다. 그러나 이 과정에서 curMembers를 증가시키는 로직이 전혀 없습니다.

더 심각한 문제는 joinMatch 메서드도 curMembers 필드를 사용하지 않고, 대신 matchParticipantRepository.countByMatchRoomAndStatus(..., JOINED)로 참여자를 동적으로 계산한다는 점입니다. 따라서 curMembers는 초기화 후 영구적으로 0으로 유지되며, isFull() 메서드는 항상 false를 반환하게 됩니다.

curMembers를 사용할 예정이라면 필드를 유지관리해야 하고, 그렇지 않다면 제거해야 합니다.

🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java`
around lines 29 - 45, createMatch currently creates a MatchRoom with
curMembers=0 via MatchRoom.create(request), then saves a MatchParticipant but
never updates curMembers, leaving isFull() wrong; either maintain curMembers or
remove the field. Fix by updating the MatchRoom when adding the creator: after
matchParticipantRepository.save(new MatchParticipant(savedRoom, user)) call,
increment and persist the participant count on the same entity (e.g., call a
MatchRoom.incrementCurMembers() or setCurMembers(savedRoom.getCurMembers()+1)
and save via matchRoomRepository.save(savedRoom)), or alternatively remove
curMembers and change isFull() to compute size from
matchParticipantRepository.countByMatchRoomAndStatus(..., JOINED) consistently
(update createMatch to reflect whichever approach you choose).

Copy link
Copy Markdown

@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: 4

🤖 Fix all issues with AI agents
In
`@src/main/java/com/be/sportizebe/domain/facility/controller/SportsFacilityController.java`:
- Around line 46-50: The create endpoint in SportsFacilityController (method
create handling FacilityCreateRequest and returning FacilityResponse) lacks
authorization and returns the default 200; add role-based authorization (e.g.,
annotate the method with `@PreAuthorize`("hasRole('ADMIN') or
hasRole('DEVELOPER')") or `@Secured` as appropriate per the project's security
setup) to restrict access, and change the response to return HTTP 201 Created
(either annotate the method with `@ResponseStatus`(HttpStatus.CREATED) or return a
ResponseEntity<FacilityResponse> with status CREATED) so the endpoint both
enforces permissions and returns the correct status.

In
`@src/main/java/com/be/sportizebe/domain/facility/dto/request/FacilityCreateRequest.java`:
- Around line 27-35: In FacilityCreateRequest change the primitive fields lat
and lng to the wrapper type Double and annotate them with `@NotNull` so missing
JSON keys won't silently bind to 0.0; update the declarations for lat and lng in
the FacilityCreateRequest class and ensure imports for
javax.validation.constraints.NotNull (or jakarta equivalent) are added and
existing `@DecimalMin/`@DecimalMax stay applied.

In
`@src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityServiceImpl.java`:
- Around line 23-26: The class-level `@Transactional` on SportsFacilityServiceImpl
is currently read-write and causes read-only methods to run with write
transactions; fix by reverting the transactional strategy so reads remain
read-only: either change the class-level annotation to `@Transactional`(readOnly =
true) and add `@Transactional` (read-write) only on the create method, or keep the
current class-level `@Transactional` and explicitly annotate getNear and
getMarkers with `@Transactional`(readOnly = true) to ensure they use read-only
transactions.
- Around line 90-97: Replace the IllegalArgumentException in
SportsFacilityServiceImpl.getById with the project's custom exception pattern:
create a FacilityErrorCode enum containing FACILITY_NOT_FOUND (matching other
domain error enums) and throw new
CustomException(FacilityErrorCode.FACILITY_NOT_FOUND) when
sportsFacilityRepository.findById(facilityId) is empty so the global handler
returns 404; update imports and ensure
FacilityMapper.toFacilityResponse(facility) remains unchanged.
🧹 Nitpick comments (1)
src/main/java/com/be/sportizebe/domain/facility/mapper/FacilityMapper.java (1)

32-45: locationnull일 경우 NPE 가능성이 있습니다.

DB 컬럼에 nullable = false 제약이 있으나, 런타임에 sf.getLocation()null이면 Line 33-34에서 NullPointerException이 발생합니다. 방어 코드를 추가하는 것을 권장합니다.

♻️ 방어 코드 제안
     static FacilityResponse toFacilityResponse(SportsFacility sf) {
-        double lat = sf.getLocation().getY();
-        double lng = sf.getLocation().getX();
+        Point location = sf.getLocation();
+        double lat = location != null ? location.getY() : 0.0;
+        double lng = location != null ? location.getX() : 0.0;

         return new FacilityResponse(
                 sf.getId(),

Comment on lines +46 to +50
@Operation(summary = "체육시설 등록", description = "체육시설을 등록합니다. (관리자/개발자용)")
@PostMapping
public FacilityResponse create(@Valid @RequestBody FacilityCreateRequest request) {
return sportsFacilityService.create(request);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

create 엔드포인트에 인증/인가 처리가 누락되었습니다.

Swagger 설명에 "(관리자/개발자용)"이라고 명시되어 있지만, 실제로 권한 검증 로직(@PreAuthorize, @Secured 등)이 없어 누구나 체육시설을 등록할 수 있습니다. 또한 POST 요청의 응답 상태 코드가 기본값 200이 아닌 201(Created)이어야 합니다.

🔒 인가 및 응답 상태 코드 개선 제안
     `@Operation`(summary = "체육시설 등록", description = "체육시설을 등록합니다. (관리자/개발자용)")
     `@PostMapping`
-    public FacilityResponse create(`@Valid` `@RequestBody` FacilityCreateRequest request) {
-        return sportsFacilityService.create(request);
+    `@ResponseStatus`(HttpStatus.CREATED)
+    public FacilityResponse create(`@Valid` `@RequestBody` FacilityCreateRequest request) {
+        return sportsFacilityService.create(request);
     }

인가 처리는 프로젝트의 보안 설정 방식에 따라 별도로 적용해 주세요.

🤖 Prompt for AI Agents
In
`@src/main/java/com/be/sportizebe/domain/facility/controller/SportsFacilityController.java`
around lines 46 - 50, The create endpoint in SportsFacilityController (method
create handling FacilityCreateRequest and returning FacilityResponse) lacks
authorization and returns the default 200; add role-based authorization (e.g.,
annotate the method with `@PreAuthorize`("hasRole('ADMIN') or
hasRole('DEVELOPER')") or `@Secured` as appropriate per the project's security
setup) to restrict access, and change the response to return HTTP 201 Created
(either annotate the method with `@ResponseStatus`(HttpStatus.CREATED) or return a
ResponseEntity<FacilityResponse> with status CREATED) so the endpoint both
enforces permissions and returns the correct status.

Comment on lines +27 to +35
@Schema(description = "위도", example = "37.563")
@DecimalMin("-90.0")
@DecimalMax("90.0")
double lat,

@Schema(description = "경도", example = "126.982")
@DecimalMin("-180.0")
@DecimalMax("180.0")
double lng
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

lat/lng에 primitive double 사용 시 JSON에서 값이 누락되면 기본값 0.0으로 바인딩됩니다.

double 원시 타입은 JSON에서 해당 필드가 누락되어도 0.0으로 바인딩되어, 유효 좌표 범위 내이므로 validation을 통과합니다. Double 래퍼 타입 + @NotNull을 사용하면 좌표 누락을 명시적으로 거부할 수 있습니다.

🛡️ 래퍼 타입 사용 제안
         `@Schema`(description = "위도", example = "37.563")
+        `@NotNull`
         `@DecimalMin`("-90.0")
         `@DecimalMax`("90.0")
-        double lat,
+        Double lat,

         `@Schema`(description = "경도", example = "126.982")
+        `@NotNull`
         `@DecimalMin`("-180.0")
         `@DecimalMax`("180.0")
-        double lng
+        Double lng
📝 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
@Schema(description = "위도", example = "37.563")
@DecimalMin("-90.0")
@DecimalMax("90.0")
double lat,
@Schema(description = "경도", example = "126.982")
@DecimalMin("-180.0")
@DecimalMax("180.0")
double lng
`@Schema`(description = "위도", example = "37.563")
`@NotNull`
`@DecimalMin`("-90.0")
`@DecimalMax`("90.0")
Double lat,
`@Schema`(description = "경도", example = "126.982")
`@NotNull`
`@DecimalMin`("-180.0")
`@DecimalMax`("180.0")
Double lng
🤖 Prompt for AI Agents
In
`@src/main/java/com/be/sportizebe/domain/facility/dto/request/FacilityCreateRequest.java`
around lines 27 - 35, In FacilityCreateRequest change the primitive fields lat
and lng to the wrapper type Double and annotate them with `@NotNull` so missing
JSON keys won't silently bind to 0.0; update the declarations for lat and lng in
the FacilityCreateRequest class and ensure imports for
javax.validation.constraints.NotNull (or jakarta equivalent) are added and
existing `@DecimalMin/`@DecimalMax stay applied.

Comment on lines 23 to 26
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Transactional
public class SportsFacilityServiceImpl implements SportsFacilityService {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

클래스 레벨 @TransactionalreadOnly=true에서 read-write로 변경되어 읽기 전용 메서드에 영향을 줍니다.

getNeargetMarkers는 읽기 전용 작업인데, 클래스 레벨 @Transactional이 read-write로 변경되면서 불필요하게 쓰기 가능 트랜잭션으로 실행됩니다. DB read-only 최적화(예: 복제본 라우팅, dirty checking 비활성화)를 활용할 수 없게 됩니다.

getNeargetMarkers에도 @Transactional(readOnly = true)를 명시하거나, 클래스 레벨을 @Transactional(readOnly = true)로 유지하고 create 메서드에만 @Transactional을 오버라이드하세요.

♻️ 권장 패턴: 클래스 레벨 readOnly + 쓰기 메서드만 오버라이드
 `@Service`
 `@RequiredArgsConstructor`
-@Transactional
+@Transactional(readOnly = true)
 public class SportsFacilityServiceImpl implements SportsFacilityService {

그리고 create 메서드에 쓰기 트랜잭션 명시:

     `@Override`
+    `@Transactional`
     public FacilityResponse create(FacilityCreateRequest request) {
🤖 Prompt for AI Agents
In
`@src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityServiceImpl.java`
around lines 23 - 26, The class-level `@Transactional` on
SportsFacilityServiceImpl is currently read-write and causes read-only methods
to run with write transactions; fix by reverting the transactional strategy so
reads remain read-only: either change the class-level annotation to
`@Transactional`(readOnly = true) and add `@Transactional` (read-write) only on the
create method, or keep the current class-level `@Transactional` and explicitly
annotate getNear and getMarkers with `@Transactional`(readOnly = true) to ensure
they use read-only transactions.

Comment on lines +90 to +97
@Override
@Transactional(readOnly = true)
public FacilityResponse getById(Long facilityId) {
SportsFacility facility = sportsFacilityRepository.findById(facilityId)
.orElseThrow(() -> new IllegalArgumentException("시설이 존재하지 않습니다. id=" + facilityId));

return FacilityMapper.toFacilityResponse(facility);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find global exception handler or controller advice
find . -type f -name "*.java" | head -20 | xargs grep -l "@ControllerAdvice\|@ExceptionHandler" | head -10

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Search for exception-related files
fd -e java "Exception\|Handler" --type f | head -20

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Search for how IllegalArgumentException is handled
rg "IllegalArgumentException" -A5 -B2 --type java | head -50

Repository: Sportize/be

Length of output: 3935


🏁 Script executed:

#!/bin/bash
# Search for custom exceptions in the project
find . -type f -name "*.java" -path "*/exception*" -o -name "*Exception.java" | head -20

Repository: Sportize/be

Length of output: 1126


🏁 Script executed:

#!/bin/bash
# Check the global exception handler
cat src/main/java/com/be/sportizebe/global/exception/GlobalExceptionHandler.java

Repository: Sportize/be

Length of output: 1928


🏁 Script executed:

#!/bin/bash
# Check if there are facility-specific exception error codes
find . -path "*/facility/exception*" -type f -name "*.java"

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Check the CustomException implementation
cat src/main/java/com/be/sportizebe/global/exception/CustomException.java

Repository: Sportize/be

Length of output: 416


🏁 Script executed:

#!/bin/bash
# Check the GlobalErrorCode
cat src/main/java/com/be/sportizebe/global/exception/GlobalErrorCode.java

Repository: Sportize/be

Length of output: 748


🏁 Script executed:

#!/bin/bash
# Check how other services handle resource not found scenarios
rg "orElseThrow" -B2 -A2 --type java | head -100

Repository: Sportize/be

Length of output: 11049


🏁 Script executed:

#!/bin/bash
# Check if there's a facility error code file
find . -type f -name "*FacilityErrorCode*" -o -name "*Facility*Exception*"

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Look at how other services use CustomException for resource not found
rg "CustomException.*RESOURCE_NOT_FOUND\|RESOURCE_NOT_FOUND.*CustomException" --type java -B2 -A2

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Let's check if FacilityErrorCode exists or what pattern should be followed
find . -type f -name "*ErrorCode.java" | xargs ls -la | grep -i facility

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Check structure of an existing error code like UserErrorCode to understand the pattern
cat src/main/java/com/be/sportizebe/domain/user/exception/UserErrorCode.java

Repository: Sportize/be

Length of output: 1201


🏁 Script executed:

#!/bin/bash
# Count how many services incorrectly use IllegalArgumentException for resource not found
rg "orElseThrow.*IllegalArgumentException" --type java

Repository: Sportize/be

Length of output: 407


IllegalArgumentException 대신 프로젝트 커스텀 예외 패턴을 따르세요.

현재 코드는 IllegalArgumentException을 던지므로 글로벌 예외 핸들러에서 일반 Exception으로 처리되어 HTTP 500으로 응답됩니다. 존재하지 않는 리소스 조회 시에는 404 응답이 적절합니다.

프로젝트 내 다른 서비스들(UserServiceImpl, PostServiceImpl, MatchServiceImpl 등)은 CustomException과 도메인별 에러 코드 enum을 사용하여 올바르게 404를 반환합니다. 동일한 패턴을 따르기 위해 FacilityErrorCode enum을 생성하고 CustomException(FacilityErrorCode.FACILITY_NOT_FOUND)을 사용하세요.

🤖 Prompt for AI Agents
In
`@src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityServiceImpl.java`
around lines 90 - 97, Replace the IllegalArgumentException in
SportsFacilityServiceImpl.getById with the project's custom exception pattern:
create a FacilityErrorCode enum containing FACILITY_NOT_FOUND (matching other
domain error enums) and throw new
CustomException(FacilityErrorCode.FACILITY_NOT_FOUND) when
sportsFacilityRepository.findById(facilityId) is empty so the global handler
returns 404; update imports and ensure
FacilityMapper.toFacilityResponse(facility) remains unchanged.

Copy link
Copy Markdown

@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: 4

🧹 Nitpick comments (7)
src/main/java/com/be/sportizebe/domain/match/service/MatchService.java (1)

8-8: 사용되지 않는 User import.

MatchService 인터페이스의 모든 메서드가 Long userId를 사용하므로 User import는 불필요합니다.

♻️ 수정 제안
-import com.be.sportizebe.domain.user.entity.User;
-
🤖 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/MatchService.java` at
line 8, Remove the unused User import from MatchService: the interface methods
use Long userId, so delete the line importing
com.be.sportizebe.domain.user.entity.User from MatchService.java and run a
compile/check to ensure no other references to User remain in the MatchService
interface or its methods.
CLAUDE.md (2)

90-98: 테이블 앞뒤에 빈 줄을 추가해 주세요.

markdownlint MD058 규칙에 따라 테이블은 빈 줄로 둘러싸여야 합니다.

♻️ 수정 제안
 ### 캐시 전략 (Redis)
+
 | 캐시명 | 키 | TTL | 무효화 시점 |
 |--------|----|-----|------------|
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` around lines 90 - 98, The Markdown table under the "### 캐시 전략
(Redis)" section isn't surrounded by blank lines which violates markdownlint
MD058; add a single blank line immediately before the table (above the line " |
캐시명 | 키 | TTL | 무효화 시점 |") and a single blank line immediately after the table
(after the last row "`| facilityNear` / `facilityMarkers` | 좌표+반경 | 5분 | — |`")
in CLAUDE.md so the table is isolated by empty lines.

40-45: 코드 블록에 언어 식별자를 추가해 주세요.

정적 분석 도구(markdownlint)가 Line 40과 51의 fenced code block에 언어 식별자 누락을 보고하고 있습니다.

♻️ 수정 제안
-```
+```properties
 POSTGRES_USER=angora
 POSTGRES_PASSWORD=password
 AWS_ACCESS_KEY=...
 AWS_SECRET_KEY=...

Line 51의 프로젝트 구조 블록도 동일하게 ` ```text `로 변경해 주세요.
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @CLAUDE.md around lines 40 - 45, Add the missing fenced-code block language
identifiers in CLAUDE.md: update the environment variable block containing
"POSTGRES_USER=angora POSTGRES_PASSWORD=password AWS_ACCESS_KEY=...
AWS_SECRET_KEY=..." to use the "properties" language identifier (properties) and change the project structure fenced block (the one reported on line 51) to use "text" (text) so markdownlint no longer flags missing language
specifiers.


</details>

</blockquote></details>
<details>
<summary>src/main/java/com/be/sportizebe/domain/match/dto/request/MatchNearRequest.java (1)</summary><blockquote>

`10-27`: **`@Schema` 어노테이션 누락 및 `limit` 파라미터 부재.**

1. **`@Schema` 누락:** PR 목표에서 "Swagger `@Schema` 적용"을 명시하고 있고, 기존 `FacilityNearRequest`에도 모든 필드에 `@Schema`가 적용되어 있습니다. 일관성을 위해 추가해 주세요.

2. **`limit` 파라미터 부재:** `FacilityNearRequest`에는 `limit` 필드(기본 50)가 있지만, `MatchNearRequest`에는 없습니다. `MatchRoomRepository.findNear` 쿼리에도 `LIMIT` 절이 없어서, 반경 내 매칭 데이터가 많을 경우 전체 결과가 반환됩니다. 성능 및 안정성을 위해 `limit` 파라미터와 쿼리의 `LIMIT` 절 추가를 권장합니다.


<details>
<summary>♻️ `@Schema` 및 limit 추가 제안</summary>

```diff
 `@Getter`
 `@Setter`
 public class MatchNearRequest {
 
+    `@Schema`(description = "위도", example = "37.2662", requiredMode = Schema.RequiredMode.REQUIRED)
     `@NotNull`(message = "위도(lat)는 필수입니다")
     `@DecimalMin`(value = "-90.0", message = "위도는 -90.0 이상이어야 합니다")
     `@DecimalMax`(value = "90.0", message = "위도는 90.0 이하여야 합니다")
     private Double lat;
 
+    `@Schema`(description = "경도", example = "127.0006", requiredMode = Schema.RequiredMode.REQUIRED)
     `@NotNull`(message = "경도(lng)는 필수입니다")
     `@DecimalMin`(value = "-180.0", message = "경도는 -180.0 이상이어야 합니다")
     `@DecimalMax`(value = "180.0", message = "경도는 180.0 이하여야 합니다")
     private Double lng;
 
+    `@Schema`(description = "반경(미터)", example = "1000", defaultValue = "1000")
     `@Min`(value = 100, message = "반경은 최소 100m 이상이어야 합니다")
     `@Max`(value = 10000, message = "반경은 최대 10km까지 가능합니다")
     private Integer radiusM = 1000;
 
-    private SportType sportsName;
+    `@Schema`(description = "조회 개수 제한", example = "50", defaultValue = "50")
+    `@Min`(value = 1, message = "limit는 최소 1 이상이어야 합니다")
+    `@Max`(value = 100, message = "limit는 최대 100까지 가능합니다")
+    private Integer limit = 50;
+
+    `@Schema`(description = "종목 필터(선택)", example = "SOCCER", nullable = true)
+    private SportType sportsName;
 }
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/be/sportizebe/domain/match/dto/request/MatchNearRequest.java`
around lines 10 - 27, Add missing Swagger metadata and a limit parameter:
annotate MatchNearRequest and its fields (lat, lng, radiusM) with `@Schema`
descriptions consistent with FacilityNearRequest, and add a new Integer limit
field with default 50 and an appropriate `@Schema` (e.g., "max results to return")
to control result size; then update MatchRoomRepository.findNear to apply a
LIMIT using this limit parameter so the DB query only returns up to the
requested number of matches. Ensure the new field name is exactly limit and that
the repository method signature and query bind/use it.
```

</details>

</blockquote></details>
<details>
<summary>src/main/java/com/be/sportizebe/domain/match/dto/response/MatchNearResponse.java (1)</summary><blockquote>

`17-27`: **`SportType.valueOf()` / `MatchStatus.valueOf()` 호출 시 `IllegalArgumentException` 가능성.**

DB에 예기치 않은 값이 저장되어 있거나 enum이 변경되면 `valueOf()`가 `IllegalArgumentException`을 던져 500 에러가 발생합니다. 현재 `status`는 쿼리에서 `'OPEN'`으로 필터링되므로 비교적 안전하지만, `sportsName`은 필터링 없이 모든 값이 올 수 있습니다.

기존 패턴과 일관성 있으므로 즉시 수정이 필수는 아니지만, 방어적 처리를 고려해 볼 수 있습니다.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/be/sportizebe/domain/match/dto/response/MatchNearResponse.java`
around lines 17 - 27, The from factory (MatchNearResponse.from) uses
SportType.valueOf(p.getSportsName()) and MatchStatus.valueOf(p.getStatus())
which can throw IllegalArgumentException for unexpected DB values; modify from
to defensively map the projection values to enums (e.g., use a safe lookup
method on SportType and MatchStatus that returns an Optional or a default like
UNKNOWN/OPEN) or wrap the valueOf calls in a try/catch and substitute a sensible
default, ensuring you reference MatchNearProjection.getSportsName/getStatus when
validating; update any helper methods (e.g., SportType::fromString or
MatchStatus::fromString) to centralize the safe parsing logic.
```

</details>

</blockquote></details>
<details>
<summary>src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java (2)</summary><blockquote>

`23-30`: **`@Transactional` 패턴 역전 — 읽기 전용을 클래스 레벨 기본값으로 설정 권장.**

현재 클래스 레벨에 `@Transactional`(쓰기)을 설정하고 읽기 메서드마다 `@Transactional(readOnly = true)`로 재정의하는 구조입니다. 관용적인 Spring 패턴은 클래스 레벨에 `readOnly = true`를 지정하고, 쓰기가 필요한 메서드에서만 `@Transactional`로 재정의하는 방식입니다.

<details>
<summary>♻️ 권장 패턴으로 전환</summary>

```diff
-@Transactional
+@Transactional(readOnly = true)
 public class MatchServiceImpl implements MatchService {

     `@Override`
+    `@Transactional`
     public MatchResponse createMatch(Long userId, MatchCreateRequest request) { ... }

     `@Override`
+    `@Transactional`
     public void joinMatch(Long matchId, Long userId) { ... }

     `@Override`
-    `@Transactional`(readOnly = true)
     public MatchDetailResponse getMatchDetail(Long matchId, Long userId) { ... }

     `@Override`
-    `@Transactional`(readOnly = true)
     public List<MatchNearResponse> getNearMatches(MatchNearRequest request) { ... }
 }
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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 23 - 30, Change the transactional default on MatchServiceImpl from
write-enabled to read-only by replacing the class-level `@Transactional` with
`@Transactional`(readOnly = true) on the MatchServiceImpl class, and then annotate
any mutating methods (e.g., methods that call matchRoomRepository.save(...),
matchParticipantRepository.save(...), delete or update operations) with a plain
`@Transactional` to re-enable write transactions; update only the annotations—do
not change repository calls or method signatures.
```

</details>

---

`95-107`: **`getSportsName()` null 처리 → `Optional`로 간소화 가능.**

lines 98–99의 3항 연산자는 `Optional.map`으로 더 간결하게 표현할 수 있습니다. `MatchRoomRepository.findNear()`는 sportsName 파라미터로 null을 허용하므로 `orElse(null)`을 통한 null 처리가 적절합니다.

<details>
<summary>♻️ 제안 리팩토링</summary>

```diff
-        String sportsName = request.getSportsName() == null
-                ? null : request.getSportsName().name();
+        String sportsName = Optional.ofNullable(request.getSportsName())
+                .map(Enum::name)
+                .orElse(null);
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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 95 - 107, Replace the ternary null-check in getNearMatches with an
Optional-based mapping: take MatchNearRequest.getSportsName(), wrap it with
Optional.ofNullable(...).map(enum -> enum.name()).orElse(null) and pass that
result into matchRoomRepository.findNear (which accepts null). Update the local
variable currently named sportsName accordingly in the getNearMatches method so
the code is more concise and null-safe while keeping the same behavior.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In
@src/main/java/com/be/sportizebe/domain/match/repository/MatchRoomRepository.java:

  • Around line 13-36: The query in MatchRoomRepository (the native @Query in the
    repository method that returns nearby open match rooms) is missing a LIMIT
    clause which can cause unbounded result sets; modify the SQL to accept and apply
    a limit (e.g., add "LIMIT :limit" after ORDER BY distanceM) and update the
    repository method signature to accept an int/long limit parameter (or switch to
    a Pageable parameter) so callers can bound results; ensure the parameter name
    matches (e.g., :limit) and the method (in MatchRoomRepository) uses that
    parameter when executing the native query.

In @src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java:

  • Around line 32-35: Remove the informal/dummy comment from the production
    service method and replace the temporary implementation with a safe placeholder:
    in MatchServiceImpl, update createMatch(Long userId, MatchCreateRequest request)
    by deleting the HACK comment and either (a) add a clear TODO comment referencing
    an issue/owner and leave a proper implementation task, or (b) if the method
    shouldn't be callable yet, make it explicitly fail fast by throwing new
    UnsupportedOperationException("createMatch is admin-only / not implemented - see
    ISSUE-XXXXX") so the admin-only behavior is enforced until a proper
    implementation is provided.
  • Around line 66-80: In joinMatch (MatchServiceImpl) after saving the new
    MatchParticipant, increment matchRoom.curMembers and if curMembers >=
    matchRoom.getMaxMembers() set matchRoom.status to FULL, then persist the updated
    matchRoom (e.g., via matchRoomRepository.save or matchRoom.update/save method)
    so DB state and DTOs (curMembers/status) stay consistent; ensure you adjust any
    helper methods like MatchRoom.incrementCurMembers() if present and keep this
    update inside the existing transaction surrounding joinMatch.

In @src/main/java/com/be/sportizebe/domain/user/entity/SportType.java:

  • Around line 3-8: Sport and facility types are not validated together, allowing
    e.g. SOCCER matches at TENNIS facilities; add a validation that ensures the
    chosen SportType is supported by the FacilityType before creating a match.
    Implement this by adding a support-check method (e.g., a boolean
    supports(SportType) on FacilityType or a static mapping in SportType) and call
    it from the match creation flow (e.g., in MatchCreateRequest validation or in
    the MatchService method that handles creation) to reject/throw when the
    facilityId's FacilityType does not support the requested SportType. Ensure the
    check uses the FacilityType enum value resolved from facilityId and produces a
    clear validation error if unsupported.

Duplicate comments:
In @src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java:

  • Around line 83-93: getMatchDetail currently calls
    MatchDetailResponse.of(matchRoom, user) but that static factory may not exist
    and MatchRoom might not contain participants, causing compile/runtime errors;
    either add a public static MatchDetailResponse.of(MatchRoom, User) method to
    MatchDetailResponse that internally pulls whatever fields it needs (including
    participants) or change MatchServiceImpl.getMatchDetail to explicitly load
    participants via matchParticipantRepository (e.g., findByMatchRoom or similar)
    and then call an available factory/signature (e.g.,
    MatchDetailResponse.of(matchRoom, user, participants)) or construct the DTO
    directly; update the code paths that reference MatchDetailResponse.of and any
    constructors to match the chosen signature so the code compiles and participants
    are included in the response.
  • Around line 50-81: The joinMatch method has a TOCTOU race: it reads
    joinedCount via countByMatchRoomAndStatus and then saves a participant, allowing
    concurrent threads to exceed maxMembers; fix by adding a pessimistic-write
    repository method (e.g., MatchRoomRepository.findByIdForUpdate annotated with
    @Lock(LockModeType.PESSIMISTIC_WRITE) and a matching @Query) and use that method
    inside joinMatch to load and lock the MatchRoom before performing
    countByMatchRoomAndStatus, existsByMatchRoomAndUserAndStatus, and
    matchParticipantRepository.save(new MatchParticipant(...)); ensure joinMatch
    remains @Transactional so the lock is held for the checks and save.
  • Around line 41-45: When auto-adding the host after MatchRoom.create(request)
    you must update the MatchRoom.curMembers field to reflect the new participant;
    after matchParticipantRepository.save(new MatchParticipant(savedRoom, user))
    increment or set savedRoom.curMembers (e.g.,
    savedRoom.setCurMembers(savedRoom.getCurMembers() + 1) or
    savedRoom.setCurMembers(1) for a fresh room) and persist it via
    matchRoomRepository.save(savedRoom) so curMembers, isFull() and other
    field-based logic stay consistent with the actual participants. Ensure you
    reference MatchRoom.create(request), the curMembers field/getter/setter on
    MatchRoom, matchParticipantRepository.save(...) and
    matchRoomRepository.save(...) when making the change.

Nitpick comments:
In @CLAUDE.md:

  • Around line 90-98: The Markdown table under the "### 캐시 전략 (Redis)" section
    isn't surrounded by blank lines which violates markdownlint MD058; add a single
    blank line immediately before the table (above the line " | 캐시명 | 키 | TTL | 무효화
    시점 |") and a single blank line immediately after the table (after the last row
    "| facilityNear / facilityMarkers | 좌표+반경 | 5분 | — |`") in CLAUDE.md so the
    table is isolated by empty lines.
  • Around line 40-45: Add the missing fenced-code block language identifiers in
    CLAUDE.md: update the environment variable block containing
    "POSTGRES_USER=angora POSTGRES_PASSWORD=password AWS_ACCESS_KEY=...
    AWS_SECRET_KEY=..." to use the "properties" language identifier (properties) and change the project structure fenced block (the one reported on line 51) to use "text" (text) so markdownlint no longer flags missing language
    specifiers.

In
@src/main/java/com/be/sportizebe/domain/match/dto/request/MatchNearRequest.java:

  • Around line 10-27: Add missing Swagger metadata and a limit parameter:
    annotate MatchNearRequest and its fields (lat, lng, radiusM) with @Schema
    descriptions consistent with FacilityNearRequest, and add a new Integer limit
    field with default 50 and an appropriate @Schema (e.g., "max results to return")
    to control result size; then update MatchRoomRepository.findNear to apply a
    LIMIT using this limit parameter so the DB query only returns up to the
    requested number of matches. Ensure the new field name is exactly limit and that
    the repository method signature and query bind/use it.

In
@src/main/java/com/be/sportizebe/domain/match/dto/response/MatchNearResponse.java:

  • Around line 17-27: The from factory (MatchNearResponse.from) uses
    SportType.valueOf(p.getSportsName()) and MatchStatus.valueOf(p.getStatus())
    which can throw IllegalArgumentException for unexpected DB values; modify from
    to defensively map the projection values to enums (e.g., use a safe lookup
    method on SportType and MatchStatus that returns an Optional or a default like
    UNKNOWN/OPEN) or wrap the valueOf calls in a try/catch and substitute a sensible
    default, ensuring you reference MatchNearProjection.getSportsName/getStatus when
    validating; update any helper methods (e.g., SportType::fromString or
    MatchStatus::fromString) to centralize the safe parsing logic.

In @src/main/java/com/be/sportizebe/domain/match/service/MatchService.java:

  • Line 8: Remove the unused User import from MatchService: the interface methods
    use Long userId, so delete the line importing
    com.be.sportizebe.domain.user.entity.User from MatchService.java and run a
    compile/check to ensure no other references to User remain in the MatchService
    interface or its methods.

In @src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java:

  • Around line 23-30: Change the transactional default on MatchServiceImpl from
    write-enabled to read-only by replacing the class-level @Transactional with
    @Transactional(readOnly = true) on the MatchServiceImpl class, and then annotate
    any mutating methods (e.g., methods that call matchRoomRepository.save(...),
    matchParticipantRepository.save(...), delete or update operations) with a plain
    @Transactional to re-enable write transactions; update only the annotations—do
    not change repository calls or method signatures.
  • Around line 95-107: Replace the ternary null-check in getNearMatches with an
    Optional-based mapping: take MatchNearRequest.getSportsName(), wrap it with
    Optional.ofNullable(...).map(enum -> enum.name()).orElse(null) and pass that
    result into matchRoomRepository.findNear (which accepts null). Update the local
    variable currently named sportsName accordingly in the getNearMatches method so
    the code is more concise and null-safe while keeping the same behavior.

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +13 to +36
@Query(value = """
SELECT
mr.id AS matchId,
mr.sports_name AS sportsName,
mr.facility_id AS facilityId,
sf.facility_name AS facilityName,
mr.cur_members AS curMembers,
mr.max_members AS maxMembers,
mr.status AS status,
ST_Distance(
sf.location,
ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography
) AS distanceM
FROM match_rooms mr
JOIN sports_facilities sf ON mr.facility_id = sf.id
WHERE ST_DWithin(
sf.location,
ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography,
:radiusM
)
AND mr.status = 'OPEN'
AND (:sportsName IS NULL OR mr.sports_name = :sportsName)
ORDER BY distanceM
""", nativeQuery = true)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

LIMIT 절 누락 — 대량 결과 반환 가능.

SportsFacilityRepository.findNear와 달리 LIMIT 절이 없습니다. 반경 내 OPEN 상태의 매칭이 많을 경우 모든 행이 반환되어 성능 저하 및 메모리 문제가 발생할 수 있습니다.

🐛 LIMIT 절 및 파라미터 추가 제안
         AND mr.status = 'OPEN'
         AND (:sportsName IS NULL OR mr.sports_name = :sportsName)
         ORDER BY distanceM
+        LIMIT :limit
         """, nativeQuery = true)
     List<MatchNearProjection> findNear(
             `@Param`("lat") double lat,
             `@Param`("lng") double lng,
             `@Param`("radiusM") int radiusM,
-            `@Param`("sportsName") String sportsName
+            `@Param`("sportsName") String sportsName,
+            `@Param`("limit") int limit
     );
🤖 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/MatchRoomRepository.java`
around lines 13 - 36, The query in MatchRoomRepository (the native `@Query` in the
repository method that returns nearby open match rooms) is missing a LIMIT
clause which can cause unbounded result sets; modify the SQL to accept and apply
a limit (e.g., add "LIMIT :limit" after ORDER BY distanceM) and update the
repository method signature to accept an int/long limit parameter (or switch to
a Pageable parameter) so callers can bound results; ensure the parameter name
matches (e.g., :limit) and the method (in MatchRoomRepository) uses that
parameter when executing the native query.

Comment on lines +32 to +35
@Override
// 실제로는 관리자용 메서드인데 더미 넣으려고 만듦
public MatchResponse createMatch(Long userId, MatchCreateRequest request) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

임시/더미 코드 주석을 프로덕션 코드에서 제거해야 합니다.

Line 33의 // 실제로는 관리자용 메서드인데 더미 넣으려고 만듦 주석은 해당 메서드가 테스트용 임시 구현임을 명시하고 있습니다. 프로덕션 서비스 레이어에 HACK성 주석이 남아 있으면 실수로 해당 코드가 배포될 위험이 있습니다. 관리자 전용 기능이라면 별도 TODO 이슈로 분리하거나, 실제 구현 전까지 throw new UnsupportedOperationException()으로 막아두는 것이 안전합니다.

🤖 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 32 - 35, Remove the informal/dummy comment from the production
service method and replace the temporary implementation with a safe placeholder:
in MatchServiceImpl, update createMatch(Long userId, MatchCreateRequest request)
by deleting the HACK comment and either (a) add a clear TODO comment referencing
an issue/owner and leave a proper implementation task, or (b) if the method
shouldn't be callable yet, make it explicitly fail fast by throwing new
UnsupportedOperationException("createMatch is admin-only / not implemented - see
ISSUE-XXXXX") so the admin-only behavior is enforced until a proper
implementation is provided.

Comment on lines +66 to +80
if (joinedCount >= matchRoom.getMaxMembers()) {
throw new CustomException(MatchErrorCode.MATCH_FULL);
}

// 4) 동일 유저가 이미 참가(JOINED) 중인지 중복 체크
boolean alreadyJoined = matchParticipantRepository.existsByMatchRoomAndUserAndStatus(
matchRoom, user, MatchParticipantStatus.JOINED
);

if (alreadyJoined) {
throw new CustomException(MatchErrorCode.ALREADY_JOINED);
}

// 5) 참가자 엔티티 생성 후 저장
matchParticipantRepository.save(new MatchParticipant(matchRoom, user));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find MatchRoom and MatchStatus related files
fd -e java | grep -iE '(MatchRoom|MatchStatus|MatchService)' | head -20

Repository: Sportize/be

Length of output: 417


🏁 Script executed:

#!/bin/bash
# Search for updateStatus and setStatus methods in Java files
rg -n 'updateStatus|setStatus|MatchStatus\.' --type java | head -30

Repository: Sportize/be

Length of output: 444


🏁 Script executed:

#!/bin/bash
# Get the full joinMatch method context
rg -n 'joinMatch' --type java -B 5 -A 50 | head -100

Repository: Sportize/be

Length of output: 11484


🏁 Script executed:

#!/bin/bash
# Read the MatchRoom entity
cat src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java

Repository: Sportize/be

Length of output: 1629


🏁 Script executed:

#!/bin/bash
# Read the MatchStatus enum
cat src/main/java/com/be/sportizebe/domain/match/entity/MatchStatus.java

Repository: Sportize/be

Length of output: 198


🏁 Script executed:

#!/bin/bash
# Search for curMembers updates and setter methods
rg -n 'curMembers|setCurMembers' --type java -B 2 -A 2

Repository: Sportize/be

Length of output: 3241


🏁 Script executed:

#!/bin/bash
# Search for any Setter annotation or status update patterns
rg -n '@Setter|\.setStatus|\.setCurMembers' --type java -B 1 -A 1

Repository: Sportize/be

Length of output: 1429


🏁 Script executed:

#!/bin/bash
# Check if there are any listeners or event handlers for MatchRoom
rg -n '@PrePersist|@PostPersist|@PreUpdate|@PostUpdate' --type java

Repository: Sportize/be

Length of output: 315


🏁 Script executed:

#!/bin/bash
# Check how curMembers is used in queries and responses
rg -n 'curMembers|cur_members' --type java -B 1 -A 1

Repository: Sportize/be

Length of output: 2007


🏁 Script executed:

#!/bin/bash
# Check if there are any queries that filter by status
rg -n 'MatchStatus\.|\.status|= status' --type java -B 2 -A 2

Repository: Sportize/be

Length of output: 12257


정원 충족 시 MatchRoom.statuscurMembers 동기화 필요

joinMatch 메서드에서 참가자를 저장한 후 정원이 다 찼을 때 MatchRoom.statusFULL로 갱신되지 않습니다. 또한 curMembers도 함께 증가하지 않아 데이터 불일치가 발생합니다.

이로 인해:

  • getNearMatches에서 status = 'OPEN' 필터링 시 정원이 찬 방이 여전히 노출될 수 있습니다
  • 응답 DTO의 curMembers 값이 실제 참가 인원과 맞지 않습니다
🐛 정원 충족 시 상태 및 인원 동기화
         // 5) 참가자 엔티티 생성 후 저장
         matchParticipantRepository.save(new MatchParticipant(matchRoom, user));
+
+        // 6) 정원이 다 찼으면 매칭방 상태와 인원을 동기화
+        long newCount = joinedCount + 1;
+        if (newCount >= matchRoom.getMaxMembers()) {
+            matchRoom.setStatus(MatchStatus.FULL);
+            matchRoom.setCurMembers((int) newCount);
+            matchRoomRepository.save(matchRoom);
+        } else {
+            matchRoom.setCurMembers((int) newCount);
+            matchRoomRepository.save(matchRoom);
+        }
🤖 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 66 - 80, In joinMatch (MatchServiceImpl) after saving the new
MatchParticipant, increment matchRoom.curMembers and if curMembers >=
matchRoom.getMaxMembers() set matchRoom.status to FULL, then persist the updated
matchRoom (e.g., via matchRoomRepository.save or matchRoom.update/save method)
so DB state and DTOs (curMembers/status) stay consistent; ensure you adjust any
helper methods like MatchRoom.incrementCurMembers() if present and keep this
update inside the existing transaction surrounding joinMatch.

Comment on lines 3 to 8
public enum SportType {
SOCCER,
BASKETBALL
BASKETBALL,
BADMINTON,

}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find and display FacilityType enum definition
find . -name "FacilityType.java" -type f | head -5

Repository: Sportize/be

Length of output: 129


🏁 Script executed:

#!/bin/bash
# Search for SportType and FacilityType usage in the codebase
rg -n "SportType|FacilityType" --type=java -g '!**/test/**' | head -100

Repository: Sportize/be

Length of output: 5937


🏁 Script executed:

#!/bin/bash
# Look for any mapping or relationship between SportType and FacilityType
rg -n "facility.*sport|sport.*facility" --type=java -i -g '!**/test/**' -C2 | head -80

Repository: Sportize/be

Length of output: 9139


🏁 Script executed:

cat -n ./src/main/java/com/be/sportizebe/domain/facility/entity/FacilityType.java

Repository: Sportize/be

Length of output: 280


🏁 Script executed:

# Check MatchRoom entity to see how facility and sport are connected
cat -n ./src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java | head -50

Repository: Sportize/be

Length of output: 1687


🏁 Script executed:

# Look for any mapping or validation logic between facility and sport types
rg -n "FacilityType.*SportType|SportType.*FacilityType" --type=java -g '!**/test/**'

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Check if there's any validation logic that constrains facility type to sport type
rg -n "facilityId|sportsName" --type=java -g '!**/test/**' -C3 | grep -A5 -B5 "facility\|sport" | head -100

Repository: Sportize/be

Length of output: 10188


🏁 Script executed:

# Check MatchCreateRequest to see what parameters are passed
cat -n ./src/main/java/com/be/sportizebe/domain/match/dto/request/MatchCreateRequest.java

Repository: Sportize/be

Length of output: 812


🏁 Script executed:

# Check match service/controller to see if there's validation between facility and sport
rg -n "createMatch|MatchRoom\|create" --type=java src/main/java/com/be/sportizebe/domain/match/ -A5 | head -80

Repository: Sportize/be

Length of output: 2770


매칭 생성 시 스포츠 종목과 체육시설 타입 간 검증 로직 부재

SportType은 3가지(SOCCER, BASKETBALL, BADMINTON)만 지원하지만 FacilityType은 6가지를 지원합니다. 현재 구조에서는 매칭 생성 시 SportTypefacilityId를 독립적으로 받아 처리하며, 두 값 간의 유효성 검증이 없습니다. 따라서 사용자가 SOCCER 매칭을 TENNIS 시설에 생성하는 것을 방지하지 못합니다. MatchCreateRequest에서 선택한 스포츠 종목이 해당 시설에서 실제로 지원되는 종목인지 검증하는 로직을 추가하거나, SportType을 확장하여 두 enum을 정렬해야 합니다.

🤖 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/user/entity/SportType.java` around
lines 3 - 8, Sport and facility types are not validated together, allowing e.g.
SOCCER matches at TENNIS facilities; add a validation that ensures the chosen
SportType is supported by the FacilityType before creating a match. Implement
this by adding a support-check method (e.g., a boolean supports(SportType) on
FacilityType or a static mapping in SportType) and call it from the match
creation flow (e.g., in MatchCreateRequest validation or in the MatchService
method that handles creation) to reject/throw when the facilityId's FacilityType
does not support the requested SportType. Ensure the check uses the FacilityType
enum value resolved from facilityId and produces a clear validation error if
unsupported.

@angoroa angoroa merged commit f534071 into develop Feb 20, 2026
1 check was pending
Copy link
Copy Markdown

@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 (1)
src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java (1)

16-16: JPA 엔티티의 기본 생성자 접근 제한 권장

@NoArgsConstructorpublic 기본 생성자를 생성합니다. JPA 엔티티는 protected로 제한해도 스펙상 문제가 없으며, 외부에서의 직접 인스턴스화를 방지하는 데 도움이 됩니다.

✨ 수정 제안
-@NoArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
🤖 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/entity/MatchRoom.java` at line
16, 현재 MatchRoom 엔티티에 사용된 `@NoArgsConstructor가` public 기본 생성자를 생성하므로 JPA 관점에서 접근
범위를 좁혀야 합니다; MatchRoom 클래스에서 `@NoArgsConstructor를` `@NoArgsConstructor`(access =
AccessLevel.PROTECTED)로 변경하고 lombok.AccessLevel를 import하여 기본 생성자를 protected로 제한해
외부 직접 인스턴스화를 방지하세요.
🤖 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/match/dto/response/MatchDetailResponse.java`:
- Around line 41-44: MatchDetailResponse.of(...) is triggering N+1 queries
because p.getUser().getId() initializes lazy User proxies; fix by adding a
repository fetch method (e.g., MatchRoomRepository.findByIdWithParticipants)
that uses JOIN FETCH for m.participants and p.user, then update MatchServiceImpl
to call findByIdWithParticipants(matchId) (or equivalent) and pass the
fully-initialized MatchRoom to MatchDetailResponse.of so participantIds mapping
no longer fires extra selects.

In
`@src/main/java/com/be/sportizebe/domain/match/dto/response/MatchNearResponse.java`:
- Around line 17-28: The from(MatchNearProjection p) method currently calls
SportType.valueOf(p.getSportsName()) and MatchStatus.valueOf(p.getStatus())
without guarding against invalid DB strings; update MatchNearResponse.from to
use defensive conversion helpers (e.g., private static SportType
toSportType(String) and private static MatchStatus toMatchStatus(String)) that
catch IllegalArgumentException and throw a descriptive runtime/checked exception
or return a safe default, and replace the direct valueOf calls with those
helpers to avoid unhandled 500s when enum strings are unexpected.

---

Duplicate comments:
In `@src/main/java/com/be/sportizebe/common/enums/SportType.java`:
- Around line 3-8: SportType currently lacks values corresponding to
FacilityType (e.g., TENNIS, BOWLING, ETC) and there is no validation preventing
mismatched pairings; add the missing enum constants to SportType (e.g., TENNIS,
BOWLING, ETC) and implement a compatibility check in the matching creation flow
(e.g., MatchingService.createMatching or equivalent) that compares SportType to
FacilityType and throws/returns a validation error (IllegalArgumentException or
domain ValidationException) when they are incompatible; place the compatibility
logic in a single helper (e.g.,
MatchValidator.validateSportFacilityCompatibility) for reuse and include clear
error messages identifying the sport and facility values.

In `@src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java`:
- Line 14: The MatchRoom entity is annotated with `@Builder` while its superclass
BaseTimeEntity uses `@SuperBuilder`, so parent fields won't be included in the
generated builder; replace `@Builder` on the MatchRoom class with Lombok's
`@SuperBuilder` (and keep or add `@NoArgsConstructor/`@AllArgsConstructor as needed)
so the generated builder includes inherited fields from BaseTimeEntity and
matches the superclass builder strategy.
- Around line 32-34: MatchRoom currently has only a getter for curMembers so the
participant count never changes and isFull() will not work; add controlled
mutators on the MatchRoom entity such as incrementCurMembers(),
decrementCurMembers(), and/or setCurMembers(int) (or a single updateMembers(int
delta)) to update the `@Column` int curMembers and enforce bounds (e.g., not below
0 or above capacity) inside those methods; update any places that create/remove
participants to call these methods rather than modifying fields directly and
ensure isFull() reads curMembers to determine fullness.

---

Nitpick comments:
In `@src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java`:
- Line 16: 현재 MatchRoom 엔티티에 사용된 `@NoArgsConstructor가` public 기본 생성자를 생성하므로 JPA
관점에서 접근 범위를 좁혀야 합니다; MatchRoom 클래스에서 `@NoArgsConstructor를`
`@NoArgsConstructor`(access = AccessLevel.PROTECTED)로 변경하고 lombok.AccessLevel를
import하여 기본 생성자를 protected로 제한해 외부 직접 인스턴스화를 방지하세요.

Comment on lines +41 to +44
List<Long> participantIds = matchRoom.getParticipants().stream()
.filter(p -> p.getStatus() == MatchParticipantStatus.JOINED)
.map(p -> p.getUser().getId())
.toList();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# MatchParticipant의 필드 레벨 어노테이션 확인
find . -name "MatchParticipant.java" -type f

Repository: Sportize/be

Length of output: 130


🏁 Script executed:

# MatchRoom 리포지토리 확인
find . -name "*MatchRoom*Repository*.java" -type f

Repository: Sportize/be

Length of output: 137


🏁 Script executed:

cat -n src/main/java/com/be/sportizebe/domain/match/entity/MatchParticipant.java

Repository: Sportize/be

Length of output: 2150


🏁 Script executed:

cat -n src/main/java/com/be/sportizebe/domain/match/repository/MatchRoomRepository.java

Repository: Sportize/be

Length of output: 1919


🏁 Script executed:

cat -n src/main/java/com/be/sportizebe/domain/match/dto/response/MatchDetailResponse.java

Repository: Sportize/be

Length of output: 2316


🏁 Script executed:

# Check where MatchDetailResponse.of() is called - verify if entities are properly fetched
rg -n "MatchDetailResponse.of|findById" --type=java src/main/java/com/be/sportizebe/domain/match/

Repository: Sportize/be

Length of output: 998


p.getUser().getId() 호출이 N+1 쿼리를 유발합니다

MatchParticipant는 필드 레벨에 @ManyToOne 어노테이션을 선언하고 있으며, MatchServiceImplfindById() 호출(라인 87)은 일반 리포지토리 메서드로 participants를 eager loading하지 않습니다. 따라서 MatchDetailResponse.of()에서 스트림으로 순회하며 각 참여자마다 p.getUser().getId() 호출 시, User 엔티티의 LAZY 프록시를 초기화하기 위해 개별 SELECT가 발생합니다.

해결책: MatchRoomRepository에 다음과 같이 JOIN FETCH 메서드를 추가하고 MatchServiceImpl에서 이를 사용하세요.

예시 코드
// MatchRoomRepository에 추가
`@Query`("SELECT m FROM MatchRoom m " +
       "LEFT JOIN FETCH m.participants p " +
       "LEFT JOIN FETCH p.user " +
       "WHERE m.id = :id")
Optional<MatchRoom> findByIdWithParticipants(`@Param`("id") Long id);

// MatchServiceImpl에서 사용
MatchRoom matchRoom = matchRoomRepository.findByIdWithParticipants(matchId)
    .orElseThrow(...);
🤖 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/dto/response/MatchDetailResponse.java`
around lines 41 - 44, MatchDetailResponse.of(...) is triggering N+1 queries
because p.getUser().getId() initializes lazy User proxies; fix by adding a
repository fetch method (e.g., MatchRoomRepository.findByIdWithParticipants)
that uses JOIN FETCH for m.participants and p.user, then update MatchServiceImpl
to call findByIdWithParticipants(matchId) (or equivalent) and pass the
fully-initialized MatchRoom to MatchDetailResponse.of so participantIds mapping
no longer fires extra selects.

Comment on lines +17 to +28
public static MatchNearResponse from(MatchNearProjection p) {
return new MatchNearResponse(
p.getMatchId(),
SportType.valueOf(p.getSportsName()),
p.getFacilityId(),
p.getFacilityName(),
p.getCurMembers(),
p.getMaxMembers(),
MatchStatus.valueOf(p.getStatus()),
(int) Math.round(p.getDistanceM())
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

valueOf() 호출 시 알 수 없는 값에 대한 예외 처리 부재

SportType.valueOf(p.getSportsName())MatchStatus.valueOf(p.getStatus())는 DB에 저장된 값이 현재 enum 상수와 일치하지 않을 경우(데이터 마이그레이션 불일치, 오타 등) IllegalArgumentException을 던지며 500 에러를 야기합니다. DB 값과 enum이 @Enumerated(EnumType.STRING) 매핑으로 관리되더라도, 방어적 처리를 권장합니다.

🛡️ 방어적 처리 제안
-                SportType.valueOf(p.getSportsName()),
+                parseEnum(SportType.class, p.getSportsName()),

또는 각 변환 시 try-catch를 적용:

         return new MatchNearResponse(
                 p.getMatchId(),
-                SportType.valueOf(p.getSportsName()),
+                toSportType(p.getSportsName()),
                 p.getFacilityId(),
                 p.getFacilityName(),
                 p.getCurMembers(),
                 p.getMaxMembers(),
-                MatchStatus.valueOf(p.getStatus()),
+                toMatchStatus(p.getStatus()),
                 (int) Math.round(p.getDistanceM())
         );
private static SportType toSportType(String value) {
    try {
        return SportType.valueOf(value);
    } catch (IllegalArgumentException e) {
        throw new IllegalArgumentException("알 수 없는 SportType 값: " + value, e);
    }
}
🤖 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/dto/response/MatchNearResponse.java`
around lines 17 - 28, The from(MatchNearProjection p) method currently calls
SportType.valueOf(p.getSportsName()) and MatchStatus.valueOf(p.getStatus())
without guarding against invalid DB strings; update MatchNearResponse.from to
use defensive conversion helpers (e.g., private static SportType
toSportType(String) and private static MatchStatus toMatchStatus(String)) that
catch IllegalArgumentException and throw a descriptive runtime/checked exception
or return a safe default, and replace the direct valueOf calls with those
helpers to avoid unhandled 500s when enum strings are unexpected.

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