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/controller/PartyController.java b/src/main/java/ita/tinybite/domain/party/controller/PartyController.java index 512289f..78d2adf 100644 --- a/src/main/java/ita/tinybite/domain/party/controller/PartyController.java +++ b/src/main/java/ita/tinybite/domain/party/controller/PartyController.java @@ -10,6 +10,7 @@ import ita.tinybite.domain.auth.entity.JwtTokenProvider; import ita.tinybite.domain.chat.entity.ChatRoom; import ita.tinybite.domain.party.dto.request.PartyCreateRequest; +import ita.tinybite.domain.party.dto.request.PartyUpdateRequest; import ita.tinybite.domain.party.dto.response.ChatRoomResponse; import ita.tinybite.domain.party.dto.response.PartyDetailResponse; import ita.tinybite.domain.party.dto.response.PartyListResponse; @@ -19,14 +20,9 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.ErrorResponse; +import org.springframework.web.bind.annotation.*; import java.util.List; @@ -272,8 +268,6 @@ public ResponseEntity> getPendingParticipants( return ResponseEntity.ok(participants); } - - /** * 파티 목록 조회 (홈 화면) */ @@ -287,17 +281,12 @@ public ResponseEntity getPartyList( example = "ALL", schema = @Schema(allowableValues = {"ALL", "DELIVERY", "GROCERY", "HOUSEHOLD"}) ) - @RequestParam(defaultValue = "ALL") PartyCategory category, - - @Parameter(description = "사용자 위도", required = true, example = "37.4979") - @RequestParam String latitude, - - @Parameter(description = "사용자 경도", required = true, example = "127.0276") - @RequestParam String longitude) { + @RequestParam(defaultValue = "ALL") PartyCategory category + ) { Long userId = jwtTokenProvider.getUserId(token); PartyListResponse response = partyService.getPartyList( - userId, category, latitude, longitude); + userId, category); return ResponseEntity.ok(response); } @@ -330,12 +319,11 @@ public ResponseEntity getPartyList( @GetMapping("/{partyId}") public ResponseEntity getPartyDetail( @PathVariable Long partyId, - @RequestHeader("Authorization") String token, - @RequestParam Double latitude, - @RequestParam Double longitude) { + @RequestHeader("Authorization") String token + ) { Long userId = jwtTokenProvider.getUserId(token); - PartyDetailResponse response = partyService.getPartyDetail(partyId, userId, latitude, longitude); + PartyDetailResponse response = partyService.getPartyDetail(partyId, userId); return ResponseEntity.ok(response); } @@ -374,4 +362,90 @@ public ResponseEntity createParty( return ResponseEntity.ok(partyId); } + /** + * 파티 수정 + */ + @Operation( + summary = "파티 수정", + description = """ + 파티 정보를 수정합니다. + + **수정 권한** + - 파티 호스트만 수정 가능 + + **수정 가능 범위** + - 승인된 파티원이 없을 때: 모든 항목 수정 가능 + - 승인된 파티원이 있을 때: 설명, 이미지만 수정 가능 (가격, 인원, 수령 정보 수정 불가) + """ + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "파티 수정 성공" + ), + @ApiResponse( + responseCode = "400", + description = "잘못된 요청 (수정 권한 없음, 유효하지 않은 데이터)", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "파티를 찾을 수 없음", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + }) + @PatchMapping("/{partyId}") + public ResponseEntity updateParty( + @PathVariable Long partyId, + @AuthenticationPrincipal Long userId, + @RequestBody PartyUpdateRequest request) { + + partyService.updateParty(partyId, userId, request); + return ResponseEntity.ok().build(); + } + + /** + * 파티 삭제 + */ + @Operation( + summary = "파티 삭제", + description = """ + 파티를 삭제합니다. + + **삭제 권한** + - 파티 호스트만 삭제 가능 + + **삭제 제한** + - 승인된 파티원이 있는 경우 삭제 불가능 + - 승인된 파티원이 없을 때만 삭제 가능 + + **삭제 시 처리** + - 관련 채팅방 비활성화 + - 대기 중인 참가 신청 모두 삭제 + """ + ) + @ApiResponses({ + @ApiResponse( + responseCode = "204", + description = "파티 삭제 성공" + ), + @ApiResponse( + responseCode = "400", + description = "잘못된 요청 (삭제 권한 없음, 승인된 파티원 존재)", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "파티를 찾을 수 없음", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + }) + @DeleteMapping("/{partyId}") + public ResponseEntity deleteParty( + @PathVariable Long partyId, + @AuthenticationPrincipal Long userId) { + + partyService.deleteParty(partyId, userId); + return ResponseEntity.noContent().build(); + } } \ No newline at end of file diff --git a/src/main/java/ita/tinybite/domain/party/dto/request/PartyUpdateRequest.java b/src/main/java/ita/tinybite/domain/party/dto/request/PartyUpdateRequest.java index 30e8520..1a19b61 100644 --- a/src/main/java/ita/tinybite/domain/party/dto/request/PartyUpdateRequest.java +++ b/src/main/java/ita/tinybite/domain/party/dto/request/PartyUpdateRequest.java @@ -28,7 +28,7 @@ public class PartyUpdateRequest { private Double latitude; private Double longitude; - @Pattern(regexp = "^(https?://)?.*", message = "올바른 URL 형식으로 입력해주세요") +// @Pattern(regexp = "^(https?://)?.*", message = "올바른 URL 형식으로 입력해주세요") private String productLink; // 항상 수정 가능한 필드 diff --git a/src/main/java/ita/tinybite/domain/party/entity/Party.java b/src/main/java/ita/tinybite/domain/party/entity/Party.java index f71189d..819d350 100644 --- a/src/main/java/ita/tinybite/domain/party/entity/Party.java +++ b/src/main/java/ita/tinybite/domain/party/entity/Party.java @@ -76,7 +76,6 @@ public class Party { private LocalDateTime createdAt; // 등록시간 @UpdateTimestamp - @Column(nullable = false) private LocalDateTime updatedAt; private LocalDateTime closedAt; @@ -89,6 +88,16 @@ public class Party { @Builder.Default private List participants = new ArrayList<>(); // 파티 참여 유저 + /** + * 참여자 수 증가 + */ + public void incrementParticipants() { + if (this.currentParticipants >= this.maxParticipants) { + throw new IllegalStateException("파티 인원이 가득 찼습니다"); + } + this.currentParticipants++; + } + public String getTimeAgo() { LocalDateTime now = LocalDateTime.now(); diff --git a/src/main/java/ita/tinybite/domain/party/service/PartyService.java b/src/main/java/ita/tinybite/domain/party/service/PartyService.java index 45b4137..4080a0d 100644 --- a/src/main/java/ita/tinybite/domain/party/service/PartyService.java +++ b/src/main/java/ita/tinybite/domain/party/service/PartyService.java @@ -61,13 +61,11 @@ public Long createParty(Long userId, PartyCreateRequest request) { .maxParticipants(request.getMaxParticipants()) .pickupLocation(PickupLocation.builder() .place(request.getPickupLocation().getPlace()) -// .pickupLatitude(request.getPickupLocation().getPickupLatitude()) -// .pickupLongitude(request.getPickupLocation().getPickupLongitude()) .build()) - .image(request.getImages().get(0)) - .thumbnailImage(thumbnailImage) - .link(request.getProductLink()) - .description(request.getDescription()) + .image(getImageIfPresent(request.getImages())) + .thumbnailImage(getThumbnailIfPresent(request.getImages(), request.getCategory())) + .link(getLinkIfValid(request.getProductLink(), request.getCategory())) + .description(getDescriptionIfPresent(request.getDescription())) .currentParticipants(1) .status(PartyStatus.RECRUITING) .isClosed(false) @@ -106,8 +104,7 @@ public Long createParty(Long userId, PartyCreateRequest request) { /** * 파티 목록 조회 (홈 화면) */ - public PartyListResponse getPartyList(Long userId, PartyCategory category, - String userLat, String userLon) { + public PartyListResponse getPartyList(Long userId, PartyCategory category) { User user = null; if (userId != null) { user = userRepository.findById(userId).orElse(null); @@ -185,7 +182,7 @@ public PartyListResponse getPartyList(Long userId, PartyCategory category, /** * 파티 상세 조회 */ - public PartyDetailResponse getPartyDetail(Long partyId, Long userId, Double userLat, Double userLon) { + public PartyDetailResponse getPartyDetail(Long partyId, Long userId) { Party party = partyRepository.findById(partyId) .orElseThrow(() -> new IllegalArgumentException("파티를 찾을 수 없습니다")); @@ -390,7 +387,7 @@ public void deleteParty(Long partyId, Long userId) { */ @Transactional public void approveParticipant(Long partyId, Long participantId, Long hostId) { - Party party = partyRepository.findById(partyId) + Party party = partyRepository.findByIdWithHost(partyId) .orElseThrow(() -> new IllegalArgumentException("파티를 찾을 수 없습니다")); // 파티장 권한 확인 @@ -401,9 +398,18 @@ public void approveParticipant(Long partyId, Long participantId, Long hostId) { PartyParticipant participant = partyParticipantRepository.findById(participantId) .orElseThrow(() -> new IllegalArgumentException("참여 신청을 찾을 수 없습니다")); + + // 현재 인원이 최대 인원을 초과하는지 검증 + if (party.getCurrentParticipants() >= party.getMaxParticipants()) { + throw new IllegalStateException("파티 인원이 가득 찼습니다"); + } + // 승인 처리 participant.approve(); + // 파티 현재 참여자 수 증가 + party.incrementParticipants(); + // 단체 채팅방 조회 또는 생성 ChatRoom groupChatRoom = getOrCreateGroupChatRoom(party); @@ -601,5 +607,29 @@ private void checkAndCloseIfFull(Party party) { party.close(); } } + + // 헬퍼 메서드들 + private String getImageIfPresent(List images) { + return (images != null && !images.isEmpty()) ? images.get(0) : null; + } + + private String getThumbnailIfPresent(List images, PartyCategory category) { + if (images != null && !images.isEmpty()) { + return images.get(0); + } + return null; + } + + private String getLinkIfValid(String link, PartyCategory category) { + if (link != null && !link.isBlank()) { + validateProductLink(category, link); + return link; + } + return null; + } + + private String getDescriptionIfPresent(String description) { + return (description != null && !description.isBlank()) ? description : null; + } }