Skip to content

Commit

Permalink
Merge pull request #41 from jisung-in/feature/31-book-review-api
Browse files Browse the repository at this point in the history
[Feature] 책 리뷰 추가, 삭제 API 구현
  • Loading branch information
pdohyung authored Mar 26, 2024
2 parents 3675e63 + dbf939f commit 04cfd5a
Show file tree
Hide file tree
Showing 10 changed files with 497 additions and 3 deletions.
33 changes: 33 additions & 0 deletions src/main/java/com/jisungin/api/review/ReviewController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.jisungin.api.review;

import com.jisungin.api.ApiResponse;
import com.jisungin.api.oauth.Auth;
import com.jisungin.api.oauth.AuthContext;
import com.jisungin.api.review.request.ReviewCreateRequest;
import com.jisungin.application.review.ReviewService;
import com.jisungin.application.review.response.ReviewResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RequestMapping("/v1/reviews")
@RequiredArgsConstructor
@RestController
public class ReviewController {

private final ReviewService reviewService;

@PostMapping
public ApiResponse<ReviewResponse> createReview(@Valid @RequestBody ReviewCreateRequest request,
@Auth AuthContext authContext) {
return ApiResponse.ok(reviewService.createReview(request.toServiceRequest(), authContext.getUserId()));
}

@DeleteMapping("/{reviewId}")
public ApiResponse<Void> deleteReview(@PathVariable Long reviewId,
@Auth AuthContext authContext) {
reviewService.deleteReview(reviewId, authContext.getUserId());
return ApiResponse.ok(null);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.jisungin.api.review.request;

import com.jisungin.application.review.request.ReviewCreateServiceRequest;
import jakarta.validation.constraints.NotBlank;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class ReviewCreateRequest {

@NotBlank(message = "리뷰 작성 시 책 isbn은 필수입니다.")
private String bookIsbn;

@NotBlank(message = "리뷰 작성 시 내용은 필수입니다.")
private String content;

@NotBlank(message = "리뷰 작성 시 별점은 필수입니다.")
private String rating;

@Builder
private ReviewCreateRequest(String bookIsbn, String content, String rating) {
this.bookIsbn = bookIsbn;
this.content = content;
this.rating = rating;
}

public ReviewCreateServiceRequest toServiceRequest() {
return ReviewCreateServiceRequest.builder()
.bookIsbn(bookIsbn)
.content(content)
.rating(Double.parseDouble(rating))
.build();
}

}
56 changes: 56 additions & 0 deletions src/main/java/com/jisungin/application/review/ReviewService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.jisungin.application.review;

import com.jisungin.application.review.request.ReviewCreateServiceRequest;
import com.jisungin.application.review.response.ReviewResponse;
import com.jisungin.domain.book.Book;
import com.jisungin.domain.book.repository.BookRepository;
import com.jisungin.domain.review.Review;
import com.jisungin.domain.review.repository.ReviewRepository;
import com.jisungin.domain.user.User;
import com.jisungin.domain.user.repository.UserRepository;
import com.jisungin.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import static com.jisungin.exception.ErrorCode.*;

@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class ReviewService {

private final ReviewRepository reviewRepository;
private final UserRepository userRepository;
private final BookRepository bookRepository;

@Transactional
public ReviewResponse createReview(ReviewCreateServiceRequest request, Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException(USER_NOT_FOUND));

Book book = bookRepository.findById(request.getBookIsbn())
.orElseThrow(() -> new BusinessException(BOOK_NOT_FOUND));

Review savedReview = reviewRepository.save(Review.create(
user, book, request.getContent(), request.getRating()
));
return ReviewResponse.of(savedReview.getBook(), savedReview.getContent(), savedReview.getRating());
}

@Transactional
public void deleteReview(Long reviewId, Long userId) {
Review deleteReview = reviewRepository.findById(reviewId)
.orElseThrow(() -> new BusinessException(REVIEW_NOT_FOUND));

User reviewUser = deleteReview.getUser();
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException(USER_NOT_FOUND));

if (!user.isMe(reviewUser.getId())) {
throw new BusinessException(UNAUTHORIZED_REQUEST);
}
reviewRepository.delete(deleteReview);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.jisungin.application.review.request;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class ReviewCreateServiceRequest {

private String bookIsbn;

private String content;

private Double rating;

@Builder
public ReviewCreateServiceRequest(String bookIsbn, String content, Double rating) {
this.bookIsbn = bookIsbn;
this.content = content;
this.rating = rating;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.jisungin.application.review.response;

import com.jisungin.domain.book.Book;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class ReviewResponse {

private Book book;

private String content;

private Double rating;

@Builder
private ReviewResponse(Book book, String content, Double rating) {
this.book = book;
this.content = content;
this.rating = rating;
}

public static ReviewResponse of(Book book, String content, Double rating) {
return ReviewResponse.builder()
.book(book)
.content(content)
.rating(rating)
.build();
}

}
9 changes: 9 additions & 0 deletions src/main/java/com/jisungin/domain/review/Review.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,13 @@ private Review(User user, Book book, String content, Double rating) {
this.rating = rating;
}

public static Review create(User user, Book book, String content, Double rating) {
return Review.builder()
.user(user)
.book(book)
.content(content)
.rating(rating)
.build();
}

}
3 changes: 2 additions & 1 deletion src/main/java/com/jisungin/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public enum ErrorCode {
OAUTH_TYPE_NOT_FOUND(404, "지원하지 않는 소셜 로그인입니다."),
TALK_ROOM_NOT_FOUND(400, "토크방을 찾을 수 없습니다."),
UNAUTHORIZED_REQUEST(400, "권한이 없는 사용자입니다."),
COMMENT_NOT_FOUND(404, "의견을 찾을 수 없습니다.");
COMMENT_NOT_FOUND(404, "의견을 찾을 수 없습니다."),
REVIEW_NOT_FOUND(404, "리뷰를 찾을 수 없습니다.");


private final int code;
Expand Down
11 changes: 9 additions & 2 deletions src/test/java/com/jisungin/ControllerTestSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jisungin.api.comment.CommentController;
import com.jisungin.api.oauth.AuthContext;
import com.jisungin.api.review.ReviewController;
import com.jisungin.api.talkroom.TalkRoomController;
import com.jisungin.application.comment.CommentService;
import com.jisungin.application.review.ReviewService;
import com.jisungin.application.talkroom.TalkRoomService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
Expand All @@ -13,7 +15,8 @@

@WebMvcTest(controllers = {
TalkRoomController.class,
CommentController.class
CommentController.class,
ReviewController.class
})
public abstract class ControllerTestSupport {

Expand All @@ -23,12 +26,16 @@ public abstract class ControllerTestSupport {
@Autowired
protected ObjectMapper objectMapper;

@MockBean
protected AuthContext authContext;

@MockBean
protected TalkRoomService talkRoomService;

@MockBean
protected CommentService commentService;

@MockBean
protected AuthContext authContext;
protected ReviewService reviewService;

}
114 changes: 114 additions & 0 deletions src/test/java/com/jisungin/api/review/ReviewControllerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.jisungin.api.review;

import com.jisungin.ControllerTestSupport;
import com.jisungin.api.review.request.ReviewCreateRequest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

class ReviewControllerTest extends ControllerTestSupport {

@DisplayName("유저가 리뷰를 등록한다.")
@Test
void createReview() throws Exception {
//given
ReviewCreateRequest request = ReviewCreateRequest.builder()
.bookIsbn("123456")
.content("재밌어요.")
.rating("4.5")
.build();

//when //then
mockMvc.perform(
post("/v1/reviews")
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("200"))
.andExpect(jsonPath("$.status").value("OK"))
.andExpect(jsonPath("$.message").value("OK"))
.andDo(print());
}

@DisplayName("책 isbn과 함께 리뷰를 등록해야 한다.")
@Test
void createReviewWithoutBookIsbn() throws Exception {
//given
ReviewCreateRequest request = ReviewCreateRequest.builder()
.content("재밌어요.")
.rating("4.5")
.build();

//when //then
mockMvc.perform(
post("/v1/reviews")
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("400"))
.andExpect(jsonPath("$.message").value("리뷰 작성 시 책 isbn은 필수입니다."))
.andDo(print());
}

@DisplayName("리뷰 내용과 함께 리뷰를 등록해야 한다.")
@Test
void createReviewWithoutContent() throws Exception {
//given
ReviewCreateRequest request = ReviewCreateRequest.builder()
.bookIsbn("123456")
.rating("4.5")
.build();

//when //then
mockMvc.perform(
post("/v1/reviews")
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("400"))
.andExpect(jsonPath("$.message").value("리뷰 작성 시 내용은 필수입니다."))
.andDo(print());
}

@DisplayName("별점과 함께 리뷰를 등록해야 한다.")
@Test
void createReviewWithoutRating() throws Exception {
//given
ReviewCreateRequest request = ReviewCreateRequest.builder()
.bookIsbn("123456")
.content("재밌어요.")
.build();

//when //then
mockMvc.perform(
post("/v1/reviews")
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("400"))
.andExpect(jsonPath("$.message").value("리뷰 작성 시 별점은 필수입니다."))
.andDo(print());
}

@DisplayName("리뷰를 삭제한다.")
@Test
void deleteReview() throws Exception {
//given
Long deleteReviewId = 1L;

//when //then
mockMvc.perform(
delete("/v1/reviews/{reviewId}", deleteReviewId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("200"))
.andExpect(jsonPath("$.status").value("OK"))
.andExpect(jsonPath("$.message").value("OK"))
.andDo(print());
}

}
Loading

0 comments on commit 04cfd5a

Please sign in to comment.