상품에 대한 리뷰 작성 기능

뚜우웅이·2025년 4월 29일

캡스톤 디자인

목록 보기
19/35

Domain

Review

@Entity
@Table(name = "reviews")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Review extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "review_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "reviewer_id")
    private User reviewer; // 리뷰 작성자 (구매자)

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "target_user_id")
    private User targetUser; // 리뷰 대상자 (판매자)

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;

    @Column(nullable = false)
    private Integer rating;

    @Column(columnDefinition = "TEXT")
    private String content;

    @Builder
    public Review(User reviewer, User targetUser, Product product, Integer rating, String content) {
        this.reviewer = reviewer;
        this.targetUser = targetUser;
        this.product = product;
        this.rating = rating;
        this.content = content;
    }

    // 리뷰 내용 수정
    public void update(Integer rating, String content) {
        this.rating = rating;
        this.content = content;
    }
}

Vo

RatingCount

@Getter
@RequiredArgsConstructor
public class RatingCount {
    private final Integer rating;
    private final Long count;
}

특정 별점(rating)별 리뷰 개수(count)를 담기 위한 간단한 값 객체(Value Object)다.

RatingDistribution

@Getter
public class RatingDistribution {
    private final Map<Integer, Long> distribution;
    private final long totalCount;

    public RatingDistribution(Map<Integer, Long> distribution) {
        this.distribution = distribution;
        this.totalCount = distribution.values().stream().mapToLong(Long::valueOf).sum();
    }

    public double getPercentage(int rating) {
        if (totalCount == 0) return 0.0;
        return (distribution.getOrDefault(rating, 0L) * 100.0 / totalCount);
    }
}

리뷰 시스템에서 별점 분포와 관련된 계산 및 통계를 담당하는 값 객체(Value Object)다.

Repository

ReviewRepository

public interface ReviewRepository extends JpaRepository<Review, Long>, ReviewRepositoryCustom {

    Optional<Review> findByProduct(Product product);

    // 특정 사용자(판매자)에 대한 리뷰 목록 페이징 조회
    Page<Review> findByTargetUserOrderByCreatedDateDesc(User targetUser, Pageable pageable);

    // 특정 사용자(구매자)가 작성한 리뷰 목록 페이징 조회
    Page<Review> findByReviewerOrderByCreatedDateDesc(User reviewer, Pageable pageable);

    // 특정 사용자가 받은 리뷰 수 조회
    long countByTargetUserId(Long targetUserId);
}

ReviewRepositoryCustom

public interface ReviewRepositoryCustom {

    // 특정 별점 이상의 리뷰만 조회
    Page<Review> findByTargetUserIdAndMinimumRating(Long targetUserId, Integer minimumRating, Pageable pageable);

    // 별점 분포 통계 조회
    RatingDistribution getRatingDistributionByUserId(Long userId);

    // 평균 평점 조회
    Double getAverageRatingByUserId(Long userId);
}

ReviewRepositoryImpl

