From 964af1fd9c90430b94e7596087daa18d915624bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=ED=9B=88=EA=B8=B0?= Date: Fri, 20 Feb 2026 18:38:58 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=E2=8F=BA=20=E2=9C=A8=20Feat:=20MatchNearRe?= =?UTF-8?q?quest=20Swagger=20@Schema=20=EC=98=88=EC=8B=9C=EA=B0=92=20?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/match/dto/request/MatchNearRequest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/be/sportizebe/domain/match/dto/request/MatchNearRequest.java b/src/main/java/com/be/sportizebe/domain/match/dto/request/MatchNearRequest.java index 07ff6a2..64210e1 100644 --- a/src/main/java/com/be/sportizebe/domain/match/dto/request/MatchNearRequest.java +++ b/src/main/java/com/be/sportizebe/domain/match/dto/request/MatchNearRequest.java @@ -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; @@ -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; } From fd8795f46809fd6bab8458f5c077251ca831a201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=ED=9B=88=EA=B8=B0?= Date: Fri, 20 Feb 2026 19:32:17 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20getNearMatches=20Swa?= =?UTF-8?q?gger=20summary=EC=97=90=20OPEN=20=ED=95=84=ED=84=B0=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/sportizebe/domain/match/controller/MatchController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java b/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java index df53c77..486133b 100644 --- a/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java +++ b/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java @@ -68,7 +68,7 @@ public ResponseEntity> getMatchDetail( return ResponseEntity.ok(BaseResponse.success("매칭 상세 조회 성공", response)); } - @Operation(summary = "내 주변 매칭 목록 조회") + @Operation(summary = "내 주변 매칭 목록 조회 (매칭상태: OPEN만 보여준다.)") @GetMapping("/near") public ResponseEntity>> getNearMatches( @ParameterObject @Valid @ModelAttribute MatchNearRequest request From a071d9cef400ed1f01508ead845a27d7b44f86d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=ED=9B=88=EA=B8=B0?= Date: Fri, 20 Feb 2026 23:58:48 +0900 Subject: [PATCH 3/5] =?UTF-8?q?:memo:Docs:=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20Swagger=20Summary=EC=97=90=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=B6=94=EA=B0=80=20(=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=EC=9A=A9=20=EA=B8=B0=EB=8A=A5=20=EB=AA=85=EC=8B=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/sportizebe/domain/match/controller/MatchController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java b/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java index 486133b..57d0643 100644 --- a/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java +++ b/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java @@ -37,7 +37,7 @@ public class MatchController { private final MatchService matchService; - @Operation(summary = "매칭 생성") + @Operation(summary = "매칭 생성 (관리자 전용 기능)", description = "현재는 매칭 더미데이터 넣는 용도") @PostMapping public ResponseEntity> createMatch( @AuthenticationPrincipal UserAuthInfo userAuthInfo, From b6a9a45e336437248120b9eb26e3119b8dd9aba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=ED=9B=88=EA=B8=B0?= Date: Sat, 21 Feb 2026 00:04:41 +0900 Subject: [PATCH 4/5] =?UTF-8?q?:memo:Docs:=20swagger=20Summary=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/sportizebe/domain/match/controller/MatchController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java b/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java index 57d0643..c548d77 100644 --- a/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java +++ b/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java @@ -68,7 +68,7 @@ public ResponseEntity> getMatchDetail( return ResponseEntity.ok(BaseResponse.success("매칭 상세 조회 성공", response)); } - @Operation(summary = "내 주변 매칭 목록 조회 (매칭상태: OPEN만 보여준다.)") + @Operation(summary = "내 주변 매칭 목록 조회", description = "매칭상태가 OPEN인 매칭들만 보여준다.") @GetMapping("/near") public ResponseEntity>> getNearMatches( @ParameterObject @Valid @ModelAttribute MatchNearRequest request From 8471a96feaa48d501747d408f44b5aa6b250536a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=ED=9B=88=EA=B8=B0?= Date: Mon, 23 Feb 2026 17:14:40 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=E2=8F=BA=E2=9C=A8=20Feat:=20=EB=A7=A4?= =?UTF-8?q?=EC=B9=AD=20=EC=B7=A8=EC=86=8C=20=EB=B0=8F=20=EC=9E=AC=EC=B0=B8?= =?UTF-8?q?=EC=97=AC=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../match/controller/MatchController.java | 11 ++++++ .../match/dto/request/MatchCreateRequest.java | 9 ++++- .../match/dto/response/MatchNearResponse.java | 10 ++++- .../domain/match/entity/MatchParticipant.java | 6 +++ .../match/entity/MatchParticipantStatus.java | 3 +- .../domain/match/entity/MatchRoom.java | 19 ++++++++++ .../match/exception/MatchErrorCode.java | 6 +++ .../MatchParticipantRepository.java | 7 ++++ .../match/repository/MatchRoomRepository.java | 3 ++ .../projection/MatchNearProjection.java | 2 + .../domain/match/service/MatchService.java | 2 + .../match/service/MatchServiceImpl.java | 38 ++++++++++++++++++- 12 files changed, 110 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java b/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java index c548d77..5256d2e 100644 --- a/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java +++ b/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java @@ -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; @@ -68,6 +69,16 @@ public ResponseEntity> getMatchDetail( return ResponseEntity.ok(BaseResponse.success("매칭 상세 조회 성공", response)); } + @Operation(summary = "매칭 취소") + @DeleteMapping("/{matchId}/leave") + public ResponseEntity> 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>> getNearMatches( diff --git a/src/main/java/com/be/sportizebe/domain/match/dto/request/MatchCreateRequest.java b/src/main/java/com/be/sportizebe/domain/match/dto/request/MatchCreateRequest.java index 90c4725..4617f43 100644 --- a/src/main/java/com/be/sportizebe/domain/match/dto/request/MatchCreateRequest.java +++ b/src/main/java/com/be/sportizebe/domain/match/dto/request/MatchCreateRequest.java @@ -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, + + @Schema(description = "매칭 참여 요금", example = "4000") + @Min(value = 1000, message = "참가비는 1000원 이상이어야 합니다.") + int entryFee ) {} \ No newline at end of file diff --git a/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchNearResponse.java b/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchNearResponse.java index 0fc1db1..450c56c 100644 --- a/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchNearResponse.java +++ b/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchNearResponse.java @@ -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( @@ -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() + ); } } diff --git a/src/main/java/com/be/sportizebe/domain/match/entity/MatchParticipant.java b/src/main/java/com/be/sportizebe/domain/match/entity/MatchParticipant.java index 99bbeac..2d8eb13 100644 --- a/src/main/java/com/be/sportizebe/domain/match/entity/MatchParticipant.java +++ b/src/main/java/com/be/sportizebe/domain/match/entity/MatchParticipant.java @@ -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; + } } \ No newline at end of file diff --git a/src/main/java/com/be/sportizebe/domain/match/entity/MatchParticipantStatus.java b/src/main/java/com/be/sportizebe/domain/match/entity/MatchParticipantStatus.java index cc46c60..4e41e5d 100644 --- a/src/main/java/com/be/sportizebe/domain/match/entity/MatchParticipantStatus.java +++ b/src/main/java/com/be/sportizebe/domain/match/entity/MatchParticipantStatus.java @@ -1,5 +1,6 @@ package com.be.sportizebe.domain.match.entity; public enum MatchParticipantStatus { - JOINED, LEFT + JOINED, + LEFT } diff --git a/src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java b/src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java index 0cad30c..49c4954 100644 --- a/src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java +++ b/src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java @@ -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 participants = new ArrayList<>(); @@ -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; + } + + } \ No newline at end of file diff --git a/src/main/java/com/be/sportizebe/domain/match/exception/MatchErrorCode.java b/src/main/java/com/be/sportizebe/domain/match/exception/MatchErrorCode.java index 0c9db63..d02e750 100644 --- a/src/main/java/com/be/sportizebe/domain/match/exception/MatchErrorCode.java +++ b/src/main/java/com/be/sportizebe/domain/match/exception/MatchErrorCode.java @@ -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; diff --git a/src/main/java/com/be/sportizebe/domain/match/repository/MatchParticipantRepository.java b/src/main/java/com/be/sportizebe/domain/match/repository/MatchParticipantRepository.java index 3292af9..85be183 100644 --- a/src/main/java/com/be/sportizebe/domain/match/repository/MatchParticipantRepository.java +++ b/src/main/java/com/be/sportizebe/domain/match/repository/MatchParticipantRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface MatchParticipantRepository extends JpaRepository { @@ -18,4 +19,10 @@ public interface MatchParticipantRepository extends JpaRepository findAllByMatchRoomAndStatus(MatchRoom matchRoom, MatchParticipantStatus status); + + // 특정 유저의 특정 매칭방 참가 이력 단건 조회 (상태 포함) + Optional findByMatchRoomAndUserAndStatus(MatchRoom matchRoom, User user, MatchParticipantStatus status); + + // 특정 유저의 특정 매칭방 참가 이력 단건 조회 (상태 무관) + Optional findByMatchRoomAndUser(MatchRoom matchRoom, User user); } \ No newline at end of file diff --git a/src/main/java/com/be/sportizebe/domain/match/repository/MatchRoomRepository.java b/src/main/java/com/be/sportizebe/domain/match/repository/MatchRoomRepository.java index 4fa1970..0bc13b7 100644 --- a/src/main/java/com/be/sportizebe/domain/match/repository/MatchRoomRepository.java +++ b/src/main/java/com/be/sportizebe/domain/match/repository/MatchRoomRepository.java @@ -19,6 +19,9 @@ public interface MatchRoomRepository extends JpaRepository { 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 diff --git a/src/main/java/com/be/sportizebe/domain/match/repository/projection/MatchNearProjection.java b/src/main/java/com/be/sportizebe/domain/match/repository/projection/MatchNearProjection.java index e3ff283..5908ba2 100644 --- a/src/main/java/com/be/sportizebe/domain/match/repository/projection/MatchNearProjection.java +++ b/src/main/java/com/be/sportizebe/domain/match/repository/projection/MatchNearProjection.java @@ -9,4 +9,6 @@ public interface MatchNearProjection { Integer getMaxMembers(); String getStatus(); Double getDistanceM(); + Integer getDurationMinutes(); + Integer getEntryFee(); } diff --git a/src/main/java/com/be/sportizebe/domain/match/service/MatchService.java b/src/main/java/com/be/sportizebe/domain/match/service/MatchService.java index 1d53c99..e39bd34 100644 --- a/src/main/java/com/be/sportizebe/domain/match/service/MatchService.java +++ b/src/main/java/com/be/sportizebe/domain/match/service/MatchService.java @@ -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 getNearMatches(MatchNearRequest request); // 내 주변 매칭 목록 조회 diff --git a/src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java b/src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java index a62440f..5bcac78 100644 --- a/src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java @@ -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 @@ -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 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