Skip to content

Commit

Permalink
feat: 스타카토 수정 시 날짜, 장소 변경 가능하도록 수정 #353 (#362)
Browse files Browse the repository at this point in the history
* feat: 스타카토 수정 시 장소, 날짜, 추억도 변경할 수 있도록 서비스 메서드 구현

* feat: 스타카토 수정 시 장소, 날짜, 추억도 변경할 수 있도록 컨트롤러 메서드 구현

* test: 존재하지 않는 스타카토 수정 시 예외 발생 테스트 추가

* refactor: 가독성을 위한 메서드명, 변수명 변경

* refactor: 사용하는 DTO 클래스명과 매칭되도록 변수명 수정

* test: 컨트롤러 예외 테스트에서는 ObjectMapper를 사용하도록 테스트 변경

* test: 가독성을 위해 줄바꿈 제거

* test: 실패 케이스에서는 DTO 형식을 검증하지 않도록 변경

* test: 서비스 단에서 검증 책임이 없는 사항은 검증하지 않도록 변경
  • Loading branch information
devhoya97 authored Sep 18, 2024
1 parent 6abf3ca commit 7b272f9
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ public ResponseEntity<Void> updateMomentById(
return ResponseEntity.ok().build();
}

@PutMapping(path = "/v2/{momentId}")
public ResponseEntity<Void> 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<Void> deleteMomentById(
@LoginMember Member member,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ ResponseEntity<MomentDetailResponse> readMomentById(
@ApiResponse(description = """
<발생 가능한 케이스>
(1) 조회하려는 스타카토가 존재하지 않을 때
(1) 수정하려는 스타카토가 존재하지 않을 때
(2) Path Variable 형식이 잘못되었을 때
Expand All @@ -84,6 +84,33 @@ ResponseEntity<Void> 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<Void> 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"),
Expand Down
8 changes: 8 additions & 0 deletions backend/src/main/java/com/staccato/moment/domain/Moment.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}

Expand All @@ -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("요청하신 스타카토를 찾을 수 없어요."));
Expand All @@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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("본인 것이 아닌 스타카토를 수정하려고 하면 예외가 발생한다.")
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit 7b272f9

Please sign in to comment.