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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -37,7 +38,7 @@ public class MatchController {

private final MatchService matchService;

@Operation(summary = "매칭 생성")
@Operation(summary = "매칭 생성 (관리자 전용 기능)", description = "현재는 매칭 더미데이터 넣는 용도")
@PostMapping
public ResponseEntity<BaseResponse<MatchResponse>> createMatch(
@AuthenticationPrincipal UserAuthInfo userAuthInfo,
Expand Down Expand Up @@ -68,7 +69,17 @@ public ResponseEntity<BaseResponse<MatchDetailResponse>> getMatchDetail(
return ResponseEntity.ok(BaseResponse.success("매칭 상세 조회 성공", response));
}

@Operation(summary = "내 주변 매칭 목록 조회")
@Operation(summary = "매칭 취소")
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

@Operation summary가 의미상 불명확합니다.

"매칭 취소"는 매칭 자체를 취소(삭제)하는 것으로 오해될 수 있습니다. 참여를 철회한다는 의도를 명확히 하려면 "매칭 참여 취소" 또는 "매칭 나가기"로 수정을 권장합니다.

✏️ 수정 제안
-    `@Operation`(summary = "매칭 취소")
+    `@Operation`(summary = "매칭 참여 취소", description = "현재 참여 중인 매칭에서 나갑니다.")
📝 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
@Operation(summary = "매칭 취소")
`@Operation`(summary = "매칭 참여 취소", description = "현재 참여 중인 매칭에서 나갑니다.")
🤖 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/controller/MatchController.java`
at line 72, The Swagger `@Operation` summary "매칭 취소" in MatchController is
ambiguous; update the annotation on the relevant controller method in class
MatchController (the method annotated with `@Operation`(summary = "...")) to a
clearer phrase such as "매칭 참여 취소" or "매칭 나가기" so it explicitly indicates
withdrawing participation rather than deleting the match; ensure only the
summary string is changed in the `@Operation` annotation for that method.

@DeleteMapping("/{matchId}/leave")
public ResponseEntity<BaseResponse<Void>> leaveMatch(
@PathVariable Long matchId,
@AuthenticationPrincipal UserAuthInfo userAuthInfo
) {
matchService.leaveMatch(matchId, userAuthInfo.getId());
return ResponseEntity.ok(BaseResponse.success("매칭 취소 성공", null));
}

@Operation(summary = "내 주변 매칭 목록 조회", description = "매칭상태가 OPEN인 매칭들만 보여준다.")
@GetMapping("/near")
public ResponseEntity<BaseResponse<List<MatchNearResponse>>> getNearMatches(
@ParameterObject @Valid @ModelAttribute MatchNearRequest request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ public record MatchCreateRequest(

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

@Schema(description = "몇 분동안 진행되는 매칭인지", example = "120")
int durationMinutes,
Comment on lines +21 to +22
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

durationMinutes에 유효성 검증 제약 조건이 없습니다.

int 원시 타입이므로 JSON 요청에서 해당 필드가 누락되면 기본값 0이 사용되며, 음수 값도 아무런 오류 없이 허용됩니다. 0 또는 음수 매칭 시간은 유효하지 않은 비즈니스 데이터로 DB에 저장됩니다.

🛡️ 제안 수정
+        `@Schema`(description = "몇 분동안 진행되는 매칭인지", example = "120")
+        `@Min`(value = 1, message = "매칭 시간은 1분 이상이어야 합니다.")
+        `@Max`(value = 1440, message = "매칭 시간은 1440분(24시간) 이하이어야 합니다.")
         int durationMinutes,
🤖 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/request/MatchCreateRequest.java`
around lines 21 - 22, The durationMinutes field in MatchCreateRequest currently
uses a primitive int and has no validation, allowing missing or negative values;
change the field to an Integer and add validation annotations (e.g., `@NotNull`
and `@Positive` or `@Min`(1)) on durationMinutes in the MatchCreateRequest DTO so a
missing or non-positive value fails validation before persisting.


@Schema(description = "매칭 참여 요금", example = "4000")
@Min(value = 1000, message = "참가비는 1000원 이상이어야 합니다.")
int entryFee

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

import com.be.sportizebe.common.enums.SportType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Getter;
import lombok.Setter;
Expand All @@ -9,19 +10,23 @@
@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;

@Schema(description = "종목 필터(선택)", example = "BASKETBALL", nullable = true)
private SportType sportsName;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ public record MatchNearResponse(
int curMembers,
int maxMembers,
MatchStatus status,
int distanceM
int distanceM,
int durationMinutes,
int entryFee

) {
public static MatchNearResponse from(MatchNearProjection p) {
return new MatchNearResponse(
Expand All @@ -23,7 +26,10 @@ public static MatchNearResponse from(MatchNearProjection p) {
p.getCurMembers(),
p.getMaxMembers(),
MatchStatus.valueOf(p.getStatus()),
(int) Math.round(p.getDistanceM())
(int) Math.round(p.getDistanceM()),
p.getDurationMinutes(),
p.getEntryFee()

);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,10 @@ public void leave() {
public boolean isJoined() {
return this.status == MatchParticipantStatus.JOINED;
}

public void rejoin() {
this.status = MatchParticipantStatus.JOINED;
this.joinedAt = LocalDateTime.now();
this.leftAt = null;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.be.sportizebe.domain.match.entity;

public enum MatchParticipantStatus {
JOINED, LEFT
JOINED,
LEFT
}
19 changes: 19 additions & 0 deletions src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ public class MatchRoom extends BaseTimeEntity {
@Column(nullable = false)
private MatchStatus status;

@Column(nullable = false)
private int durationMinutes;

@Column(nullable = false)
private int entryFee;


@OneToMany(mappedBy = "matchRoom", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<MatchParticipant> participants = new ArrayList<>();
Expand All @@ -54,6 +61,18 @@ public static MatchRoom create(MatchCreateRequest request) {
.curMembers(0)
.maxMembers(request.maxMembers())
.status(MatchStatus.OPEN)
.durationMinutes(request.durationMinutes())
.entryFee(request.entryFee())
.build();
}
public void join() {
this.curMembers++;
if (isFull()) this.status = MatchStatus.FULL;
}
public void leave() {
this.curMembers--;
if (this.status == MatchStatus.FULL) this.status = MatchStatus.OPEN;
}
Comment on lines +72 to +75
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

leave()curMembers가 음수가 될 수 있습니다

서비스 레이어에서 JOINED 참가자를 먼저 조회하므로 정상 흐름에서는 이 문제가 발생하지 않지만, 엔티티 메서드 자체에 방어 로직이 없어 잘못된 호출 시 curMembers가 음수가 됩니다.

🛡️ 수정 제안
     public void leave() {
-        this.curMembers--;
+        if (this.curMembers <= 0) {
+            throw new IllegalStateException("curMembers가 이미 0입니다.");
+        }
+        this.curMembers--;
         if (this.status == MatchStatus.FULL) this.status = MatchStatus.OPEN;
     }
📝 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
public void leave() {
this.curMembers--;
if (this.status == MatchStatus.FULL) this.status = MatchStatus.OPEN;
}
public void leave() {
if (this.curMembers <= 0) {
throw new IllegalStateException("curMembers가 이미 0입니다.");
}
this.curMembers--;
if (this.status == MatchStatus.FULL) this.status = MatchStatus.OPEN;
}
🤖 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` around
lines 72 - 75, In MatchRoom.leave(), add defensive logic to prevent curMembers
from going negative: check if this.curMembers <= 0 and throw an
IllegalStateException (or return early) with a clear message referencing
MatchRoom.leave; otherwise decrement this.curMembers and if (this.status ==
MatchStatus.FULL) set this.status = MatchStatus.OPEN. Update any callers/tests
accordingly to expect the new guard.



}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ public enum MatchErrorCode implements BaseErrorCode {
HttpStatus.BAD_REQUEST,
"MATCH_400_ALREADY_JOINED",
"이미 해당 매칭에 참가 중입니다."
),

NOT_JOINED(
HttpStatus.BAD_REQUEST,
"MATCH_400_NOT_JOINED",
"해당 매칭에 참가 중이지 않습니다."
);

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

public interface MatchParticipantRepository extends JpaRepository<MatchParticipant, Long> {

Expand All @@ -18,4 +19,10 @@ public interface MatchParticipantRepository extends JpaRepository<MatchParticipa

// 주어진 방에 참여중인 참가자 리스트 전체 조회
List<MatchParticipant> findAllByMatchRoomAndStatus(MatchRoom matchRoom, MatchParticipantStatus status);

// 특정 유저의 특정 매칭방 참가 이력 단건 조회 (상태 포함)
Optional<MatchParticipant> findByMatchRoomAndUserAndStatus(MatchRoom matchRoom, User user, MatchParticipantStatus status);

// 특정 유저의 특정 매칭방 참가 이력 단건 조회 (상태 무관)
Optional<MatchParticipant> findByMatchRoomAndUser(MatchRoom matchRoom, User user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ public interface MatchRoomRepository extends JpaRepository<MatchRoom, Long> {
mr.cur_members AS curMembers,
mr.max_members AS maxMembers,
mr.status AS status,
mr.duration_minutes AS durationMinutes,
mr.entry_fee AS entryFee,

ST_Distance(
sf.location,
ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public interface MatchNearProjection {
Integer getMaxMembers();
String getStatus();
Double getDistanceM();
Integer getDurationMinutes();
Integer getEntryFee();
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public interface MatchService {

void joinMatch(Long matchId, Long userId); // 매칭방 참여(정원 체크 + 중복 참가 체크 + 참가자 저장)

void leaveMatch(Long matchId, Long userId); // 매칭 취소(참가자 상태 LEFT + 인원 감소 + 상태 복구)

MatchDetailResponse getMatchDetail(Long matchId, Long userId); // 매칭방 상세 조회(방 정보 + 유저 기준 정보)

List<MatchNearResponse> getNearMatches(MatchNearRequest request); // 내 주변 매칭 목록 조회
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.springframework.transaction.annotation.Transactional;
import com.be.sportizebe.domain.match.dto.request.MatchCreateRequest;
import java.util.List;
import java.util.Optional;

@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -76,8 +77,41 @@ public void joinMatch(Long matchId, Long userId) {
throw new CustomException(MatchErrorCode.ALREADY_JOINED);
}

// 5) 참가자 엔티티 생성 후 저장
matchParticipantRepository.save(new MatchParticipant(matchRoom, user));
// 5) 기존 참가 이력 있으면 재활성화, 없으면 신규 생성
Optional<MatchParticipant> existing =
matchParticipantRepository.findByMatchRoomAndUser(matchRoom, user);

if (existing.isPresent()) {
existing.get().rejoin();
} else {
matchParticipantRepository.save(new MatchParticipant(matchRoom, user));
}

// 6) 매칭방 인원 증가 및 상태 갱신 (DDD)
matchRoom.join();
}

@Override
public void leaveMatch(Long matchId, Long userId) {

// 1) 매칭방 존재 확인
MatchRoom matchRoom = matchRoomRepository.findById(matchId)
.orElseThrow(() -> new CustomException(MatchErrorCode.MATCH_NOT_FOUND));

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

// 3) JOINED 상태 참가자 조회
MatchParticipant participant = matchParticipantRepository
.findByMatchRoomAndUserAndStatus(matchRoom, user, MatchParticipantStatus.JOINED)
.orElseThrow(() -> new CustomException(MatchErrorCode.NOT_JOINED));

// 4) 참가자 상태 변경 (DDD)
participant.leave();

// 5) 매칭방 인원 감소 및 상태 복구 (DDD)
matchRoom.leave();
}

@Override
Expand Down