@RequiredArgsConstructor
public class ReviewRepositoryImpl implements ReviewRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    @Override
    public Page<Review> findByTargetUserIdAndMinimumRating(Long targetUserId, Integer minimumRating, Pageable pageable) {
        List<Review> content = queryFactory
                .selectFrom(review)
                .where(review.targetUser.id.eq(targetUserId)
                        .and(review.rating.goe(minimumRating)))
                .orderBy(review.createdDate.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        JPAQuery<Long> count = queryFactory
                .select(review.count())
                .from(review)
                .where(review.targetUser.id.eq(targetUserId)
                        .and(review.rating.goe(minimumRating)));

        return PageableExecutionUtils.getPage(content, pageable, count::fetchOne);
    }

    @Override
    public RatingDistribution getRatingDistributionByUserId(Long userId) {
        List<RatingCount> ratingCount = queryFactory
                .select(Projections.constructor(RatingCount.class,
                        review.rating,
                        review.count()))
                .from(review)
                .where(review.targetUser.id.eq(userId))
                .groupBy(review.rating)
                .fetch();

        Map<Integer, Long> distribution = new HashMap<>();
        for (int i = 1; i <= 5; i++) {
            distribution.put(i, 0L);
        }

        // 조회 결과로 맵 업데이트
        for (RatingCount count : ratingCount) {
            distribution.put(count.getRating(), count.getCount());
        }

        return new RatingDistribution(distribution);
    }

    @Override
    public Double getAverageRatingByUserId(Long userId) {
        Double avgRating = queryFactory
                .select(review.rating.avg())
                .from(review)
                .where(review.targetUser.id.eq(userId))
                .fetchOne();

        return avgRating != null ? avgRating : 0.0;
    }
}
  • findByTargetUserIdAndMinimumRating

    • 최소 별점 이상의 리뷰 조회
    • 조건부 필터링:
      • review.targetUser.id.eq(targetUserId): 특정 판매자에 대한 리뷰만 선택
      • review.rating.goe(minimumRating): 최소 별점 이상의 리뷰만 선택 (goe = greater or equal)
    • 정렬 및 페이징:
      • orderBy(review.createdDate.desc()): 최신 리뷰부터 정렬
      • offset(pageable.getOffset()): 페이지 시작점 설정
      • limit(pageable.getPageSize()): 한 페이지에 표시할 개수 설정
    • 페이지 객체 생성:
      - PageableExecutionUtils.getPage(): 효율적인 페이지 객체 생성
      두 번째 쿼리(count)는 필요할 때만 실행되어 성능 최적화
  • getRatingDistributionByUserId

    • 별점 분포 조회
    • 그룹화 및 집계 쿼리:
      • Projections.constructor(RatingCount.class, ...): 쿼리 결과를 RatingCount 객체로 매핑
      • review.rating.count(): 각 별점별 리뷰 개수 계산
      • groupBy(review.rating): 별점별로 그룹화
    • 기본값 설정 및 결과 처리:
      • 모든 별점(1-5)에 대해 기본값 0으로 초기화
      • 쿼리 결과로 얻은 실제 리뷰 개수로 맵 업데이트
      • 결과를 RatingDistribution 객체로 래핑하여 반환
    • 데이터 준비:
      • 별점 분포 차트나 통계에 사용할 완전한 데이터셋 제공
  • getAverageRatingByUserId

    • 평균 별점 조회
    • 집계 함수 사용:
      • review.rating.avg(): SQL의 AVG 함수와 같은 역할
      • QueryDSL이 자동으로 SELECT AVG(review.rating) FROM review WHERE ... SQL로 변환
    • Null 처리:
      • 리뷰가 없는 경우 null을 반환할 수 있으므로, null 체크 후 기본값 0.0 반환
      • 안전한 값 사용을 보장

DTO

public class ReviewDto {

    @Schema(description = "리뷰 작성 요청")
    public record ReviewCreateRequest(
        @Schema(description = "상품 ID", example = "123")
        @NotNull(message = "상품 ID는 필수 입력값입니다.")
        Long productId,

        @Schema(description = "별점 (1-5)", example = "4")
        @NotNull(message = "별점은 필수 입력값입니다.")
        @Min(value = 1, message = "별점은 1점 이상이어야 합니다.")
        @Max(value = 5, message = "별점은 5점 이하여야 합니다.")
        Integer rating,

        @Schema(description = "리뷰 내용", example = "친절하고 빠른 거래였습니다.")
        String content
    ) {}

    @Schema(description = "리뷰 수정 요청")
    public record ReviewUpdateRequest(
            @Schema(description = "별점 (1-5)", example = "5")
            @NotNull(message = "별점은 필수 입력값입니다.")
            @Min(value = 1, message = "별점은 1점 이상이어야 합니다.")
            @Max(value = 5, message = "별점은 5점 이하여야 합니다.")
            Integer rating,

            @Schema(description = "리뷰 내용", example = "정말 만족스러운 거래였습니다.")
            String content
    ) {}

    @Builder
    @Schema(description = "리뷰 응답")
    public record ReviewResponse(
            @Schema(description = "리뷰 ID", example = "1")
            Long id,

            @Schema(description = "리뷰어 ID (구매자)", example = "100")
            Long reviewerId,

            @Schema(description = "리뷰어 이름", example = "홍길동")
            String reviewerName,

            @Schema(description = "대상자 ID (판매자)", example = "200")
            Long targetUserId,

            @Schema(description = "대상자 이름", example = "김판매")
            String targetUserName,

            @Schema(description = "상품 ID", example = "123")
            Long productId,

            @Schema(description = "상품명", example = "중고 노트북")
            String productName,

            @Schema(description = "별점 (1-5)", example = "4")
            Integer rating,

            @Schema(description = "리뷰 내용", example = "친절하고 빠른 거래였습니다.")
            String content,

            @Schema(description = "작성일", example = "2023-09-15T14:30:00")
            LocalDateTime createdDate
    ) {
        public static ReviewResponse from(Review review) {
            return ReviewResponse.builder()
                    .id(review.getId())
                    .reviewerId(review.getReviewer().getId())
                    .reviewerName(review.getReviewer().getName())
                    .targetUserId(review.getTargetUser().getId())
                    .targetUserName(review.getTargetUser().getName())
                    .productId(review.getProduct().getId())
                    .productName(review.getProduct().getName())
                    .rating(review.getRating())
                    .content(review.getContent())
                    .createdDate(review.getCreatedDate())
                    .build();
        }
    }

