ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,72 @@
+package com.frame2.server.core.board.api;
+
+import com.frame2.server.core.board.application.ProductReviewService;
+import com.frame2.server.core.board.payload.request.ProductReviewModifyRequest;
+import com.frame2.server.core.board.payload.request.ProductReviewRequest;
+import com.frame2.server.core.board.payload.response.ProductReviewResponse;
+import com.frame2.server.core.support.annotations.Auth;
+import com.frame2.server.core.support.annotations.MemberOnly;
+import com.frame2.server.core.support.request.User;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/products/{saleProductId}/reviews")
+public class ProductReviewApi implements ProductReviewApiSpec {
+
+    private final ProductReviewService productReviewService;
+
+    // create
+    @Override
+    @MemberOnly
+    @PostMapping
+    public void createProductReview(
+            @Valid @RequestBody ProductReviewRequest productReviewRequest,
+            @Auth User user,
+            @PathVariable("saleProductId") Long saleProductId
+    ) {
+        productReviewService.productReviewCreate(productReviewRequest, user.id(), saleProductId);
+    }
+
+    // update
+    @Override
+    @MemberOnly
+    @PatchMapping("/{productReviewId}")
+    public void updateProductReview(
+            @Valid @RequestBody ProductReviewModifyRequest ProductReviewModifyRequest,
+            @Auth User user,
+            @PathVariable("productReviewId") Long productReviewId
+    ) {
+        productReviewService.productReviewModify(ProductReviewModifyRequest, productReviewId);
+    }
+
+    // delete
+    @Override
+    @MemberOnly
+    @DeleteMapping("/{productReviewId}")
+    public void deleteProductReview(@PathVariable("productReviewId") Long id, @Auth User user) {
+        productReviewService.remove(id);
+    }
+
+    // 단건 조회
+    @Override
+    @GetMapping("/{productReviewId}")
+    public ResponseEntity<ProductReviewResponse> getProductReview(@PathVariable("productReviewId") Long productReviewId) {
+        ProductReviewResponse productReviewResponse = productReviewService.getProductReview(productReviewId);
+        return ResponseEntity.ok().body(productReviewResponse);
+    }
+
+    // 한 상품에 대한 전체 리뷰 조회
+    @Override
+    @GetMapping
+    public ResponseEntity<List<ProductReviewResponse>> getProductReviewList(
+            @PathVariable("saleProductId") Long saleProductId) {
+        List<ProductReviewResponse> productReviewList = productReviewService.getProductReviewList(saleProductId);
+        return ResponseEntity.ok().body(productReviewList);
+    }
+}

리뷰: 아주 좋은 것 같습니다 수정했습니다
@@ -0,0 +1,72 @@
+package com.frame2.server.core.board.api;
+
+import com.frame2.server.core.board.application.ProductReviewService;
+import com.frame2.server.core.board.payload.request.ProductReviewModifyRequest;
+import com.frame2.server.core.board.payload.request.ProductReviewRequest;
+import com.frame2.server.core.board.payload.response.ProductReviewResponse;
+import com.frame2.server.core.support.annotations.Auth;
+import com.frame2.server.core.support.annotations.MemberOnly;
+import com.frame2.server.core.support.request.User;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/products/{saleProductId}/reviews")
+public class ProductReviewApi implements ProductReviewApiSpec {
+
+    private final ProductReviewService productReviewService;
+
+    // create
+    @Override
+    @MemberOnly
+    @PostMapping
+    public void createProductReview(
+            @Valid @RequestBody ProductReviewRequest productReviewRequest,
+            @Auth User user,
+            @PathVariable("saleProductId") Long saleProductId
+    ) {
+        productReviewService.productReviewCreate(productReviewRequest, user.id(), saleProductId);
+    }
+
+    // update
+    @Override
+    @MemberOnly
+    @PatchMapping("/{productReviewId}")
+    public void updateProductReview(
+            @Valid @RequestBody ProductReviewModifyRequest ProductReviewModifyRequest,
+            @Auth User user,
+            @PathVariable("productReviewId") Long productReviewId
+    ) {
+        productReviewService.productReviewModify(ProductReviewModifyRequest, productReviewId);
+    }
+
+    // delete
+    @Override
+    @MemberOnly
+    @DeleteMapping("/{productReviewId}")
+    public void deleteProductReview(@PathVariable("productReviewId") Long id, @Auth User user) {
+        productReviewService.remove(id);
+    }
+
+    // 단건 조회
+    @Override
+    @GetMapping("/{productReviewId}")
+    public ResponseEntity<ProductReviewResponse> getProductReview(@PathVariable("productReviewId") Long productReviewId) {
+        ProductReviewResponse productReviewResponse = productReviewService.getProductReview(productReviewId);
+        return ResponseEntity.ok().body(productReviewResponse);
+    }
+
+    // 한 상품에 대한 전체 리뷰 조회
+    @Override
+    @GetMapping
+    public ResponseEntity<List<ProductReviewResponse>> getProductReviewList(
+            @PathVariable("saleProductId") Long saleProductId) {
+        List<ProductReviewResponse> productReviewList = productReviewService.getProductReviewList(saleProductId);
+        return ResponseEntity.ok().body(productReviewList);
+    }
+}

