Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] 책 리뷰 추가, 삭제 API 구현 #41

Merged
merged 5 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
));
Comment on lines +35 to +37
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

객체간 의존성을 줄이기 위해 request의 값을 꺼내어 String 값을 넘겨주는 건가요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 맞습니다 ~ 의존성 문제도 있지만 create는 Review의 생성 함수입니다.
Review 도메인은 퍼시스턴스 계층이기 때문에 만약 request를 넘겨준다면 해당 계층이 비즈니스 계층의 내용을 알게 됩니다.
그래서 해당 코드와 같이 값을 뽑아서 전달했습니다 !

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
Loading