    @Builder
    @Schema(description = "리뷰 통계 응답")
    public record ReviewStatsResponse (
            @Schema(description = "평균 평점", example = "4.3")
            double averageRating,

            @Schema(description = "총 리뷰 수", example = "27")
            long totalReviews,

            @Schema(description = "별점 분포 (각 별점별 개수)", example = "{\"1\": 2, \"2\": 3, \"3\": 5, \"4\": 10, \"5\": 7}")
            Map<Integer, Long> ratingDistribution,

            @Schema(description = "별점 분포 퍼센트", example = "{\"1\": 7.4, \"2\": 11.1, \"3\": 18.5, \"4\": 37.0, \"5\": 25.9}")
            Map<Integer, Double> ratingPercentages
    ) {}

    @Builder
    @Schema(description = "리뷰 목록 응답")
    public record ReviewListResponse(
            @Schema(description = "리뷰 목록")
            java.util.List<ReviewResponse> reviews,

            @Schema(description = "리뷰 통계")
            ReviewStatsResponse stats,

            @Schema(description = "총 페이지 수", example = "3")
            int totalPages,

            @Schema(description = "총 리뷰 수", example = "27")
            long totalElements
    ) {}

    @Builder
    @Schema(description = "내가 작성한 리뷰 목록 응답")
    public record MyReviewListResponse(
            @Schema(description = "리뷰 목록")
            List<ReviewResponse> reviews,

            @Schema(description = "총 페이지 수", example = "3")
            int totalPages,

            @Schema(description = "총 리뷰 수", example = "27")
            long totalElements
    ) {}
}