리뷰: 말씀해주신 부분 공부해 보고 적용해 보겠습니다.
@@ -0,0 +1,70 @@
+package com.frame2.server.core.board.application;
+
+import com.frame2.server.core.board.domain.ProductReview;
+import com.frame2.server.core.board.infrastructure.ProductReviewRepository;
+import com.frame2.server.core.board.payload.request.ProductReviewModifyRequest;
+import com.frame2.server.core.board.payload.request.ProductReviewRequest;
+import com.frame2.server.core.board.payload.response.ProductReviewResponse;
+import com.frame2.server.core.member.domain.Member;
+import com.frame2.server.core.member.infrastructure.MemberRepository;
+import com.frame2.server.core.order.domain.OrderDetail;
+import com.frame2.server.core.order.infrastructure.OrderDetailRepository;
+import com.frame2.server.core.product.domain.SaleProduct;
+import com.frame2.server.core.product.infrastructure.SaleProductRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class ProductReviewServiceImpl implements ProductReviewService {
+
+    private final ProductReviewRepository productReviewRepository;
+    private final SaleProductRepository saleProductRepository;
+    private final MemberRepository memberRepository;
+    private final OrderDetailRepository orderDetailRepository;
+
+    // 생성
+    @Override
+    public void productReviewCreate(ProductReviewRequest request, Long memberId, Long saleProductId) {
+        Member member = memberRepository.findOne(memberId);
+        SaleProduct saleProduct = saleProductRepository.findOne(saleProductId);
+        OrderDetail orderDetail = orderDetailRepository.findOne(request.orderDetailId());
+
+        ProductReview productReview = request.toEntity(member, saleProduct, orderDetail);
+        productReviewRepository.save(productReview);
+    }
+
+    // 수정
+    @Override
+    public void productReviewModify(ProductReviewModifyRequest request, Long productReviewId) {
+        productReviewRepository.findOne(productReviewId).updateProductReview(request.toEntity());
+    }
+
+    // 삭제
+    @Override
+    public void remove(Long productReviewId) {
+        productReviewRepository.findOne(productReviewId).delete();
+    }
+
+    // 단건 조회
+    @Override
+    @Transactional(readOnly = true)
+    public ProductReviewResponse getProductReview(Long productReviewId) {
+        ProductReview productReview = productReviewRepository.findOne(productReviewId);
+        return ProductReviewResponse.from(productReview);
+    }
+
+    // 한 상품에 대한 전체 리뷰 조회
+    @Override
+    @Transactional(readOnly = true)
+    public List<ProductReviewResponse> getProductReviewList(Long saleProductId) {
+        List<ProductReview> productReviewList = productReviewRepository.findAllBySaleProductId(saleProductId);
+        return productReviewList.stream()
+                .map(ProductReviewResponse::from)
+                .toList();
+    }
+}

