diff --git a/src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java b/src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java index e72514c1..38945b05 100644 --- a/src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java +++ b/src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java @@ -47,8 +47,8 @@ public void buyAiImagePermissionByCoin(Long userId, Long postId) { // 2) 권한 업서트 + 증가 (원자적) aiChatRoomRepository.upsertAndIncrease(userId, postId, AI_IMAGE_PERMISSION_COUNT); - // 3) 코인 후차감, 코인 거래는 별도의 트랜잭션으로 동작하여 후처리, 예외는 전파 - accountService.consumeCoin(userId, post.getNonCommercialPrice()); + // 3) 코인을 원작자에게 전송, 코인 거래는 별도의 트랜잭션으로 동작하여 후처리, 예외는 전파 + accountService.transferCoin(userId, post.getUserId(), post.getNonCommercialPrice()); // 4) 알림 UserEntity user = userEntityRepository.findById(userId) diff --git a/src/main/java/hanium/modic/backend/domain/profile/service/ProfileService.java b/src/main/java/hanium/modic/backend/domain/profile/service/ProfileService.java index da667ff2..529bd117 100644 --- a/src/main/java/hanium/modic/backend/domain/profile/service/ProfileService.java +++ b/src/main/java/hanium/modic/backend/domain/profile/service/ProfileService.java @@ -11,6 +11,7 @@ import hanium.modic.backend.common.error.exception.AppException; import hanium.modic.backend.domain.follow.repository.FollowEntityRepository; import hanium.modic.backend.domain.post.repository.PostEntityRepository; +import hanium.modic.backend.domain.transaction.entity.Account; import hanium.modic.backend.domain.transaction.repository.AccountRepository; import hanium.modic.backend.domain.user.entity.UserEntity; import hanium.modic.backend.domain.user.entity.UserImageEntity; @@ -48,7 +49,7 @@ public GetMyProfileResponse getMyProfile(final UserEntity user) { .map(UserImageEntity::getId); final boolean hasUserImage = userImageUrl.isPresent(); final long coinAmount = accountRepository.findById(user.getId()) - .map(account -> account.getCoin()) + .map(Account::getPostedBalance) .orElseThrow(() -> new AppException(ACCOUNT_NOT_FOUND_EXCEPTION)); return new GetMyProfileResponse( diff --git a/src/main/java/hanium/modic/backend/domain/transaction/entity/Account.java b/src/main/java/hanium/modic/backend/domain/transaction/entity/Account.java index 59c6293a..f02ea513 100644 --- a/src/main/java/hanium/modic/backend/domain/transaction/entity/Account.java +++ b/src/main/java/hanium/modic/backend/domain/transaction/entity/Account.java @@ -9,7 +9,6 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; -import jakarta.persistence.Version; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -29,12 +28,12 @@ public class Account { @Column(name = "user_id", nullable = false, unique = true) private Long userId; - @Column(name = "coin", nullable = false) - private Long coin = 0L; + // Ledger가 관리하는 version + @Column(name = "ledger_version", nullable = false) + private Long ledgerVersion = 0L; - @Version - @Column(name = "version", nullable = false) - private Long version = 0L; + @Column(name = "posted_balance", nullable = false) + private Long postedBalance = 0L; /** * 사용자 아이디로 계좌 생성 @@ -45,10 +44,13 @@ private Account(Long userId) { this.userId = userId; } - public void addCoin(Long coin) { - if (this.coin + coin < 0) { + // 잔액 업데이트 + public void updateBalance(Long newBalance) { + if (newBalance < 0) { throw new AppException(COIN_NOT_ENOUGH_EXCEPTION); } - this.coin += coin; + + this.postedBalance = newBalance; + this.ledgerVersion++; // Balance 변경 시마다 version 증가 } } diff --git a/src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransactionEntity.java b/src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransactionEntity.java index 38f41c22..f134335b 100644 --- a/src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransactionEntity.java +++ b/src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransactionEntity.java @@ -23,7 +23,7 @@ /** * Transaction 거래의 단위 거래(debit, credit) */ -@Table(name = "coin_tranaction_entities") +@Table(name = "coin_transaction_entities") @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/src/main/java/hanium/modic/backend/domain/transaction/repository/AccountRepository.java b/src/main/java/hanium/modic/backend/domain/transaction/repository/AccountRepository.java index a183a637..44e726d4 100644 --- a/src/main/java/hanium/modic/backend/domain/transaction/repository/AccountRepository.java +++ b/src/main/java/hanium/modic/backend/domain/transaction/repository/AccountRepository.java @@ -1,8 +1,12 @@ package hanium.modic.backend.domain.transaction.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import hanium.modic.backend.domain.transaction.entity.Account; public interface AccountRepository extends JpaRepository { + + Optional findByUserId(Long userId); } diff --git a/src/main/java/hanium/modic/backend/domain/transaction/repository/CoinTransactionEntityRepository.java b/src/main/java/hanium/modic/backend/domain/transaction/repository/CoinTransactionEntityRepository.java new file mode 100644 index 00000000..60a3153f --- /dev/null +++ b/src/main/java/hanium/modic/backend/domain/transaction/repository/CoinTransactionEntityRepository.java @@ -0,0 +1,8 @@ +package hanium.modic.backend.domain.transaction.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import hanium.modic.backend.domain.transaction.entity.CoinTransactionEntity; + +public interface CoinTransactionEntityRepository extends JpaRepository { +} diff --git a/src/main/java/hanium/modic/backend/domain/transaction/repository/CoinTransactionRepository.java b/src/main/java/hanium/modic/backend/domain/transaction/repository/CoinTransactionRepository.java new file mode 100644 index 00000000..90e4a0d4 --- /dev/null +++ b/src/main/java/hanium/modic/backend/domain/transaction/repository/CoinTransactionRepository.java @@ -0,0 +1,8 @@ +package hanium.modic.backend.domain.transaction.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import hanium.modic.backend.domain.transaction.entity.CoinTransaction; + +public interface CoinTransactionRepository extends JpaRepository { +} diff --git a/src/main/java/hanium/modic/backend/domain/transaction/service/AccountService.java b/src/main/java/hanium/modic/backend/domain/transaction/service/AccountService.java index f5d57e28..f986b920 100644 --- a/src/main/java/hanium/modic/backend/domain/transaction/service/AccountService.java +++ b/src/main/java/hanium/modic/backend/domain/transaction/service/AccountService.java @@ -2,18 +2,15 @@ import static hanium.modic.backend.common.error.ErrorCode.*; -import java.util.List; - import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import hanium.modic.backend.common.error.exception.AppException; -import hanium.modic.backend.common.error.exception.LockException; import hanium.modic.backend.domain.notification.dto.NotificationPayload; import hanium.modic.backend.domain.notification.enums.NotificationType; import hanium.modic.backend.domain.notification.service.NotificationService; import hanium.modic.backend.domain.transaction.entity.Account; import hanium.modic.backend.domain.transaction.repository.AccountRepository; -import hanium.modic.backend.infra.redis.distributedLock.LockManager; import hanium.modic.backend.domain.user.entity.UserEntity; import hanium.modic.backend.domain.user.repository.UserEntityRepository; import hanium.modic.backend.web.transaction.dto.response.GetCoinBalanceResponse; @@ -23,11 +20,14 @@ @RequiredArgsConstructor public class AccountService { + // 계좌, 거래 관련 private final AccountRepository accountRepository; + private final LedgerService ledgerService; - private final LockManager lockManager; - + // 알림 관련 private final NotificationService notificationService; + + // 유저 관련 private final UserEntityRepository userEntityRepository; // 코인 잔액 조회 @@ -35,23 +35,12 @@ public GetCoinBalanceResponse getCoinBalance(final Long userId) { Account account = accountRepository.findById(userId) .orElseThrow(() -> new AppException(ACCOUNT_NOT_FOUND_EXCEPTION)); - return new GetCoinBalanceResponse(account.getCoin()); - } - - // 코인 충전 - public void chargeCoin(final long userId, final long coin) { - try { - lockManager.accountLock(userId, () -> { - addCoin(userId, coin); - }); - } catch (LockException e) { - // Todo: 추후 결제 포함될 시, 결제 취소 로직 필요 - throw new AppException(COIN_TRANSFER_FAIL_EXCEPTION); - } + return new GetCoinBalanceResponse(account.getPostedBalance()); } // 코인 양도 - public void transferCoin(final long fromUserId, final long toUserId, long coin) throws AppException { + @Transactional + public void transferCoin(final long fromUserId, final long toUserId, long coin) { // 자기 자신에게 양도 불가 if (fromUserId == toUserId) { throw new AppException(COIN_TRANSFER_SAME_USER_EXCEPTION); @@ -60,20 +49,9 @@ public void transferCoin(final long fromUserId, final long toUserId, long coin) // 받는 사람 존재 확인 UserEntity toUser = userEntityRepository.findById(toUserId) .orElseThrow(() -> new AppException(USER_NOT_FOUND_EXCEPTION)); - accountRepository.findById(toUserId) - .orElseThrow(() -> new AppException(ACCOUNT_NOT_FOUND_EXCEPTION)); - // 락 걸고 양도 처리 - try { - lockManager.multipleUserLock(List.of(fromUserId, toUserId), () -> { - // 출금 - addCoin(fromUserId, -coin); - // 입금 - addCoin(toUserId, coin); - }); - } catch (LockException e) { - throw new AppException(COIN_TRANSFER_FAIL_EXCEPTION); - } + // 양도 처리 + ledgerService.transfer(fromUserId, toUserId, coin); // 알림 notificationService.createNotification( @@ -84,24 +62,4 @@ public void transferCoin(final long fromUserId, final long toUserId, long coin) .build() ); } - - // 코인 소비 - public void consumeCoin(final long userId, long coin) throws AppException { - try { - lockManager.accountLock(userId, () -> { - addCoin(userId, -coin); - }); - } catch (LockException e) { - throw new AppException(COIN_TRANSFER_FAIL_EXCEPTION); - } - } - - // 코인 추가, 트랜잭션은 lockManager에 의해 관리됨 - private void addCoin(final long userId, final long coin) { - Account account = accountRepository.findById(userId) - .orElseThrow(() -> new AppException(ACCOUNT_NOT_FOUND_EXCEPTION)); - - account.addCoin(coin); - accountRepository.save(account); - } } diff --git a/src/main/java/hanium/modic/backend/domain/transaction/service/LedgerService.java b/src/main/java/hanium/modic/backend/domain/transaction/service/LedgerService.java new file mode 100644 index 00000000..b442f3bd --- /dev/null +++ b/src/main/java/hanium/modic/backend/domain/transaction/service/LedgerService.java @@ -0,0 +1,80 @@ +package hanium.modic.backend.domain.transaction.service; + +import static hanium.modic.backend.common.error.ErrorCode.*; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.stereotype.Service; + +import hanium.modic.backend.common.error.exception.AppException; +import hanium.modic.backend.common.error.exception.LockException; +import hanium.modic.backend.domain.transaction.entity.Account; +import hanium.modic.backend.domain.transaction.entity.CoinTransaction; +import hanium.modic.backend.domain.transaction.entity.CoinTransactionEntity; +import hanium.modic.backend.domain.transaction.enums.TransactionDirection; +import hanium.modic.backend.domain.transaction.enums.TransactionStatus; +import hanium.modic.backend.domain.transaction.repository.AccountRepository; +import hanium.modic.backend.domain.transaction.repository.CoinTransactionEntityRepository; +import hanium.modic.backend.domain.transaction.repository.CoinTransactionRepository; +import hanium.modic.backend.infra.redis.distributedLock.LockManager; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class LedgerService { + + // 계좌, 거래 관련 + private final AccountRepository accountRepository; + private final CoinTransactionRepository coinTransactionRepository; + private final CoinTransactionEntityRepository entryRepository; + + // 기타 + private final LockManager lockManager; + + // 코인 전송(게좌 변경, 트랜잭션, 엔티티 저장) 후 알림 처리 + public void transfer(final long fromUserId, final long toUserId, final long amount) { + // 락 걸고 양도 처리 + try { + lockManager.multipleAccountLock(List.of(fromUserId, toUserId), () -> { + // 1) 계좌 읽기 + Account fromAccount = accountRepository.findByUserId(fromUserId) + .orElseThrow(() -> new AppException(ACCOUNT_NOT_FOUND_EXCEPTION)); + Account toAccount = accountRepository.findByUserId(toUserId) + .orElseThrow(() -> new AppException(ACCOUNT_NOT_FOUND_EXCEPTION)); + + // 2) posted_balance 업데이트 + fromAccount.updateBalance(fromAccount.getPostedBalance() - amount); + toAccount.updateBalance(toAccount.getPostedBalance() + amount); + + // 3) 버전 캡처, 반드시 Account 업데이트 이후에 호출되어야 함 + long fromVersionBefore = fromAccount.getLedgerVersion(); + long toVersionBefore = toAccount.getLedgerVersion(); + + LocalDateTime now = LocalDateTime.now(); + + // 4) 트랜잭션 생성 (posted) + CoinTransaction txn = coinTransactionRepository.save( + CoinTransaction.builder() + .status(TransactionStatus.POSTED) + .effectiveAt(now) + .build() + ); + + // 5) 엔트리 생성 + entryRepository.save(CoinTransactionEntity.createPostedTransaction( + fromAccount.getId(), fromVersionBefore, txn.getId(), + TransactionDirection.DEBIT, amount, now + )); + entryRepository.save(CoinTransactionEntity.createPostedTransaction( + toAccount.getId(), toVersionBefore, txn.getId(), + TransactionDirection.CREDIT, amount, now + )); + accountRepository.save(fromAccount); + accountRepository.save(toAccount); + }); + } catch (LockException e) { + throw new AppException(COIN_TRANSFER_FAIL_EXCEPTION); + } + } +} diff --git a/src/main/java/hanium/modic/backend/infra/redis/distributedLock/LockManager.java b/src/main/java/hanium/modic/backend/infra/redis/distributedLock/LockManager.java index a4d5df32..403ef166 100644 --- a/src/main/java/hanium/modic/backend/infra/redis/distributedLock/LockManager.java +++ b/src/main/java/hanium/modic/backend/infra/redis/distributedLock/LockManager.java @@ -32,8 +32,12 @@ public class LockManager { private final String VOTE_STREAK_PREFIX = "lock:vote:streak:"; private final String ACCOUNT_PREFIX = "lock:account:"; - public void accountLock(long userId, Runnable block) throws LockException { - exec.withLock(ACCOUNT_PREFIX + userId, block); + // 유저당 계좌가 한 개라 userId로 락을 걸어도 무방 + public void multipleAccountLock(List userIds, Runnable block) throws LockException { + List keys = userIds.stream() + .map(id -> ACCOUNT_PREFIX + id) + .toList(); + exec.withMultiLock(keys, block); } public void multipleUserLock(List userIds, Runnable block) throws LockException { diff --git a/src/main/java/hanium/modic/backend/web/transaction/controller/AccountController.java b/src/main/java/hanium/modic/backend/web/transaction/controller/AccountController.java index c625a999..65abb59d 100644 --- a/src/main/java/hanium/modic/backend/web/transaction/controller/AccountController.java +++ b/src/main/java/hanium/modic/backend/web/transaction/controller/AccountController.java @@ -45,6 +45,7 @@ public ResponseEntity> getUserCoins(@Current description = "유저가 다른 유저에게 코인을 송금합니다." ) @ApiErrorMapping({ + USER_NOT_FOUND_EXCEPTION, COIN_NOT_ENOUGH_EXCEPTION, COIN_TRANSFER_SAME_USER_EXCEPTION, COIN_TRANSFER_FAIL_EXCEPTION, diff --git a/src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java b/src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java index e44f4549..8226705c 100644 --- a/src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java +++ b/src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java @@ -76,7 +76,7 @@ void buyAiImagePermissionByCoin_Success() { // then verify(postRepository).findById(testPost.getId()); verify(aiChatRoomRepository).upsertAndIncrease(testUser.getId(), testPost.getId(), 20); - verify(accountService).consumeCoin(testUser.getId(), testPost.getNonCommercialPrice()); + verify(accountService).transferCoin(testUser.getId(), testPost.getUserId(), testPost.getNonCommercialPrice()); } @Test @@ -96,7 +96,7 @@ void buyAiImagePermissionByCoin_PostNotFound() { verify(postRepository).findById(testPost.getId()); verify(aiChatRoomRepository, never()).upsertAndIncrease(anyLong(), anyLong(), anyInt()); - verify(accountService, never()).consumeCoin(anyLong(), anyLong()); + verify(accountService, never()).transferCoin(anyLong(), anyLong(), anyLong()); } @Test @@ -110,7 +110,7 @@ void buyAiImagePermissionByCoin_InsufficientCoin() { when(aiChatRoomRepository.upsertAndIncrease(anyLong(), anyLong(), anyInt())) .thenReturn(1); doThrow(new AppException(COIN_NOT_ENOUGH_EXCEPTION)) - .when(accountService).consumeCoin(anyLong(), anyLong()); + .when(accountService).transferCoin(anyLong(), anyLong(), anyLong()); // when & then AppException exception = assertThrows(AppException.class, @@ -120,7 +120,7 @@ void buyAiImagePermissionByCoin_InsufficientCoin() { verify(postRepository).findById(testPost.getId()); verify(aiChatRoomRepository).upsertAndIncrease(testUser.getId(), testPost.getId(), 20); - verify(accountService).consumeCoin(testUser.getId(), testPost.getNonCommercialPrice()); + verify(accountService).transferCoin(testUser.getId(), testPost.getUserId(), testPost.getNonCommercialPrice()); } @Test diff --git a/src/test/java/hanium/modic/backend/domain/transaction/AccountServiceIntegrationTest.java b/src/test/java/hanium/modic/backend/domain/transaction/AccountServiceIntegrationTest.java index 0fa1fe62..c850b86d 100644 --- a/src/test/java/hanium/modic/backend/domain/transaction/AccountServiceIntegrationTest.java +++ b/src/test/java/hanium/modic/backend/domain/transaction/AccountServiceIntegrationTest.java @@ -32,9 +32,11 @@ class AccountServiceIntegrationTest extends BaseIntegrationTest { private UserEntity userA; private UserEntity userB; + private UserEntity userC; private Account accountA; private Account accountB; + private Account accountC; @BeforeEach void setup() { @@ -43,40 +45,25 @@ void setup() { accountA = accountRepository.save(Account.builder().userId(userA.getId()).build()); userB = userRepository.save(UserFactory.createMockUserWithoutId("userB")); accountB = accountRepository.save(Account.builder().userId(userB.getId()).build()); + userC = userRepository.save(UserFactory.createMockUserWithoutId("userC")); + accountC = accountRepository.save(Account.builder().userId(userC.getId()).build()); } @Test - @DisplayName("TEST1: 동시에 코인 충전이 되면 차례대로 처리된다") - void coinChargeConcurrencyTest() throws InterruptedException { - int threadCount = 5; - ExecutorService executor = Executors.newFixedThreadPool(threadCount); - CountDownLatch latch = new CountDownLatch(threadCount); - - for (int i = 0; i < threadCount; i++) { - executor.execute(() -> { - try { - accountService.chargeCoin(userA.getId(), 100); - } finally { - latch.countDown(); - } - }); - } - - latch.await(); - Account account = accountRepository.findById(userA.getId()).orElseThrow(); - assertThat(account.getCoin()).isEqualTo(100 * threadCount); - } - - @Test - @DisplayName("TEST2: 동시에 A유저가 코인을 충전하고, B유저가 A유저에게 코인을 양도하면 무사히 처리된다") + @DisplayName("TEST1: 동시에 B, C유저가 A유저에게 코인을 양도하면 무사히 처리된다") void chargeAndTransferCoinConcurrencyTest() throws InterruptedException { + accountB.updateBalance(500L); + accountC.updateBalance(500L); + accountRepository.save(accountB); + accountRepository.save(accountC); + int threadCount = 2; ExecutorService executor = Executors.newFixedThreadPool(threadCount); CountDownLatch latch = new CountDownLatch(threadCount); executor.execute(() -> { try { - accountService.chargeCoin(userA.getId(), 200); + accountService.transferCoin(userC.getId(), userA.getId(), 500); } finally { latch.countDown(); } @@ -84,7 +71,7 @@ void chargeAndTransferCoinConcurrencyTest() throws InterruptedException { executor.execute(() -> { try { - accountService.transferCoin(userA.getId(), userB.getId(), 100); + accountService.transferCoin(userB.getId(), userA.getId(), 500); } finally { latch.countDown(); } @@ -93,34 +80,7 @@ void chargeAndTransferCoinConcurrencyTest() throws InterruptedException { latch.await(); Account accountA = accountRepository.findById(userA.getId()).orElseThrow(); - Account accountB = accountRepository.findById(userB.getId()).orElseThrow(); - - assertThat(accountA.getCoin() + accountB.getCoin()).isEqualTo(200); - } - - @Test - @DisplayName("TEST3: 동시에 코인 소비가 되면 차례대로 처리된다") - void coinConsumeConcurrencyTest() throws InterruptedException { - // 사전 충전 - accountService.chargeCoin(userA.getId(), 500); - - int threadCount = 5; - ExecutorService executor = Executors.newFixedThreadPool(threadCount); - CountDownLatch latch = new CountDownLatch(threadCount); - - for (int i = 0; i < threadCount; i++) { - executor.execute(() -> { - try { - accountService.consumeCoin(userA.getId(), 50); - } finally { - latch.countDown(); - } - }); - } - - latch.await(); - Account updatedAccount = accountRepository.findById(userA.getId()).orElseThrow(); - assertThat(updatedAccount.getCoin()).isEqualTo(500 - (50 * threadCount)); + assertThat(accountA.getPostedBalance()).isEqualTo(1000L); } } diff --git a/src/test/java/hanium/modic/backend/web/ai/controller/AiImagePermissionControllerIntegrationTest.java b/src/test/java/hanium/modic/backend/web/ai/controller/AiImagePermissionControllerIntegrationTest.java index 1f73ff43..d443bd91 100644 --- a/src/test/java/hanium/modic/backend/web/ai/controller/AiImagePermissionControllerIntegrationTest.java +++ b/src/test/java/hanium/modic/backend/web/ai/controller/AiImagePermissionControllerIntegrationTest.java @@ -23,6 +23,7 @@ import hanium.modic.backend.domain.transaction.entity.Account; import hanium.modic.backend.domain.transaction.repository.AccountRepository; import hanium.modic.backend.domain.user.entity.UserEntity; +import hanium.modic.backend.domain.user.factory.UserFactory; import hanium.modic.backend.domain.user.repository.UserEntityRepository; import hanium.modic.backend.web.ai.aiChat.dto.request.BuyAiImagePermissionRequest; @@ -66,7 +67,7 @@ private Account createAccountForUser(UserEntity user, Long initialCoin) { Account account = Account.builder() .userId(user.getId()) .build(); - account.addCoin(initialCoin); + account.updateBalance(initialCoin); return accountRepository.save(account); } @@ -77,7 +78,10 @@ void buyAiImagePermissionWithCoin_Success() throws Exception { // given UserEntity user = userRepository.findByEmail("test@test.com").orElseThrow(); Account account = createAccountForUser(user, 10000L); - PostEntity post = createTestPost(user); + + UserEntity user2 = userRepository.save(UserFactory.createMockUserWithoutId("sellerUser")); + Account account2 = createAccountForUser(user2, 0L); + PostEntity post = createTestPost(user2); BuyAiImagePermissionRequest request = new BuyAiImagePermissionRequest(post.getId()); @@ -101,7 +105,7 @@ void buyAiImagePermissionWithCoin_Success() throws Exception { UserEntity updatedUser = userRepository.findById(user.getId()).orElse(null); Account updatedAccount = accountRepository.findById(user.getId()).orElse(null); assertThat(updatedUser).isNotNull(); - assertThat(updatedAccount.getCoin()).isEqualTo(5000L); // 10000 - 5000 + assertThat(updatedAccount.getPostedBalance()).isEqualTo(5000L); // 10000 - 5000 } @Test @@ -111,7 +115,10 @@ void buyAiImagePermissionWithCoin_DuplicatePurchase_IncreasesGenerations() throw // given UserEntity user = userRepository.findByEmail("test@test.com").orElseThrow(); Account account = createAccountForUser(user, 10000L); - PostEntity post = createTestPost(user); + + UserEntity user2 = userRepository.save(UserFactory.createMockUserWithoutId("sellerUser")); + Account account2 = createAccountForUser(user2, 0L); + PostEntity post = createTestPost(user2); // 이미 구매한 권한 생성 AiChatRoomEntity existingPermission = AiChatRoomEntity.builder() @@ -147,7 +154,10 @@ void buyAiImagePermissionWithCoin_InsufficientCoin() throws Exception { // given UserEntity user = userRepository.findByEmail("test@test.com").orElseThrow(); Account account = createAccountForUser(user, 500L); - PostEntity post = createTestPost(user); + + UserEntity user2 = userRepository.save(UserFactory.createMockUserWithoutId("sellerUser")); + Account account2 = createAccountForUser(user2, 0L); + PostEntity post = createTestPost(user2); BuyAiImagePermissionRequest request = new BuyAiImagePermissionRequest(post.getId());