Service

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

    private final ReviewRepository reviewRepository;
    private final UserRepository userRepository;
    private final ProductRepository productRepository;

    // 리뷰 생성
    @Transactional
    public ReviewDto.ReviewResponse createReview(Long reviewerId, ReviewDto.ReviewCreateRequest request) {
        User reviewer = getUser(reviewerId);
        Product product = getProduct(request.productId());

        // 판매자가 리뷰 작성자와 같으면 예외 발생
        if (product.getSeller().getId().equals(reviewerId)) {
            throw new ReviewException.SelfReviewException();
        }

        // 상품이 판매완료 상태가 아니면 예외 발생
        if (product.getStatus() != ProductStatus.SOLD_OUT) {
            throw new ReviewException.ProductNotSoldException();
        }

        // 구매자가 리뷰 작성자와 같은지 확인
        if (product.getBuyer() == null || !product.getBuyer().getId().equals(reviewerId)) {
            throw new ReviewException.NotBuyerException();
        }

        // 이미 리뷰가 있는지 확인
        if (reviewRepository.findByProduct(product).isPresent()) {
            throw new ReviewException.ReviewAlreadyExistsException();
        }

        User targetUser = product.getSeller();

        Review review = Review.builder()
                .reviewer(reviewer)
                .targetUser(targetUser)
                .product(product)
                .rating(request.rating())
                .content(request.content())
                .build();

        Review savedReview = reviewRepository.save(review);
        log.info("리뷰 생성 완료: 리뷰어 ID {}, 판매자 ID {}, 상품 ID {}", reviewerId, targetUser.getId(), product.getId());

        return ReviewDto.ReviewResponse.from(review);
    }

    // 리뷰 수정
    @Transactional
    public ReviewDto.ReviewResponse updateReview(Long reviewerId, Long reviewId, ReviewDto.ReviewUpdateRequest request) {
        Review review = getReviewWithReviewerCheck(reviewId, reviewerId);
        review.update(request.rating(), request.content());

        log.info("리뷰 수정 완료: 리뷰 ID {}, 리뷰어 ID {}", reviewId, reviewerId);

        return ReviewDto.ReviewResponse.from(review);
    }

    // 리뷰 삭제
    @Transactional
    public void deleteReview(Long reviewerId, Long reviewId) {
        Review review = getReviewWithReviewerCheck(reviewId, reviewerId);
        reviewRepository.delete(review);

        log.info("리뷰 삭제 완료: 리뷰 ID {}, 리뷰어 ID {}", reviewId, reviewerId);
    }

    // 리뷰 조회
    public ReviewDto.ReviewResponse findReview(Long reviewId) {
        Review review = getReview(reviewId);
        return ReviewDto.ReviewResponse.from(review);
    }

    // 특정 사용자에 대한 리뷰 목록 조회 (판매자 대상)
    public ReviewDto.ReviewListResponse getUserReviews(Long targetUserId, Pageable pageable) {
        User targetUser = getUser(targetUserId);

        Page<Review> reviewsPage = reviewRepository.findByTargetUserOrderByCreatedDateDesc(targetUser, pageable);

        List<ReviewDto.ReviewResponse> reviews = reviewsPage.getContent().stream()
                .map(ReviewDto.ReviewResponse::from)
                .collect(Collectors.toList());

        // 평점 통계 조회
        Double averageRating = reviewRepository.getAverageRatingByUserId(targetUserId);
        if (averageRating == null) averageRating = 0.0;

        long totalReviews = reviewRepository.countByTargetUserId(targetUserId);

        // 별점 분포 조회
        RatingDistribution ratingDistribution = reviewRepository.getRatingDistributionByUserId(targetUserId);

        Map<Integer, Double> percentages = new HashMap<>();
        for (int i = 1; i <= 5; i++) {
            percentages.put(i, ratingDistribution.getPercentage(i));
        }

        ReviewDto.ReviewStatsResponse stats = ReviewDto.ReviewStatsResponse.builder()
                .averageRating(averageRating)
                .totalReviews(totalReviews)
                .ratingDistribution(ratingDistribution.getDistribution())
                .ratingPercentages(percentages)
                .build();

        return ReviewDto.ReviewListResponse.builder()
                .reviews(reviews)
                .stats(stats)
                .totalPages(reviewsPage.getTotalPages())
                .totalElements(reviewsPage.getTotalElements())
                .build();
    }

    // 자신이 작성한 리뷰 목록 조회
    public ReviewDto.MyReviewListResponse getMyReviews(Long reviewerId, Pageable pageable) {
        User reviewer = getUser(reviewerId);

        Page<Review> reviewsPage = reviewRepository.findByReviewerOrderByCreatedDateDesc(reviewer, pageable);

        List<ReviewDto.ReviewResponse> reviews = reviewsPage.getContent().stream()
                .map(ReviewDto.ReviewResponse::from)
                .collect(Collectors.toList());

        return ReviewDto.MyReviewListResponse.builder()
                .reviews(reviews)
                .totalPages(reviewsPage.getTotalPages())
                .totalElements(reviewsPage.getTotalElements())
                .build();
    }

    // 특정 사용자의 평균 평점 조회
    public double getUserAverageRating(Long userId) {
        Double averageRating = reviewRepository.getAverageRatingByUserId(userId);
        return averageRating != null ? averageRating : 0.0;
    }


    // 권한 확인 후 리뷰 조회
    private Review getReviewWithReviewerCheck(Long reviewId, Long reviewerId) {
        Review review = getReview(reviewId);

        if (!review.getReviewer().getId().equals(reviewerId)) {
            throw new ReviewException.ReviewAccessDeniedException();
        }

        return review;
    }

    private Review getReview(Long reviewId) {
        return reviewRepository.findById(reviewId)
                .orElseThrow(() -> new ReviewException.ReviewNotFoundException(reviewId));
    }

    private User getUser(Long userId) {
        return userRepository.findById(userId)
                .orElseThrow(() -> new UserException.UserNotFoundException(userId));
    }

    private Product getProduct(Long productId) {
        return productRepository.findById(productId)
                .orElseThrow(() -> new ProductException.ProductNotFoundException(productId));
    }
}
  • createReview
    • 다양한 비즈니스 규칙 검증:
      • 자신의 상품에 리뷰 작성 불가
      • 판매완료 상태인 상품만 리뷰 작성 가능
      • 구매자만 리뷰 작성 가능
      • 이미 리뷰가 있는 상품에 중복 리뷰 작성 불가
  • updateReview
    • 권한 검증 로직을 getReviewWithReviewerCheck 헬퍼 메서드로 분리
    • 도메인 모델의 update 메서드를 활용한 객체 지향적 설계
    • JPA의 변경 감지(Dirty Checking)를 활용하여 명시적 save 없이 업데이트
  • deleteReview
    • 권한 검증 후 리뷰 삭제 수행
  • getUserReviews
    • 판매자(targetUser)가 받은 리뷰 목록과 통계 정보를 종합적으로 제공
    • 페이징 처리로 대량 데이터 효율적 처리
    • 다양한 통계 정보 수집:
      • 평균 평점
      • 총 리뷰 수
      • 별점 분포 및 백분율
    • Stream API를 활용한 데이터 처리
  • getMyReviews
    • 통계 정보 없이 자신이 작성한 리뷰 목록만 제공
    • 인터페이스 분리 원칙(ISP)에 따라 MyReviewListResponse 사용

