[패스트캠퍼스X야놀자 : 미니 프로젝트] 리뷰 CRUD 기능 구현

꼬마요리사레미·2023년 12월 7일

ReviewStatus enum class

public enum ReviewStatus {
     NOT_WRITABLE, WRITABLE, WRITTEN, DELETED
}
  1. ReviewStatus 열거형은 다음과 같이 네 가지 상태를 표현할 수 있다.
  • NOT_WRITABLE: 체크아웃 전이라 리뷰 작성이 불가능한 상태를 나타낸다.
  • WRITABLE: 체크아웃 이후라 리뷰 작성이 가능한 상태를 나타낸다.
  • WRITTEN: 리뷰가 작성된 상태를 나타낸다. 수정이 가능한 상태이다.
  • DELETED: 리뷰가 삭제된 상태를 나타낸다. 더이상 재작성은 불가능한 상태이다.
  1. 주문 상세 조회에서는 이러한 리뷰 상태를 이용하여 각 주문 상품의 리뷰 작성 여부를 확인할 수 있다.

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;
}

1. Review Create

Review Create API

  1. 요청 메서드 : POST
  2. 엔드포인트 : /reviews
  3. 요청 데이터 : ReviewCreateRequest 객체
{
  "orderItemId": 1,
  "score": 4.5,
  "content": "대체적으로 만족합니다!"
}
  1. 응답 데이터
  • 성공 시 200 OK : 등록된 리뷰 정보를 담은 ReviewCreateResponse 객체를 반환 ( 해당 주문 상품의 reviewStatus 값 NOT_WRITABLE → WRITABLE 전환 )
{
  "message": "리뷰가 성공적으로 작성되었습니다.",
  "review": {
    "reviewId": 1,
    "reviewDate": "2023-11-27",
    "score": 4.5,
    "content": "대체적으로 만족합니다!"
  }
}
  • 실패 시 409 Conflict : 이미 리뷰를 작성했거나 삭제했을 때 발생하는 ReviewException 에 대한 응답을 반환

Review Create Request & Resposne DTO

ReviewCreateRequest

@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();
    }
}

ReviewCreateResponse

@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();
    }
}

Review Controller & Service Class

ReviewController - createReview

@GetMapping
public ResponseEntity<List<UserReviewResponse>> getUserReviews() {
    List<UserReviewResponse> userReviewListResponse = reviewService.getUserReviews();
    return ResponseEntity.ok(userReviewListResponse);
}

ReviewService - createReview

@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

  1. 사용자 정보 조회 : 현재 사용자의 정보를 가져온다.
  2. 주문 상품 정보 조회 : 주어진 주문 상품 ID에 해당하는 주문 상품 정보를 데이터베이스에서 조회한다.
  3. 주문 상품의 리뷰 상태 확인: 조회된 주문 상품의 리뷰 상태를 확인하고, 이미 리뷰가 작성되었거나 삭제된 경우에는 예외를 발생시킨다.
  4. 상품 정보 조회: 주문 상품에 연결된 상품 정보를 조회한다.
    ✔️ 상품 엔티티와 리뷰 엔티티 사이의 연관관계를 맺어주기 위해서이다.
    ✔️ 숙소 리뷰 조회 시 해당 상품과 연관된 리뷰의 정보를 가져올 수 있다.
  5. 리뷰 엔티티 생성 및 저장 : 주어진 요청 데이터를 기반으로 새로운 리뷰 엔티티를 생성하고 저장한다.
    ✔️ 리뷰는 사용자, 사용자의 주문 상품과도 연관관계를 가지게 된다.
    ✔️ 사용자 리뷰 조회 및 주문 상품별 리뷰 관리를 수행할 수 있다.
  6. 주문 상품의 리뷰 작성 상태 변경 : NO_WRITTEN → WRITTEN
  7. 생성된 리뷰 엔티티를 응답 객체로 변환하여 반환한다.

리뷰 작성 후 주문 상품에 대한 리뷰 상태 변경

{
    "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
        }
    ]
}

2. Review Update

