diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bde864f --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +up: + docker compose -f docker/docker-compose.local.yml up -d + +down: + docker compose -f docker/docker-compose.local.yml down + +restart: + docker compose -f docker/docker-compose.local.yml down && \ + docker compose -f docker/docker-compose.local.yml up -d + +logs: + docker compose -f docker/docker-compose.local.yml logs -f \ No newline at end of file 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 42c8d38..efa6bbb 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 @@ -3,6 +3,7 @@ import com.be.sportizebe.domain.match.dto.request.MatchNearRequest; import com.be.sportizebe.domain.match.dto.response.MatchDetailResponse; import com.be.sportizebe.domain.match.dto.response.MatchNearResponse; +import com.be.sportizebe.domain.match.dto.response.MyMatchResponse; import com.be.sportizebe.domain.match.service.MatchService; import com.be.sportizebe.global.cache.dto.UserAuthInfo; import com.be.sportizebe.global.response.BaseResponse; @@ -65,7 +66,16 @@ public ResponseEntity> leaveMatch( return ResponseEntity.ok(BaseResponse.success("매칭 취소 성공", null)); } - @Operation(summary = "내 주변 매칭 목록 조회", description = "매칭상태가 OPEN인 매칭들만 보여준다.") + @Operation(summary = "내가 참여 중인 매칭 목록", description = "JOINED 상태인 매칭만 반환한다.") + @GetMapping("/me") + public ResponseEntity>> getMyMatches( + @AuthenticationPrincipal UserAuthInfo userAuthInfo + ) { + List response = matchService.getMyMatches(userAuthInfo.getId()); + return ResponseEntity.ok(BaseResponse.success("내 매칭 목록 조회 성공", response)); + } + +@Operation(summary = "내 주변 매칭 목록 조회", description = "매칭상태가 OPEN인 매칭들만 보여준다.") @GetMapping("/near") public ResponseEntity>> getNearMatches( @ParameterObject @Valid @ModelAttribute MatchNearRequest request diff --git a/src/main/java/com/be/sportizebe/domain/match/dto/response/MyMatchResponse.java b/src/main/java/com/be/sportizebe/domain/match/dto/response/MyMatchResponse.java new file mode 100644 index 0000000..a26458c --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/match/dto/response/MyMatchResponse.java @@ -0,0 +1,64 @@ +package com.be.sportizebe.domain.match.dto.response; + +import com.be.sportizebe.common.enums.SportType; +import com.be.sportizebe.domain.match.entity.MatchParticipant; +import com.be.sportizebe.domain.match.entity.MatchParticipantStatus; +import com.be.sportizebe.domain.match.entity.MatchRoom; +import com.be.sportizebe.domain.match.entity.MatchStatus; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDateTime; + +@Schema(description = "내 매칭 이력 응답") +public record MyMatchResponse( + + @Schema(description = "매칭방 ID", example = "42") + Long matchId, + + @Schema(description = "스포츠 종류", example = "SOCCER") + SportType sportsName, + + @Schema(description = "체육시설 ID", example = "7") + Long facilityId, + + @Schema(description = "최대 인원", example = "12") + int maxMembers, + + @Schema(description = "현재 인원", example = "8") + int curMembers, + + @Schema(description = "매칭 상태", example = "OPEN") + MatchStatus matchStatus, + + @Schema(description = "매칭 시작 일시", example = "2026-03-10T18:00:00") + LocalDateTime scheduledAt, + + @Schema(description = "진행 시간(분)", example = "60") + int durationMinutes, + + @Schema(description = "참가비", example = "5000") + int entryFee, + + @Schema(description = "내 참가 상태 (JOINED: 참여 중, LEFT: 취소)", example = "JOINED") + MatchParticipantStatus participantStatus, + + @Schema(description = "참가 일시", example = "2026-03-07T12:00:00") + LocalDateTime joinedAt +) { + public static MyMatchResponse from(MatchParticipant participant) { + MatchRoom room = participant.getMatchRoom(); + return new MyMatchResponse( + room.getId(), + room.getSportsName(), + room.getFacilityId(), + room.getMaxMembers(), + room.getCurMembers(), + room.getStatus(), + room.getScheduledAt(), + room.getDurationMinutes(), + room.getEntryFee(), + participant.getStatus(), + participant.getJoinedAt() + ); + } +} 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 85be183..ad7a326 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 @@ -5,6 +5,8 @@ import com.be.sportizebe.domain.match.entity.MatchRoom; import com.be.sportizebe.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; @@ -25,4 +27,9 @@ public interface MatchParticipantRepository extends JpaRepository findByMatchRoomAndUser(MatchRoom matchRoom, User user); + + // 내 참여 중인 매칭 목록 (JOINED, N+1 방지 fetch join) + @Query("SELECT mp FROM MatchParticipant mp JOIN FETCH mp.matchRoom WHERE mp.user.id = :userId AND mp.status = :status ORDER BY mp.joinedAt DESC") + List findAllByUserIdAndStatusFetch(@Param("userId") Long userId, @Param("status") MatchParticipantStatus status); + } \ No newline at end of file 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 c225293..a4898e1 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 @@ -5,7 +5,7 @@ import com.be.sportizebe.domain.match.dto.response.MatchDetailResponse; import com.be.sportizebe.domain.match.dto.response.MatchNearResponse; import com.be.sportizebe.domain.match.dto.response.MatchResponse; -import com.be.sportizebe.domain.user.entity.User; +import com.be.sportizebe.domain.match.dto.response.MyMatchResponse; import java.util.List; @@ -23,4 +23,6 @@ public interface MatchService { void deleteMatch(Long matchId); // 매칭 삭제 (관리자 전용) + List getMyMatches(Long userId); // 참여 중인 매칭 목록 + } 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 721c39e..a474455 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 @@ -4,6 +4,7 @@ import com.be.sportizebe.domain.match.dto.response.MatchDetailResponse; import com.be.sportizebe.domain.match.dto.response.MatchNearResponse; import com.be.sportizebe.domain.match.dto.response.MatchResponse; +import com.be.sportizebe.domain.match.dto.response.MyMatchResponse; import com.be.sportizebe.domain.match.entity.MatchParticipant; import com.be.sportizebe.domain.match.entity.MatchParticipantStatus; import com.be.sportizebe.domain.match.entity.MatchRoom; @@ -135,6 +136,16 @@ public void deleteMatch(Long matchId) { } @Override + @Transactional(readOnly = true) + public List getMyMatches(Long userId) { + return matchParticipantRepository + .findAllByUserIdAndStatusFetch(userId, MatchParticipantStatus.JOINED) + .stream() + .map(MyMatchResponse::from) + .toList(); + } + +@Override @Transactional(readOnly = true) public List getNearMatches(MatchNearRequest request) { String sportsName = request.getSportsName() == null