Controller

@Slf4j
@RestController
@RequestMapping("/api/reviews")
@RequiredArgsConstructor
@Tag(name = "리뷰", description = "리뷰 관련 API")
public class ReviewController {

    private final ReviewService reviewService;

    @Operation(summary = "리뷰 작성", description = "구매한 상품에 대해 판매자에게 리뷰를 작성합니다.")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "201", description = "리뷰 작성 성공"),
            @ApiResponse(responseCode = "400", description = "잘못된 요청"),
            @ApiResponse(responseCode = "401", description = "인증 실패"),
            @ApiResponse(responseCode = "403", description = "권한 없음"),
            @ApiResponse(responseCode = "404", description = "상품 또는 사용자를 찾을 수 없음"),
            @ApiResponse(responseCode = "409", description = "이미 리뷰가 존재함")
    })
    @PostMapping
    public ResponseEntity<ResponseDTO<ReviewDto.ReviewResponse>> createReview(
            @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
            @Parameter(description = "리뷰 작성 정보", required = true)
            @Valid @RequestBody ReviewDto.ReviewCreateRequest request) {

        log.info("리뷰 작성 요청: 사용자 ID {}, 상품 ID {}", userDetails.getUserId(), request.productId());
        ReviewDto.ReviewResponse response = reviewService.createReview(userDetails.getUserId(), request);

        return ResponseEntity.status(HttpStatus.CREATED).body(ResponseDTO.success(response, "리뷰가 성공적으로 작성되었습니다."));
    }

    @Operation(summary = "리뷰 수정", description = "자신이 작성한 리뷰를 수정합니다.")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "리뷰 수정 성공"),
            @ApiResponse(responseCode = "400", description = "잘못된 요청"),
            @ApiResponse(responseCode = "401", description = "인증 실패"),
            @ApiResponse(responseCode = "403", description = "권한 없음"),
            @ApiResponse(responseCode = "404", description = "리뷰를 찾을 수 없음")
    })
    @PatchMapping("/{reviewId}")
    public ResponseEntity<ResponseDTO<ReviewDto.ReviewResponse>> updateReview(
            @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
            @Parameter(description = "수정할 리뷰 ID", required = true) @PathVariable Long reviewId,
            @Parameter(description = "리뷰 수정 정보", required = true)
            @Valid @RequestBody ReviewDto.ReviewUpdateRequest request) {

        log.info("리뷰 수정 요청: 사용자 ID {}, 리뷰 ID {}", userDetails.getUserId(), reviewId);
        ReviewDto.ReviewResponse response = reviewService.updateReview(userDetails.getUserId(), reviewId, request);

        return ResponseEntity.ok(ResponseDTO.success(response, "리뷰가 성공적으로 수정되었습니다."));
    }

    @Operation(summary = "리뷰 삭제", description = "자신이 작성한 리뷰를 삭제합니다.")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "리뷰 삭제 성공"),
            @ApiResponse(responseCode = "401", description = "인증 실패"),
            @ApiResponse(responseCode = "403", description = "권한 없음"),
            @ApiResponse(responseCode = "404", description = "리뷰를 찾을 수 없음")
    })
    @DeleteMapping("/{reviewId}")
    public ResponseEntity<ResponseDTO<Void>> deleteReview(
            @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
            @Parameter(description = "삭제할 리뷰 ID", required = true) @PathVariable Long reviewId) {

        log.info("리뷰 삭제 요청: 사용자 ID {}, 리뷰 ID {}", userDetails.getUserId(), reviewId);
        reviewService.deleteReview(userDetails.getUserId(), reviewId);

        return ResponseEntity.ok(ResponseDTO.success(null, "리뷰가 성공적으로 삭제되었습니다."));
    }

    @Operation(summary = "리뷰 상세 조회", description = "특정 리뷰의 상세 정보를 조회합니다.")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "리뷰 조회 성공"),
            @ApiResponse(responseCode = "404", description = "리뷰를 찾을 수 없음")
    })
    @GetMapping("/{reviewId}")
    public ResponseEntity<ResponseDTO<ReviewDto.ReviewResponse>> findReview(
            @Parameter(description = "조회할 리뷰 ID", required = true) @PathVariable Long reviewId) {

        log.info("리뷰 상세 조회 요청: 리뷰 ID {}", reviewId);
        ReviewDto.ReviewResponse response = reviewService.findReview(reviewId);

        return ResponseEntity.ok(ResponseDTO.success(response));
    }

    @Operation(summary = "사용자가 받은 리뷰 목록 조회", description = "특정 사용자가 판매자로서 받은 리뷰 목록을 조회합니다.")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "리뷰 목록 조회 성공"),
            @ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음")
    })
    @GetMapping("/users/{userId}")
    public ResponseEntity<ResponseDTO<ReviewDto.ReviewListResponse>> getUserReviews(
            @Parameter(description = "조회할 사용자 ID (판매자)", required = true) @PathVariable Long userId,
            @Parameter(description = "페이지네이션 정보") @PageableDefault(size = 10) Pageable pageable) {

        log.info("사용자가 받은 리뷰 목록 조회 요청: 사용자 ID {}", userId);
        ReviewDto.ReviewListResponse response = reviewService.getUserReviews(userId, pageable);

        return ResponseEntity.ok(ResponseDTO.success(response));
    }

    @Operation(summary = "내가 작성한 리뷰 목록 조회", description = "로그인한 사용자가 작성한 리뷰 목록을 조회합니다.")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "리뷰 목록 조회 성공"),
            @ApiResponse(responseCode = "401", description = "인증 실패")
    })
    @GetMapping("/me")
    public ResponseEntity<ResponseDTO<ReviewDto.MyReviewListResponse>> getMyReviews(
            @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
            @Parameter(description = "페이지네이션 정보") @PageableDefault(size = 10) Pageable pageable) {

        log.info("내가 작성한 리뷰 목록 조회 요청: 사용자 ID {}", userDetails.getUserId());
        ReviewDto.MyReviewListResponse response = reviewService.getMyReviews(userDetails.getUserId(), pageable);

        return ResponseEntity.ok(ResponseDTO.success(response));
    }
}