Review Update API

  1. 요청 메서드 : PUT
  2. 엔드포인트 : /reviews/{reviewId}
  3. 요청 데이터 : reviewId 및 ReviewUpdateRequest 객체
{
    "score": 5.0,
    "content": "대체적으로 만족합니다!",
}
  1. 응답 데이터
  • 성공 시 200 OK : 수정된 리뷰 정보를 담은 ReviewUpdateResponse 객체를 반환
{
  "message": "리뷰가 성공적으로 수정되었습니다.",
  "review": {
    "reviewId": 12,
    "updateDate": "2023-12-08",
    "score": 5.0,
    "content": "대체적으로 만족합니다!"
  }
}
  • 실패 시 404 Not Found : 리뷰가 존재하지 않을 때 발생하는 ReviewException 에 대한 응답을 반환

Review Update Request & Resposne DTO

ReviewUpdateRequest

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ReviewUpdateRequest {

    private double score;
    private String content;
}

ReviewUpdateResponse

@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();
    }
}   

Review Controller & Service Class

ReviewController - updateReview

public ResponseEntity<ReviewUpdateResponse> updateReview(
        @PathVariable Long reviewId,
        @Valid @RequestBody ReviewUpdateRequest reviewUpdateRequest
) {
    ReviewUpdateResponse reviewUpdateResponse = reviewService.updateReview(reviewId, reviewUpdateRequest);
    return ResponseEntity.ok(reviewUpdateResponse);
}

ReviewService - updateReview

@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

  1. 리뷰 조회: 주어진 리뷰 아이디를 사용하여 데이터베이스에서 해당 리뷰를 조회한다. 리뷰가 이미 삭제하여 존재하지 않는 경우에는 예외가 발생한다.
  2. 리뷰 업데이트: ReviewUpdateRequest 객체에 따라 리뷰 엔티티를 업데이트한다.
  3. 업데이트된 리뷰 저장: 변경된 리뷰를 데이터베이스에 저장한다.
  4. 업데이트된 리뷰로 응답 객체 생성 및 반환: 업데이트된 리뷰를 기반으로 ReviewUpdateResponse 객체를 생성하고 반환한다.

3. Review Delete

Review Delete API

  1. 요청 메서드 : DELETE
  2. 2.엔드포인트 : /reviews/{reviewId}
  3. 요청 데이터 : reviewId
  4. 응답 데이터
  • 성공 시 200 OK : 수정된 리뷰 정보를 담은 ReviewDeleteResponse 객체를 반환
{
  "message": "리뷰가 성공적으로 삭제되었습니다.",
  "review": {
    "reviewId": 12,
    "deleteDate": "2023-11-27"
  }
}
  • 실패 시 404 Not Found : 리뷰가 존재하지 않을 때 발생하는 ReviewException 에 대한 응답을 반환

Review Delete Resposne DTO

ReviewDeleteResponse

@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();
    }
}

Review Controller & Service Class

ReviewController - deleteReview

public ResponseEntity<ReviewDeleteResponse> deleteReview(
        @PathVariable Long reviewId
) {
    ReviewDeleteResponse reviewDeleteResponse = reviewService.deleteReview(reviewId);
    return ResponseEntity.ok(reviewDeleteResponse);
}

ReviewService - deleteReview

@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 객체를 생성하고 반환한다.

4. Review Read - Accommodation

  1. 요청 메서드 : GET
  2. 엔드포인트 : /reviews/{accommodationId}
  3. 요청 데이터 : accommodationId
  4. 응답 데이터
  • 성공 시 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": "캐릭터 룸"
        }
    }
]

Review Read Resposne DTO

ProductReviewResponse

@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();
    }
}

Review Controller & Service Class

ReviewController - getProductReviews

@GetMapping
public ResponseEntity<List<UserReviewResponse>> getUserReviews() {
    List<UserReviewResponse> userReviewListResponse = reviewService.getUserReviews();
    return ResponseEntity.ok(userReviewListResponse);
}

ReviewService - getProductReviews

@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;
}

5. Review Read - User

Review Read API

  1. 요청 메서드 : GET
  2. 엔드포인트 : /reviews
  3. 요청 데이터 : 없음
  4. 응답 데이터
  • 성공 시 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": "일반실(한실)"
        }
    }
]

Review Read Resposne DTO

UserReviewResponse

@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();
    }
}

Review Controller & Service Class

ReviewController - getUserReviews

@GetMapping
public ResponseEntity<List<UserReviewResponse>> getUserReviews() {
    List<UserReviewResponse> userReviewListResponse = reviewService.getUserReviews();
    return ResponseEntity.ok(userReviewListResponse);
}

ReviewService - getUserReviews

@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;
}

0개의 댓글