diff --git a/src/main/java/umc/th/juinjang/api/pencil/controller/PencilController.java b/src/main/java/umc/th/juinjang/api/pencil/controller/PencilController.java index c1d987b9..103d049c 100644 --- a/src/main/java/umc/th/juinjang/api/pencil/controller/PencilController.java +++ b/src/main/java/umc/th/juinjang/api/pencil/controller/PencilController.java @@ -18,6 +18,8 @@ import umc.th.juinjang.api.pencil.controller.request.AppleIAPPurchaseRequest; import umc.th.juinjang.api.pencil.service.PencilCommandService; import umc.th.juinjang.api.pencil.service.PencilQueryService; +import umc.th.juinjang.api.pencil.service.response.AcquiredPencilReadResponse; +import umc.th.juinjang.api.pencil.service.response.AcquiredPencilReadStatusResponse; import umc.th.juinjang.api.pencil.service.response.AcquiredPencilResponse; import umc.th.juinjang.api.pencil.service.response.AppleIAPPurchaseResponse; import umc.th.juinjang.api.pencil.service.response.PurchasedPencilResponse; @@ -40,10 +42,21 @@ public ApiResponse> getAcquiredPencilHistory(@Authe @Operation(summary = "얻은 연필 목록에서 읽음 처리를 진행한다.") @PatchMapping("/acquired/{acquiredPencilId}/read") - public ApiResponse markAcquiredPencilAsRead( + public ApiResponse markAcquiredPencilAsRead( @PathVariable Long acquiredPencilId, @AuthenticationPrincipal Member member) { - return ApiResponse.onSuccess(pencilCommandService.markAcquiredPencilAsRead(acquiredPencilId)); + return ApiResponse.onSuccess( + AcquiredPencilReadResponse.of(pencilCommandService.markAcquiredPencilAsRead(acquiredPencilId), + pencilQueryService.isAcquiredPencilReadStatus(member))); + } + + @Operation(summary = "얻은 연필 목록에서 읽지 않은 항목이 존재하는 여부를 확인한다.") + @GetMapping("/acquired/is-total-read") + public ApiResponse isAcquiredPencilReadStatus( + @AuthenticationPrincipal Member member + ) { + return ApiResponse.onSuccess( + AcquiredPencilReadStatusResponse.of(pencilQueryService.isAcquiredPencilReadStatus(member))); } @Operation(summary = "구매한 연필 목록을 불러온다") @@ -66,6 +79,7 @@ public ApiResponse purchasePencil( @AuthenticationPrincipal Member member, @RequestBody AppleIAPPurchaseRequest request ) { - return ApiResponse.onSuccess(pencilCommandService.processAppleIAPPurchase(request , member, LocalDateTime.now())); + return ApiResponse.onSuccess( + pencilCommandService.processAppleIAPPurchase(request, member, LocalDateTime.now())); } } diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/AcquiredPencilFinder.java b/src/main/java/umc/th/juinjang/api/pencil/service/AcquiredPencilFinder.java index de1010ba..c9ef1eb6 100644 --- a/src/main/java/umc/th/juinjang/api/pencil/service/AcquiredPencilFinder.java +++ b/src/main/java/umc/th/juinjang/api/pencil/service/AcquiredPencilFinder.java @@ -19,6 +19,10 @@ public List findAllByMemberOrderByCreatedAtDesc(Member member) { return acquiredPencilRepository.findAllByMemberOrderByCreatedAtDesc(member); } + public boolean existsByMemberAndIsReadFalse(Member member) { + return acquiredPencilRepository.existsByMemberAndIsReadFalse(member); + } + public AcquiredPencil findById(Long id) { return acquiredPencilRepository.findById(id).orElse(null); } diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/PencilCommandService.java b/src/main/java/umc/th/juinjang/api/pencil/service/PencilCommandService.java index d60734fd..f4499073 100644 --- a/src/main/java/umc/th/juinjang/api/pencil/service/PencilCommandService.java +++ b/src/main/java/umc/th/juinjang/api/pencil/service/PencilCommandService.java @@ -19,6 +19,7 @@ import umc.th.juinjang.domain.pencil.acquired.model.AcquiredPencil; import umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil; import umc.th.juinjang.domain.pencil.purchased.model.TransactionStatus; +import umc.th.juinjang.domain.pencilaccount.model.PencilAccount; @Slf4j @Service @@ -48,7 +49,7 @@ public Boolean markAcquiredPencilAsRead(Long acquiredPencilId) { public AppleIAPPurchaseResponse processAppleIAPPurchase(AppleIAPPurchaseRequest request, Member member, LocalDateTime now) { String transactionId = request.getTransactionId(); - + Long purchaseQuantity = request.getPencilQuantity(); Optional existing = purchasedPencilFinder.findByTransactionIdAndMember(transactionId, member); if (existing.isEmpty()) { @@ -59,32 +60,36 @@ public AppleIAPPurchaseResponse processAppleIAPPurchase(AppleIAPPurchaseRequest PurchasedPencil pencil = existing.get(); TransactionStatus status = pencil.getTransactionStatus(); + PencilAccount buyer = pencilAccountFinder.findByMember(member); if (status == TransactionStatus.SUCCESS) { // 트랜잭션이 정상적으로 성공된 기록이 있는 경우 - return AppleIAPPurchaseResponse.ofSuccess(transactionId); + return AppleIAPPurchaseResponse.ofSuccess(transactionId, purchaseQuantity, buyer.getTotalBalance()); } PurchasedPencil newPencil = retryPurchasedPencil(pencil, member); // 실패 재시도 처리 - return AppleIAPPurchaseResponse.of(transactionId, newPencil.getTransactionStatus()); + return AppleIAPPurchaseResponse.of(transactionId, newPencil.getTransactionStatus(), purchaseQuantity, + buyer.getTotalBalance()); } - - @Transactional - public AppleIAPPurchaseResponse validateAndCommitApplePurchase(AppleIAPPurchaseRequest request, Member member, LocalDateTime now) { + public AppleIAPPurchaseResponse validateAndCommitApplePurchase(AppleIAPPurchaseRequest request, Member member, + LocalDateTime now) { String transactionId = request.getTransactionId(); - VerificationResult verificationResult = appleService.verifyAppleTransaction(AppleTransactionVerifyCommand.fromRequest(request)); + VerificationResult verificationResult = appleService.verifyAppleTransaction( + AppleTransactionVerifyCommand.fromRequest(request)); - if (VerificationResult.isSuccess(verificationResult)){ - // 성공 시, DB에 저장z + if (VerificationResult.isSuccess(verificationResult)) { + // 성공 시, DB에 저장 handleSuccessfulApplePurchase(request, member, now); // TODO : 디스코드 알림 추가 필요 // paymentEventPublisher.publishPaymentEvent(member,request.getPrice(), pencilAmount,TransactionStatus.SUCCESS); - return AppleIAPPurchaseResponse.ofSuccess(transactionId); - }else{ + PencilAccount buyer = pencilAccountFinder.findByMember(member); + return AppleIAPPurchaseResponse.ofSuccess(transactionId, request.getPencilQuantity(), + buyer.getTotalBalance()); + } else { // 실패 시, DB에 저장 handleFailureApplePurchase(request, member, now); @@ -112,12 +117,13 @@ public void handleFailureApplePurchase(AppleIAPPurchaseRequest request, Member m Long pencilAmount = request.getPencilQuantity(); String title = createTitle(pencilAmount); - purchasedPencilUpdater.save(PurchasedPencil.failedDueToValidation(member, title, pencilAmount, request.getPrice(), - request.getPlayTime(), transactionId, request.getAppAccountToken(), now)); + purchasedPencilUpdater.save( + PurchasedPencil.failedDueToValidation(member, title, pencilAmount, request.getPrice(), + request.getPlayTime(), transactionId, request.getAppAccountToken(), now)); } private PurchasedPencil retryPurchasedPencil(PurchasedPencil pencil, Member member) { - if ( pencil.getRetryCount() >= 3 ) { // 재시도 횟수가 3회 이상일 경우 실패로 처리 + if (pencil.getRetryCount() >= 3) { // 재시도 횟수가 3회 이상일 경우 실패로 처리 return pencil; } @@ -137,7 +143,6 @@ private PurchasedPencil retryPurchasedPencil(PurchasedPencil pencil, Member memb return pencil; } - private String createTitle(Long pencilAmount) { return String.format("연필 %d개 구매", pencilAmount); } diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/PencilQueryService.java b/src/main/java/umc/th/juinjang/api/pencil/service/PencilQueryService.java index 4004874d..d912dbfb 100644 --- a/src/main/java/umc/th/juinjang/api/pencil/service/PencilQueryService.java +++ b/src/main/java/umc/th/juinjang/api/pencil/service/PencilQueryService.java @@ -42,4 +42,9 @@ public List getUsedPencils(Member member) { .map(UsedPencilResponse::from) .toList(); } + + public boolean isAcquiredPencilReadStatus(Member member) { + // false 인 것이 존재하면 안됨. + return !acquiredPencilFinder.existsByMemberAndIsReadFalse(member); + } } diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/response/AcquiredPencilReadResponse.java b/src/main/java/umc/th/juinjang/api/pencil/service/response/AcquiredPencilReadResponse.java new file mode 100644 index 00000000..6c182bba --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/response/AcquiredPencilReadResponse.java @@ -0,0 +1,10 @@ +package umc.th.juinjang.api.pencil.service.response; + +public record AcquiredPencilReadResponse( + boolean isMarked, + boolean isTotalRead +) { + public static AcquiredPencilReadResponse of(boolean isMarked, boolean isTotalRead) { + return new AcquiredPencilReadResponse(isMarked, isTotalRead); + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/response/AcquiredPencilReadStatusResponse.java b/src/main/java/umc/th/juinjang/api/pencil/service/response/AcquiredPencilReadStatusResponse.java new file mode 100644 index 00000000..10f21000 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/response/AcquiredPencilReadStatusResponse.java @@ -0,0 +1,9 @@ +package umc.th.juinjang.api.pencil.service.response; + +public record AcquiredPencilReadStatusResponse( + boolean isTotalRead +) { + public static AcquiredPencilReadStatusResponse of(boolean isTotalRead) { + return new AcquiredPencilReadStatusResponse(isTotalRead); + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/response/AppleIAPPurchaseResponse.java b/src/main/java/umc/th/juinjang/api/pencil/service/response/AppleIAPPurchaseResponse.java index 2aa763fd..69fee1bd 100644 --- a/src/main/java/umc/th/juinjang/api/pencil/service/response/AppleIAPPurchaseResponse.java +++ b/src/main/java/umc/th/juinjang/api/pencil/service/response/AppleIAPPurchaseResponse.java @@ -7,33 +7,58 @@ @Getter public class AppleIAPPurchaseResponse { - private TransactionStatus status; - private String transactionId; + private final TransactionStatus status; + private final String transactionId; + private final Long purchaseQuantity; + private final Long remainQuantity; @Builder - private AppleIAPPurchaseResponse(TransactionStatus status,String transactionId) { + private AppleIAPPurchaseResponse( + TransactionStatus status, + String transactionId, + Long purchaseQuantity, + Long remainQuantity + ) { this.status = status; this.transactionId = transactionId; + this.purchaseQuantity = purchaseQuantity; + this.remainQuantity = remainQuantity; } - public static AppleIAPPurchaseResponse of(String transactionId, TransactionStatus status) { + /** + * 일반적인 정적 팩토리: 모든 필드 수동 지정 + */ + public static AppleIAPPurchaseResponse of(String transactionId, TransactionStatus status, Long purchaseQuantity, + Long remainQuantity) { return AppleIAPPurchaseResponse.builder() .transactionId(transactionId) .status(status) + .purchaseQuantity(purchaseQuantity) + .remainQuantity(remainQuantity) .build(); } - public static AppleIAPPurchaseResponse ofSuccess(String transactionId) { + /** + * 성공 응답용 팩토리 + */ + public static AppleIAPPurchaseResponse ofSuccess(String transactionId, Long purchaseQuantity, Long remainQuantity) { return AppleIAPPurchaseResponse.builder() .status(TransactionStatus.SUCCESS) .transactionId(transactionId) + .purchaseQuantity(purchaseQuantity) + .remainQuantity(remainQuantity) .build(); } + /** + * 검증 실패 응답용 팩토리 + */ public static AppleIAPPurchaseResponse ofValidationFailure(String transactionId) { return AppleIAPPurchaseResponse.builder() .status(TransactionStatus.VALIDATION_FAILED) .transactionId(transactionId) + .purchaseQuantity(0L) + .remainQuantity(null) .build(); } } diff --git a/src/main/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountFinder.java b/src/main/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountFinder.java index 02f99a94..35b44ded 100644 --- a/src/main/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountFinder.java +++ b/src/main/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountFinder.java @@ -2,11 +2,8 @@ import static umc.th.juinjang.common.code.status.ErrorStatus.*; -import org.springframework.data.jpa.repository.Lock; -import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Component; -import jakarta.persistence.LockModeType; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import umc.th.juinjang.common.exception.handler.PencilAccountHandler; @@ -21,7 +18,7 @@ public class PencilAccountFinder { private final PencilAccountRepository pencilAccountRepository; - protected PencilAccount findByMember(Member member) { + public PencilAccount findByMember(Member member) { return pencilAccountRepository.findByMember(member).orElseThrow( () -> { log.error("[PENCIL_ACCOUNT]"); diff --git a/src/main/java/umc/th/juinjang/domain/pencil/acquired/repository/AcquiredPencilRepository.java b/src/main/java/umc/th/juinjang/domain/pencil/acquired/repository/AcquiredPencilRepository.java index daa2c3a9..4d0cb022 100644 --- a/src/main/java/umc/th/juinjang/domain/pencil/acquired/repository/AcquiredPencilRepository.java +++ b/src/main/java/umc/th/juinjang/domain/pencil/acquired/repository/AcquiredPencilRepository.java @@ -9,4 +9,6 @@ public interface AcquiredPencilRepository extends JpaRepository { List findAllByMemberOrderByCreatedAtDesc(Member member); + + boolean existsByMemberAndIsReadFalse(Member member); } diff --git a/src/test/java/umc/th/juinjang/api/pencil/service/PencilQueryServiceTest.java b/src/test/java/umc/th/juinjang/api/pencil/service/PencilQueryServiceTest.java index 9979a30b..e76dbe94 100644 --- a/src/test/java/umc/th/juinjang/api/pencil/service/PencilQueryServiceTest.java +++ b/src/test/java/umc/th/juinjang/api/pencil/service/PencilQueryServiceTest.java @@ -1,7 +1,6 @@ package umc.th.juinjang.api.pencil.service; import static org.assertj.core.api.Assertions.*; -import static umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil.*; import java.time.LocalDateTime; import java.util.List; @@ -156,11 +155,16 @@ void getPurchasedPencilsOrderedByCreatedAtDesc() { UUID uuid5 = UUID.randomUUID(); // 명확한 순서로 데이터 생성 (시간 역순으로) - PurchasedPencil pencil1 = PurchasedPencil.successOf(member, "10개 연필팩", 10L, 1000L, 0L,"transaction1", uuid1, time1); - PurchasedPencil pencil2 = PurchasedPencil.successOf(member, "20개 연필팩", 20L, 2000L, 0L,"transaction2", uuid2, time2); - PurchasedPencil pencil3 = PurchasedPencil.successOf(member, "30개 연필팩", 30L, 3000L,0L ,"transaction3", uuid3, time3); - PurchasedPencil pencil4 = PurchasedPencil.successOf(member, "15개 연필팩", 15L, 1500L, 0L,"transaction4", uuid4, time4); - PurchasedPencil pencil5 = PurchasedPencil.successOf(member, "25개 연필팩", 25L, 2500L, 0L,"transaction5", uuid5, time5); + PurchasedPencil pencil1 = PurchasedPencil.successOf(member, "10개 연필팩", 10L, 1000L, 0L, "transaction1", uuid1, + time1); + PurchasedPencil pencil2 = PurchasedPencil.successOf(member, "20개 연필팩", 20L, 2000L, 0L, "transaction2", uuid2, + time2); + PurchasedPencil pencil3 = PurchasedPencil.successOf(member, "30개 연필팩", 30L, 3000L, 0L, "transaction3", uuid3, + time3); + PurchasedPencil pencil4 = PurchasedPencil.successOf(member, "15개 연필팩", 15L, 1500L, 0L, "transaction4", uuid4, + time4); + PurchasedPencil pencil5 = PurchasedPencil.successOf(member, "25개 연필팩", 25L, 2500L, 0L, "transaction5", uuid5, + time5); purchasedPencilRepository.saveAll(List.of(pencil1, pencil2, pencil3, pencil4, pencil5)); @@ -193,7 +197,8 @@ void getPurchasedPencilsWithoutDeliveryStatus() { LocalDateTime time = LocalDateTime.now(); UUID uuid = UUID.randomUUID(); - PurchasedPencil pencil = PurchasedPencil.failedDueToServerError(member, "10개 연필팩", 10L, 1000L, 10L,"transaction1", uuid, time); + PurchasedPencil pencil = PurchasedPencil.failedDueToServerError(member, "10개 연필팩", 10L, 1000L, 10L, + "transaction1", uuid, time); purchasedPencilRepository.saveAll(List.of(pencil)); @@ -239,6 +244,42 @@ void getUsedPencilsOrderedByCreatedAtDesc() { ); } + @DisplayName("얻은 연필 목록 중 읽지 않은 연필이 없으면 전체 읽음으로 판단된다.") + @Test + void returnTrueIfAllAcquiredPencilsAreRead() { + // given + Member member = MemberFixture.createDefaultMember(); + memberRepository.save(member); + + AcquiredPencil pencil1 = AcquiredPencil.create(member, "연필 1", 1L, 10L, true, AcquiredType.NOTE); + AcquiredPencil pencil2 = AcquiredPencil.create(member, "연필 2", 2L, 20L, true, AcquiredType.SOLD); + acquiredPencilRepository.saveAll(List.of(pencil1, pencil2)); + + // when + boolean isTotalRead = pencilService.isAcquiredPencilReadStatus(member); + + // then + assertThat(isTotalRead).isTrue(); + } + + @DisplayName("얻은 연필 목록 중 읽지 않은 연필이 하나라도 있으면 전체 읽음으로 판단되지 않는다.") + @Test + void returnFalseIfAnyAcquiredPencilIsUnread() { + // given + Member member = MemberFixture.createDefaultMember(); + memberRepository.save(member); + + AcquiredPencil pencil1 = AcquiredPencil.create(member, "연필 1", 1L, 10L, true, AcquiredType.NOTE); + AcquiredPencil pencil2 = AcquiredPencil.create(member, "연필 2", 2L, 20L, false, AcquiredType.SOLD); + acquiredPencilRepository.saveAll(List.of(pencil1, pencil2)); + + // when + boolean isTotalRead = pencilService.isAcquiredPencilReadStatus(member); + + // then + assertThat(isTotalRead).isFalse(); + } + private AcquiredPencil createAcquiredPencilWithTime(LocalDateTime createdAt, String content, Long sharedNoteId, Long acquiredQuantity, boolean isRead, AcquiredType type, Member member) { return AcquiredPencil.createWithDate(member, content, sharedNoteId, acquiredQuantity, isRead, type, createdAt);