추가로 수정된 코드

User

UserProfileService

private final ReviewService reviewService;

// 평균 평점 조회
        double averageRating = reviewService.getUserAverageRating(userId);

TODO 표시 해뒀던 평균 평점 조회는 reviewService를 이용하여 조회한다.

Product

ProductStatusService

private final ReviewRepository reviewRepository;

// 판매완료 취소 처리 메서드
    @Transactional
    public ProductDto.ProductBaseResponse cancelProductSold(Long sellerId, Long productId) {
        Product product = productManagementService.getProductWithSellerCheck(productId, sellerId);

        if (product.getStatus() != ProductStatus.SOLD_OUT) {
            throw new ProductException.NotSoldProductException();
        }

        // 리뷰가 작성되었는지 확인
        boolean reviewExists = reviewRepository.findByProduct(product).isPresent();
        if (reviewExists) {
            throw new ProductException.CannotCancelSoldProductException("이미 리뷰가 작성된 상품은 판매완료 취소가 불가능합니다.");
        }

        product.cancelSold();
        log.info("판매완료 취소 처리: 상품 ID {}, 판매자 ID {}", productId, sellerId);
        return ProductDto.ProductBaseResponse.from(product);
    }

판매 취소 메서드에서 리뷰가 작성 되어 있으면 판매 취소 처리를 할 수 없게 수정한다.

ProductException

    public static class CannotCancelSoldProductException extends ProductException {
        public CannotCancelSoldProductException(String message) {
            super(message, HttpStatus.BAD_REQUEST, "CANNOT_CANCEL_SOLD_PRODUCT");
        }
    }

테스트

판매자의 상품에 리뷰 작성

자신의 상품에 리뷰 작성

리뷰 수정

리뷰 상세 조회

사용자가 받은 리뷰 목록 조회

내가 작성한 리뷰 목록 조회

판매 완료 취소 불가능

리뷰 삭제

profile
공부하는 초보 개발자

0개의 댓글