Skip to content

Commit fd9bba4

Browse files
authored
Merge pull request #411 from Juinjang/feat/#410
[feat] 얻은 연필 목록 전체 읽음 여부 확인 API 및 응답 필드 추가
2 parents d10d42e + 23841df commit fd9bba4

10 files changed

Lines changed: 146 additions & 34 deletions

File tree

src/main/java/umc/th/juinjang/api/pencil/controller/PencilController.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import umc.th.juinjang.api.pencil.controller.request.AppleIAPPurchaseRequest;
1919
import umc.th.juinjang.api.pencil.service.PencilCommandService;
2020
import umc.th.juinjang.api.pencil.service.PencilQueryService;
21+
import umc.th.juinjang.api.pencil.service.response.AcquiredPencilReadResponse;
22+
import umc.th.juinjang.api.pencil.service.response.AcquiredPencilReadStatusResponse;
2123
import umc.th.juinjang.api.pencil.service.response.AcquiredPencilResponse;
2224
import umc.th.juinjang.api.pencil.service.response.AppleIAPPurchaseResponse;
2325
import umc.th.juinjang.api.pencil.service.response.PurchasedPencilResponse;
@@ -40,10 +42,21 @@ public ApiResponse<List<AcquiredPencilResponse>> getAcquiredPencilHistory(@Authe
4042

4143
@Operation(summary = "얻은 연필 목록에서 읽음 처리를 진행한다.")
4244
@PatchMapping("/acquired/{acquiredPencilId}/read")
43-
public ApiResponse<Boolean> markAcquiredPencilAsRead(
45+
public ApiResponse<AcquiredPencilReadResponse> markAcquiredPencilAsRead(
4446
@PathVariable Long acquiredPencilId,
4547
@AuthenticationPrincipal Member member) {
46-
return ApiResponse.onSuccess(pencilCommandService.markAcquiredPencilAsRead(acquiredPencilId));
48+
return ApiResponse.onSuccess(
49+
AcquiredPencilReadResponse.of(pencilCommandService.markAcquiredPencilAsRead(acquiredPencilId),
50+
pencilQueryService.isAcquiredPencilReadStatus(member)));
51+
}
52+
53+
@Operation(summary = "얻은 연필 목록에서 읽지 않은 항목이 존재하는 여부를 확인한다.")
54+
@GetMapping("/acquired/is-total-read")
55+
public ApiResponse<AcquiredPencilReadStatusResponse> isAcquiredPencilReadStatus(
56+
@AuthenticationPrincipal Member member
57+
) {
58+
return ApiResponse.onSuccess(
59+
AcquiredPencilReadStatusResponse.of(pencilQueryService.isAcquiredPencilReadStatus(member)));
4760
}
4861

4962
@Operation(summary = "구매한 연필 목록을 불러온다")
@@ -66,6 +79,7 @@ public ApiResponse<AppleIAPPurchaseResponse> purchasePencil(
6679
@AuthenticationPrincipal Member member,
6780
@RequestBody AppleIAPPurchaseRequest request
6881
) {
69-
return ApiResponse.onSuccess(pencilCommandService.processAppleIAPPurchase(request , member, LocalDateTime.now()));
82+
return ApiResponse.onSuccess(
83+
pencilCommandService.processAppleIAPPurchase(request, member, LocalDateTime.now()));
7084
}
7185
}

src/main/java/umc/th/juinjang/api/pencil/service/AcquiredPencilFinder.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ public List<AcquiredPencil> findAllByMemberOrderByCreatedAtDesc(Member member) {
1919
return acquiredPencilRepository.findAllByMemberOrderByCreatedAtDesc(member);
2020
}
2121

22+
public boolean existsByMemberAndIsReadFalse(Member member) {
23+
return acquiredPencilRepository.existsByMemberAndIsReadFalse(member);
24+
}
25+
2226
public AcquiredPencil findById(Long id) {
2327
return acquiredPencilRepository.findById(id).orElse(null);
2428
}

src/main/java/umc/th/juinjang/api/pencil/service/PencilCommandService.java

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import umc.th.juinjang.domain.pencil.acquired.model.AcquiredPencil;
2020
import umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil;
2121
import umc.th.juinjang.domain.pencil.purchased.model.TransactionStatus;
22+
import umc.th.juinjang.domain.pencilaccount.model.PencilAccount;
2223

2324
@Slf4j
2425
@Service
@@ -48,7 +49,7 @@ public Boolean markAcquiredPencilAsRead(Long acquiredPencilId) {
4849
public AppleIAPPurchaseResponse processAppleIAPPurchase(AppleIAPPurchaseRequest request, Member member,
4950
LocalDateTime now) {
5051
String transactionId = request.getTransactionId();
51-
52+
Long purchaseQuantity = request.getPencilQuantity();
5253
Optional<PurchasedPencil> existing = purchasedPencilFinder.findByTransactionIdAndMember(transactionId, member);
5354

5455
if (existing.isEmpty()) {
@@ -59,32 +60,36 @@ public AppleIAPPurchaseResponse processAppleIAPPurchase(AppleIAPPurchaseRequest
5960

6061
PurchasedPencil pencil = existing.get();
6162
TransactionStatus status = pencil.getTransactionStatus();
63+
PencilAccount buyer = pencilAccountFinder.findByMember(member);
6264

6365
if (status == TransactionStatus.SUCCESS) {
6466
// 트랜잭션이 정상적으로 성공된 기록이 있는 경우
65-
return AppleIAPPurchaseResponse.ofSuccess(transactionId);
67+
return AppleIAPPurchaseResponse.ofSuccess(transactionId, purchaseQuantity, buyer.getTotalBalance());
6668
}
6769

6870
PurchasedPencil newPencil = retryPurchasedPencil(pencil, member); // 실패 재시도 처리
69-
return AppleIAPPurchaseResponse.of(transactionId, newPencil.getTransactionStatus());
71+
return AppleIAPPurchaseResponse.of(transactionId, newPencil.getTransactionStatus(), purchaseQuantity,
72+
buyer.getTotalBalance());
7073
}
7174

72-
73-
7475
@Transactional
75-
public AppleIAPPurchaseResponse validateAndCommitApplePurchase(AppleIAPPurchaseRequest request, Member member, LocalDateTime now) {
76+
public AppleIAPPurchaseResponse validateAndCommitApplePurchase(AppleIAPPurchaseRequest request, Member member,
77+
LocalDateTime now) {
7678
String transactionId = request.getTransactionId();
7779

78-
VerificationResult verificationResult = appleService.verifyAppleTransaction(AppleTransactionVerifyCommand.fromRequest(request));
80+
VerificationResult verificationResult = appleService.verifyAppleTransaction(
81+
AppleTransactionVerifyCommand.fromRequest(request));
7982

80-
if (VerificationResult.isSuccess(verificationResult)){
81-
// 성공 시, DB에 저장z
83+
if (VerificationResult.isSuccess(verificationResult)) {
84+
// 성공 시, DB에 저장
8285
handleSuccessfulApplePurchase(request, member, now);
8386

8487
// TODO : 디스코드 알림 추가 필요
8588
// paymentEventPublisher.publishPaymentEvent(member,request.getPrice(), pencilAmount,TransactionStatus.SUCCESS);
86-
return AppleIAPPurchaseResponse.ofSuccess(transactionId);
87-
}else{
89+
PencilAccount buyer = pencilAccountFinder.findByMember(member);
90+
return AppleIAPPurchaseResponse.ofSuccess(transactionId, request.getPencilQuantity(),
91+
buyer.getTotalBalance());
92+
} else {
8893
// 실패 시, DB에 저장
8994
handleFailureApplePurchase(request, member, now);
9095

@@ -112,12 +117,13 @@ public void handleFailureApplePurchase(AppleIAPPurchaseRequest request, Member m
112117
Long pencilAmount = request.getPencilQuantity();
113118

114119
String title = createTitle(pencilAmount);
115-
purchasedPencilUpdater.save(PurchasedPencil.failedDueToValidation(member, title, pencilAmount, request.getPrice(),
116-
request.getPlayTime(), transactionId, request.getAppAccountToken(), now));
120+
purchasedPencilUpdater.save(
121+
PurchasedPencil.failedDueToValidation(member, title, pencilAmount, request.getPrice(),
122+
request.getPlayTime(), transactionId, request.getAppAccountToken(), now));
117123
}
118124

119125
private PurchasedPencil retryPurchasedPencil(PurchasedPencil pencil, Member member) {
120-
if ( pencil.getRetryCount() >= 3 ) { // 재시도 횟수가 3회 이상일 경우 실패로 처리
126+
if (pencil.getRetryCount() >= 3) { // 재시도 횟수가 3회 이상일 경우 실패로 처리
121127
return pencil;
122128
}
123129

@@ -137,7 +143,6 @@ private PurchasedPencil retryPurchasedPencil(PurchasedPencil pencil, Member memb
137143
return pencil;
138144
}
139145

140-
141146
private String createTitle(Long pencilAmount) {
142147
return String.format("연필 %d개 구매", pencilAmount);
143148
}

src/main/java/umc/th/juinjang/api/pencil/service/PencilQueryService.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,9 @@ public List<UsedPencilResponse> getUsedPencils(Member member) {
4242
.map(UsedPencilResponse::from)
4343
.toList();
4444
}
45+
46+
public boolean isAcquiredPencilReadStatus(Member member) {
47+
// false 인 것이 존재하면 안됨.
48+
return !acquiredPencilFinder.existsByMemberAndIsReadFalse(member);
49+
}
4550
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package umc.th.juinjang.api.pencil.service.response;
2+
3+
public record AcquiredPencilReadResponse(
4+
boolean isMarked,
5+
boolean isTotalRead
6+
) {
7+
public static AcquiredPencilReadResponse of(boolean isMarked, boolean isTotalRead) {
8+
return new AcquiredPencilReadResponse(isMarked, isTotalRead);
9+
}
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package umc.th.juinjang.api.pencil.service.response;
2+
3+
public record AcquiredPencilReadStatusResponse(
4+
boolean isTotalRead
5+
) {
6+
public static AcquiredPencilReadStatusResponse of(boolean isTotalRead) {
7+
return new AcquiredPencilReadStatusResponse(isTotalRead);
8+
}
9+
}

src/main/java/umc/th/juinjang/api/pencil/service/response/AppleIAPPurchaseResponse.java

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,58 @@
77
@Getter
88
public class AppleIAPPurchaseResponse {
99

10-
private TransactionStatus status;
11-
private String transactionId;
10+
private final TransactionStatus status;
11+
private final String transactionId;
12+
private final Long purchaseQuantity;
13+
private final Long remainQuantity;
1214

1315
@Builder
14-
private AppleIAPPurchaseResponse(TransactionStatus status,String transactionId) {
16+
private AppleIAPPurchaseResponse(
17+
TransactionStatus status,
18+
String transactionId,
19+
Long purchaseQuantity,
20+
Long remainQuantity
21+
) {
1522
this.status = status;
1623
this.transactionId = transactionId;
24+
this.purchaseQuantity = purchaseQuantity;
25+
this.remainQuantity = remainQuantity;
1726
}
1827

19-
public static AppleIAPPurchaseResponse of(String transactionId, TransactionStatus status) {
28+
/**
29+
* 일반적인 정적 팩토리: 모든 필드 수동 지정
30+
*/
31+
public static AppleIAPPurchaseResponse of(String transactionId, TransactionStatus status, Long purchaseQuantity,
32+
Long remainQuantity) {
2033
return AppleIAPPurchaseResponse.builder()
2134
.transactionId(transactionId)
2235
.status(status)
36+
.purchaseQuantity(purchaseQuantity)
37+
.remainQuantity(remainQuantity)
2338
.build();
2439
}
2540

26-
public static AppleIAPPurchaseResponse ofSuccess(String transactionId) {
41+
/**
42+
* 성공 응답용 팩토리
43+
*/
44+
public static AppleIAPPurchaseResponse ofSuccess(String transactionId, Long purchaseQuantity, Long remainQuantity) {
2745
return AppleIAPPurchaseResponse.builder()
2846
.status(TransactionStatus.SUCCESS)
2947
.transactionId(transactionId)
48+
.purchaseQuantity(purchaseQuantity)
49+
.remainQuantity(remainQuantity)
3050
.build();
3151
}
3252

53+
/**
54+
* 검증 실패 응답용 팩토리
55+
*/
3356
public static AppleIAPPurchaseResponse ofValidationFailure(String transactionId) {
3457
return AppleIAPPurchaseResponse.builder()
3558
.status(TransactionStatus.VALIDATION_FAILED)
3659
.transactionId(transactionId)
60+
.purchaseQuantity(0L)
61+
.remainQuantity(null)
3762
.build();
3863
}
3964
}

src/main/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountFinder.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@
22

33
import static umc.th.juinjang.common.code.status.ErrorStatus.*;
44

5-
import org.springframework.data.jpa.repository.Lock;
6-
import org.springframework.data.jpa.repository.Query;
75
import org.springframework.stereotype.Component;
86

9-
import jakarta.persistence.LockModeType;
107
import lombok.RequiredArgsConstructor;
118
import lombok.extern.slf4j.Slf4j;
129
import umc.th.juinjang.common.exception.handler.PencilAccountHandler;
@@ -21,7 +18,7 @@ public class PencilAccountFinder {
2118

2219
private final PencilAccountRepository pencilAccountRepository;
2320

24-
protected PencilAccount findByMember(Member member) {
21+
public PencilAccount findByMember(Member member) {
2522
return pencilAccountRepository.findByMember(member).orElseThrow(
2623
() -> {
2724
log.error("[PENCIL_ACCOUNT]");

src/main/java/umc/th/juinjang/domain/pencil/acquired/repository/AcquiredPencilRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@
99

1010
public interface AcquiredPencilRepository extends JpaRepository<AcquiredPencil, Long> {
1111
List<AcquiredPencil> findAllByMemberOrderByCreatedAtDesc(Member member);
12+
13+
boolean existsByMemberAndIsReadFalse(Member member);
1214
}

src/test/java/umc/th/juinjang/api/pencil/service/PencilQueryServiceTest.java

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package umc.th.juinjang.api.pencil.service;
22

33
import static org.assertj.core.api.Assertions.*;
4-
import static umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil.*;
54

65
import java.time.LocalDateTime;
76
import java.util.List;
@@ -156,11 +155,16 @@ void getPurchasedPencilsOrderedByCreatedAtDesc() {
156155
UUID uuid5 = UUID.randomUUID();
157156

158157
// 명확한 순서로 데이터 생성 (시간 역순으로)
159-
PurchasedPencil pencil1 = PurchasedPencil.successOf(member, "10개 연필팩", 10L, 1000L, 0L,"transaction1", uuid1, time1);
160-
PurchasedPencil pencil2 = PurchasedPencil.successOf(member, "20개 연필팩", 20L, 2000L, 0L,"transaction2", uuid2, time2);
161-
PurchasedPencil pencil3 = PurchasedPencil.successOf(member, "30개 연필팩", 30L, 3000L,0L ,"transaction3", uuid3, time3);
162-
PurchasedPencil pencil4 = PurchasedPencil.successOf(member, "15개 연필팩", 15L, 1500L, 0L,"transaction4", uuid4, time4);
163-
PurchasedPencil pencil5 = PurchasedPencil.successOf(member, "25개 연필팩", 25L, 2500L, 0L,"transaction5", uuid5, time5);
158+
PurchasedPencil pencil1 = PurchasedPencil.successOf(member, "10개 연필팩", 10L, 1000L, 0L, "transaction1", uuid1,
159+
time1);
160+
PurchasedPencil pencil2 = PurchasedPencil.successOf(member, "20개 연필팩", 20L, 2000L, 0L, "transaction2", uuid2,
161+
time2);
162+
PurchasedPencil pencil3 = PurchasedPencil.successOf(member, "30개 연필팩", 30L, 3000L, 0L, "transaction3", uuid3,
163+
time3);
164+
PurchasedPencil pencil4 = PurchasedPencil.successOf(member, "15개 연필팩", 15L, 1500L, 0L, "transaction4", uuid4,
165+
time4);
166+
PurchasedPencil pencil5 = PurchasedPencil.successOf(member, "25개 연필팩", 25L, 2500L, 0L, "transaction5", uuid5,
167+
time5);
164168

165169
purchasedPencilRepository.saveAll(List.of(pencil1, pencil2, pencil3, pencil4, pencil5));
166170

@@ -193,7 +197,8 @@ void getPurchasedPencilsWithoutDeliveryStatus() {
193197
LocalDateTime time = LocalDateTime.now();
194198
UUID uuid = UUID.randomUUID();
195199

196-
PurchasedPencil pencil = PurchasedPencil.failedDueToServerError(member, "10개 연필팩", 10L, 1000L, 10L,"transaction1", uuid, time);
200+
PurchasedPencil pencil = PurchasedPencil.failedDueToServerError(member, "10개 연필팩", 10L, 1000L, 10L,
201+
"transaction1", uuid, time);
197202

198203
purchasedPencilRepository.saveAll(List.of(pencil));
199204

@@ -239,6 +244,42 @@ void getUsedPencilsOrderedByCreatedAtDesc() {
239244
);
240245
}
241246

247+
@DisplayName("얻은 연필 목록 중 읽지 않은 연필이 없으면 전체 읽음으로 판단된다.")
248+
@Test
249+
void returnTrueIfAllAcquiredPencilsAreRead() {
250+
// given
251+
Member member = MemberFixture.createDefaultMember();
252+
memberRepository.save(member);
253+
254+
AcquiredPencil pencil1 = AcquiredPencil.create(member, "연필 1", 1L, 10L, true, AcquiredType.NOTE);
255+
AcquiredPencil pencil2 = AcquiredPencil.create(member, "연필 2", 2L, 20L, true, AcquiredType.SOLD);
256+
acquiredPencilRepository.saveAll(List.of(pencil1, pencil2));
257+
258+
// when
259+
boolean isTotalRead = pencilService.isAcquiredPencilReadStatus(member);
260+
261+
// then
262+
assertThat(isTotalRead).isTrue();
263+
}
264+
265+
@DisplayName("얻은 연필 목록 중 읽지 않은 연필이 하나라도 있으면 전체 읽음으로 판단되지 않는다.")
266+
@Test
267+
void returnFalseIfAnyAcquiredPencilIsUnread() {
268+
// given
269+
Member member = MemberFixture.createDefaultMember();
270+
memberRepository.save(member);
271+
272+
AcquiredPencil pencil1 = AcquiredPencil.create(member, "연필 1", 1L, 10L, true, AcquiredType.NOTE);
273+
AcquiredPencil pencil2 = AcquiredPencil.create(member, "연필 2", 2L, 20L, false, AcquiredType.SOLD);
274+
acquiredPencilRepository.saveAll(List.of(pencil1, pencil2));
275+
276+
// when
277+
boolean isTotalRead = pencilService.isAcquiredPencilReadStatus(member);
278+
279+
// then
280+
assertThat(isTotalRead).isFalse();
281+
}
282+
242283
private AcquiredPencil createAcquiredPencilWithTime(LocalDateTime createdAt, String content, Long sharedNoteId,
243284
Long acquiredQuantity, boolean isRead, AcquiredType type, Member member) {
244285
return AcquiredPencil.createWithDate(member, content, sharedNoteId, acquiredQuantity, isRead, type, createdAt);

0 commit comments

Comments
 (0)