From 64fddc264b89e01b0f60b6d5555d96e66846a0f1 Mon Sep 17 00:00:00 2001 From: bbangjae Date: Fri, 17 Oct 2025 10:28:24 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat(point):=20PointHistory,=20PointWallet?= =?UTF-8?q?=20=EC=97=94=ED=8B=B0=ED=8B=B0,=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tdd/domain/point/entity/PointHistory.java | 77 +++++++++++++++++++ .../tdd/domain/point/entity/PointWallet.java | 47 +++++++++++ .../tdd/domain/point/enums/PointType.java | 15 ++++ .../repository/PointHistoryRepository.java | 11 +++ .../repository/PointWalletRepository.java | 10 +++ 5 files changed, 160 insertions(+) create mode 100644 src/main/java/com/sparta/tdd/domain/point/entity/PointHistory.java create mode 100644 src/main/java/com/sparta/tdd/domain/point/entity/PointWallet.java create mode 100644 src/main/java/com/sparta/tdd/domain/point/enums/PointType.java create mode 100644 src/main/java/com/sparta/tdd/domain/point/repository/PointHistoryRepository.java create mode 100644 src/main/java/com/sparta/tdd/domain/point/repository/PointWalletRepository.java diff --git a/src/main/java/com/sparta/tdd/domain/point/entity/PointHistory.java b/src/main/java/com/sparta/tdd/domain/point/entity/PointHistory.java new file mode 100644 index 0000000..1611f68 --- /dev/null +++ b/src/main/java/com/sparta/tdd/domain/point/entity/PointHistory.java @@ -0,0 +1,77 @@ +package com.sparta.tdd.domain.point.entity; + +import com.sparta.tdd.domain.point.dto.PointEarnRequest; +import com.sparta.tdd.domain.point.enums.PointType; +import com.sparta.tdd.global.model.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "p_point_history") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PointHistory extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Column(name = "point_history_id") + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "point_wallet_id") + private PointWallet wallet; + + @Column(name = "reference_id") + private UUID referenceId; + + @Column(name = "amount") + private Long amount; + + @Column(name = "type") + @Enumerated(EnumType.STRING) + private PointType type; + + @Column(name = "description") + private String description; + + @Column(name = "expire_at") + private LocalDateTime expireAt; + + @Builder + private PointHistory(PointWallet wallet, UUID referenceId, Long amount, + PointType type, String description, LocalDateTime expireAt) { + this.wallet = wallet; + this.referenceId = referenceId; + this.amount = amount; + this.type = type; + this.description = description; + this.expireAt = expireAt; + } + + public static PointHistory create(PointWallet wallet, PointEarnRequest request, + LocalDateTime expireAt) { + return PointHistory.builder() + .wallet(wallet) + .referenceId(request.referenceId()) + .amount(request.amount()) + .type(request.type()) + .description(request.description()) + .expireAt(expireAt) + .build(); + } +} diff --git a/src/main/java/com/sparta/tdd/domain/point/entity/PointWallet.java b/src/main/java/com/sparta/tdd/domain/point/entity/PointWallet.java new file mode 100644 index 0000000..f3691e6 --- /dev/null +++ b/src/main/java/com/sparta/tdd/domain/point/entity/PointWallet.java @@ -0,0 +1,47 @@ +package com.sparta.tdd.domain.point.entity; + +import com.sparta.tdd.domain.user.entity.User; +import com.sparta.tdd.global.model.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "p_point_wallet") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PointWallet extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Column(name = "point_wallet_id", nullable = false, updatable = false) + private UUID id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "balance") + private Long balance; + + @Builder + public PointWallet(User user) { + this.user = user; + this.balance = 0L; + } + + public void addBalance(Long earnAmount) { + this.balance += earnAmount; + } +} diff --git a/src/main/java/com/sparta/tdd/domain/point/enums/PointType.java b/src/main/java/com/sparta/tdd/domain/point/enums/PointType.java new file mode 100644 index 0000000..4b0db3f --- /dev/null +++ b/src/main/java/com/sparta/tdd/domain/point/enums/PointType.java @@ -0,0 +1,15 @@ +package com.sparta.tdd.domain.point.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum PointType { + PAYMENT_EARNED("결제 적립"), + REVIEW_EARNED("리뷰 적립"), + USED("사용"), + EXPIRED("만료"); + + private final String description; +} diff --git a/src/main/java/com/sparta/tdd/domain/point/repository/PointHistoryRepository.java b/src/main/java/com/sparta/tdd/domain/point/repository/PointHistoryRepository.java new file mode 100644 index 0000000..267e2f7 --- /dev/null +++ b/src/main/java/com/sparta/tdd/domain/point/repository/PointHistoryRepository.java @@ -0,0 +1,11 @@ +package com.sparta.tdd.domain.point.repository; + +import com.sparta.tdd.domain.point.entity.PointHistory; +import com.sparta.tdd.domain.point.enums.PointType; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PointHistoryRepository extends JpaRepository { + + boolean existsByReferenceIdAndTypeAndDeletedAtIsNull(UUID referenceId, PointType type); +} diff --git a/src/main/java/com/sparta/tdd/domain/point/repository/PointWalletRepository.java b/src/main/java/com/sparta/tdd/domain/point/repository/PointWalletRepository.java new file mode 100644 index 0000000..b330ece --- /dev/null +++ b/src/main/java/com/sparta/tdd/domain/point/repository/PointWalletRepository.java @@ -0,0 +1,10 @@ +package com.sparta.tdd.domain.point.repository; + +import com.sparta.tdd.domain.point.entity.PointWallet; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PointWalletRepository extends JpaRepository { + + Optional findByUserId(Long id); +} From 898594ad84f7965b0076503646d6289962693ba8 Mon Sep 17 00:00:00 2001 From: bbangjae Date: Fri, 17 Oct 2025 10:31:26 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat(review):=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=83=9D=EC=84=B1=20=EB=B0=A9=EC=A7=80=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sparta/tdd/domain/review/service/ReviewService.java | 8 ++++++++ .../java/com/sparta/tdd/global/exception/ErrorCode.java | 1 + 2 files changed, 9 insertions(+) diff --git a/src/main/java/com/sparta/tdd/domain/review/service/ReviewService.java b/src/main/java/com/sparta/tdd/domain/review/service/ReviewService.java index 932e38f..f9cdaac 100644 --- a/src/main/java/com/sparta/tdd/domain/review/service/ReviewService.java +++ b/src/main/java/com/sparta/tdd/domain/review/service/ReviewService.java @@ -49,6 +49,8 @@ public ReviewResponseDto createReview(Long userId, UUID orderId, ReviewRequestDt Store store = findStoreById(request.storeId()); Order order = findOrderById(orderId); + existsByOrderId(orderId); + Review review = request.toEntity(user, store, order); Review savedReview = reviewRepository.save(review); @@ -145,4 +147,10 @@ private Order findOrderById(UUID orderId) { return orderRepository.findById(orderId) .orElseThrow(() -> new BusinessException(ErrorCode.ORDER_NOT_FOUND)); } + + private void existsByOrderId(UUID orderId) { + if (reviewRepository.existsByOrderId(orderId)) { + throw new BusinessException(ErrorCode.DUPLICATE_REVIEW); + } + } } \ No newline at end of file diff --git a/src/main/java/com/sparta/tdd/global/exception/ErrorCode.java b/src/main/java/com/sparta/tdd/global/exception/ErrorCode.java index 39cd924..7d2aa8f 100644 --- a/src/main/java/com/sparta/tdd/global/exception/ErrorCode.java +++ b/src/main/java/com/sparta/tdd/global/exception/ErrorCode.java @@ -53,6 +53,7 @@ public enum ErrorCode { REVIEW_REPLY_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 답글이 존재합니다."), REVIEW_REPLY_PERMISSION_DENIED(HttpStatus.FORBIDDEN, "해당 가게의 소유자만 답글을 작성할 수 있습니다."), REVIEW_REPLY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 답글입니다."), + DUPLICATE_REVIEW(HttpStatus.CONFLICT, "이미 해당 주문에 대한 리뷰가 존재합니다."), // PAYMENT 도메인 관련 PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 결제 내역입니다."), From 6feb432d199da8ecb7196fc8dc33d38cf8c0b14b Mon Sep 17 00:00:00 2001 From: bbangjae Date: Fri, 17 Oct 2025 18:21:58 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat(point):=20=ED=8F=AC=EC=9D=B8=ED=8A=B8?= =?UTF-8?q?=20=ED=83=80=EC=9E=85=20=EC=B7=A8=EC=86=8C=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/com/sparta/tdd/domain/point/enums/PointType.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/sparta/tdd/domain/point/enums/PointType.java b/src/main/java/com/sparta/tdd/domain/point/enums/PointType.java index 4b0db3f..d756e06 100644 --- a/src/main/java/com/sparta/tdd/domain/point/enums/PointType.java +++ b/src/main/java/com/sparta/tdd/domain/point/enums/PointType.java @@ -8,6 +8,7 @@ public enum PointType { PAYMENT_EARNED("결제 적립"), REVIEW_EARNED("리뷰 적립"), + PAYMENT_CANCELLED("결제 취소"), USED("사용"), EXPIRED("만료"); From 8b176e8bb8af78d5c60a39123c60a92e0a341926 Mon Sep 17 00:00:00 2001 From: bbangjae Date: Fri, 17 Oct 2025 22:37:13 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat(point):=20=ED=8F=AC=EC=9D=B8=ED=8A=B8?= =?UTF-8?q?=20=EC=A0=81=EB=A6=BD,=20=EA=B2=B0=EC=A0=9C=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=20=EC=8B=9C=20=EC=A0=81=EB=A6=BD=EA=B8=88=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/repository/PaymentRepository.java | 6 ++ .../service/PaymentResultProcessService.java | 2 - .../tdd/domain/point/dto/PointRequest.java | 38 +++++++ .../tdd/domain/point/entity/PointHistory.java | 4 +- .../tdd/domain/point/entity/PointWallet.java | 10 ++ .../repository/PointHistoryRepository.java | 4 + .../domain/point/service/PointService.java | 85 ++++++++++++++++ .../review/repository/ReviewRepository.java | 3 +- .../sparta/tdd/global/aop/PointAspect.java | 99 +++++++++++++++++++ .../tdd/global/exception/ErrorCode.java | 4 +- 10 files changed, 249 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/sparta/tdd/domain/point/dto/PointRequest.java create mode 100644 src/main/java/com/sparta/tdd/domain/point/service/PointService.java create mode 100644 src/main/java/com/sparta/tdd/global/aop/PointAspect.java diff --git a/src/main/java/com/sparta/tdd/domain/payment/repository/PaymentRepository.java b/src/main/java/com/sparta/tdd/domain/payment/repository/PaymentRepository.java index 258bbba..c5c1be9 100644 --- a/src/main/java/com/sparta/tdd/domain/payment/repository/PaymentRepository.java +++ b/src/main/java/com/sparta/tdd/domain/payment/repository/PaymentRepository.java @@ -2,7 +2,9 @@ import com.sparta.tdd.domain.payment.entity.Payment; import java.time.LocalDateTime; +import java.util.Optional; import java.util.UUID; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -18,4 +20,8 @@ void bulkSoftDeleteByUserId( @Param("deletedAt") LocalDateTime deletedAt, @Param("deletedBy") Long deletedBy ); + + @SuppressWarnings("NullableProblems") + @EntityGraph(attributePaths = {"order", "user"}) + Optional findById(UUID id); } diff --git a/src/main/java/com/sparta/tdd/domain/payment/service/PaymentResultProcessService.java b/src/main/java/com/sparta/tdd/domain/payment/service/PaymentResultProcessService.java index 580f60a..3ddd061 100644 --- a/src/main/java/com/sparta/tdd/domain/payment/service/PaymentResultProcessService.java +++ b/src/main/java/com/sparta/tdd/domain/payment/service/PaymentResultProcessService.java @@ -38,8 +38,6 @@ private void processApproved(Payment payment, Order order) { // 주문 상태 변경 (PENDING -> DELIVERED) order.nextStatus(); - - // TODO: 포인트 적립 } private void processCancelled(Payment payment, Order order) { diff --git a/src/main/java/com/sparta/tdd/domain/point/dto/PointRequest.java b/src/main/java/com/sparta/tdd/domain/point/dto/PointRequest.java new file mode 100644 index 0000000..a027df3 --- /dev/null +++ b/src/main/java/com/sparta/tdd/domain/point/dto/PointRequest.java @@ -0,0 +1,38 @@ +package com.sparta.tdd.domain.point.dto; + +import com.sparta.tdd.domain.point.enums.PointType; +import com.sparta.tdd.domain.user.entity.User; +import java.util.UUID; +import lombok.Builder; + +@Builder +public record PointRequest( + User user, + UUID referenceId, + PointType type, + Long amount, + String description +) { + + public static PointRequest forPayment(User user, UUID orderId, Long amount, + String description) { + return PointRequest.builder() + .user(user) + .referenceId(orderId) + .amount(amount) + .type(PointType.PAYMENT_EARNED) + .description(description) + .build(); + } + + public static PointRequest forReview(User user, UUID reviewId, Long amount, + String description) { + return PointRequest.builder() + .user(user) + .referenceId(reviewId) + .amount(amount) + .type(PointType.REVIEW_EARNED) + .description(description) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/sparta/tdd/domain/point/entity/PointHistory.java b/src/main/java/com/sparta/tdd/domain/point/entity/PointHistory.java index 1611f68..d2e85a1 100644 --- a/src/main/java/com/sparta/tdd/domain/point/entity/PointHistory.java +++ b/src/main/java/com/sparta/tdd/domain/point/entity/PointHistory.java @@ -1,6 +1,6 @@ package com.sparta.tdd.domain.point.entity; -import com.sparta.tdd.domain.point.dto.PointEarnRequest; +import com.sparta.tdd.domain.point.dto.PointRequest; import com.sparta.tdd.domain.point.enums.PointType; import com.sparta.tdd.global.model.BaseEntity; import jakarta.persistence.Column; @@ -63,7 +63,7 @@ private PointHistory(PointWallet wallet, UUID referenceId, Long amount, this.expireAt = expireAt; } - public static PointHistory create(PointWallet wallet, PointEarnRequest request, + public static PointHistory create(PointWallet wallet, PointRequest request, LocalDateTime expireAt) { return PointHistory.builder() .wallet(wallet) diff --git a/src/main/java/com/sparta/tdd/domain/point/entity/PointWallet.java b/src/main/java/com/sparta/tdd/domain/point/entity/PointWallet.java index f3691e6..7b0e026 100644 --- a/src/main/java/com/sparta/tdd/domain/point/entity/PointWallet.java +++ b/src/main/java/com/sparta/tdd/domain/point/entity/PointWallet.java @@ -44,4 +44,14 @@ public PointWallet(User user) { public void addBalance(Long earnAmount) { this.balance += earnAmount; } + + public Long subtractBalance(Long amount) { + if (this.balance < amount) { + Long currentBalance = this.balance; + this.balance = 0L; + return amount - currentBalance; + } + this.balance -= amount; + return amount; + } } diff --git a/src/main/java/com/sparta/tdd/domain/point/repository/PointHistoryRepository.java b/src/main/java/com/sparta/tdd/domain/point/repository/PointHistoryRepository.java index 267e2f7..d21ccc5 100644 --- a/src/main/java/com/sparta/tdd/domain/point/repository/PointHistoryRepository.java +++ b/src/main/java/com/sparta/tdd/domain/point/repository/PointHistoryRepository.java @@ -2,10 +2,14 @@ import com.sparta.tdd.domain.point.entity.PointHistory; import com.sparta.tdd.domain.point.enums.PointType; +import java.util.Optional; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; public interface PointHistoryRepository extends JpaRepository { boolean existsByReferenceIdAndTypeAndDeletedAtIsNull(UUID referenceId, PointType type); + + Optional findByReferenceIdAndTypeAndDeletedAtIsNull(UUID paymentId, + PointType pointType); } diff --git a/src/main/java/com/sparta/tdd/domain/point/service/PointService.java b/src/main/java/com/sparta/tdd/domain/point/service/PointService.java new file mode 100644 index 0000000..5737a95 --- /dev/null +++ b/src/main/java/com/sparta/tdd/domain/point/service/PointService.java @@ -0,0 +1,85 @@ +package com.sparta.tdd.domain.point.service; + +import com.sparta.tdd.domain.point.dto.PointRequest; +import com.sparta.tdd.domain.point.entity.PointHistory; +import com.sparta.tdd.domain.point.entity.PointWallet; +import com.sparta.tdd.domain.point.enums.PointType; +import com.sparta.tdd.domain.point.repository.PointHistoryRepository; +import com.sparta.tdd.domain.point.repository.PointWalletRepository; +import com.sparta.tdd.domain.user.entity.User; +import com.sparta.tdd.global.exception.BusinessException; +import com.sparta.tdd.global.exception.ErrorCode; +import java.time.LocalDateTime; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PointService { + + private final PointWalletRepository walletRepository; + private final PointHistoryRepository historyRepository; + + @Transactional + public void earnPoints(PointRequest request) { + if (isDuplicateEarn(request.referenceId(), request.type())) { + return; + } + + PointWallet wallet = getOrCreateWallet(request.user()); + wallet.addBalance(request.amount()); + + PointHistory pointHistory = PointHistory.create(wallet, request, + LocalDateTime.now().plusYears(1)); + + historyRepository.save(pointHistory); + } + + @Transactional + public void losePoints(User user, UUID paymentId) { + PointWallet wallet = findWalletById(user.getId()); + PointHistory pointHistory = findPointHistory(paymentId); + Long usedAmount = wallet.subtractBalance(pointHistory.getAmount()); + + PointHistory cancelledHistory = PointHistory.builder() + .wallet(wallet) + .referenceId(paymentId) + .amount(usedAmount) + .type(PointType.PAYMENT_CANCELLED) + .description("결제 취소 (결제번호: " + paymentId + ")") + .expireAt(LocalDateTime.now()) + .build(); + + historyRepository.save(cancelledHistory); + } + + private PointWallet findWalletById(Long userId) { + return walletRepository.findByUserId(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + } + + private PointHistory findPointHistory(UUID paymentId) { + return historyRepository.findByReferenceIdAndTypeAndDeletedAtIsNull(paymentId, + PointType.PAYMENT_EARNED) + .orElseThrow(() -> new BusinessException(ErrorCode.PAYMENT_NOT_FOUND)); + } + + + private PointWallet getOrCreateWallet(User user) { + return walletRepository.findByUserId(user.getId()) + .orElseGet(() -> walletRepository.save(PointWallet.builder() + .user(user) + .build())); + } + + private boolean isDuplicateEarn(UUID referenceId, PointType type) { + if (referenceId == null || type == null) { + return false; + } + return historyRepository.existsByReferenceIdAndTypeAndDeletedAtIsNull(referenceId, type); + } +} diff --git a/src/main/java/com/sparta/tdd/domain/review/repository/ReviewRepository.java b/src/main/java/com/sparta/tdd/domain/review/repository/ReviewRepository.java index 571d7ac..cec2a20 100644 --- a/src/main/java/com/sparta/tdd/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/sparta/tdd/domain/review/repository/ReviewRepository.java @@ -56,5 +56,6 @@ void bulkSoftDeleteByUserId( @Query("SELECT r.id FROM Review r WHERE r.store.id IN :storeIds AND r.deletedAt IS NULL") List findReviewIdsByStoreIds(@Param("storeIds") List storeIds); - + @Query("SELECT COUNT(r) > 0 FROM Review r WHERE r.order.id = :orderId AND r.deletedAt IS NULL") + boolean existsByOrderId(@Param("orderId") UUID orderId); } \ No newline at end of file diff --git a/src/main/java/com/sparta/tdd/global/aop/PointAspect.java b/src/main/java/com/sparta/tdd/global/aop/PointAspect.java new file mode 100644 index 0000000..0c84875 --- /dev/null +++ b/src/main/java/com/sparta/tdd/global/aop/PointAspect.java @@ -0,0 +1,99 @@ +package com.sparta.tdd.global.aop; + +import com.sparta.tdd.domain.payment.entity.Payment; +import com.sparta.tdd.domain.payment.enums.PaymentStatus; +import com.sparta.tdd.domain.point.dto.PointRequest; +import com.sparta.tdd.domain.point.service.PointService; +import com.sparta.tdd.domain.review.dto.response.ReviewResponseDto; +import com.sparta.tdd.domain.review.entity.Review; +import com.sparta.tdd.domain.review.repository.ReviewRepository; +import com.sparta.tdd.domain.user.entity.User; +import com.sparta.tdd.global.exception.BusinessException; +import com.sparta.tdd.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Aspect +@Component +@Slf4j +@RequiredArgsConstructor +public class PointAspect { + + private final PointService pointService; + private final ReviewRepository reviewRepository; + + @Value("${point.earn.rate}") + private double orderPointRate; + + @Value("${point.review.amount}") + private Long reviewPointRate; + + + @AfterReturning( + pointcut = "execution(* com.sparta.tdd.domain.payment.service.PaymentResultProcessService.processPaymentResult(..)) && args(payment)", + argNames = "payment" + ) + public void processPointsAfterPaymentResult(Payment payment) { + try { + + User user = payment.getUser(); + + if (payment.getStatus() == PaymentStatus.COMPLETED) { + + log.info("결제 완료 payment_Id={}", payment.getId()); + + Long totalAmount = payment.getAmount(); + Long earnAmount = (long) Math.floor(totalAmount * orderPointRate); + + pointService.earnPoints(PointRequest.forPayment( + user, + payment.getId(), + earnAmount, + "결제 완료 적립 (결제번호 번호: " + payment.getId() + ")" + )); + } + + if (payment.getStatus() == PaymentStatus.CANCELLED) { + log.info("취소 완료 payment_Id={}", payment.getId()); + + pointService.losePoints(user, payment.getId()); + } + + } catch (BusinessException e) { + throw e; + } catch (Exception e) { + log.error(e.getMessage()); + throw new BusinessException(ErrorCode.POINT_PROCESSING_FAILED); + } + } + + @AfterReturning( + pointcut = "execution(* com.sparta.tdd.domain.review.service.ReviewService.createReview(..))", + returning = "result" + ) + public void earnPointsAfterReviewCreation(ReviewResponseDto result) { + try { + log.info("리뷰 작성 완료", result.reviewId()); + + Review review = reviewRepository.findById(result.reviewId()) + .orElseThrow(() -> new BusinessException(ErrorCode.REVIEW_NOT_FOUND)); + + User user = review.getUser(); + Long earnAmount = reviewPointRate; + + pointService.earnPoints(PointRequest.forReview( + user, + review.getId(), + earnAmount, + "리뷰 적립 (리뷰 번호: " + review.getId() + ")" + )); + } catch (Exception e) { + log.error(e.getMessage()); + throw new BusinessException(ErrorCode.POINT_PROCESSING_FAILED); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/sparta/tdd/global/exception/ErrorCode.java b/src/main/java/com/sparta/tdd/global/exception/ErrorCode.java index 7d2aa8f..a98d309 100644 --- a/src/main/java/com/sparta/tdd/global/exception/ErrorCode.java +++ b/src/main/java/com/sparta/tdd/global/exception/ErrorCode.java @@ -61,6 +61,7 @@ public enum ErrorCode { GET_STORE_PAYMENT_DENIED(HttpStatus.FORBIDDEN, "본인의 상점의 결제 내역만 조회할 수 있습니다."), PAYMENT_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "주문에 대한 결제건이 이미 존재합니다."), INVALID_PAYMENT_REQUEST(HttpStatus.BAD_REQUEST, "올바른 주문 요청이 아닙니다."), + POINT_PROCESSING_FAILED(HttpStatus.BAD_REQUEST, "포인트 적립 관련 오류입니다."), // AI 도메인 관련 @@ -69,7 +70,8 @@ public enum ErrorCode { CART_ITEM_NOT_FOUND(HttpStatus.NOT_FOUND, "장바구니 아이템을 찾을 수 없습니다."), CART_ITEM_INVALID_QUANTITY(HttpStatus.BAD_REQUEST, "수량은 1개 이상이어야 합니다."), CART_ITEM_NOT_OWNED(HttpStatus.FORBIDDEN, "본인의 장바구니 아이템만 수정할 수 있습니다."), - CART_DIFFERENT_STORE(HttpStatus.BAD_REQUEST, "장바구니에는 한 가게의 메뉴만 담을 수 있습니다. 기존 장바구니를 비우고 다시 시도해주세요."), + CART_DIFFERENT_STORE(HttpStatus.BAD_REQUEST, + "장바구니에는 한 가게의 메뉴만 담을 수 있습니다. 기존 장바구니를 비우고 다시 시도해주세요."), // COUPON 도메인 관련 COUPON_BAD_REQUEST(HttpStatus.BAD_REQUEST, "Scope 설정이 잘못되었습니다."), From ade4478f59d5298216b1cc3b396424509db8faca Mon Sep 17 00:00:00 2001 From: bbangjae Date: Fri, 17 Oct 2025 22:38:03 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat(point):=20=ED=8F=AC=EC=9D=B8=ED=8A=B8?= =?UTF-8?q?=20=EC=A0=81=EB=A6=BD,=20=EA=B2=B0=EC=A0=9C=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=20=EC=8B=9C=20=EC=A0=81=EB=A6=BD=EA=B8=88=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5cd3704..bc90dba 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -34,6 +34,12 @@ ai: google: api-key: ${GOOGLE_API_KEY} +point: + earn: + rate: 0.01 + review: + amount: 500 + --- spring: config: From ef836b2ffda6368cd75939f2dec27d9fcbc82cb7 Mon Sep 17 00:00:00 2001 From: bbangjae Date: Fri, 17 Oct 2025 23:52:39 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat(point):=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=205=EB=B6=84=20=EC=A0=9C=ED=95=9C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/payment/service/PaymentResultProcessService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/sparta/tdd/domain/payment/service/PaymentResultProcessService.java b/src/main/java/com/sparta/tdd/domain/payment/service/PaymentResultProcessService.java index 3ddd061..644fb5c 100644 --- a/src/main/java/com/sparta/tdd/domain/payment/service/PaymentResultProcessService.java +++ b/src/main/java/com/sparta/tdd/domain/payment/service/PaymentResultProcessService.java @@ -6,6 +6,7 @@ import com.sparta.tdd.domain.payment.enums.PaymentStatus; import com.sparta.tdd.global.exception.BusinessException; import com.sparta.tdd.global.exception.ErrorCode; +import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -41,12 +42,13 @@ private void processApproved(Payment payment, Order order) { } private void processCancelled(Payment payment, Order order) { + if (LocalDateTime.now().minusMinutes(5).isAfter(payment.getCreatedAt())) { + throw new BusinessException(ErrorCode.PAYMENT_CANCEL_TIME_EXPIRED); + } // 주문 상태를 PENDING으로 복구 order.changeOrderStatus(OrderStatus.PENDING); // 환불처리는 진행 된 것으로 가정하겠습니다. - - // TODO 재고 복구, 적립된 포인트 회수 등 } private void processFailed(Payment payment, Order order) { From ee5cf39f0340d4113a35a1babc93fe78ae6a9b03 Mon Sep 17 00:00:00 2001 From: bbangjae Date: Fri, 17 Oct 2025 23:54:02 +0900 Subject: [PATCH 7/9] =?UTF-8?q?feat(payment):=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=20=EB=B6=88=EA=B0=80=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=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/com/sparta/tdd/global/exception/ErrorCode.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/sparta/tdd/global/exception/ErrorCode.java b/src/main/java/com/sparta/tdd/global/exception/ErrorCode.java index a98d309..39dddef 100644 --- a/src/main/java/com/sparta/tdd/global/exception/ErrorCode.java +++ b/src/main/java/com/sparta/tdd/global/exception/ErrorCode.java @@ -62,6 +62,7 @@ public enum ErrorCode { PAYMENT_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "주문에 대한 결제건이 이미 존재합니다."), INVALID_PAYMENT_REQUEST(HttpStatus.BAD_REQUEST, "올바른 주문 요청이 아닙니다."), POINT_PROCESSING_FAILED(HttpStatus.BAD_REQUEST, "포인트 적립 관련 오류입니다."), + PAYMENT_CANCEL_TIME_EXPIRED(HttpStatus.CONFLICT, "결제 후 5분이 지나 취소할 수 없습니다."), // AI 도메인 관련 From f509c01c794e11fd715a73a375117cbf9dddbe73 Mon Sep 17 00:00:00 2001 From: bbangjae <120161896+bbangjae@users.noreply.github.com> Date: Sun, 19 Oct 2025 18:25:49 +0900 Subject: [PATCH 8/9] =?UTF-8?q?yml=EC=97=90=EC=84=9C=20=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=81=EB=A6=BD=20=EA=B0=92=20=EC=A3=BC=EC=9E=85?= =?UTF-8?q?=EC=97=90=EC=84=9C=20->=20=EC=83=81=EC=88=98=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 --- .../com/sparta/tdd/global/aop/PointAspect.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/sparta/tdd/global/aop/PointAspect.java b/src/main/java/com/sparta/tdd/global/aop/PointAspect.java index 0c84875..c633fcd 100644 --- a/src/main/java/com/sparta/tdd/global/aop/PointAspect.java +++ b/src/main/java/com/sparta/tdd/global/aop/PointAspect.java @@ -26,12 +26,8 @@ public class PointAspect { private final PointService pointService; private final ReviewRepository reviewRepository; - @Value("${point.earn.rate}") - private double orderPointRate; - - @Value("${point.review.amount}") - private Long reviewPointRate; - + private static final double ORDER_POINT_RATE = 0.01; + private static final long REVIEW_POINT_AMOUNT = 500L; @AfterReturning( pointcut = "execution(* com.sparta.tdd.domain.payment.service.PaymentResultProcessService.processPaymentResult(..)) && args(payment)", @@ -47,7 +43,7 @@ public void processPointsAfterPaymentResult(Payment payment) { log.info("결제 완료 payment_Id={}", payment.getId()); Long totalAmount = payment.getAmount(); - Long earnAmount = (long) Math.floor(totalAmount * orderPointRate); + Long earnAmount = (long) Math.floor(totalAmount * ORDER_POINT_RATE); pointService.earnPoints(PointRequest.forPayment( user, @@ -83,7 +79,7 @@ public void earnPointsAfterReviewCreation(ReviewResponseDto result) { .orElseThrow(() -> new BusinessException(ErrorCode.REVIEW_NOT_FOUND)); User user = review.getUser(); - Long earnAmount = reviewPointRate; + Long earnAmount = REVIEW_POINT_AMOUNT; pointService.earnPoints(PointRequest.forReview( user, @@ -96,4 +92,4 @@ public void earnPointsAfterReviewCreation(ReviewResponseDto result) { throw new BusinessException(ErrorCode.POINT_PROCESSING_FAILED); } } -} \ No newline at end of file +} From 1718ede993b9a1c7e67ceffb46cbbdaec1b8da3b Mon Sep 17 00:00:00 2001 From: bbangjae <120161896+bbangjae@users.noreply.github.com> Date: Sun, 19 Oct 2025 18:26:09 +0900 Subject: [PATCH 9/9] =?UTF-8?q?Remove=20point=20earning=20and=20review=20s?= =?UTF-8?q?ettingsyml=EC=97=90=EC=84=9C=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=81=EB=A6=BD=20=EA=B0=92=20=EC=A3=BC=EC=9E=85=EC=97=90?= =?UTF-8?q?=EC=84=9C=20->=20=EC=83=81=EC=88=98=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed point earning and review configurations from application.yml --- src/main/resources/application.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index bc90dba..91687ab 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -34,12 +34,6 @@ ai: google: api-key: ${GOOGLE_API_KEY} -point: - earn: - rate: 0.01 - review: - amount: 500 - --- spring: config: @@ -100,4 +94,4 @@ jwt: ai: google: - api-key: testApiKey \ No newline at end of file + api-key: testApiKey