Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ public class GroupBuyPost extends BaseTimeEntity {
@Column(name = "current_quantity", nullable = false)
private int currentQuantity; // 현재 인원

@Column(name = "rating_avg", nullable = true)
private double ratingAvg = 0.0; //공구 글 당 평점

@Column(name = "rating_sum", nullable = true)
private long ratingSum = 0L;

@Column(name = "rating_count", nullable = true)
private int ratingCount = 0;

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 30)
private GroupBuyPostStatus status;
Expand Down Expand Up @@ -198,4 +207,20 @@ public void completePostDelivery() {
this.status = GroupBuyPostStatus.DELIVERED;
}

public void addRating(int score) {
validateScore(score);

this.ratingSum += score;
this.ratingCount += 1;

double avg = (double) this.ratingSum / this.ratingCount;
this.ratingAvg = Math.round(avg * 10) / 10.0;
}

private void validateScore(int score) {
if (score < 1 || score > 5) {
throw new BusinessException(ErrorStatus.INVALID_RATING_SCORE);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package org.sopt.poti.domain.groupbuy.repository;

import jakarta.persistence.LockModeType;
import java.util.List;
import java.util.Optional;
import org.sopt.poti.domain.groupbuy.entity.GroupBuyPost;
import org.sopt.poti.domain.groupbuy.entity.GroupBuyPostStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
Expand All @@ -28,4 +30,8 @@ List<GroupBuyPost> findByLeader_IdAndStatusInOrderByCreatedAtDesc(Long leaderId,
List<GroupBuyPostStatus> statuses);

boolean existsByOrderNumber(String orderNumber);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select p from GroupBuyPost p where p.id = :postId")
java.util.Optional<GroupBuyPost> findByIdWithLock(@Param("postId") Long postId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -497,4 +497,22 @@ public GroupBuySaleDetailResponse getGroupBuyPostDetailForSale(Long userId, Long
public List<String> searchTitlesNgram(Long artistId, String keyword) {
return groupBuyRepository.findTitlesByNgram(artistId, keyword, 50);
}

@Transactional
public void addPostRating(Long postId, int score) {
GroupBuyPost post = groupBuyRepository.findByIdWithLock(postId)
.orElseThrow(() -> new BusinessException(ErrorStatus.POST_NOT_FOUND));

post.addRating(score);
}

@Transactional
public GroupBuyPost getPostWithLock(Long postId) {
return groupBuyRepository.findByIdWithLock(postId)
.orElseThrow(() -> new BusinessException(ErrorStatus.POST_NOT_FOUND));
}

public int countPostsByLeader(Long leaderId) {
return groupBuyRepository.countByLeader_Id(leaderId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.sopt.poti.domain.review.application;

import lombok.RequiredArgsConstructor;
import org.sopt.poti.domain.groupbuy.entity.GroupBuyPost;
import org.sopt.poti.domain.groupbuy.service.GroupBuyService;
import org.sopt.poti.domain.order.entity.Order;
import org.sopt.poti.domain.order.service.OrderService;
import org.sopt.poti.domain.review.dto.request.ReviewRequest;
import org.sopt.poti.domain.review.service.ReviewService;
import org.sopt.poti.domain.user.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class ReviewApplicationService {

private final OrderService orderService;
private final ReviewService reviewService;
private final UserService userService;
private final GroupBuyService groupBuyService;

@Transactional
public Long createReview(Long writerUserId, ReviewRequest request) {
Long orderId = request.transactionId();
int score = request.star();

Order order = orderService.getOrderById(orderId);

orderService.validateDelivered(order);
orderService.validateOrderOwner(order, writerUserId);

Long reviewId = reviewService.createReviewEntity(writerUserId, order, score);

Long sellerId = order.getGroupBuyPost().getLeader().getId();
Long postId = order.getGroupBuyPost().getId();

// 1 팟 내부 단순평균 업데이트
groupBuyService.addPostRating(postId, score);

// 2 최신 팟 상태 조회 및 데이터 수집
GroupBuyPost post = groupBuyService.getPostWithLock(postId);
int reviewCount = post.getRatingCount();
double postAvg = post.getRatingAvg();

int postCount = groupBuyService.countPostsByLeader(sellerId);

// 3 판매자 평점 반영
userService.applyPostContribution(sellerId, postId, postAvg, reviewCount, postCount);

return reviewId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,37 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.sopt.poti.domain.review.application.ReviewApplicationService;
import org.sopt.poti.domain.review.dto.request.ReviewRequest;
import org.sopt.poti.domain.review.dto.response.ReviewResponse;
import org.sopt.poti.domain.review.service.ReviewService;
import org.sopt.poti.global.common.ApiResponse;
import org.sopt.poti.global.common.SuccessStatus;
import org.sopt.poti.global.security.UserPrincipal;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

private final ReviewService reviewService;
private final ReviewApplicationService reviewApplicationService;

@PostMapping
@Operation(summary = "리뷰 작성(별점 작성)", description = "로그인 한 유저가 거래가 완료된 주문에 대해 별점을 줍니다.")
public ResponseEntity<ApiResponse<ReviewResponse>> createReview(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@Valid @RequestBody ReviewRequest request
) {
Long reviewId = reviewService.createReview(userPrincipal.getUserId(), request);
return ResponseEntity
.status(SuccessStatus.CREATED.getHttpStatus())
.body(ApiResponse.created(SuccessStatus.CREATED, new ReviewResponse(reviewId)));
}
@PostMapping
@Operation(summary = "리뷰 작성(별점 작성)", description = "로그인 한 유저가 거래가 완료된 주문에 대해 별점을 줍니다.")
public ResponseEntity<ApiResponse<ReviewResponse>> createReview(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@Valid @RequestBody ReviewRequest request
) {
Long reviewId = reviewApplicationService.createReview(userPrincipal.getUserId(), request);

return ResponseEntity
.status(SuccessStatus.CREATED.getHttpStatus())
.body(ApiResponse.created(SuccessStatus.CREATED, new ReviewResponse(reviewId)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@

import org.sopt.poti.domain.review.entity.Review;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ReviewRepository extends JpaRepository<Review, Long> {

boolean existsByOrder_Id(Long orderId);

@Query("select coalesce(avg(r.score), 0) from Review r where r.seller.id = :sellerId")
double avgScoreBySellerId(@Param("sellerId") Long sellerId);

long countBySeller_Id(Long sellerId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

import lombok.RequiredArgsConstructor;
import org.sopt.poti.domain.order.entity.Order;
import org.sopt.poti.domain.order.service.OrderService;
import org.sopt.poti.domain.review.dto.request.ReviewRequest;
import org.sopt.poti.domain.review.entity.Review;
import org.sopt.poti.domain.review.repository.ReviewRepository;
import org.sopt.poti.domain.user.entity.User;
import org.sopt.poti.domain.user.service.UserService;
import org.sopt.poti.global.error.BusinessException;
import org.sopt.poti.global.error.ErrorStatus;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -19,33 +18,26 @@
public class ReviewService {

private final ReviewRepository reviewRepository;
private final OrderService orderService;
private final UserService userService;

public Long createReview(Long writerUserId, ReviewRequest request) {
Long orderId = request.transactionId();

Order order = orderService.getOrderById(orderId);

orderService.validateDelivered(order);
orderService.validateOrderOwner(order, writerUserId);
public Long createReviewEntity(Long writerUserId, Order order, int score) {
Long orderId = order.getId();

if (reviewRepository.existsByOrder_Id(orderId)) {
throw new BusinessException(ErrorStatus.REVIEW_ALREADY_EXISTS);
}

User writer = userService.getUserById(writerUserId);

User seller = order.getGroupBuyPost().getLeader();

Review review = Review.create(request.star(), order, writer, seller);
Review saved = reviewRepository.save(review);
Review review = Review.create(score, order, writer, seller);

double rawAvg = reviewRepository.avgScoreBySellerId(seller.getId());
double roundAvg = Math.round(rawAvg * 10) / 10.0;
seller.updateRatingAvg(roundAvg);

return saved.getId();
try {
Review saved = reviewRepository.save(review);
return saved.getId();
} catch (DataIntegrityViolationException e) {
throw new BusinessException(ErrorStatus.REVIEW_ALREADY_EXISTS);
}
}

public Integer countReviewsForSeller(Long sellerId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.sopt.poti.domain.user.entity;


import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@Table(
name = "seller_post_rating_contributions",
uniqueConstraints = @UniqueConstraint(name = "uk_seller_post", columnNames = {"seller_id",
"post_id"})
)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SellerPostRatingContribution {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "seller_id", nullable = false)
private Long sellerId;

@Column(name = "post_id", nullable = false)
private Long postId;

@Column(name = "applied_avg", nullable = false)
private double appliedAvg; // 해당 post가 seller에 반영된 평균값

@Column(name = "applied_weight", nullable = false)
private double appliedWeight; // 해당 post가 seller에 반영된 가중치

@Builder
private SellerPostRatingContribution(Long sellerId, Long postId, double appliedAvg,
double appliedWeight) {
this.sellerId = sellerId;
this.postId = postId;
this.appliedAvg = appliedAvg;
this.appliedWeight = appliedWeight;
}

public static SellerPostRatingContribution create(Long sellerId, Long postId) {
return SellerPostRatingContribution.builder()
.sellerId(sellerId)
.postId(postId)
.appliedAvg(0.0)
.appliedWeight(0.0)
.build();
}

public void update(double avg, double weight) {
this.appliedAvg = avg;
this.appliedWeight = weight;
}
}
32 changes: 28 additions & 4 deletions src/main/java/org/sopt/poti/domain/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ public class User extends BaseSoftDeleteEntity {
@Column(name = "rating_avg")
private Double ratingAvg;

@Column(name = "rating_sum", nullable = false)
private long ratingSum = 0L;

@Column(name = "rating_count", nullable = false)
private int ratingCount = 0;
Comment on lines +59 to +63
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for ratingSum and ratingCount usages across the codebase

echo "=== Searching for ratingSum usages ==="
rg -n --type=java 'ratingSum' -C 2

echo ""
echo "=== Searching for ratingCount usages ==="
rg -n --type=java 'ratingCount' -C 2

Repository: team-poti/POTI-SERVER

Length of output: 3467


ratingSumratingCount 필드가 User 엔티티 내에서 사용되고 있지 않아요.

이 두 필드는 선언만 되어 있고 User 클래스의 어떤 메서드에서도 실제로 사용되지 않고 있네요. 흥미롭게도 GroupBuyPost 엔티티에는 동일한 필드들이 있고 여기서는 적극적으로 사용 중입니다 (rating 계산에서 합계와 개수를 누적하는 형태).

혹시 다른 레이어(서비스, 리포지토리)에서 이 필드들을 사용 중이라면 괜찮지만, 엔티티 단에서 전혀 사용되지 않는다면 불필요한 DB 컬럼 추가가 될 수 있습니다. 한 번 확인해 보시는 게 좋을 것 같아요.

🤖 Prompt for AI Agents
In `@src/main/java/org/sopt/poti/domain/user/entity/User.java` around lines 59 -
63, The User entity declares ratingSum and ratingCount but they are never used;
either remove these unused fields to avoid adding unnecessary DB columns (delete
ratingSum and ratingCount from class and update DB migration/schema) or, if
intended to track user ratings like GroupBuyPost, add explicit usages: implement
methods in User such as addRating(long delta) / incrementRatingCount() /
getAverageRating() and ensure service/repository code updates these fields
(referencing the User class and its ratingSum/ratingCount symbols) so they are
actually maintained; choose one approach and apply corresponding schema
migration changes.


@Column(name = "rating_weighted_sum", nullable = false)
private double ratingWeightedSum = 0.0;

@Column(name = "rating_weight_total", nullable = false)
private double ratingWeightTotal = 0.0;

@Enumerated(EnumType.STRING)
private UserStatus status;

Expand Down Expand Up @@ -103,10 +115,6 @@ public void updateFavoriteArtist(Artist artist) {
this.favoriteArtist = artist;
}

public void updateRatingAvg(double avg) {
this.ratingAvg = avg;
}

public void withdraw() {
this.status = UserStatus.WITHDRAWN;
this.deletedAt = LocalDateTime.now();
Expand All @@ -115,4 +123,20 @@ public void withdraw() {
this.profileImageUrl = null;
this.socialId = "deleted_" + this.id + "_" + UUID.randomUUID(); // 유니크 제약 회피
}

public void addRatingWeightedDelta(double deltaWeightedSum, double deltaWeight) {
this.ratingWeightedSum += deltaWeightedSum;
this.ratingWeightTotal += deltaWeight;

if (this.ratingWeightTotal <= 0.0) {
this.ratingWeightedSum = 0.0;
this.ratingWeightTotal = 0.0;
this.ratingAvg = 0.0;
return;
}

double avg = this.ratingWeightedSum / this.ratingWeightTotal;
this.ratingAvg = Math.round(avg * 10) / 10.0;
}

}
Loading