ReviewStatus enum class
public enum ReviewStatus { NOT_WRITABLE, WRITABLE, WRITTEN, DELETED }
- ReviewStatus 열거형은 다음과 같이 네 가지 상태를 표현할 수 있다.
- NOT_WRITABLE: 체크아웃 전이라 리뷰 작성이 불가능한 상태를 나타낸다.
- WRITABLE: 체크아웃 이후라 리뷰 작성이 가능한 상태를 나타낸다.
- WRITTEN: 리뷰가 작성된 상태를 나타낸다. 수정이 가능한 상태이다.
- DELETED: 리뷰가 삭제된 상태를 나타낸다. 더이상 재작성은 불가능한 상태이다.
- 주문 상세 조회에서는 이러한 리뷰 상태를 이용하여 각 주문 상품의 리뷰 작성 여부를 확인할 수 있다.
Review entity class
- 데이터베이스에 리뷰 데이터를 나타내고 저장하기 위해 설계되었으며, 평가, 리뷰 날짜, 내용, 연관된 주문 항목, 사용자, 제품과 같은 정보를 포함한다.
- 다른 엔터티들과의 관계를 정의한다. (OrderItem, User, Product)
@Entity
@Table (name = "review")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Review {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "review_id")
private Long id;
private double score;
private LocalDate reviewDate;
private String content;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_item_id")
private OrderItem orderItem;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
}
Review Create API
- 요청 메서드 : POST
- 엔드포인트 : /reviews
- 요청 데이터 : ReviewCreateRequest 객체
{ "orderItemId": 1, "score": 4.5, "content": "대체적으로 만족합니다!" }
- 응답 데이터
- 성공 시 200 OK : 등록된 리뷰 정보를 담은 ReviewCreateResponse 객체를 반환 ( 해당 주문 상품의 reviewStatus 값 NOT_WRITABLE → WRITABLE 전환 )
{ "message": "리뷰가 성공적으로 작성되었습니다.", "review": { "reviewId": 1, "reviewDate": "2023-11-27", "score": 4.5, "content": "대체적으로 만족합니다!" } }
- 실패 시 409 Conflict : 이미 리뷰를 작성했거나 삭제했을 때 발생하는 ReviewException 에 대한 응답을 반환
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ReviewCreateRequest {
private Long orderItemId;
private double score;
private String content;
public Review toEntity() {
return Review.builder()
.score(score)
.content(content)
.reviewDate(LocalDate.now())
.build();
}
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ReviewCreateResponse {
private String message;
private ReviewDetails review;
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class ReviewDetails {
private Long reviewId;
private LocalDate reviewDate;
private double score;
private String content;
public static ReviewDetails fromEntity(Review review) {
return ReviewDetails.builder()
.reviewId(review.getId())
.reviewDate(review.getReviewDate())
.score(review.getScore())
.content(review.getContent())
.build();
}
}
public static ReviewCreateResponse fromEntity(Review review) {
return ReviewCreateResponse.builder()
.message("리뷰가 성공적으로 작성되었습니다.")
.review(ReviewDetails.fromEntity(review))
.build();
}
}
@GetMapping
public ResponseEntity<List<UserReviewResponse>> getUserReviews() {
List<UserReviewResponse> userReviewListResponse = reviewService.getUserReviews();
return ResponseEntity.ok(userReviewListResponse);
}
@Transactional
public ReviewCreateResponse createReview(ReviewCreateRequest reviewCreateRequest) {
User user = getUser();
OrderItem orderItem = getOrderItem(reviewCreateRequest.getOrderItemId());
checkReviewStatus(orderItem);
Product product = orderItem.getProduct();
Review review = reviewCreateRequest.toEntity();
review.setOrderItem(orderItem);
review.setUser(user);
review.setProduct(product);
Review savedReview = reviewRepository.save(review);
orderItem.setReviewStatus(ReviewStatus.WRITTEN);
return ReviewCreateResponse.fromEntity(savedReview);
}
createReview Method Logic
- 사용자 정보 조회 : 현재 사용자의 정보를 가져온다.
- 주문 상품 정보 조회 : 주어진 주문 상품 ID에 해당하는 주문 상품 정보를 데이터베이스에서 조회한다.
- 주문 상품의 리뷰 상태 확인: 조회된 주문 상품의 리뷰 상태를 확인하고, 이미 리뷰가 작성되었거나 삭제된 경우에는 예외를 발생시킨다.
- 상품 정보 조회: 주문 상품에 연결된 상품 정보를 조회한다.
✔️ 상품 엔티티와 리뷰 엔티티 사이의 연관관계를 맺어주기 위해서이다.
✔️ 숙소 리뷰 조회 시 해당 상품과 연관된 리뷰의 정보를 가져올 수 있다.- 리뷰 엔티티 생성 및 저장 : 주어진 요청 데이터를 기반으로 새로운 리뷰 엔티티를 생성하고 저장한다.
✔️ 리뷰는 사용자, 사용자의 주문 상품과도 연관관계를 가지게 된다.
✔️ 사용자 리뷰 조회 및 주문 상품별 리뷰 관리를 수행할 수 있다.- 주문 상품의 리뷰 작성 상태 변경 : NO_WRITTEN → WRITTEN
- 생성된 리뷰 엔티티를 응답 객체로 변환하여 반환한다.
{
"orderId": 1,
"orderItemList": [
{
"orderItemId": 2,
"checkIn": "2023-12-07",
"checkOut": "2023-12-09",
"personNumber": 2,
"price": 100000,
"orderItemDetail": {
"accommodationName": "브라운도트 천안성정점",
"accommodationAddress": "충청남도 천안시 서북구 성정공원4길 28 (성정동)",
"productImage": "http://tong.visitkorea.or.kr/cms/resource/50/2705650_image2_1.jpg",
"productName": "디럭스 스위트 룸"
},
"reviewStatus": "WRITTEN" // NOT_WRITTEN -> WRITTEN
}
]
}
Review Update API
- 요청 메서드 : PUT
- 엔드포인트 : /reviews/{reviewId}
- 요청 데이터 : reviewId 및 ReviewUpdateRequest 객체
{ "score": 5.0, "content": "대체적으로 만족합니다!", }
- 응답 데이터
- 성공 시 200 OK : 수정된 리뷰 정보를 담은 ReviewUpdateResponse 객체를 반환
{ "message": "리뷰가 성공적으로 수정되었습니다.", "review": { "reviewId": 12, "updateDate": "2023-12-08", "score": 5.0, "content": "대체적으로 만족합니다!" } }
- 실패 시 404 Not Found : 리뷰가 존재하지 않을 때 발생하는 ReviewException 에 대한 응답을 반환
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ReviewUpdateRequest {
private double score;
private String content;
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ReviewUpdateResponse {
private String message;
private ReviewDetails review;
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class ReviewDetails {
private Long reviewId;
private LocalDate updateDate; // 수정 날짜
@Schema(example = "5.0")
private double score;
private String content;
public static ReviewDetails fromEntity(Review review) {
return ReviewDetails.builder()
.reviewId(review.getId())
.updateDate(LocalDate.now())
.score(review.getScore())
.content(review.getContent())
.build();
}
}
public static ReviewUpdateResponse fromEntity(Review review) {
return ReviewUpdateResponse.builder()
.message("리뷰가 성공적으로 수정되었습니다.")
.review(ReviewDetails.fromEntity(review))
.build();
}
}
public ResponseEntity<ReviewUpdateResponse> updateReview(
@PathVariable Long reviewId,
@Valid @RequestBody ReviewUpdateRequest reviewUpdateRequest
) {
ReviewUpdateResponse reviewUpdateResponse = reviewService.updateReview(reviewId, reviewUpdateRequest);
return ResponseEntity.ok(reviewUpdateResponse);
}
@Transactional
public ReviewUpdateResponse updateReview(Long reviewId, ReviewUpdateRequest reviewUpdateRequest) {
Review review = getReview(reviewId);
review.update(reviewUpdateRequest);
Review updatedReview = reviewRepository.save(review);
return ReviewUpdateResponse.fromEntity(updatedReview);
}
updateReview Method Logic
- 리뷰 조회: 주어진 리뷰 아이디를 사용하여 데이터베이스에서 해당 리뷰를 조회한다. 리뷰가 이미 삭제하여 존재하지 않는 경우에는 예외가 발생한다.
- 리뷰 업데이트: ReviewUpdateRequest 객체에 따라 리뷰 엔티티를 업데이트한다.
- 업데이트된 리뷰 저장: 변경된 리뷰를 데이터베이스에 저장한다.
- 업데이트된 리뷰로 응답 객체 생성 및 반환: 업데이트된 리뷰를 기반으로 ReviewUpdateResponse 객체를 생성하고 반환한다.
Review Delete API
- 요청 메서드 : DELETE
- 2.엔드포인트 : /reviews/{reviewId}
- 요청 데이터 : reviewId
- 응답 데이터
- 성공 시 200 OK : 수정된 리뷰 정보를 담은 ReviewDeleteResponse 객체를 반환
{ "message": "리뷰가 성공적으로 삭제되었습니다.", "review": { "reviewId": 12, "deleteDate": "2023-11-27" } }
- 실패 시 404 Not Found : 리뷰가 존재하지 않을 때 발생하는 ReviewException 에 대한 응답을 반환
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ReviewDeleteResponse {
private String message;
private ReviewDetails review;
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class ReviewDetails {
private Long reviewId;
private LocalDate deleteDate;
public static ReviewDetails fromEntity(Review review) {
return ReviewDetails.builder()
.reviewId(review.getId())
.deleteDate(LocalDate.now())
.build();
}
}
public static ReviewDeleteResponse fromEntity(Review review) {
return ReviewDeleteResponse.builder()
.message("리뷰가 성공적으로 삭제되었습니다.")
.review(ReviewDetails.fromEntity(review))
.build();
}
}
public ResponseEntity<ReviewDeleteResponse> deleteReview(
@PathVariable Long reviewId
) {
ReviewDeleteResponse reviewDeleteResponse = reviewService.deleteReview(reviewId);
return ResponseEntity.ok(reviewDeleteResponse);
}
@Transactional
public ReviewDeleteResponse deleteReview(Long reviewId) {
Review review = getReview(reviewId);
OrderItem orderItem = getOrderItem(review.getOrderItem().getId());
reviewRepository.delete(review);
orderItem.setReviewStatus(ReviewStatus.DELETED);
return ReviewDeleteResponse.fromEntity(review);
}
deleteReview Method Logic
리뷰 조회: 주어진 리뷰 아이디를 사용하여 데이터베이스에서 해당 리뷰를 조회한다. 리뷰가 이미 삭제하여 존재하지 않는 경우에는 예외가 발생한다.
리뷰 삭제: 데이터베이스에서 리뷰 엔티티를 삭제한다.
OrderItem 리뷰 상태 업데이트: 연관된 OrderItem의 리뷰 상태를 DELETED로 설정한다.
응답 객체 생성 및 반환: 삭제된 Review 엔티티에서 ReviewDeleteResponse 객체를 생성하고 반환한다.
- 요청 메서드 : GET
- 엔드포인트 : /reviews/{accommodationId}
- 요청 데이터 : accommodationId
- 응답 데이터
- 성공 시 200 OK : 해당 숙소의 객실 별 작성된 리뷰 정보를 담은 ProductReviewResponse 객체의 리스트를 반환
[ { "reviewId": 2, "reviewDate": "2023-12-08", "score": 5.0, "content": "굿굿", "userDetails": { "userId": 3, "userName": "test01" }, "productDetails": { "productId": 34, "productImage": "http://tong.visitkorea.or.kr/cms/resource/50/2705650_image2_1.jpg", "productName": "캐릭터 룸" } }, { "reviewId": 10, "reviewDate": "2023-12-07", "score": 1.0, "content": "너무좋아요!", "userDetails": { "userId": 10, "userName": "test02" }, "productDetails": { "productId": 34, "productImage": "http://tong.visitkorea.or.kr/cms/resource/50/2705650_image2_1.jpg", "productName": "캐릭터 룸" } } ]
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ProductReviewResponse {
private Long reviewId;
private LocalDate reviewDate;
private double score;
private String content;
private UserDetailsResponse userDetails;
private ProductDetailsResponse productDetails;
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class UserDetailsResponse {
private Long userId;
private String userName;
public static UserDetailsResponse fromEntity(Review review) {
return UserDetailsResponse.builder()
.userId(review.getUser().getId())
.userName(review.getUser().getName())
.build();
}
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class ProductDetailsResponse {
private Long productId;
private String productImage;
private String productName;
public static ProductDetailsResponse fromEntity(Review review) {
return ProductDetailsResponse.builder()
.productId(review.getProduct().getId())
.productImage(review.getProduct().getProductImageList().get(0).getImageUrl())
.productName(review.getProduct().getName())
.build();
}
}
public static ProductReviewResponse fromEntity(Review review) {
return ProductReviewResponse.builder()
.reviewId(review.getId())
.userDetails(UserDetailsResponse.fromEntity(review))
.productDetails(ProductDetailsResponse.fromEntity(review))
.score(review.getScore())
.reviewDate(review.getReviewDate())
.content(review.getContent())
.build();
}
}
@GetMapping
public ResponseEntity<List<UserReviewResponse>> getUserReviews() {
List<UserReviewResponse> userReviewListResponse = reviewService.getUserReviews();
return ResponseEntity.ok(userReviewListResponse);
}
@Transactional
public List<ProductReviewResponse> getProductReviews(Long accommodationId) {
Accommodation accommodation = getAccommodation(accommodationId);
List<Product> products = accommodation.getProductList();
List<ProductReviewResponse> productReviewResponseList = new ArrayList<>();
for (Product product : products) {
List<Review> reviews = product.getReviewList();
productReviewResponseList.addAll(reviews.stream()
.map(ProductReviewResponse::fromEntity)
.collect(Collectors.toList()));
}
return productReviewResponseList;
}
Review Read API
- 요청 메서드 : GET
- 엔드포인트 : /reviews
- 요청 데이터 : 없음
- 응답 데이터
- 성공 시 200 OK : 현재 로그인한 사용자가 작성한 전체 리뷰 정보를 담은 UserReviewResponse 객체의 리스트를 반환
[ { "reviewId": 2, "reviewDate": "2023-12-08", "score": 5.0, "content": "굿굿", "orderItemId": 21, "accommodationDetails": { "accommodationId": 11, "accommodationName": "브라운도트호텔 정관점" }, "productDetails": { "productId": 34, "productImage": "http://tong.visitkorea.or.kr/cms/resource/50/2705650_image2_1.jpg", "productName": "캐릭터 룸" } }, { "reviewId": 12, "reviewDate": "2023-12-08", "score": 5.0, "content": "굿굿", "orderItemId": 22, "accommodationDetails": { "accommodationId": 6, "accommodationName": "세심천온천호텔" }, "productDetails": { "productId": 15, "productImage": "http://tong.visitkorea.or.kr/cms/resource/61/3039161_image2_1.JPG", "productName": "일반실(한실)" } } ]
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserReviewResponse {
private Long reviewId;
private LocalDate reviewDate;
private double score;
private String content;
private Long orderItemId;
private AccommodationDetailsResponse accommodationDetails;
private ProductDetailsResponse productDetails;
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class AccommodationDetailsResponse {
private Long accommodationId;
private String accommodationName;
public static AccommodationDetailsResponse fromEntity(Review review) {
return AccommodationDetailsResponse.builder()
.accommodationId(review.getProduct().getAccommodation().getId())
.accommodationName(review.getProduct().getAccommodation().getName())
.build();
}
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class ProductDetailsResponse {
private Long productId;
private String productImage;
private String productName;
public static ProductDetailsResponse fromEntity(Review review) {
return ProductDetailsResponse.builder()
.productId(review.getProduct().getId())
.productImage(review.getProduct().getProductImageList().get(0).getImageUrl())
.productName(review.getProduct().getName())
.build();
}
}
public static UserReviewResponse fromEntity(Review review) {
return UserReviewResponse.builder()
.reviewId(review.getId())
.reviewDate(review.getReviewDate())
.score(review.getScore())
.content(review.getContent())
.orderItemId(review.getOrderItem().getId())
.accommodationDetails(AccommodationDetailsResponse.fromEntity(review))
.productDetails(ProductDetailsResponse.fromEntity(review))
.build();
}
}
@GetMapping
public ResponseEntity<List<UserReviewResponse>> getUserReviews() {
List<UserReviewResponse> userReviewListResponse = reviewService.getUserReviews();
return ResponseEntity.ok(userReviewListResponse);
}
@Transactional
public List<UserReviewResponse> getUserReviews() {
User user = getUser();
List<Review> reviews = reviewRepository.getReviewsByUserWithDetails(user);
List<UserReviewResponse> userReviewResponseList = reviews.stream()
.map(UserReviewResponse::fromEntity)
.collect(Collectors.toList());
return userReviewResponseList;
}