diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 09930c1d..8c7d3bde 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1367,3 +1367,30 @@ include::{snippets}/starRating/checkRated/notExist/response-body.adoc[] .response-fields include::{snippets}/starRating/checkRated/notExist/response-fields.adoc[] + +=== PATCH api/v1/ratings/:animeId +.curl-request +include::{snippets}/starRating/patchScore/success/curl-request.adoc[] + +.http-request +include::{snippets}/starRating/patchScore/success/http-request.adoc[] + +.path-parameters +include::{snippets}/starRating/patchScore/success/path-parameters.adoc[] + +.request-header +include::{snippets}/starRating/patchScore/success/request-headers.adoc[] + +.request-body +include::{snippets}/starRating/patchScore/success/request-body.adoc[] + +.request-fields +include::{snippets}/starRating/patchScore/success/request-fields.adoc[] + +==== 성공시 +.http-response +include::{snippets}/starRating/patchScore/success/http-response.adoc[] + +==== 실패시 +.http-response +include::{snippets}/starRating/patchScore/failure/http-response.adoc[] diff --git a/src/main/java/io/oduck/api/domain/starRating/controller/StarRatingController.java b/src/main/java/io/oduck/api/domain/starRating/controller/StarRatingController.java index f6bac5ce..3462be52 100644 --- a/src/main/java/io/oduck/api/domain/starRating/controller/StarRatingController.java +++ b/src/main/java/io/oduck/api/domain/starRating/controller/StarRatingController.java @@ -1,6 +1,6 @@ package io.oduck.api.domain.starRating.controller; -import io.oduck.api.domain.starRating.dto.StarRatingReqDto.CreateReq; +import io.oduck.api.domain.starRating.dto.StarRatingReqDto.CreateAndPatchReq; import io.oduck.api.domain.starRating.service.StarRatingService; import io.oduck.api.global.security.auth.dto.AuthUser; import io.oduck.api.global.security.auth.dto.LoginUser; @@ -11,6 +11,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -27,7 +28,7 @@ public class StarRatingController { @PostMapping("/{animeId}") public ResponseEntity PostScore( @PathVariable("animeId") @Positive Long animeId, - @RequestBody @Valid CreateReq body, + @RequestBody @Valid CreateAndPatchReq body, @LoginUser AuthUser user ) { boolean res = starRatingService.createScore(user.getId(), animeId, body.getScore()); @@ -41,4 +42,14 @@ public ResponseEntity GetScore( ) { return ResponseEntity.ok(starRatingService.checkRated(user.getId(), animeId)); } + + @PatchMapping("/{animeId}") + public ResponseEntity PatchScore( + @PathVariable("animeId") @Positive Long animeId, + @RequestBody @Valid CreateAndPatchReq body, + @LoginUser AuthUser user + ) { + boolean res = starRatingService.updateScore(user.getId(), animeId, body.getScore()); + return ResponseEntity.status(res ? HttpStatus.NO_CONTENT : HttpStatus.CONFLICT).build(); + } } diff --git a/src/main/java/io/oduck/api/domain/starRating/dto/StarRatingReqDto.java b/src/main/java/io/oduck/api/domain/starRating/dto/StarRatingReqDto.java index cf7a86d3..a072325f 100644 --- a/src/main/java/io/oduck/api/domain/starRating/dto/StarRatingReqDto.java +++ b/src/main/java/io/oduck/api/domain/starRating/dto/StarRatingReqDto.java @@ -12,7 +12,7 @@ public class StarRatingReqDto { @Builder @NoArgsConstructor @AllArgsConstructor - public static class CreateReq { + public static class CreateAndPatchReq { @Min(value = 1, message = "score must be greater than or equal to 1") @Max(value = 10, message = "score must be less than or equal to 10") private int score; diff --git a/src/main/java/io/oduck/api/domain/starRating/entity/StarRating.java b/src/main/java/io/oduck/api/domain/starRating/entity/StarRating.java index 3d6dfe9f..65200327 100644 --- a/src/main/java/io/oduck/api/domain/starRating/entity/StarRating.java +++ b/src/main/java/io/oduck/api/domain/starRating/entity/StarRating.java @@ -49,4 +49,8 @@ public void relateMember(Member member) { public void relateAnime(Anime anime) { this.anime = anime; } + + public void updateScore(int score) { + this.score = score; + } } diff --git a/src/main/java/io/oduck/api/domain/starRating/service/StarRatingService.java b/src/main/java/io/oduck/api/domain/starRating/service/StarRatingService.java index ca5db69a..d472ae20 100644 --- a/src/main/java/io/oduck/api/domain/starRating/service/StarRatingService.java +++ b/src/main/java/io/oduck/api/domain/starRating/service/StarRatingService.java @@ -5,4 +5,5 @@ public interface StarRatingService { boolean createScore(Long memberId, Long animeId, int score); RatedDateTimeRes checkRated(Long memberId, Long animeId); + boolean updateScore(Long memberId, Long animeId, int score); } diff --git a/src/main/java/io/oduck/api/domain/starRating/service/StarRatingServiceImpl.java b/src/main/java/io/oduck/api/domain/starRating/service/StarRatingServiceImpl.java index 82338a8a..3d166bbf 100644 --- a/src/main/java/io/oduck/api/domain/starRating/service/StarRatingServiceImpl.java +++ b/src/main/java/io/oduck/api/domain/starRating/service/StarRatingServiceImpl.java @@ -58,6 +58,21 @@ public RatedDateTimeRes checkRated(Long memberId, Long animeId) { .build(); } + @Override + public boolean updateScore(Long memberId, Long animeId, int score) { + StarRating foundStarRating = findByMemberIdAndAnimeId(memberId, animeId) + .orElseThrow(() -> new NotFoundException("StarRating")); + + if (foundStarRating.getScore() == score) { + return false; + } + + foundStarRating.updateScore(score); + + starRatingRepository.save(foundStarRating); + return true; + } + private Optional findByMemberIdAndAnimeId(Long memberId, Long animeId) { return starRatingRepository.findByMemberIdAndAnimeId(memberId, animeId); } diff --git a/src/test/java/io/oduck/api/e2e/starRating/StarRatingControllerTest.java b/src/test/java/io/oduck/api/e2e/starRating/StarRatingControllerTest.java index 6b10952d..976ce9ec 100644 --- a/src/test/java/io/oduck/api/e2e/starRating/StarRatingControllerTest.java +++ b/src/test/java/io/oduck/api/e2e/starRating/StarRatingControllerTest.java @@ -5,6 +5,7 @@ import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; @@ -20,7 +21,7 @@ import com.google.gson.Gson; import io.oduck.api.domain.member.entity.Role; -import io.oduck.api.domain.starRating.dto.StarRatingReqDto.CreateReq; +import io.oduck.api.domain.starRating.dto.StarRatingReqDto.CreateAndPatchReq; import io.oduck.api.global.mockMember.WithCustomMockMember; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -64,7 +65,7 @@ void createScore() throws Exception { Long animeId = 2L; int score = 5; - CreateReq body = CreateReq.builder() + CreateAndPatchReq body = CreateAndPatchReq.builder() .score(score) .build(); @@ -115,7 +116,7 @@ void createScoreIfAlreadyExist() throws Exception { Long animeId = 1L; int score = 5; - CreateReq body = CreateReq.builder() + CreateAndPatchReq body = CreateAndPatchReq.builder() .score(score) .build(); @@ -247,6 +248,111 @@ void checkRatedNotExist() throws Exception { ) ); } + } + + @Nested + @DisplayName("PATCH /ratings/{animeId}") + class PatchScore { + @Test + @DisplayName("평점 수정 성공시 204 No Content 반환") + @WithCustomMockMember(id = 2L, email = "john", password = "Qwer!234", role = Role.MEMBER) + void patchScore() throws Exception { + // given + Long animeId = 1L; + int score = 5; + + CreateAndPatchReq body = CreateAndPatchReq.builder() + .score(score) + .build(); + + String content = gson.toJson(body); + + // when + ResultActions actions = mockMvc.perform( + patch(BASE_URL + "/{animeId}", animeId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .header(HttpHeaders.COOKIE, "oDuckio.sid={SESSION_VALUE}") + .content(content) + ); + + // then + actions + .andExpect(status().isNoContent()) + .andDo(document("starRating/patchScore/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("animeId") + .description("애니메이션 식별자") + ), + requestHeaders( + headerWithName(HttpHeaders.COOKIE) + .attributes(field("constraints", "oDuckio.sid={SESSION_VALUE}")) + .optional() + .description("Header Cookie, 세션 쿠키") + ), + requestFields( + attributes(key("title") + .value("Fields for starRating creation")), + fieldWithPath("score") + .type(JsonFieldType.NUMBER) + .attributes(field("constraints", "1~10 사이의 정수")) + .description("애니메 별점") + ) + ) + ); + } + + @Test + @DisplayName("기존 평점과 동일하다면 없다면 409 Conflict 반환") + @WithCustomMockMember(id = 3L, email = "john", password = "Qwer!234", role = Role.MEMBER) + void patchScoreIfNotExist() throws Exception { + // given + Long animeId = 1L; + int score = 3; + CreateAndPatchReq body = CreateAndPatchReq.builder() + .score(score) + .build(); + + String content = gson.toJson(body); + + // when + ResultActions actions = mockMvc.perform( + patch(BASE_URL + "/{animeId}", animeId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .header(HttpHeaders.COOKIE, "oDuckio.sid={SESSION_VALUE}") + .content(content) + ); + + // then + actions + .andExpect(status().isConflict()) + .andDo(document("starRating/patchScore/failure", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("animeId") + .description("애니메이션 식별자") + ), + requestHeaders( + headerWithName(HttpHeaders.COOKIE) + .attributes(field("constraints", "oDuckio.sid={SESSION_VALUE}")) + .optional() + .description("Header Cookie, 세션 쿠키") + ), + requestFields( + attributes(key("title") + .value("Fields for starRating creation")), + fieldWithPath("score") + .type(JsonFieldType.NUMBER) + .attributes(field("constraints", "1~10 사이의 정수")) + .description("애니메 별점") + ) + ) + ); + } } } diff --git a/src/test/java/io/oduck/api/unit/starRating/service/StarRatingServiceTest.java b/src/test/java/io/oduck/api/unit/starRating/service/StarRatingServiceTest.java index 0508f7a8..44006e42 100644 --- a/src/test/java/io/oduck/api/unit/starRating/service/StarRatingServiceTest.java +++ b/src/test/java/io/oduck/api/unit/starRating/service/StarRatingServiceTest.java @@ -147,4 +147,61 @@ void chekRatedIfNotExist() { assertThrows(NotFoundException.class, () -> starRatingService.checkRated(memberId, animeId)); } } + + @Nested + @DisplayName("별점 수정") + class UpdateScore { + Long memberId = 1L; + Long animeId = 1L; + int score = 2; + + Member member = Member.builder() + .id(memberId) + .build(); + Anime anime = Anime.builder() + .id(animeId) + .build(); + + @Test + @DisplayName("별점 수정 성공") + void updateScore() { + // given + StarRating starRating = StarRating.builder() + .member(member) + .anime(anime) + .score(score) + .build(); + + given(starRatingRepository.findByMemberIdAndAnimeId(memberId, animeId)) + .willReturn(Optional.ofNullable(starRating)); + + // when + boolean result = starRatingService.updateScore(memberId, animeId, 5); + + // then + assertDoesNotThrow(() -> starRatingService.updateScore(memberId, animeId, score)); + assertTrue(result); + } + + @Test + @DisplayName("별점 수정 실패") + void updateScoreIfNotExist() { + // given + StarRating starRating = StarRating.builder() + .member(member) + .anime(anime) + .score(score) + .build(); + + given(starRatingRepository.findByMemberIdAndAnimeId(memberId, animeId)) + .willReturn(Optional.ofNullable(starRating)); + + // when + boolean result = starRatingService.updateScore(memberId, animeId, score); + + // then + assertDoesNotThrow(() -> starRatingService.updateScore(memberId, animeId, score)); + assertFalse(result); + } + } }