From f2c8a97325daf2ed837da7f5e41003a0cbd6ce95 Mon Sep 17 00:00:00 2001 From: Donghun Won Date: Thu, 1 Jan 2026 22:12:23 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat=20:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=99=9C=EC=84=B1=20=ED=8C=8C=ED=8B=B0=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../PartyParticipantRepository.java | 20 +++++++++ .../user/controller/UserController.java | 19 ++++++++ .../domain/user/dto/res/PartyResponse.java | 45 +++++++++++++++++++ .../domain/user/service/UserService.java | 33 +++++++++++++- .../domain/user/service/UserServiceTest.java | 6 ++- 6 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ita/tinybite/domain/user/dto/res/PartyResponse.java diff --git a/.gitignore b/.gitignore index e85caac..2d026e6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ out/ .env .terraform *.pem +/src/main/resources/firebase diff --git a/src/main/java/ita/tinybite/domain/party/repository/PartyParticipantRepository.java b/src/main/java/ita/tinybite/domain/party/repository/PartyParticipantRepository.java index fbb9974..a03256b 100644 --- a/src/main/java/ita/tinybite/domain/party/repository/PartyParticipantRepository.java +++ b/src/main/java/ita/tinybite/domain/party/repository/PartyParticipantRepository.java @@ -6,6 +6,7 @@ import ita.tinybite.domain.party.entity.Party; import ita.tinybite.domain.party.entity.PartyParticipant; import ita.tinybite.domain.party.enums.ParticipantStatus; +import ita.tinybite.domain.party.enums.PartyStatus; import ita.tinybite.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -24,4 +25,23 @@ public interface PartyParticipantRepository extends JpaRepository findByPartyAndStatus(Party party, ParticipantStatus status); boolean existsByPartyAndUserAndStatus(Party party, User user, ParticipantStatus status); + + @Query("SELECT pp FROM PartyParticipant pp " + + "WHERE pp.user.id = :userId " + + "AND pp.party.status =:partyStatus " + + "AND pp.status = :participantStatus") + List findActivePartiesByUserId( + @Param("userId") Long userId, + @Param("partyStatuses") PartyStatus partyStatus, + @Param("participantStatus") ParticipantStatus participantStatus + ); + + @Query("SELECT COUNT(pp) FROM PartyParticipant pp " + + "WHERE pp.party.id = :partyId " + + "AND pp.status = :status") + int countByPartyIdAndStatus( + @Param("partyId") Long partyId, + @Param("status") ParticipantStatus status + ); + } \ No newline at end of file diff --git a/src/main/java/ita/tinybite/domain/user/controller/UserController.java b/src/main/java/ita/tinybite/domain/user/controller/UserController.java index d1d72a6..7947c6b 100644 --- a/src/main/java/ita/tinybite/domain/user/controller/UserController.java +++ b/src/main/java/ita/tinybite/domain/user/controller/UserController.java @@ -42,6 +42,25 @@ public APIResponse deleteUser() { return success(); } + @Operation(summary = "활성 파티 목록 조회", description = "사용자가 참여 중인 활성 파티 목록을 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = PartyResponse.class)))), + @ApiResponse(responseCode = "401", description = "인증 실패") + }) + @GetMapping("/parties/active") + public ResponseEntity> getActiveParties( + @AuthenticationPrincipal Long userId) { + List response = userService.getActiveParties(userId); + return ResponseEntity.ok(response); + } + + @Operation(summary = "닉네임 중복 확인", description = "닉네임 사용 가능 여부를 확인합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "사용 가능한 닉네임"), + @ApiResponse(responseCode = "400", description = "이미 사용 중인 닉네임", + content = @Content(schema = @Schema(implementation = APIResponse.class))) + }) @GetMapping("/nickname/check") public APIResponse validateNickname(@RequestParam String nickname) { userService.validateNickname(nickname); diff --git a/src/main/java/ita/tinybite/domain/user/dto/res/PartyResponse.java b/src/main/java/ita/tinybite/domain/user/dto/res/PartyResponse.java new file mode 100644 index 0000000..6fd56df --- /dev/null +++ b/src/main/java/ita/tinybite/domain/user/dto/res/PartyResponse.java @@ -0,0 +1,45 @@ +package ita.tinybite.domain.user.dto.res; + +import ita.tinybite.domain.party.entity.Party; +import ita.tinybite.domain.party.enums.ParticipantStatus; +import ita.tinybite.domain.party.enums.PartyStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +@Builder +public class PartyResponse { + private Long id; + private String title; + private String description; + private Integer maxParticipants; + private Integer currentParticipants; + private PartyStatus status; + private String hostUsername; + private LocalDateTime startDate; + private LocalDateTime endDate; + private LocalDateTime createdAt; + private boolean isHost; + private ParticipantStatus participantStatus; + + public static PartyResponse from(Party party, int currentParticipants, boolean isHost, ParticipantStatus participantStatus) { + return PartyResponse.builder() + .id(party.getId()) + .title(party.getTitle()) + .description(party.getDescription()) + .maxParticipants(party.getMaxParticipants()) + .currentParticipants(currentParticipants) + .status(party.getStatus()) + .hostUsername(party.getHost().getNickname()) + .startDate(party.getCreatedAt()) + .endDate(party.getClosedAt()) + .createdAt(party.getCreatedAt()) + .isHost(isHost) + .participantStatus(participantStatus) + .build(); + } +} diff --git a/src/main/java/ita/tinybite/domain/user/service/UserService.java b/src/main/java/ita/tinybite/domain/user/service/UserService.java index 11ccd54..95ff669 100644 --- a/src/main/java/ita/tinybite/domain/user/service/UserService.java +++ b/src/main/java/ita/tinybite/domain/user/service/UserService.java @@ -1,8 +1,14 @@ package ita.tinybite.domain.user.service; import ita.tinybite.domain.auth.service.SecurityProvider; +import ita.tinybite.domain.party.entity.Party; +import ita.tinybite.domain.party.entity.PartyParticipant; +import ita.tinybite.domain.party.enums.ParticipantStatus; +import ita.tinybite.domain.party.enums.PartyStatus; +import ita.tinybite.domain.party.repository.PartyParticipantRepository; import ita.tinybite.domain.user.constant.UserStatus; import ita.tinybite.domain.user.dto.req.UpdateUserReqDto; +import ita.tinybite.domain.user.dto.res.PartyResponse; import ita.tinybite.domain.user.dto.res.UserResDto; import ita.tinybite.domain.user.entity.User; import ita.tinybite.domain.user.repository.UserRepository; @@ -11,19 +17,25 @@ import ita.tinybite.global.location.LocationService; import org.springframework.stereotype.Service; +import java.util.List; +import java.util.stream.Collectors; + @Service public class UserService { private final SecurityProvider securityProvider; private final UserRepository userRepository; private final LocationService locationService; + private final PartyParticipantRepository participantRepository; public UserService(SecurityProvider securityProvider, UserRepository userRepository, - LocationService locationService) { + LocationService locationService, + PartyParticipantRepository participantRepository) { this.securityProvider = securityProvider; this.userRepository = userRepository; this.locationService = locationService; + this.participantRepository = participantRepository; } public UserResDto getUser() { @@ -50,4 +62,23 @@ public void validateNickname(String nickname) { if(userRepository.existsByNickname(nickname)) throw BusinessException.of(AuthErrorCode.DUPLICATED_NICKNAME); } + + public List getActiveParties(Long userId) { + List participants = participantRepository + .findActivePartiesByUserId( + userId, + PartyStatus.RECRUITING, + ParticipantStatus.APPROVED + ); + + return participants.stream() + .map(pp -> { + Party party = pp.getParty(); + int currentParticipants = participantRepository + .countByPartyIdAndStatus(party.getId(), ParticipantStatus.APPROVED); + boolean isHost = party.getHost().getUserId().equals(userId); + return PartyResponse.from(party, currentParticipants, isHost,pp.getStatus()); + }) + .collect(Collectors.toList()); + } } diff --git a/src/test/java/ita/tinybite/domain/user/service/UserServiceTest.java b/src/test/java/ita/tinybite/domain/user/service/UserServiceTest.java index b6bfdea..7a05fba 100644 --- a/src/test/java/ita/tinybite/domain/user/service/UserServiceTest.java +++ b/src/test/java/ita/tinybite/domain/user/service/UserServiceTest.java @@ -1,6 +1,8 @@ package ita.tinybite.domain.user.service; import ita.tinybite.domain.auth.service.AuthService; +import ita.tinybite.domain.party.entity.Party; +import ita.tinybite.domain.party.repository.PartyParticipantRepository; import ita.tinybite.domain.user.constant.LoginType; import ita.tinybite.domain.user.constant.UserStatus; import ita.tinybite.domain.user.dto.req.UpdateUserReqDto; @@ -23,6 +25,8 @@ class UserServiceTest { @Autowired private UserRepository userRepository; + private PartyParticipantRepository participantRepository; + @Autowired private AuthService authService; @@ -37,7 +41,7 @@ class UserServiceTest { void setUp() { securityProvider = new FakeSecurityProvider(userRepository); locationService = new FakeLocationService(); - userService = new UserService(securityProvider, userRepository, locationService); + userService = new UserService(securityProvider, userRepository, locationService,participantRepository); User user = User.builder() .email("yyytir777@gmail.com") From 596b0eabc7bb40200b3ebaf80f98de6a93c5c495 Mon Sep 17 00:00:00 2001 From: Donghun Won Date: Thu, 1 Jan 2026 22:13:00 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs=20:=20=EC=9C=A0=EC=A0=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20swagger=20=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/java/ita/tinybite/domain/user/controller/UserController.java b/src/main/java/ita/tinybite/domain/user/controller/UserController.java index 7947c6b..af7f0b5 100644 --- a/src/main/java/ita/tinybite/domain/user/controller/UserController.java +++ b/src/main/java/ita/tinybite/domain/user/controller/UserController.java @@ -1,11 +1,24 @@ package ita.tinybite.domain.user.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import ita.tinybite.domain.auth.dto.response.UserDto; import ita.tinybite.domain.user.dto.req.UpdateUserReqDto; +import ita.tinybite.domain.user.dto.res.PartyResponse; +import ita.tinybite.domain.user.dto.res.UserResDto; import ita.tinybite.domain.user.service.UserService; import ita.tinybite.global.response.APIResponse; import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import java.util.List; + import static ita.tinybite.global.response.APIResponse.success; @RestController @@ -18,17 +31,36 @@ public UserController(UserService userService) { this.userService = userService; } + @Operation(summary = "내 정보 조회", description = "현재 로그인한 사용자의 정보를 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = UserResDto.class))), + @ApiResponse(responseCode = "401", description = "인증 실패", + content = @Content(schema = @Schema(implementation = APIResponse.class))) + }) @GetMapping("/me") - public APIResponse getUser() { + public APIResponse getUser() { return success(userService.getUser()); } + @Operation(summary = "내 정보 수정", description = "현재 로그인한 사용자의 정보를 수정합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "수정 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청", + content = @Content(schema = @Schema(implementation = APIResponse.class))), + @ApiResponse(responseCode = "401", description = "인증 실패") + }) @PatchMapping("/me") public APIResponse updateUser(@Valid @RequestBody UpdateUserReqDto req) { userService.updateUser(req); return success(); } + @Operation(summary = "위치 정보 수정", description = "사용자의 현재 위치(위도, 경도)를 업데이트합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "위치 업데이트 성공"), + @ApiResponse(responseCode = "401", description = "인증 실패") + }) @PatchMapping("/me/location") public APIResponse updateLocation(@RequestParam(defaultValue = "37.3623504988728") String latitude, @RequestParam(defaultValue = "127.117057453619") String longitude) { @@ -36,6 +68,11 @@ public APIResponse updateLocation(@RequestParam(defaultValue = "37.3623504988 return success(); } + @Operation(summary = "회원 탈퇴", description = "현재 로그인한 사용자를 삭제합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "탈퇴 성공"), + @ApiResponse(responseCode = "401", description = "인증 실패") + }) @DeleteMapping("/me") public APIResponse deleteUser() { userService.deleteUser();