리뷰: 테스트 코드를 작성하지 않았지만 테스트를 좀 더 쉽게 하기 위해 엔티티를 반환 하도록 만들었습니다
불필요한 설계라면 바로 바꾸겠습니다.
@@ -0,0 +1,70 @@
+package com.frame2.server.core.board.application;
+
+import com.frame2.server.core.board.domain.ProductReview;
+import com.frame2.server.core.board.infrastructure.ProductReviewRepository;
+import com.frame2.server.core.board.payload.request.ProductReviewModifyRequest;
+import com.frame2.server.core.board.payload.request.ProductReviewRequest;
+import com.frame2.server.core.board.payload.response.ProductReviewResponse;
+import com.frame2.server.core.member.domain.Member;
+import com.frame2.server.core.member.infrastructure.MemberRepository;
+import com.frame2.server.core.order.domain.OrderDetail;
+import com.frame2.server.core.order.infrastructure.OrderDetailRepository;
+import com.frame2.server.core.product.domain.SaleProduct;
+import com.frame2.server.core.product.infrastructure.SaleProductRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class ProductReviewServiceImpl implements ProductReviewService {
+
+    private final ProductReviewRepository productReviewRepository;
+    private final SaleProductRepository saleProductRepository;
+    private final MemberRepository memberRepository;
+    private final OrderDetailRepository orderDetailRepository;
+
+    // 생성
+    @Override
+    public void productReviewCreate(ProductReviewRequest request, Long memberId, Long saleProductId) {
+        Member member = memberRepository.findOne(memberId);
+        SaleProduct saleProduct = saleProductRepository.findOne(saleProductId);
+        OrderDetail orderDetail = orderDetailRepository.findOne(request.orderDetailId());
+
+        ProductReview productReview = request.toEntity(member, saleProduct, orderDetail);
+        productReviewRepository.save(productReview);
+    }
+
+    // 수정
+    @Override
+    public void productReviewModify(ProductReviewModifyRequest request, Long productReviewId) {
+        productReviewRepository.findOne(productReviewId).updateProductReview(request.toEntity());
+    }
+
+    // 삭제
+    @Override
+    public void remove(Long productReviewId) {
+        productReviewRepository.findOne(productReviewId).delete();
+    }
+
+    // 단건 조회
+    @Override
+    @Transactional(readOnly = true)
+    public ProductReviewResponse getProductReview(Long productReviewId) {
+        ProductReview productReview = productReviewRepository.findOne(productReviewId);
+        return ProductReviewResponse.from(productReview);
+    }
+
+    // 한 상품에 대한 전체 리뷰 조회
+    @Override
+    @Transactional(readOnly = true)
+    public List<ProductReviewResponse> getProductReviewList(Long saleProductId) {
+        List<ProductReview> productReviewList = productReviewRepository.findAllBySaleProductId(saleProductId);
+        return productReviewList.stream()
+                .map(ProductReviewResponse::from)
+                .toList();
+    }
+}

리뷰: 말씀하신 것처럼 ProductReviewModifyRequest 객체가 필요한 정보를 다 받고 있기 때문에 굳이 엔티티로 변환할 필요는 없지만 엔티티로 변환하고 반환하면 테스트를 좀 더 쉽게 할 수 있지 않을까 하여 이렇게 작성 하였습니다.
@@ -0,0 +1,70 @@
+package com.frame2.server.core.board.application;
+
+import com.frame2.server.core.board.domain.ProductReview;
+import com.frame2.server.core.board.infrastructure.ProductReviewRepository;
+import com.frame2.server.core.board.payload.request.ProductReviewModifyRequest;
+import com.frame2.server.core.board.payload.request.ProductReviewRequest;
+import com.frame2.server.core.board.payload.response.ProductReviewResponse;
+import com.frame2.server.core.member.domain.Member;
+import com.frame2.server.core.member.infrastructure.MemberRepository;
+import com.frame2.server.core.order.domain.OrderDetail;
+import com.frame2.server.core.order.infrastructure.OrderDetailRepository;
+import com.frame2.server.core.product.domain.SaleProduct;
+import com.frame2.server.core.product.infrastructure.SaleProductRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class ProductReviewServiceImpl implements ProductReviewService {
+
+    private final ProductReviewRepository productReviewRepository;
+    private final SaleProductRepository saleProductRepository;
+    private final MemberRepository memberRepository;
+    private final OrderDetailRepository orderDetailRepository;
+
+    // 생성
+    @Override
+    public void productReviewCreate(ProductReviewRequest request, Long memberId, Long saleProductId) {
+        Member member = memberRepository.findOne(memberId);
+        SaleProduct saleProduct = saleProductRepository.findOne(saleProductId);
+        OrderDetail orderDetail = orderDetailRepository.findOne(request.orderDetailId());
+
+        ProductReview productReview = request.toEntity(member, saleProduct, orderDetail);
+        productReviewRepository.save(productReview);
+    }
+
+    // 수정
+    @Override
+    public void productReviewModify(ProductReviewModifyRequest request, Long productReviewId) {
+        productReviewRepository.findOne(productReviewId).updateProductReview(request.toEntity());
+    }
+
+    // 삭제
+    @Override
+    public void remove(Long productReviewId) {
+        productReviewRepository.findOne(productReviewId).delete();
+    }
+
+    // 단건 조회
+    @Override
+    @Transactional(readOnly = true)
+    public ProductReviewResponse getProductReview(Long productReviewId) {
+        ProductReview productReview = productReviewRepository.findOne(productReviewId);
+        return ProductReviewResponse.from(productReview);
+    }
+
+    // 한 상품에 대한 전체 리뷰 조회
+    @Override
+    @Transactional(readOnly = true)
+    public List<ProductReviewResponse> getProductReviewList(Long saleProductId) {
+        List<ProductReview> productReviewList = productReviewRepository.findAllBySaleProductId(saleProductId);
+        return productReviewList.stream()
+                .map(ProductReviewResponse::from)
+                .toList();
+    }
+}

