From 60cbb0c61874e64e8f61d4f92620291971ab0fee Mon Sep 17 00:00:00 2001 From: 88guri Date: Fri, 23 Jan 2026 04:18:24 +0900 Subject: [PATCH 1/9] =?UTF-8?q?fix:=20#187=20postId=EB=8A=94=20postID,=20l?= =?UTF-8?q?eaderId=EB=8A=94=20leaderId=EB=A5=BC=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../participation/dto/response/LeaderUserIdResponse.java | 1 + .../domain/participation/service/ParticipationService.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/sopt/poti/domain/participation/dto/response/LeaderUserIdResponse.java b/src/main/java/org/sopt/poti/domain/participation/dto/response/LeaderUserIdResponse.java index 0ffb4fe..0383805 100644 --- a/src/main/java/org/sopt/poti/domain/participation/dto/response/LeaderUserIdResponse.java +++ b/src/main/java/org/sopt/poti/domain/participation/dto/response/LeaderUserIdResponse.java @@ -1,6 +1,7 @@ package org.sopt.poti.domain.participation.dto.response; public record LeaderUserIdResponse( + Long postId, Long leaderUserId ) { diff --git a/src/main/java/org/sopt/poti/domain/participation/service/ParticipationService.java b/src/main/java/org/sopt/poti/domain/participation/service/ParticipationService.java index 550354b..040a97b 100644 --- a/src/main/java/org/sopt/poti/domain/participation/service/ParticipationService.java +++ b/src/main/java/org/sopt/poti/domain/participation/service/ParticipationService.java @@ -73,6 +73,7 @@ public LeaderUserIdResponse confirmDelivered(Long userId, Long participationId) // 2 해당 공구글의 모든 주문(OrderStatus)이 배송완료인지 검사 Long postId = order.getGroupBuyPost().getId(); + Long leaderUserId = order.getGroupBuyPost().getLeader().getId(); long remaining = orderService.countByGroupBuyPostIdAndStatusNot(postId, OrderStatus.DELIVERED); // 3) 남은 주문이 0개 -> GroupBuyPostStatus도 배송완료로 변경 @@ -85,7 +86,7 @@ public LeaderUserIdResponse confirmDelivered(Long userId, Long participationId) } } - return new LeaderUserIdResponse(postId); + return new LeaderUserIdResponse(postId, leaderUserId); } From 7c5f5a2376984d4951eb68c3bd529f60553a7d13 Mon Sep 17 00:00:00 2001 From: 88guri Date: Fri, 23 Jan 2026 08:05:47 +0900 Subject: [PATCH 2/9] =?UTF-8?q?refactor:=20#79=20ema=EA=B4=80=EB=A0=A8=20R?= =?UTF-8?q?epository=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../groupbuy/repository/GroupBuyRepository.java | 6 ++++++ .../domain/review/repository/ReviewRepository.java | 5 ----- .../domain/user/repository/UserRepository.java | 14 ++++++++++++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/sopt/poti/domain/groupbuy/repository/GroupBuyRepository.java b/src/main/java/org/sopt/poti/domain/groupbuy/repository/GroupBuyRepository.java index d0fcae0..9bb0af3 100644 --- a/src/main/java/org/sopt/poti/domain/groupbuy/repository/GroupBuyRepository.java +++ b/src/main/java/org/sopt/poti/domain/groupbuy/repository/GroupBuyRepository.java @@ -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; @@ -28,4 +30,8 @@ List findByLeader_IdAndStatusInOrderByCreatedAtDesc(Long leaderId, List statuses); boolean existsByOrderNumber(String orderNumber); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select p from GroupBuyPost p where p.id = :postId") + java.util.Optional findByIdWithLock(@Param("postId") Long postId); } diff --git a/src/main/java/org/sopt/poti/domain/review/repository/ReviewRepository.java b/src/main/java/org/sopt/poti/domain/review/repository/ReviewRepository.java index b0ed30b..64fc8d2 100644 --- a/src/main/java/org/sopt/poti/domain/review/repository/ReviewRepository.java +++ b/src/main/java/org/sopt/poti/domain/review/repository/ReviewRepository.java @@ -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 { 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); } \ No newline at end of file diff --git a/src/main/java/org/sopt/poti/domain/user/repository/UserRepository.java b/src/main/java/org/sopt/poti/domain/user/repository/UserRepository.java index 4ad1ecb..c42b52d 100644 --- a/src/main/java/org/sopt/poti/domain/user/repository/UserRepository.java +++ b/src/main/java/org/sopt/poti/domain/user/repository/UserRepository.java @@ -1,13 +1,23 @@ package org.sopt.poti.domain.user.repository; +import jakarta.persistence.LockModeType; import java.util.Optional; import org.sopt.poti.domain.user.entity.SocialType; import org.sopt.poti.domain.user.entity.User; 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; @Repository public interface UserRepository extends JpaRepository { - Optional findBySocialIdAndSocialType(String socialId, SocialType socialType); - boolean existsByNickname(String nickname); + + Optional findBySocialIdAndSocialType(String socialId, SocialType socialType); + + boolean existsByNickname(String nickname); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select u from User u where u.id = :userId") + java.util.Optional findByIdWithLock(@Param("userId") Long userId); } From 78cb26752bf17699f31fd8734fdf0e2acb513f00 Mon Sep 17 00:00:00 2001 From: 88guri Date: Fri, 23 Jan 2026 08:06:05 +0900 Subject: [PATCH 3/9] =?UTF-8?q?refactor:=20#79=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/org/sopt/poti/global/error/ErrorStatus.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/sopt/poti/global/error/ErrorStatus.java b/src/main/java/org/sopt/poti/global/error/ErrorStatus.java index 6491b81..8335ee1 100644 --- a/src/main/java/org/sopt/poti/global/error/ErrorStatus.java +++ b/src/main/java/org/sopt/poti/global/error/ErrorStatus.java @@ -31,6 +31,7 @@ public enum ErrorStatus { GROUP_BUY_POST_INVALID_INCREASE_COUNT(40016, HttpStatus.BAD_REQUEST, "증가 수량은 1 이상이어야 합니다."), ORDER_NOT_SHIPPED(40017, HttpStatus.BAD_REQUEST, "배송 시작 상태에서만 배송 완료로 변경할 수 있습니다."), POST_NOT_SHIPPING(40018, HttpStatus.BAD_REQUEST, "배송 중인 공구글만 배송 완료로 변경할 수 있습니다."), + INVALID_RATING_SCORE(40019, HttpStatus.BAD_REQUEST, "별점은 1~5 사이여야 합니다."), /** * 401 Unauthorized From bfbe9a777c76010fc3372579e675f4ff61fe1033 Mon Sep 17 00:00:00 2001 From: 88guri Date: Fri, 23 Jan 2026 08:07:11 +0900 Subject: [PATCH 4/9] =?UTF-8?q?refactor:=20#79=20ema=EA=B4=80=EB=A0=A8=20c?= =?UTF-8?q?olumn=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/groupbuy/entity/GroupBuyPost.java | 25 +++++++++++++++++++ .../sopt/poti/domain/user/entity/User.java | 12 +++++++++ 2 files changed, 37 insertions(+) diff --git a/src/main/java/org/sopt/poti/domain/groupbuy/entity/GroupBuyPost.java b/src/main/java/org/sopt/poti/domain/groupbuy/entity/GroupBuyPost.java index 0cf049e..871ec12 100644 --- a/src/main/java/org/sopt/poti/domain/groupbuy/entity/GroupBuyPost.java +++ b/src/main/java/org/sopt/poti/domain/groupbuy/entity/GroupBuyPost.java @@ -69,6 +69,15 @@ public class GroupBuyPost extends BaseTimeEntity { @Column(name = "current_quantity", nullable = false) private int currentQuantity; // 현재 인원 + @Column(name = "rating_avg", nullable = false) + private double ratingAvg = 0.0; //공구 글 당 평점 + + @Column(name = "rating_sum", nullable = false) + private long ratingSum = 0L; + + @Column(name = "rating_count", nullable = false) + private int ratingCount = 0; + @Enumerated(EnumType.STRING) @Column(nullable = false, length = 30) private GroupBuyPostStatus status; @@ -195,4 +204,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); + } + } + } \ No newline at end of file diff --git a/src/main/java/org/sopt/poti/domain/user/entity/User.java b/src/main/java/org/sopt/poti/domain/user/entity/User.java index 9fd868f..e3a3372 100644 --- a/src/main/java/org/sopt/poti/domain/user/entity/User.java +++ b/src/main/java/org/sopt/poti/domain/user/entity/User.java @@ -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; + + @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; From e10a9c8d7bd77a2f268f36fc01490ee33c3f6e63 Mon Sep 17 00:00:00 2001 From: 88guri Date: Fri, 23 Jan 2026 08:08:29 +0900 Subject: [PATCH 5/9] =?UTF-8?q?refactor:=20#79=20ema=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EB=93=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../groupbuy/service/GroupBuyService.java | 18 +++++ .../review/controller/ReviewController.java | 32 ++++---- .../domain/review/service/ReviewService.java | 28 +++---- .../sopt/poti/domain/user/entity/User.java | 20 ++++- .../poti/domain/user/service/UserService.java | 80 +++++++++++++++++++ 5 files changed, 142 insertions(+), 36 deletions(-) diff --git a/src/main/java/org/sopt/poti/domain/groupbuy/service/GroupBuyService.java b/src/main/java/org/sopt/poti/domain/groupbuy/service/GroupBuyService.java index 685c44c..01cd950 100644 --- a/src/main/java/org/sopt/poti/domain/groupbuy/service/GroupBuyService.java +++ b/src/main/java/org/sopt/poti/domain/groupbuy/service/GroupBuyService.java @@ -497,4 +497,22 @@ public GroupBuySaleDetailResponse getGroupBuyPostDetailForSale(Long userId, Long public List 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); + } } \ No newline at end of file diff --git a/src/main/java/org/sopt/poti/domain/review/controller/ReviewController.java b/src/main/java/org/sopt/poti/domain/review/controller/ReviewController.java index a665b31..9e81d67 100644 --- a/src/main/java/org/sopt/poti/domain/review/controller/ReviewController.java +++ b/src/main/java/org/sopt/poti/domain/review/controller/ReviewController.java @@ -4,15 +4,18 @@ 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 @@ -20,17 +23,18 @@ @RequestMapping("/api/v1/reviews") public class ReviewController { - private final ReviewService reviewService; + private final ReviewApplicationService reviewApplicationService; - @PostMapping - @Operation(summary = "리뷰 작성(별점 작성)", description = "로그인 한 유저가 거래가 완료된 주문에 대해 별점을 줍니다.") - public ResponseEntity> 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> 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))); + } } \ No newline at end of file diff --git a/src/main/java/org/sopt/poti/domain/review/service/ReviewService.java b/src/main/java/org/sopt/poti/domain/review/service/ReviewService.java index a60102b..a41b74c 100644 --- a/src/main/java/org/sopt/poti/domain/review/service/ReviewService.java +++ b/src/main/java/org/sopt/poti/domain/review/service/ReviewService.java @@ -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; @@ -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) { diff --git a/src/main/java/org/sopt/poti/domain/user/entity/User.java b/src/main/java/org/sopt/poti/domain/user/entity/User.java index e3a3372..7cd9aab 100644 --- a/src/main/java/org/sopt/poti/domain/user/entity/User.java +++ b/src/main/java/org/sopt/poti/domain/user/entity/User.java @@ -115,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(); @@ -127,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; + } + } \ No newline at end of file diff --git a/src/main/java/org/sopt/poti/domain/user/service/UserService.java b/src/main/java/org/sopt/poti/domain/user/service/UserService.java index 93ff3dc..cf23e50 100644 --- a/src/main/java/org/sopt/poti/domain/user/service/UserService.java +++ b/src/main/java/org/sopt/poti/domain/user/service/UserService.java @@ -6,8 +6,10 @@ import org.sopt.poti.domain.artist.service.ArtistService; import org.sopt.poti.domain.user.dto.request.UserOnboardingRequest; import org.sopt.poti.domain.user.dto.response.UserOnboardingResponse; +import org.sopt.poti.domain.user.entity.SellerPostRatingContribution; import org.sopt.poti.domain.user.entity.SocialType; import org.sopt.poti.domain.user.entity.User; +import org.sopt.poti.domain.user.repository.SellerPostRatingContributionRepository; import org.sopt.poti.domain.user.repository.UserRepository; import org.sopt.poti.global.error.BusinessException; import org.sopt.poti.global.error.ErrorStatus; @@ -21,6 +23,7 @@ public class UserService { private final UserRepository userRepository; private final ArtistService artistService; + private final SellerPostRatingContributionRepository contributionRepository; public User getUserById(Long userId) { return userRepository.findById(userId) @@ -55,4 +58,81 @@ public Optional findUserBySocialIdAndSocialType(String socialId, SocialTyp public void registerUser(User user) { userRepository.save(user); } + + private double computeAlpha(int postCount, int n) { + double base; + double k; + + if (postCount <= 5) { + base = 0.1; + k = 0.0; + } else if (postCount <= 10) { + base = 0.2; + k = 0.02; + } else if (postCount <= 20) { + base = 0.12; + k = 0.015; + } else if (postCount <= 30) { + base = 0.08; + k = 0.008; + } else { + base = 0.05; + k = 0.005; + } + + double alpha = base + k * n; + + double alphaMax = 0.7; + if (alpha > alphaMax) { + alpha = alphaMax; + } + + double alphaMin = 0.05; + if (alpha != 0.0 && alpha < alphaMin) { + alpha = alphaMin; + } + + return alpha; + } + + @Transactional + public void applyPostContribution( + Long sellerId, + Long postId, + double postAvg, + int reviewCount, + int postCount + ) { + // 락 관리 + + // 1 판매자 락 + User seller = userRepository.findByIdWithLock(sellerId) + .orElseThrow(() -> new BusinessException(ErrorStatus.USER_NOT_FOUND)); + + if (reviewCount <= 0) { + return; + } + + // 2 contribution 락 + SellerPostRatingContribution contrib = contributionRepository + .findBySellerIdAndPostIdWithLock(sellerId, postId) + .orElseGet(() -> contributionRepository.save( + SellerPostRatingContribution.create(sellerId, postId))); + + // 3 새 weight + double newWeight = computeAlpha(postCount, reviewCount); + + // 4 이전 기여 → 새 기여 갱신 + double oldWeighted = contrib.getAppliedAvg() * contrib.getAppliedWeight(); + double newWeighted = postAvg * newWeight; + + double deltaWeightedSum = newWeighted - oldWeighted; + double deltaWeight = newWeight - contrib.getAppliedWeight(); + + // 5 seller에 delta 반영 + seller.addRatingWeightedDelta(deltaWeightedSum, deltaWeight); + + // 6 contrib 갱신 + contrib.update(postAvg, newWeight); + } } From 01e34f622219ce4b2408581c625b7185c85055b8 Mon Sep 17 00:00:00 2001 From: 88guri Date: Fri, 23 Jan 2026 08:09:16 +0900 Subject: [PATCH 6/9] =?UTF-8?q?refactor:=20#79=20ema=EA=B4=80=EB=A0=A8=20R?= =?UTF-8?q?eviewApplicationService=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ReviewApplicationService.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/main/java/org/sopt/poti/domain/review/application/ReviewApplicationService.java diff --git a/src/main/java/org/sopt/poti/domain/review/application/ReviewApplicationService.java b/src/main/java/org/sopt/poti/domain/review/application/ReviewApplicationService.java new file mode 100644 index 0000000..8f1bbfd --- /dev/null +++ b/src/main/java/org/sopt/poti/domain/review/application/ReviewApplicationService.java @@ -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; + } +} From 0950c661413115161f3858a31277586c2ae8b67c Mon Sep 17 00:00:00 2001 From: 88guri Date: Fri, 23 Jan 2026 08:12:35 +0900 Subject: [PATCH 7/9] =?UTF-8?q?refactor:=20#79=20ema=EA=B4=80=EB=A0=A8=20e?= =?UTF-8?q?ntity=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/SellerPostRatingContribution.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/main/java/org/sopt/poti/domain/user/entity/SellerPostRatingContribution.java diff --git a/src/main/java/org/sopt/poti/domain/user/entity/SellerPostRatingContribution.java b/src/main/java/org/sopt/poti/domain/user/entity/SellerPostRatingContribution.java new file mode 100644 index 0000000..6005616 --- /dev/null +++ b/src/main/java/org/sopt/poti/domain/user/entity/SellerPostRatingContribution.java @@ -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; + } +} \ No newline at end of file From 101dcbf42108719f9a0ffe4d52ac4eb1642450cf Mon Sep 17 00:00:00 2001 From: 88guri Date: Fri, 23 Jan 2026 08:12:51 +0900 Subject: [PATCH 8/9] =?UTF-8?q?refactor:=20#79=20ema=EA=B4=80=EB=A0=A8=20r?= =?UTF-8?q?epository=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ellerPostRatingContributionRepository.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/org/sopt/poti/domain/user/repository/SellerPostRatingContributionRepository.java diff --git a/src/main/java/org/sopt/poti/domain/user/repository/SellerPostRatingContributionRepository.java b/src/main/java/org/sopt/poti/domain/user/repository/SellerPostRatingContributionRepository.java new file mode 100644 index 0000000..f9e016d --- /dev/null +++ b/src/main/java/org/sopt/poti/domain/user/repository/SellerPostRatingContributionRepository.java @@ -0,0 +1,20 @@ +package org.sopt.poti.domain.user.repository; + +import jakarta.persistence.LockModeType; +import java.util.Optional; +import org.sopt.poti.domain.user.entity.SellerPostRatingContribution; +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; + +public interface SellerPostRatingContributionRepository + extends JpaRepository { + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select c from SellerPostRatingContribution c where c.sellerId = :sellerId and c.postId = :postId") + Optional findBySellerIdAndPostIdWithLock( + @Param("sellerId") Long sellerId, + @Param("postId") Long postId + ); +} \ No newline at end of file From ee8f447b4945673682129a64a7b9845cd5afb21f Mon Sep 17 00:00:00 2001 From: 88guri Date: Fri, 23 Jan 2026 22:03:22 +0900 Subject: [PATCH 9/9] =?UTF-8?q?chore:=20#189=20nullable=20true=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/poti/domain/groupbuy/entity/GroupBuyPost.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/sopt/poti/domain/groupbuy/entity/GroupBuyPost.java b/src/main/java/org/sopt/poti/domain/groupbuy/entity/GroupBuyPost.java index 0a72cd3..de01b4a 100644 --- a/src/main/java/org/sopt/poti/domain/groupbuy/entity/GroupBuyPost.java +++ b/src/main/java/org/sopt/poti/domain/groupbuy/entity/GroupBuyPost.java @@ -72,13 +72,13 @@ public class GroupBuyPost extends BaseTimeEntity { @Column(name = "current_quantity", nullable = false) private int currentQuantity; // 현재 인원 - @Column(name = "rating_avg", nullable = false) + @Column(name = "rating_avg", nullable = true) private double ratingAvg = 0.0; //공구 글 당 평점 - @Column(name = "rating_sum", nullable = false) + @Column(name = "rating_sum", nullable = true) private long ratingSum = 0L; - @Column(name = "rating_count", nullable = false) + @Column(name = "rating_count", nullable = true) private int ratingCount = 0; @Enumerated(EnumType.STRING)