diff --git a/backend/src/main/java/com/staccato/moment/controller/MomentController.java b/backend/src/main/java/com/staccato/moment/controller/MomentController.java index c29828063..3ca4410ef 100644 --- a/backend/src/main/java/com/staccato/moment/controller/MomentController.java +++ b/backend/src/main/java/com/staccato/moment/controller/MomentController.java @@ -70,6 +70,16 @@ public ResponseEntity updateMomentById( return ResponseEntity.ok().build(); } + @PutMapping(path = "/v2/{momentId}") + public ResponseEntity updateMomentByIdV2( + @LoginMember Member member, + @PathVariable @Min(value = 1L, message = "스타카토 식별자는 양수로 이루어져야 합니다.") long momentId, + @Valid @RequestBody MomentRequest request + ) { + momentService.updateMomentByIdV2(momentId, request, member); + return ResponseEntity.ok().build(); + } + @DeleteMapping("/{momentId}") public ResponseEntity deleteMomentById( @LoginMember Member member, diff --git a/backend/src/main/java/com/staccato/moment/controller/docs/MomentControllerDocs.java b/backend/src/main/java/com/staccato/moment/controller/docs/MomentControllerDocs.java index 380aad8f5..0cb77b567 100644 --- a/backend/src/main/java/com/staccato/moment/controller/docs/MomentControllerDocs.java +++ b/backend/src/main/java/com/staccato/moment/controller/docs/MomentControllerDocs.java @@ -71,7 +71,7 @@ ResponseEntity readMomentById( @ApiResponse(description = """ <발생 가능한 케이스> - (1) 조회하려는 스타카토가 존재하지 않을 때 + (1) 수정하려는 스타카토가 존재하지 않을 때 (2) Path Variable 형식이 잘못되었을 때 @@ -84,6 +84,33 @@ ResponseEntity updateMomentById( @Parameter(description = "스타카토 ID", example = "1") @PathVariable @Min(value = 1L, message = "스타카토 식별자는 양수로 이루어져야 합니다.") long momentId, @Parameter(required = true) @Valid MomentUpdateRequest request); + @Operation(summary = "스타카토 수정", description = "스타카토를 수정합니다.") + @ApiResponses(value = { + @ApiResponse(description = "스타카토 수정 성공", responseCode = "200"), + @ApiResponse(description = """ + <발생 가능한 케이스> + + (1) 수정하려는 스타카토가 존재하지 않을 때 + + (2) Path Variable 형식이 잘못되었을 때 + + (3) 필수 값(사진을 제외한 모든 값)이 누락되었을 때 + + (4) 존재하지 않는 memoryId일 때 + + (5) 올바르지 않은 날짜 형식일 때 + + (6) 사진이 5장을 초과했을 때 + + (7) 스타카토 날짜가 추억 기간에 포함되지 않을 때 + """, + responseCode = "400") + }) + ResponseEntity updateMomentByIdV2( + @Parameter(hidden = true) Member member, + @Parameter(description = "스타카토 ID", example = "1") @PathVariable @Min(value = 1L, message = "스타카토 식별자는 양수로 이루어져야 합니다.") long momentId, + @Parameter(required = true) @Valid MomentRequest request); + @Operation(summary = "스타카토 삭제", description = "스타카토를 삭제합니다.") @ApiResponses(value = { @ApiResponse(description = "스타카토 삭제에 성공했거나 해당 스타카토가 존재하지 않는 경우", responseCode = "200"), diff --git a/backend/src/main/java/com/staccato/moment/domain/Moment.java b/backend/src/main/java/com/staccato/moment/domain/Moment.java index 4f3adef79..3c5b6ebf7 100644 --- a/backend/src/main/java/com/staccato/moment/domain/Moment.java +++ b/backend/src/main/java/com/staccato/moment/domain/Moment.java @@ -89,6 +89,14 @@ public void update(String placeName, MomentImages newMomentImages) { this.momentImages.update(newMomentImages, this); } + public void update(Moment updatedMoment) { + this.visitedAt = updatedMoment.getVisitedAt(); + this.placeName = updatedMoment.getPlaceName(); + this.spot = updatedMoment.getSpot(); + this.momentImages.update(updatedMoment.momentImages, this); + this.memory = updatedMoment.getMemory(); + } + public String getThumbnailUrl() { return momentImages.getImages().get(0).getImageUrl(); } diff --git a/backend/src/main/java/com/staccato/moment/service/MomentService.java b/backend/src/main/java/com/staccato/moment/service/MomentService.java index 56332b4a8..7737d331c 100644 --- a/backend/src/main/java/com/staccato/moment/service/MomentService.java +++ b/backend/src/main/java/com/staccato/moment/service/MomentService.java @@ -31,7 +31,7 @@ public class MomentService { @Transactional public MomentIdResponse createMoment(MomentRequest momentRequest, Member member) { Memory memory = getMemoryById(momentRequest.memoryId()); - validateOwner(memory, member); + validateMemoryOwner(memory, member); Moment moment = momentRequest.toMoment(memory); momentRepository.save(moment); @@ -52,7 +52,7 @@ public MomentLocationResponses readAllMoment(Member member) { public MomentDetailResponse readMomentById(long momentId, Member member) { Moment moment = getMomentById(momentId); - validateOwner(moment.getMemory(), member); + validateMemoryOwner(moment.getMemory(), member); return new MomentDetailResponse(moment); } @@ -63,10 +63,26 @@ public void updateMomentById( Member member ) { Moment moment = getMomentById(momentId); - validateOwner(moment.getMemory(), member); + validateMemoryOwner(moment.getMemory(), member); moment.update(momentUpdateRequest.placeName(), momentUpdateRequest.toMomentImages()); } + @Transactional + public void updateMomentByIdV2( + long momentId, + MomentRequest momentRequest, + Member member + ) { + Moment moment = getMomentById(momentId); + validateMemoryOwner(moment.getMemory(), member); + + Memory targetMemory = getMemoryById(momentRequest.memoryId()); + validateMemoryOwner(targetMemory, member); + + Moment updatedMoment = momentRequest.toMoment(targetMemory); + moment.update(updatedMoment); + } + private Moment getMomentById(long momentId) { return momentRepository.findById(momentId) .orElseThrow(() -> new StaccatoException("요청하신 스타카토를 찾을 수 없어요.")); @@ -75,22 +91,22 @@ private Moment getMomentById(long momentId) { @Transactional public void deleteMomentById(long momentId, Member member) { momentRepository.findById(momentId).ifPresent(moment -> { - validateOwner(moment.getMemory(), member); + validateMemoryOwner(moment.getMemory(), member); momentRepository.deleteById(momentId); }); } - private void validateOwner(Memory memory, Member member) { - if (memory.isNotOwnedBy(member)) { - throw new ForbiddenException(); - } - } - @Transactional public void updateMomentFeelingById(long momentId, Member member, FeelingRequest feelingRequest) { Moment moment = getMomentById(momentId); - validateOwner(moment.getMemory(), member); + validateMemoryOwner(moment.getMemory(), member); Feeling feeling = feelingRequest.toFeeling(); moment.changeFeeling(feeling); } + + private void validateMemoryOwner(Memory memory, Member member) { + if (memory.isNotOwnedBy(member)) { + throw new ForbiddenException(); + } + } } diff --git a/backend/src/test/java/com/staccato/moment/controller/MomentControllerTest.java b/backend/src/test/java/com/staccato/moment/controller/MomentControllerTest.java index 9d7ca1784..871477a9a 100644 --- a/backend/src/test/java/com/staccato/moment/controller/MomentControllerTest.java +++ b/backend/src/test/java/com/staccato/moment/controller/MomentControllerTest.java @@ -19,6 +19,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -268,6 +269,80 @@ void failUpdateMomentByPlaceName() throws Exception { .andExpect(content().json(objectMapper.writeValueAsString(exceptionResponse))); } + @Nested + @DisplayName("updateMomentByIdV2 테스트") + class updateMomentByIdV2Test { + @DisplayName("적합한 경로변수를 통해 스타카토 수정에 성공한다.") + @Test + void updateMomentById() throws Exception { + // given + long momentId = 1L; + String momentRequest = """ + { + "placeName": "placeName", + "address": "address", + "latitude": 1.0, + "longitude": 1.0, + "visitedAt": "2023-07-01T10:00:00", + "memoryId": 1, + "momentImageUrls": [ + "https://example.com/images/namsan_tower.jpg" + ] + } + """; + when(authService.extractFromToken(anyString())).thenReturn(MemberFixture.create()); + + // when & then + mockMvc.perform(put("/moments/v2/{momentId}", momentId) + .header(HttpHeaders.AUTHORIZATION, "token") + .contentType(MediaType.APPLICATION_JSON) + .content(momentRequest)) + .andExpect(status().isOk()); + } + + @DisplayName("추가하려는 사진이 5장이 넘는다면 스타카토 수정에 실패한다.") + @Test + void failUpdateMomentByImagesSize() throws Exception { + // given + long momentId = 1L; + ExceptionResponse exceptionResponse = new ExceptionResponse(HttpStatus.BAD_REQUEST.toString(), "사진은 5장까지만 추가할 수 있어요."); + MomentRequest momentRequest = new MomentRequest("placeName", "address", BigDecimal.ONE, BigDecimal.ONE, LocalDateTime.now(), 1L, + List.of("https://example.com/images/namsan_tower1.jpg", + "https://example.com/images/namsan_tower2.jpg", + "https://example.com/images/namsan_tower3.jpg", + "https://example.com/images/namsan_tower4.jpg", + "https://example.com/images/namsan_tower5.jpg", + "https://example.com/images/namsan_tower6.jpg")); + when(authService.extractFromToken(anyString())).thenReturn(MemberFixture.create()); + + // when & then + mockMvc.perform(put("/moments/v2/{momentId}", momentId) + .header(HttpHeaders.AUTHORIZATION, "token") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(momentRequest))) + .andExpect(status().isBadRequest()) + .andExpect(content().json(objectMapper.writeValueAsString(exceptionResponse))); + } + + @DisplayName("적합하지 않은 경로변수의 경우 스타카토 수정에 실패한다.") + @Test + void failUpdateMomentById() throws Exception { + // given + long momentId = 0L; + MomentRequest momentRequest = new MomentRequest("placeName", "address", BigDecimal.ONE, BigDecimal.ONE, LocalDateTime.of(2023, 7, 1, 10, 0), 1L, List.of("https://example.com/images/namsan_tower.jpg")); + ExceptionResponse exceptionResponse = new ExceptionResponse(HttpStatus.BAD_REQUEST.toString(), "스타카토 식별자는 양수로 이루어져야 합니다."); + when(authService.extractFromToken(anyString())).thenReturn(MemberFixture.create()); + + // when & then + mockMvc.perform(put("/moments/v2/{momentId}", momentId) + .header(HttpHeaders.AUTHORIZATION, "token") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(momentRequest))) + .andExpect(status().isBadRequest()) + .andExpect(content().json(objectMapper.writeValueAsString(exceptionResponse))); + } + } + @DisplayName("스타카토를 삭제한다.") @Test void deleteMomentById() throws Exception { diff --git a/backend/src/test/java/com/staccato/moment/service/MomentServiceTest.java b/backend/src/test/java/com/staccato/moment/service/MomentServiceTest.java index 93fed916f..bd3ef3bc2 100644 --- a/backend/src/test/java/com/staccato/moment/service/MomentServiceTest.java +++ b/backend/src/test/java/com/staccato/moment/service/MomentServiceTest.java @@ -11,6 +11,7 @@ import java.util.List; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -192,18 +193,7 @@ void updateMomentById() { // then Moment foundedMoment = momentRepository.findById(moment.getId()).get(); - assertAll( - () -> assertThat(foundedMoment.getPlaceName()).isEqualTo("newPlaceName"), - () -> assertThat(momentImageRepository.findById(1L)).isEmpty(), - () -> assertThat(momentImageRepository.findById(2L)).isEmpty(), - () -> assertThat(momentImageRepository.findById(3L).get().getImageUrl()).isEqualTo("https://existExample.com.jpg"), - () -> assertThat(momentImageRepository.findById(4L).get().getImageUrl()).isEqualTo("https://existExample2.com.jpg"), - () -> assertThat(momentImageRepository.findById(3L).get().getMoment().getId()).isEqualTo(foundedMoment.getId()), - () -> assertThat(momentImageRepository.findById(4L).get().getMoment().getId()).isEqualTo(foundedMoment.getId()), - () -> assertThat(momentImageRepository.findAll().get(0).getImageUrl()).isEqualTo("https://existExample.com.jpg"), - () -> assertThat(momentImageRepository.findAll().get(1).getImageUrl()).isEqualTo("https://existExample2.com.jpg"), - () -> assertThat(momentImageRepository.findAll().size()).isEqualTo(2) - ); + assertThat(foundedMoment.getPlaceName()).isEqualTo("newPlaceName"); } @DisplayName("본인 것이 아닌 스타카토를 수정하려고 하면 예외가 발생한다.") @@ -235,6 +225,76 @@ void failUpdateMomentById() { .hasMessageContaining("요청하신 스타카토를 찾을 수 없어요."); } + @Nested + @DisplayName("updateMomentByIdV2 테스트") + class updateMomentByIdV2Test { + + @DisplayName("스타카토 수정에 성공한다.") + @Test + void updateMomentById() { + // given + Member member = saveMember(); + Memory memory = saveMemory(member); + Memory memory2 = saveMemory(member); + Moment moment = saveMomentWithImages(memory); + + // when + MomentRequest momentRequest = new MomentRequest("newPlaceName", "newAddress", BigDecimal.ONE, BigDecimal.ONE, LocalDateTime.now(), memory2.getId(), List.of("https://existExample.com.jpg", "https://existExample2.com.jpg")); + momentService.updateMomentByIdV2(moment.getId(), momentRequest, member); + + // then + Moment foundedMoment = momentRepository.findById(moment.getId()).get(); + assertThat(foundedMoment.getPlaceName()).isEqualTo("newPlaceName"); + } + + @DisplayName("본인 것이 아닌 스타카토를 수정하려고 하면 예외가 발생한다.") + @Test + void failToUpdateMomentOfOther() { + // given + Member member = saveMember(); + Member otherMember = saveMember(); + Memory memory = saveMemory(member); + Moment moment = saveMomentWithImages(memory); + MomentRequest momentRequest = new MomentRequest("newPlaceName", "newAddress", BigDecimal.ONE, BigDecimal.ONE, LocalDateTime.now(), memory.getId(), List.of("https://existExample.com.jpg", "https://existExample2.com.jpg")); + + // when & then + assertThatThrownBy(() -> momentService.updateMomentByIdV2(moment.getId(), momentRequest, otherMember)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("요청하신 작업을 처리할 권한이 없습니다."); + } + + @DisplayName("본인 것이 아닌 추억에 속하도록 스타카토를 수정하려고 하면 예외가 발생한다.") + @Test + void failToUpdateMomentToOtherMemory() { + // given + Member member = saveMember(); + Member otherMember = saveMember(); + Memory memory = saveMemory(member); + Memory otherMemory = saveMemory(otherMember); + Moment moment = saveMomentWithImages(memory); + MomentRequest momentRequest = new MomentRequest("placeName", "address", BigDecimal.ONE, BigDecimal.ONE, LocalDateTime.now(), otherMemory.getId(), List.of()); + + // when & then + assertThatThrownBy(() -> momentService.updateMomentByIdV2(moment.getId(), momentRequest, member)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("요청하신 작업을 처리할 권한이 없습니다."); + } + + @DisplayName("존재하지 않는 스타카토를 수정하면 예외가 발생한다.") + @Test + void failToUpdateNotExistMoment() { + // given + Member member = saveMember(); + Memory memory = saveMemory(member); + MomentRequest momentUpdateRequest = new MomentRequest("placeName", "address", BigDecimal.ONE, BigDecimal.ONE, LocalDateTime.now(), memory.getId(), List.of()); + + // when & then + assertThatThrownBy(() -> momentService.updateMomentByIdV2(1L, momentUpdateRequest, member)) + .isInstanceOf(StaccatoException.class) + .hasMessageContaining("요청하신 스타카토를 찾을 수 없어요."); + } + } + @DisplayName("Moment을 삭제하면 이에 포함된 MomentImage와 MomentLog도 모두 삭제된다.") @Test void deleteMomentById() {