리뷰: request 
productReviewRepository.findOne(request.id()) 
이렇게 사용 하는 것 보다는 가독성 측면과  의미가 좀 더 명확하게 할 수 있지 않을까 하여 사용했습니다.
@@ -0,0 +1,70 @@
+package com.frame2.server.core.board.application;
+
+import com.frame2.server.core.board.domain.ProductReview;
+import com.frame2.server.core.board.infrastructure.ProductReviewRepository;
+import com.frame2.server.core.board.payload.request.ProductReviewModifyRequest;
+import com.frame2.server.core.board.payload.request.ProductReviewRequest;
+import com.frame2.server.core.board.payload.response.ProductReviewResponse;
+import com.frame2.server.core.member.domain.Member;
+import com.frame2.server.core.member.infrastructure.MemberRepository;
+import com.frame2.server.core.order.domain.OrderDetail;
+import com.frame2.server.core.order.infrastructure.OrderDetailRepository;
+import com.frame2.server.core.product.domain.SaleProduct;
+import com.frame2.server.core.product.infrastructure.SaleProductRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class ProductReviewServiceImpl implements ProductReviewService {
+
+    private final ProductReviewRepository productReviewRepository;
+    private final SaleProductRepository saleProductRepository;
+    private final MemberRepository memberRepository;
+    private final OrderDetailRepository orderDetailRepository;
+
+    // 생성
+    @Override
+    public void productReviewCreate(ProductReviewRequest request, Long memberId, Long saleProductId) {
+        Member member = memberRepository.findOne(memberId);
+        SaleProduct saleProduct = saleProductRepository.findOne(saleProductId);
+        OrderDetail orderDetail = orderDetailRepository.findOne(request.orderDetailId());
+
+        ProductReview productReview = request.toEntity(member, saleProduct, orderDetail);
+        productReviewRepository.save(productReview);
+    }
+
+    // 수정
+    @Override
+    public void productReviewModify(ProductReviewModifyRequest request, Long productReviewId) {
+        productReviewRepository.findOne(productReviewId).updateProductReview(request.toEntity());
+    }
+
+    // 삭제
+    @Override
+    public void remove(Long productReviewId) {
+        productReviewRepository.findOne(productReviewId).delete();
+    }
+
+    // 단건 조회
+    @Override
+    @Transactional(readOnly = true)
+    public ProductReviewResponse getProductReview(Long productReviewId) {
+        ProductReview productReview = productReviewRepository.findOne(productReviewId);
+        return ProductReviewResponse.from(productReview);
+    }
+
+    // 한 상품에 대한 전체 리뷰 조회
+    @Override
+    @Transactional(readOnly = true)
+    public List<ProductReviewResponse> getProductReviewList(Long saleProductId) {
+        List<ProductReview> productReviewList = productReviewRepository.findAllBySaleProductId(saleProductId);
+        return productReviewList.stream()
+                .map(ProductReviewResponse::from)
+                .toList();
+    }
+}

리뷰: 제 의도로는 한 상품에 대한 모든 리뷰 조회 였는데 생각이 많이 짧았던 것 같습니다 코드 수정 후 다시 올리겠습니다
@@ -0,0 +1,70 @@
+package com.frame2.server.core.board.application;
+
+import com.frame2.server.core.board.domain.ProductReview;
+import com.frame2.server.core.board.infrastructure.ProductReviewRepository;
+import com.frame2.server.core.board.payload.request.ProductReviewModifyRequest;
+import com.frame2.server.core.board.payload.request.ProductReviewRequest;
+import com.frame2.server.core.board.payload.response.ProductReviewResponse;
+import com.frame2.server.core.member.domain.Member;
+import com.frame2.server.core.member.infrastructure.MemberRepository;
+import com.frame2.server.core.order.domain.OrderDetail;
+import com.frame2.server.core.order.infrastructure.OrderDetailRepository;
+import com.frame2.server.core.product.domain.SaleProduct;
+import com.frame2.server.core.product.infrastructure.SaleProductRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class ProductReviewServiceImpl implements ProductReviewService {
+
+    private final ProductReviewRepository productReviewRepository;
+    private final SaleProductRepository saleProductRepository;
+    private final MemberRepository memberRepository;
+    private final OrderDetailRepository orderDetailRepository;
+
+    // 생성
+    @Override
+    public void productReviewCreate(ProductReviewRequest request, Long memberId, Long saleProductId) {
+        Member member = memberRepository.findOne(memberId);
+        SaleProduct saleProduct = saleProductRepository.findOne(saleProductId);
+        OrderDetail orderDetail = orderDetailRepository.findOne(request.orderDetailId());
+
+        ProductReview productReview = request.toEntity(member, saleProduct, orderDetail);
+        productReviewRepository.save(productReview);
+    }
+
+    // 수정
+    @Override
+    public void productReviewModify(ProductReviewModifyRequest request, Long productReviewId) {
+        productReviewRepository.findOne(productReviewId).updateProductReview(request.toEntity());
+    }
+
+    // 삭제
+    @Override
+    public void remove(Long productReviewId) {
+        productReviewRepository.findOne(productReviewId).delete();
+    }
+
+    // 단건 조회
+    @Override
+    @Transactional(readOnly = true)
+    public ProductReviewResponse getProductReview(Long productReviewId) {
+        ProductReview productReview = productReviewRepository.findOne(productReviewId);
+        return ProductReviewResponse.from(productReview);
+    }
+
+    // 한 상품에 대한 전체 리뷰 조회
+    @Override
+    @Transactional(readOnly = true)
+    public List<ProductReviewResponse> getProductReviewList(Long saleProductId) {
+        List<ProductReview> productReviewList = productReviewRepository.findAllBySaleProductId(saleProductId);
+        return productReviewList.stream()
+                .map(ProductReviewResponse::from)
+                .toList();
+    }
+}

리뷰: 레포지토리 부분에서 id로 엔티티를 찾지 못하는 경우에 예외 처리를 해놨습니다 
"존재하지 않는 리뷰입니다" 라는 에러 메시지가 나타나게 됩니다
@@ -0,0 +1,45 @@
+package com.frame2.server.core.board.domain;
+
+import com.frame2.server.core.member.domain.Member;
+import com.frame2.server.core.order.domain.OrderDetail;
+import com.frame2.server.core.product.domain.SaleProduct;
+import com.frame2.server.core.support.entity.BaseEntity;
+import jakarta.persistence.*;
+import lombok.*;
+
+@Entity
+@Getter
+@Builder
+@AllArgsConstructor
+@Table(name = "product_reviews")
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class ProductReview extends BaseEntity {
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "sale_product_id", nullable = false)
+    private SaleProduct saleProduct;
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "member_id", nullable = false)
+    private Member member;
+
+    @OneToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "order_detail_id", nullable = false)
+    private OrderDetail orderDetail;
+
+    // 평점
+    @Column(nullable = false)
+    private int rating;
+
+    //리뷰 내용
+    @Column(nullable = false)
+    private String contents;
+
+    private String image;
+
+    public void updateProductReview(ProductReview newProductReview) {
+        this.rating = newProductReview.getRating();
+        this.contents = newProductReview.getContents();
+        this.image = newProductReview.getImage();
+    }
+}

리뷰: 엔티티에서는 validation 사용하지 않는게 좋습니다.
@@ -0,0 +1,45 @@
+package com.frame2.server.core.board.domain;
+
+import com.frame2.server.core.member.domain.Member;
+import com.frame2.server.core.order.domain.OrderDetail;
+import com.frame2.server.core.product.domain.SaleProduct;
+import com.frame2.server.core.support.entity.BaseEntity;
+import jakarta.persistence.*;
+import lombok.*;
+
+@Entity
+@Getter
+@Builder
+@AllArgsConstructor
+@Table(name = "product_reviews")
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class ProductReview extends BaseEntity {
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "sale_product_id", nullable = false)
+    private SaleProduct saleProduct;
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "member_id", nullable = false)
+    private Member member;
+
+    @OneToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "order_detail_id", nullable = false)
+    private OrderDetail orderDetail;
+
+    // 평점
+    @Column(nullable = false)
+    private int rating;
+
+    //리뷰 내용
+    @Column(nullable = false)
+    private String contents;
+
+    private String image;
+
+    public void updateProductReview(ProductReview newProductReview) {
+        this.rating = newProductReview.getRating();
+        this.contents = newProductReview.getContents();
+        this.image = newProductReview.getImage();
+    }
+}

리뷰: ProductReview는 반환하지 않아도 됩니다.