From e0329f08286d70ae11291f7e06781d708ae4395a Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Tue, 18 Mar 2025 23:17:05 +0900 Subject: [PATCH 01/15] =?UTF-8?q?:sparkles:=20feat=20:=20item=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=EB=8B=88,=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/runimo/runimo/item/domain/Item.java | 36 +++++++++++++++++++ .../runimo/runimo/item/domain/ItemType.java | 7 ++++ .../item/repository/ItemRepository.java | 9 +++++ .../runimo/item/service/ItemFinder.java | 23 ++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 src/main/java/org/runimo/runimo/item/domain/Item.java create mode 100644 src/main/java/org/runimo/runimo/item/domain/ItemType.java create mode 100644 src/main/java/org/runimo/runimo/item/repository/ItemRepository.java create mode 100644 src/main/java/org/runimo/runimo/item/service/ItemFinder.java diff --git a/src/main/java/org/runimo/runimo/item/domain/Item.java b/src/main/java/org/runimo/runimo/item/domain/Item.java new file mode 100644 index 00000000..df0443b9 --- /dev/null +++ b/src/main/java/org/runimo/runimo/item/domain/Item.java @@ -0,0 +1,36 @@ +package org.runimo.runimo.item.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.NaturalId; +import org.runimo.runimo.common.BaseEntity; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Item extends BaseEntity { + + @NaturalId + private String itemCode; + + private String name; + + private String description; + + private String imgUrl; + + @Enumerated(EnumType.STRING) + private ItemType itemType; + + @Builder + public Item(String itemCode, String name, String description, String imgUrl, ItemType itemType) { + this.itemCode = itemCode; + this.name = name; + this.description = description; + this.imgUrl = imgUrl; + this.itemType = itemType; + } +} diff --git a/src/main/java/org/runimo/runimo/item/domain/ItemType.java b/src/main/java/org/runimo/runimo/item/domain/ItemType.java new file mode 100644 index 00000000..e366cce1 --- /dev/null +++ b/src/main/java/org/runimo/runimo/item/domain/ItemType.java @@ -0,0 +1,7 @@ +package org.runimo.runimo.item.domain; + +public enum ItemType { + USABLE, + EQUIPMENT, + ETC +} diff --git a/src/main/java/org/runimo/runimo/item/repository/ItemRepository.java b/src/main/java/org/runimo/runimo/item/repository/ItemRepository.java new file mode 100644 index 00000000..85726d4c --- /dev/null +++ b/src/main/java/org/runimo/runimo/item/repository/ItemRepository.java @@ -0,0 +1,9 @@ +package org.runimo.runimo.item.repository; + +import org.runimo.runimo.item.domain.Item; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ItemRepository extends JpaRepository { +} diff --git a/src/main/java/org/runimo/runimo/item/service/ItemFinder.java b/src/main/java/org/runimo/runimo/item/service/ItemFinder.java new file mode 100644 index 00000000..74a95389 --- /dev/null +++ b/src/main/java/org/runimo/runimo/item/service/ItemFinder.java @@ -0,0 +1,23 @@ +package org.runimo.runimo.item.service; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.item.domain.Item; +import org.runimo.runimo.item.repository.ItemRepository; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class ItemFinder { + + private final ItemRepository itemRepository; + + public Optional findById(Long itemId) { + return itemRepository.findById(itemId); + } + + public Boolean isItemExist(Long itemId) { + return itemRepository.existsById(itemId); + } +} From 480e3cb489735344fe4afdf88ea734a0a8c45eef Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Tue, 18 Mar 2025 23:17:47 +0900 Subject: [PATCH 02/15] =?UTF-8?q?:sparkles:=20feat=20:=20ItemActivity=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1,=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runimo/item/domain/ActivityType.java | 8 +++++ .../runimo/item/domain/ItemActivity.java | 32 +++++++++++++++++++ .../repository/ItemActivityRepository.java | 9 ++++++ .../item/service/CreateActivityCommand.java | 11 +++++++ .../item/service/ItemActivityCreator.java | 5 +++ .../item/service/ItemActivityCreatorImpl.java | 26 +++++++++++++++ 6 files changed, 91 insertions(+) create mode 100644 src/main/java/org/runimo/runimo/item/domain/ActivityType.java create mode 100644 src/main/java/org/runimo/runimo/item/domain/ItemActivity.java create mode 100644 src/main/java/org/runimo/runimo/item/repository/ItemActivityRepository.java create mode 100644 src/main/java/org/runimo/runimo/item/service/CreateActivityCommand.java create mode 100644 src/main/java/org/runimo/runimo/item/service/ItemActivityCreator.java create mode 100644 src/main/java/org/runimo/runimo/item/service/ItemActivityCreatorImpl.java diff --git a/src/main/java/org/runimo/runimo/item/domain/ActivityType.java b/src/main/java/org/runimo/runimo/item/domain/ActivityType.java new file mode 100644 index 00000000..b966eca9 --- /dev/null +++ b/src/main/java/org/runimo/runimo/item/domain/ActivityType.java @@ -0,0 +1,8 @@ +package org.runimo.runimo.item.domain; + +public enum ActivityType { + GAIN, + CONSUME, + REFUND, + EXPIRE +} diff --git a/src/main/java/org/runimo/runimo/item/domain/ItemActivity.java b/src/main/java/org/runimo/runimo/item/domain/ItemActivity.java new file mode 100644 index 00000000..a618e2a4 --- /dev/null +++ b/src/main/java/org/runimo/runimo/item/domain/ItemActivity.java @@ -0,0 +1,32 @@ +package org.runimo.runimo.item.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.runimo.runimo.common.BaseEntity; + +// append only entity +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ItemActivity extends BaseEntity { + @Column(name = "activity_user_id", nullable = false) + private Long userId; + @Column(name = "activity_event_id", nullable = false) + private Long itemId; + + private Long quantity; + @Column(name = "activity_event_type", nullable = false) + private ActivityType type; + + @Builder + public ItemActivity(Long userId, Long itemId, Long quantity, ActivityType type) { + this.userId = userId; + this.itemId = itemId; + this.quantity = quantity; + this.type = type; + } +} diff --git a/src/main/java/org/runimo/runimo/item/repository/ItemActivityRepository.java b/src/main/java/org/runimo/runimo/item/repository/ItemActivityRepository.java new file mode 100644 index 00000000..622ff311 --- /dev/null +++ b/src/main/java/org/runimo/runimo/item/repository/ItemActivityRepository.java @@ -0,0 +1,9 @@ +package org.runimo.runimo.item.repository; + +import org.runimo.runimo.item.domain.ItemActivity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ItemActivityRepository extends JpaRepository { +} diff --git a/src/main/java/org/runimo/runimo/item/service/CreateActivityCommand.java b/src/main/java/org/runimo/runimo/item/service/CreateActivityCommand.java new file mode 100644 index 00000000..842a6c68 --- /dev/null +++ b/src/main/java/org/runimo/runimo/item/service/CreateActivityCommand.java @@ -0,0 +1,11 @@ +package org.runimo.runimo.item.service; + +import org.runimo.runimo.item.domain.ActivityType; + +public record CreateActivityCommand( + Long itemId, + Long userId, + Long quantity, + ActivityType activityType +) { +} diff --git a/src/main/java/org/runimo/runimo/item/service/ItemActivityCreator.java b/src/main/java/org/runimo/runimo/item/service/ItemActivityCreator.java new file mode 100644 index 00000000..9311bf46 --- /dev/null +++ b/src/main/java/org/runimo/runimo/item/service/ItemActivityCreator.java @@ -0,0 +1,5 @@ +package org.runimo.runimo.item.service; + +public interface ItemActivityCreator { + void createItemActivity(CreateActivityCommand command); +} diff --git a/src/main/java/org/runimo/runimo/item/service/ItemActivityCreatorImpl.java b/src/main/java/org/runimo/runimo/item/service/ItemActivityCreatorImpl.java new file mode 100644 index 00000000..af7ae535 --- /dev/null +++ b/src/main/java/org/runimo/runimo/item/service/ItemActivityCreatorImpl.java @@ -0,0 +1,26 @@ +package org.runimo.runimo.item.service; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.item.repository.ItemActivityRepository; +import org.runimo.runimo.item.domain.ItemActivity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ItemActivityCreatorImpl implements ItemActivityCreator { + + private final ItemActivityRepository itemActivityRepository; + + @Override + @Transactional + public void createItemActivity(CreateActivityCommand command) { + ItemActivity activity = ItemActivity.builder() + .userId(command.userId()) + .itemId(command.itemId()) + .quantity(command.quantity()) + .type(command.activityType()) + .build(); + itemActivityRepository.save(activity); + } +} From dc66bcb865719cf659b478cedb11b8d93f32ae74 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Tue, 18 Mar 2025 23:18:12 +0900 Subject: [PATCH 03/15] =?UTF-8?q?:sparkles:=20feat=20:=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 32 +++++++++++++-- .../runimo/runimo/user/controller/UserId.java | 3 ++ .../UserIdResolver.java | 4 +- .../controller/request/UseItemRequest.java | 12 ++++++ .../runimo/runimo/user/domain/UserItem.java | 41 +++++++++++++++++++ .../user/enums/UserHttpResponseCode.java | 2 + .../user/repository/UserItemRepository.java | 15 +++++++ .../user/service/UseItemUsecaseImpl.java | 30 ++++++++++++++ .../runimo/user/service/UserItemFinder.java | 21 ++++++++++ ...Service.java => UserOAuthUsecaseImpl.java} | 2 +- .../user/service/dtos/UseItemCommand.java | 15 +++++++ .../user/service/dtos/UseItemResponse.java | 7 ++++ .../user/service/usecases/UseItemUsecase.java | 9 ++++ 13 files changed, 186 insertions(+), 7 deletions(-) rename src/main/java/org/runimo/runimo/user/{service => controller}/UserIdResolver.java (94%) create mode 100644 src/main/java/org/runimo/runimo/user/controller/request/UseItemRequest.java create mode 100644 src/main/java/org/runimo/runimo/user/domain/UserItem.java create mode 100644 src/main/java/org/runimo/runimo/user/repository/UserItemRepository.java create mode 100644 src/main/java/org/runimo/runimo/user/service/UseItemUsecaseImpl.java create mode 100644 src/main/java/org/runimo/runimo/user/service/UserItemFinder.java rename src/main/java/org/runimo/runimo/user/service/{UserOAuthService.java => UserOAuthUsecaseImpl.java} (97%) create mode 100644 src/main/java/org/runimo/runimo/user/service/dtos/UseItemCommand.java create mode 100644 src/main/java/org/runimo/runimo/user/service/dtos/UseItemResponse.java create mode 100644 src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecase.java diff --git a/src/main/java/org/runimo/runimo/user/controller/UserController.java b/src/main/java/org/runimo/runimo/user/controller/UserController.java index 066d69e0..a3bb2850 100644 --- a/src/main/java/org/runimo/runimo/user/controller/UserController.java +++ b/src/main/java/org/runimo/runimo/user/controller/UserController.java @@ -11,11 +11,11 @@ import org.runimo.runimo.common.response.SuccessResponse; import org.runimo.runimo.user.controller.request.AuthLoginRequest; import org.runimo.runimo.user.controller.request.AuthSignupRequest; +import org.runimo.runimo.user.controller.request.UseItemRequest; import org.runimo.runimo.user.domain.SocialProvider; import org.runimo.runimo.user.enums.UserHttpResponseCode; -import org.runimo.runimo.user.service.dtos.AuthResponse; -import org.runimo.runimo.user.service.dtos.SignupUserInfo; -import org.runimo.runimo.user.service.dtos.TokenPair; +import org.runimo.runimo.user.service.dtos.*; +import org.runimo.runimo.user.service.usecases.UseItemUsecase; import org.runimo.runimo.user.service.usecases.UserOAuthUsecase; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -27,11 +27,12 @@ @Tag(name = "USER", description = "사용자 관련 API") @RestController -@RequestMapping("/api/v1/user") +@RequestMapping("/api/v1/users") @RequiredArgsConstructor public class UserController { private final UserOAuthUsecase userOAuthUsecase; + private final UseItemUsecase useItemUsecase; @Operation(summary = "사용자 로그인", description = "사용자가 OIDC 토큰을 사용하여 로그인합니다.") @ApiResponses(value = { @@ -75,4 +76,27 @@ public ResponseEntity> signupAndLogin( UserHttpResponseCode.SIGNUP_SUCCESS, new AuthResponse(authResult.tokenPair()))); } + + @Operation(summary = "아이템 사용", description = "사용자가 아이템을 사용합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "아이템 사용 성공", + content = @Content(schema = @Schema(implementation = UseItemResponse.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), + @ApiResponse(responseCode = "401", description = "인증 실패"), + @ApiResponse(responseCode = "404", description = "아이템 없음") + }) + @PostMapping("/me/items/use") + public ResponseEntity> useItem( + @UserId Long userId, + @Valid @RequestBody UseItemRequest request + ) { + UseItemResponse useItemResponse = useItemUsecase.useItem( + new UseItemCommand(userId, request.itemId(), request.quantity()) + ); + return ResponseEntity.ok().body( + SuccessResponse.of( + UserHttpResponseCode.USE_ITEM_SUCCESS, + useItemResponse + )); + } } diff --git a/src/main/java/org/runimo/runimo/user/controller/UserId.java b/src/main/java/org/runimo/runimo/user/controller/UserId.java index a1de34d0..219c98db 100644 --- a/src/main/java/org/runimo/runimo/user/controller/UserId.java +++ b/src/main/java/org/runimo/runimo/user/controller/UserId.java @@ -1,5 +1,7 @@ package org.runimo.runimo.user.controller; +import io.swagger.v3.oas.annotations.media.Schema; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -7,5 +9,6 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) +@Schema(description = "JWT 토큰 내 사용자 ID") public @interface UserId { } diff --git a/src/main/java/org/runimo/runimo/user/service/UserIdResolver.java b/src/main/java/org/runimo/runimo/user/controller/UserIdResolver.java similarity index 94% rename from src/main/java/org/runimo/runimo/user/service/UserIdResolver.java rename to src/main/java/org/runimo/runimo/user/controller/UserIdResolver.java index 5298e30d..580955a8 100644 --- a/src/main/java/org/runimo/runimo/user/service/UserIdResolver.java +++ b/src/main/java/org/runimo/runimo/user/controller/UserIdResolver.java @@ -1,8 +1,8 @@ -package org.runimo.runimo.user.service; +package org.runimo.runimo.user.controller; import lombok.RequiredArgsConstructor; -import org.runimo.runimo.user.controller.UserId; import org.runimo.runimo.user.domain.User; +import org.runimo.runimo.user.service.UserFinder; import org.springframework.core.MethodParameter; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; diff --git a/src/main/java/org/runimo/runimo/user/controller/request/UseItemRequest.java b/src/main/java/org/runimo/runimo/user/controller/request/UseItemRequest.java new file mode 100644 index 00000000..7e5bcc92 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/controller/request/UseItemRequest.java @@ -0,0 +1,12 @@ +package org.runimo.runimo.user.controller.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "아이템 사용 요청 DTO") +public record UseItemRequest( + @Schema(description = "아이템 ID", example = "1") + Long itemId, + @Schema(description = "수량", example = "1") + Long quantity +) { +} diff --git a/src/main/java/org/runimo/runimo/user/domain/UserItem.java b/src/main/java/org/runimo/runimo/user/domain/UserItem.java new file mode 100644 index 00000000..fcbb61e8 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/domain/UserItem.java @@ -0,0 +1,41 @@ +package org.runimo.runimo.user.domain; + +import jakarta.persistence.Entity; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.runimo.runimo.common.BaseEntity; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UserItem extends BaseEntity { + private Long userId; + private Long itemId; + private Long quantity; + + public UserItem(Long userId, Long itemId, Long quantity) { + this.userId = userId; + this.itemId = itemId; + this.quantity = quantity; + validateQuantity(quantity); + } + + public void useItem(Long quantity) { + updateQuantity(this.quantity - quantity); + } + + public void gainItem(Long quantity) { + updateQuantity(this.quantity + quantity); + } + + private void updateQuantity(Long quantity) { + validateQuantity(quantity); + this.quantity = quantity; + } + + + private void validateQuantity(Long quantity) { + if(quantity < 0) throw new IllegalArgumentException("quantity must be greater than zero"); + } +} diff --git a/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java b/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java index 2981ed23..e3b6f374 100644 --- a/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java +++ b/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java @@ -8,6 +8,8 @@ public enum UserHttpResponseCode implements CustomResponseCode { SIGNUP_SUCCESS("USH2002", "회원가입 성공", "회원가입 성공"), LOGIN_SUCCESS("USH2003", "로그인 성공", "로그인 성공"), REFRESH_SUCCESS("USH2004", "토큰 재발급 성공", "토큰 재발급 성공"), + + USE_ITEM_SUCCESS("USH2005", "아이템 사용 성공", "아이템 사용 성공"), ; private final String code; diff --git a/src/main/java/org/runimo/runimo/user/repository/UserItemRepository.java b/src/main/java/org/runimo/runimo/user/repository/UserItemRepository.java new file mode 100644 index 00000000..23e6fe38 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/repository/UserItemRepository.java @@ -0,0 +1,15 @@ +package org.runimo.runimo.user.repository; + +import org.runimo.runimo.user.domain.UserItem; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserItemRepository extends JpaRepository { + + @Query("select ui from UserItem ui where ui.userId = :userId and ui.itemId = :itemId") + Optional findByUserIdAndItemId(Long userId, Long itemId); +} diff --git a/src/main/java/org/runimo/runimo/user/service/UseItemUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/UseItemUsecaseImpl.java new file mode 100644 index 00000000..208ea1a9 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/UseItemUsecaseImpl.java @@ -0,0 +1,30 @@ +package org.runimo.runimo.user.service; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.item.service.ItemActivityCreator; +import org.runimo.runimo.user.domain.UserItem; +import org.runimo.runimo.user.service.dtos.UseItemCommand; +import org.runimo.runimo.user.service.dtos.UseItemResponse; +import org.runimo.runimo.user.service.usecases.UseItemUsecase; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.NoSuchElementException; + +@Service +@RequiredArgsConstructor +public class UseItemUsecaseImpl implements UseItemUsecase { + + private final UserItemFinder userItemFinder; + private final ItemActivityCreator itemActivityCreator; + + @Override + @Transactional + public UseItemResponse useItem(UseItemCommand command) { + UserItem userItem = userItemFinder.findByUserIdAndItemId(command.userId(), command.itemId()) + .orElseThrow(NoSuchElementException::new); + userItem.useItem(command.quantity()); + itemActivityCreator.createItemActivity(UseItemCommand.toItemUseActivityCommand(command)); + return new UseItemResponse(userItem.getId(), userItem.getQuantity()); + } +} diff --git a/src/main/java/org/runimo/runimo/user/service/UserItemFinder.java b/src/main/java/org/runimo/runimo/user/service/UserItemFinder.java new file mode 100644 index 00000000..c73287fe --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/UserItemFinder.java @@ -0,0 +1,21 @@ +package org.runimo.runimo.user.service; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.user.domain.UserItem; +import org.runimo.runimo.user.repository.UserItemRepository; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class UserItemFinder { + + private final UserItemRepository userItemRepository; + + @Transactional(readOnly = true) + public Optional findByUserIdAndItemId(Long userId, Long itemId) { + return userItemRepository.findByUserIdAndItemId(userId,itemId); + } +} diff --git a/src/main/java/org/runimo/runimo/user/service/UserOAuthService.java b/src/main/java/org/runimo/runimo/user/service/UserOAuthUsecaseImpl.java similarity index 97% rename from src/main/java/org/runimo/runimo/user/service/UserOAuthService.java rename to src/main/java/org/runimo/runimo/user/service/UserOAuthUsecaseImpl.java index 85e06105..e1d8b35d 100644 --- a/src/main/java/org/runimo/runimo/user/service/UserOAuthService.java +++ b/src/main/java/org/runimo/runimo/user/service/UserOAuthUsecaseImpl.java @@ -22,7 +22,7 @@ @Service @RequiredArgsConstructor -public class UserOAuthService implements UserOAuthUsecase { +public class UserOAuthUsecaseImpl implements UserOAuthUsecase { private final JwtTokenFactory jwtfactory; private final OidcService oidcService; private final OidcNonceService oidcNonceService; diff --git a/src/main/java/org/runimo/runimo/user/service/dtos/UseItemCommand.java b/src/main/java/org/runimo/runimo/user/service/dtos/UseItemCommand.java new file mode 100644 index 00000000..70006f04 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/dtos/UseItemCommand.java @@ -0,0 +1,15 @@ +package org.runimo.runimo.user.service.dtos; + +import org.runimo.runimo.item.domain.ActivityType; +import org.runimo.runimo.item.service.CreateActivityCommand; + +public record UseItemCommand( + Long userId, + Long itemId, + Long quantity +) { + + public static CreateActivityCommand toItemUseActivityCommand(UseItemCommand command) { + return new CreateActivityCommand(command.itemId(), command.userId(), command.quantity(), ActivityType.CONSUME); + } +} diff --git a/src/main/java/org/runimo/runimo/user/service/dtos/UseItemResponse.java b/src/main/java/org/runimo/runimo/user/service/dtos/UseItemResponse.java new file mode 100644 index 00000000..6061ccbf --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/dtos/UseItemResponse.java @@ -0,0 +1,7 @@ +package org.runimo.runimo.user.service.dtos; + +public record UseItemResponse( + Long itemId, + Long quantity +) { +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecase.java b/src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecase.java new file mode 100644 index 00000000..f572b929 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecase.java @@ -0,0 +1,9 @@ +package org.runimo.runimo.user.service.usecases; + +import org.runimo.runimo.user.service.dtos.UseItemCommand; +import org.runimo.runimo.user.service.dtos.UseItemResponse; + +public interface UseItemUsecase { + UseItemResponse useItem(UseItemCommand command); + +} From 4b434f1618cee158353d43fc5c61cd1f984b1d31 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 19 Mar 2025 10:03:28 +0900 Subject: [PATCH 04/15] =?UTF-8?q?:white=5Fcheck=5Fmark:=20test=20:=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runimo/user/domain/UserItemTest.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/test/java/org/runimo/runimo/user/domain/UserItemTest.java diff --git a/src/test/java/org/runimo/runimo/user/domain/UserItemTest.java b/src/test/java/org/runimo/runimo/user/domain/UserItemTest.java new file mode 100644 index 00000000..81b1c336 --- /dev/null +++ b/src/test/java/org/runimo/runimo/user/domain/UserItemTest.java @@ -0,0 +1,59 @@ +package org.runimo.runimo.user.domain; + + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +class UserItemTest { + + @Test + void 아이템_사용_수량감소_테스트() { + //given + UserItem userItem = UserItemFixtures.getUserItemWithQuantity(10L); + //when + userItem.useItem(10L); + //then + assertEquals(0L, userItem.getQuantity()); + } + + @Test + void 아이템_사용_보유_개수_초과사용시_에러_테스트() { + //given + UserItem userItem = UserItemFixtures.getUserItemWithQuantity(10L); + + //when + assertThrows(IllegalArgumentException.class, ()-> userItem.useItem(20L)); + assertEquals(10L, userItem.getQuantity()); + } + + @Test + void 아이템_획득_수량증가_테스트() { + //given + UserItem userItem = UserItemFixtures.getUserItemWithQuantity(10L); + //when + userItem.gainItem(10L); + //then + assertEquals(20L, userItem.getQuantity()); + } + + + + +} + +class UserItemFixtures { + + private static final Long DEFAULT_USER_ID = 1L; + private static final Long DEFAULT_ITEM_ID = 1L; + + public static UserItem getUserItemWithQuantity(Long quantity) { + return UserItem.builder() + .userId(DEFAULT_USER_ID) + .itemId(DEFAULT_ITEM_ID) + .quantity(quantity) + .build(); + } +} \ No newline at end of file From 8a71fe92fee086ec3dbb85009138324ea9353108 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 19 Mar 2025 11:18:49 +0900 Subject: [PATCH 05/15] =?UTF-8?q?:sparkles:=20feat=20:=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=8B=9C=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=ED=85=9C=20=EB=B3=B4=EC=9C=A0=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=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 --- .../runimo/user/service/UserCreator.java | 37 +++++++++++++++++++ .../runimo/user/service/UserItemCreator.java | 36 ++++++++++++++++++ .../{ => usecases}/UserOAuthUsecaseImpl.java | 25 +++++-------- 3 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/runimo/runimo/user/service/UserCreator.java create mode 100644 src/main/java/org/runimo/runimo/user/service/UserItemCreator.java rename src/main/java/org/runimo/runimo/user/service/{ => usecases}/UserOAuthUsecaseImpl.java (77%) diff --git a/src/main/java/org/runimo/runimo/user/service/UserCreator.java b/src/main/java/org/runimo/runimo/user/service/UserCreator.java new file mode 100644 index 00000000..9b8d9740 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/UserCreator.java @@ -0,0 +1,37 @@ +package org.runimo.runimo.user.service; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.user.domain.OAuthInfo; +import org.runimo.runimo.user.domain.SocialProvider; +import org.runimo.runimo.user.domain.User; +import org.runimo.runimo.user.repository.OAuthInfoRepository; +import org.runimo.runimo.user.repository.UserRepository; +import org.runimo.runimo.user.service.dtos.UserSignupCommand; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class UserCreator { + private final UserRepository userRepository; + private final OAuthInfoRepository oAuthInfoRepository; + + @Transactional + public User createUser(UserSignupCommand command) { + User user = User.builder() + .nickname(command.nickname()) + .imgUrl(command.imgUrl()) + .build(); + return userRepository.saveAndFlush(user); + } + + @Transactional + public OAuthInfo createUserOAuthInfo(User user, SocialProvider provider, String providerId) { + OAuthInfo oAuthInfo = OAuthInfo.builder() + .user(user) + .provider(provider) + .providerId(providerId) + .build(); + return oAuthInfoRepository.save(oAuthInfo); + } +} diff --git a/src/main/java/org/runimo/runimo/user/service/UserItemCreator.java b/src/main/java/org/runimo/runimo/user/service/UserItemCreator.java new file mode 100644 index 00000000..8204c53d --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/UserItemCreator.java @@ -0,0 +1,36 @@ +package org.runimo.runimo.user.service; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.item.repository.ItemRepository; +import org.runimo.runimo.user.domain.UserItem; +import org.runimo.runimo.user.repository.UserItemRepository; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class UserItemCreator { + + private final UserItemRepository userItemRepository; + private final ItemRepository itemRepository; + + @Transactional + public UserItem create(UserItem userItem) { + return userItemRepository.save(userItem); + } + + /* + * 아이템ID, 유저ID의 모든 순서쌍을 저장한다. + * 회원가입 시 실행된다. + * */ + @Transactional + public void createAll(Long userId) { + List itemIds = itemRepository.findAllItemIds(); + userItemRepository.saveAll( + itemIds.stream() + .map(itemId -> new UserItem(userId, itemId, 0L)) + .toList()); + } +} diff --git a/src/main/java/org/runimo/runimo/user/service/UserOAuthUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/UserOAuthUsecaseImpl.java similarity index 77% rename from src/main/java/org/runimo/runimo/user/service/UserOAuthUsecaseImpl.java rename to src/main/java/org/runimo/runimo/user/service/usecases/UserOAuthUsecaseImpl.java index e1d8b35d..748f50a6 100644 --- a/src/main/java/org/runimo/runimo/user/service/UserOAuthUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/UserOAuthUsecaseImpl.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.user.service; +package org.runimo.runimo.user.service.usecases; import com.auth0.jwt.JWT; import com.auth0.jwt.interfaces.DecodedJWT; @@ -11,11 +11,11 @@ import org.runimo.runimo.user.domain.SocialProvider; import org.runimo.runimo.user.domain.User; import org.runimo.runimo.user.repository.OAuthInfoRepository; -import org.runimo.runimo.user.repository.UserRepository; +import org.runimo.runimo.user.service.UserCreator; +import org.runimo.runimo.user.service.UserItemCreator; import org.runimo.runimo.user.service.dtos.SignupUserInfo; import org.runimo.runimo.user.service.dtos.TokenPair; import org.runimo.runimo.user.service.dtos.UserSignupCommand; -import org.runimo.runimo.user.service.usecases.UserOAuthUsecase; import org.springframework.stereotype.Service; import java.util.NoSuchElementException; @@ -26,8 +26,9 @@ public class UserOAuthUsecaseImpl implements UserOAuthUsecase { private final JwtTokenFactory jwtfactory; private final OidcService oidcService; private final OidcNonceService oidcNonceService; + private final UserItemCreator userItemCreator; private final OAuthInfoRepository oAuthInfoRepository; - private final UserRepository userRepository; + private final UserCreator userCreator; @Override @Transactional @@ -50,17 +51,9 @@ public SignupUserInfo validateAndSignup(final UserSignupCommand command, final S throw new IllegalArgumentException(); }); - User user = User.builder() - .nickname(command.nickname()) - .imgUrl(command.imgUrl()) - .build(); - userRepository.saveAndFlush(user); - OAuthInfo oAuthInfo = new OAuthInfo( - user, - command.provider(), - pid - ); - oAuthInfoRepository.save(oAuthInfo); - return new SignupUserInfo(user.getId(), jwtfactory.generateTokenPair(user)); + User savedUser = userCreator.createUser(command); + userCreator.createUserOAuthInfo(savedUser, provider, pid); + userItemCreator.createAll(savedUser.getId()); + return new SignupUserInfo(savedUser.getId(), jwtfactory.generateTokenPair(savedUser)); } } From c056625684d934d359be68252fb500b99834cb8e Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 19 Mar 2025 11:22:59 +0900 Subject: [PATCH 06/15] =?UTF-8?q?:sparkles:=20feat=20:=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=ED=85=9C=20=ED=9A=8D=EB=93=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/service/dtos/GainItemCommand.java | 8 +++++ .../user/service/dtos/GainItemResponse.java | 7 +++++ .../service/usecases/GainItemUsecase.java | 8 +++++ .../service/usecases/GainItemUsecaseImpl.java | 31 +++++++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 src/main/java/org/runimo/runimo/user/service/dtos/GainItemCommand.java create mode 100644 src/main/java/org/runimo/runimo/user/service/dtos/GainItemResponse.java create mode 100644 src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecase.java create mode 100644 src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseImpl.java diff --git a/src/main/java/org/runimo/runimo/user/service/dtos/GainItemCommand.java b/src/main/java/org/runimo/runimo/user/service/dtos/GainItemCommand.java new file mode 100644 index 00000000..e65b3af6 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/dtos/GainItemCommand.java @@ -0,0 +1,8 @@ +package org.runimo.runimo.user.service.dtos; + +public record GainItemCommand( + Long userId, + Long itemId, + Long quantity +) { +} diff --git a/src/main/java/org/runimo/runimo/user/service/dtos/GainItemResponse.java b/src/main/java/org/runimo/runimo/user/service/dtos/GainItemResponse.java new file mode 100644 index 00000000..0da2c76f --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/dtos/GainItemResponse.java @@ -0,0 +1,7 @@ +package org.runimo.runimo.user.service.dtos; + +public record GainItemResponse( + Long itemId, + Long quantity +) { +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecase.java b/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecase.java new file mode 100644 index 00000000..09a060ce --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecase.java @@ -0,0 +1,8 @@ +package org.runimo.runimo.user.service.usecases; + +import org.runimo.runimo.user.service.dtos.GainItemCommand; +import org.runimo.runimo.user.service.dtos.GainItemResponse; + +public interface GainItemUsecase { + GainItemResponse gainItem(GainItemCommand command); +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseImpl.java new file mode 100644 index 00000000..a3a263a7 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseImpl.java @@ -0,0 +1,31 @@ +package org.runimo.runimo.user.service.usecases; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.user.domain.UserItem; +import org.runimo.runimo.user.repository.UserItemRepository; +import org.runimo.runimo.user.service.UserItemFinder; +import org.runimo.runimo.user.service.dtos.GainItemCommand; +import org.runimo.runimo.user.service.dtos.GainItemResponse; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class GainItemUsecaseImpl implements GainItemUsecase { + + private final UserItemFinder userItemFinder; + private final UserItemRepository userItemRepository; + + @Override + @Transactional + public GainItemResponse gainItem(GainItemCommand command) { + UserItem userItem = userItemFinder.findByUserIdAndItemId(command.userId(), command.itemId()) + .orElse(new UserItem(command.userId(), command.itemId(), 0L)); + userItem.gainItem(command.quantity()); + userItemRepository.save(userItem); + return new GainItemResponse( + userItem.getItemId(), + userItem.getQuantity() + ); + } +} From aa33a0ade0f7a48d5058486950177025f9fa59b0 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 19 Mar 2025 11:23:25 +0900 Subject: [PATCH 07/15] =?UTF-8?q?:bug:=20fix=20:=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=ED=85=9C=20=EC=82=AC=EC=9A=A9=20=EC=9C=A0=EC=A6=88=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EB=B0=98=ED=99=98=EA=B0=92=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/service/{ => usecases}/UseItemUsecaseImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename src/main/java/org/runimo/runimo/user/service/{ => usecases}/UseItemUsecaseImpl.java (84%) diff --git a/src/main/java/org/runimo/runimo/user/service/UseItemUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseImpl.java similarity index 84% rename from src/main/java/org/runimo/runimo/user/service/UseItemUsecaseImpl.java rename to src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseImpl.java index 208ea1a9..a36d3221 100644 --- a/src/main/java/org/runimo/runimo/user/service/UseItemUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseImpl.java @@ -1,11 +1,11 @@ -package org.runimo.runimo.user.service; +package org.runimo.runimo.user.service.usecases; import lombok.RequiredArgsConstructor; import org.runimo.runimo.item.service.ItemActivityCreator; import org.runimo.runimo.user.domain.UserItem; +import org.runimo.runimo.user.service.UserItemFinder; import org.runimo.runimo.user.service.dtos.UseItemCommand; import org.runimo.runimo.user.service.dtos.UseItemResponse; -import org.runimo.runimo.user.service.usecases.UseItemUsecase; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,6 +25,6 @@ public UseItemResponse useItem(UseItemCommand command) { .orElseThrow(NoSuchElementException::new); userItem.useItem(command.quantity()); itemActivityCreator.createItemActivity(UseItemCommand.toItemUseActivityCommand(command)); - return new UseItemResponse(userItem.getId(), userItem.getQuantity()); + return new UseItemResponse(userItem.getItemId(), userItem.getQuantity()); } } From e72863cb1619d1c17a0a339193e1278102858524 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 19 Mar 2025 11:24:04 +0900 Subject: [PATCH 08/15] =?UTF-8?q?:recycle:=20refactor=20:=20=EB=94=94?= =?UTF-8?q?=EB=A0=89=ED=86=A0=EB=A6=AC=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/runimo/runimo/item/service/ItemActivityCreator.java | 2 ++ .../org/runimo/runimo/item/service/ItemActivityCreatorImpl.java | 1 + .../runimo/item/service/{ => dtos}/CreateActivityCommand.java | 2 +- .../org/runimo/runimo/user/service/dtos/UseItemCommand.java | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) rename src/main/java/org/runimo/runimo/item/service/{ => dtos}/CreateActivityCommand.java (79%) diff --git a/src/main/java/org/runimo/runimo/item/service/ItemActivityCreator.java b/src/main/java/org/runimo/runimo/item/service/ItemActivityCreator.java index 9311bf46..284668d2 100644 --- a/src/main/java/org/runimo/runimo/item/service/ItemActivityCreator.java +++ b/src/main/java/org/runimo/runimo/item/service/ItemActivityCreator.java @@ -1,5 +1,7 @@ package org.runimo.runimo.item.service; +import org.runimo.runimo.item.service.dtos.CreateActivityCommand; + public interface ItemActivityCreator { void createItemActivity(CreateActivityCommand command); } diff --git a/src/main/java/org/runimo/runimo/item/service/ItemActivityCreatorImpl.java b/src/main/java/org/runimo/runimo/item/service/ItemActivityCreatorImpl.java index af7ae535..e8d68079 100644 --- a/src/main/java/org/runimo/runimo/item/service/ItemActivityCreatorImpl.java +++ b/src/main/java/org/runimo/runimo/item/service/ItemActivityCreatorImpl.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.runimo.runimo.item.repository.ItemActivityRepository; import org.runimo.runimo.item.domain.ItemActivity; +import org.runimo.runimo.item.service.dtos.CreateActivityCommand; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/org/runimo/runimo/item/service/CreateActivityCommand.java b/src/main/java/org/runimo/runimo/item/service/dtos/CreateActivityCommand.java similarity index 79% rename from src/main/java/org/runimo/runimo/item/service/CreateActivityCommand.java rename to src/main/java/org/runimo/runimo/item/service/dtos/CreateActivityCommand.java index 842a6c68..1ddb4ee7 100644 --- a/src/main/java/org/runimo/runimo/item/service/CreateActivityCommand.java +++ b/src/main/java/org/runimo/runimo/item/service/dtos/CreateActivityCommand.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.item.service; +package org.runimo.runimo.item.service.dtos; import org.runimo.runimo.item.domain.ActivityType; diff --git a/src/main/java/org/runimo/runimo/user/service/dtos/UseItemCommand.java b/src/main/java/org/runimo/runimo/user/service/dtos/UseItemCommand.java index 70006f04..18c425a2 100644 --- a/src/main/java/org/runimo/runimo/user/service/dtos/UseItemCommand.java +++ b/src/main/java/org/runimo/runimo/user/service/dtos/UseItemCommand.java @@ -1,7 +1,7 @@ package org.runimo.runimo.user.service.dtos; import org.runimo.runimo.item.domain.ActivityType; -import org.runimo.runimo.item.service.CreateActivityCommand; +import org.runimo.runimo.item.service.dtos.CreateActivityCommand; public record UseItemCommand( Long userId, From 6a3846e1a1fa9ec414b131d16aca6d0411374e7a Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 19 Mar 2025 11:24:40 +0900 Subject: [PATCH 09/15] =?UTF-8?q?:sparkles:=20feat=20:=20=EB=AA=A8?= =?UTF-8?q?=EB=93=A0=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/runimo/runimo/item/repository/ItemRepository.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/runimo/runimo/item/repository/ItemRepository.java b/src/main/java/org/runimo/runimo/item/repository/ItemRepository.java index 85726d4c..f60e5497 100644 --- a/src/main/java/org/runimo/runimo/item/repository/ItemRepository.java +++ b/src/main/java/org/runimo/runimo/item/repository/ItemRepository.java @@ -2,8 +2,14 @@ import org.runimo.runimo.item.domain.Item; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface ItemRepository extends JpaRepository { + + @Query("select i.id from Item i") + List findAllItemIds(); } From 68e198f03eef8c9c2c5c23ff08a58ce0d3ee8ed7 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 19 Mar 2025 11:28:08 +0900 Subject: [PATCH 10/15] =?UTF-8?q?:white=5Fcheck=5Fmark:=20test=20:=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runimo/runimo/user/domain/UserItem.java | 3 +- .../runimo/runimo/user/UserItemFixtures.java | 17 +++ .../runimo/user/domain/UserItemTest.java | 18 +-- .../service/usecases/UseItemUsecaseTest.java | 48 ++++++++ src/test/resources/application-test.yml | 105 ++++++++++++++++++ 5 files changed, 173 insertions(+), 18 deletions(-) create mode 100644 src/test/java/org/runimo/runimo/user/UserItemFixtures.java create mode 100644 src/test/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseTest.java create mode 100644 src/test/resources/application-test.yml diff --git a/src/main/java/org/runimo/runimo/user/domain/UserItem.java b/src/main/java/org/runimo/runimo/user/domain/UserItem.java index fcbb61e8..9d77275f 100644 --- a/src/main/java/org/runimo/runimo/user/domain/UserItem.java +++ b/src/main/java/org/runimo/runimo/user/domain/UserItem.java @@ -2,6 +2,7 @@ import jakarta.persistence.Entity; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.runimo.runimo.common.BaseEntity; @@ -14,6 +15,7 @@ public class UserItem extends BaseEntity { private Long itemId; private Long quantity; + @Builder public UserItem(Long userId, Long itemId, Long quantity) { this.userId = userId; this.itemId = itemId; @@ -34,7 +36,6 @@ private void updateQuantity(Long quantity) { this.quantity = quantity; } - private void validateQuantity(Long quantity) { if(quantity < 0) throw new IllegalArgumentException("quantity must be greater than zero"); } diff --git a/src/test/java/org/runimo/runimo/user/UserItemFixtures.java b/src/test/java/org/runimo/runimo/user/UserItemFixtures.java new file mode 100644 index 00000000..fea460b0 --- /dev/null +++ b/src/test/java/org/runimo/runimo/user/UserItemFixtures.java @@ -0,0 +1,17 @@ +package org.runimo.runimo.user; + +import org.runimo.runimo.user.domain.UserItem; + +public class UserItemFixtures { + + private static final Long DEFAULT_USER_ID = 1L; + private static final Long DEFAULT_ITEM_ID = 1L; + + public static UserItem getUserItemWithQuantity(Long quantity) { + return UserItem.builder() + .userId(DEFAULT_USER_ID) + .itemId(DEFAULT_ITEM_ID) + .quantity(quantity) + .build(); + } +} diff --git a/src/test/java/org/runimo/runimo/user/domain/UserItemTest.java b/src/test/java/org/runimo/runimo/user/domain/UserItemTest.java index 81b1c336..77a94999 100644 --- a/src/test/java/org/runimo/runimo/user/domain/UserItemTest.java +++ b/src/test/java/org/runimo/runimo/user/domain/UserItemTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Test; +import org.runimo.runimo.user.UserItemFixtures; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -38,22 +39,5 @@ class UserItemTest { //then assertEquals(20L, userItem.getQuantity()); } - - - - } -class UserItemFixtures { - - private static final Long DEFAULT_USER_ID = 1L; - private static final Long DEFAULT_ITEM_ID = 1L; - - public static UserItem getUserItemWithQuantity(Long quantity) { - return UserItem.builder() - .userId(DEFAULT_USER_ID) - .itemId(DEFAULT_ITEM_ID) - .quantity(quantity) - .build(); - } -} \ No newline at end of file diff --git a/src/test/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseTest.java b/src/test/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseTest.java new file mode 100644 index 00000000..70b3687b --- /dev/null +++ b/src/test/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseTest.java @@ -0,0 +1,48 @@ +package org.runimo.runimo.user.service.usecases; + +import org.junit.jupiter.api.Test; +import org.runimo.runimo.item.service.ItemActivityCreator; +import org.runimo.runimo.user.UserItemFixtures; +import org.runimo.runimo.user.service.UserItemFinder; +import org.runimo.runimo.user.service.dtos.UseItemCommand; +import org.runimo.runimo.user.service.dtos.UseItemResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Profile; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@Profile("test") +@SpringBootTest +class UseItemUsecaseTest { + + @MockitoBean + private UserItemFinder userItemFinder; + + @MockitoBean + private ItemActivityCreator itemActivityCreator; + + @Autowired + private UseItemUsecase useItemUsecase; + + @Test + void 아이템_사용_유즈케이스_테스트() { + //given + UseItemCommand command = new UseItemCommand(1L, 1L, 10L); + when(userItemFinder.findByUserIdAndItemId(any(), any())) + .thenReturn(Optional.ofNullable(UserItemFixtures.getUserItemWithQuantity(10L))); + //when + UseItemResponse res = useItemUsecase.useItem(command); + + //then + verify(itemActivityCreator, times(1)).createItemActivity(any()); + assertNotNull(res); + assertEquals(command.itemId(), res.itemId()); + assertEquals(0, res.quantity()); + } +} \ No newline at end of file diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 00000000..84e0cba4 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,105 @@ +server: + port: 8080 + tomcat: + threads: + max: 200 + accept-count: 100 + max-connections: 8912 + max-keep-alive-requests: 200 +spring: + config: + import: + - optional:file:${ENV_PATH:.}/.env.${SPRING_PROFILES_ACTIVE:dev}[.properties] + profiles: + active: ${SPRING_PROFILES_ACTIVE:dev} + datasource: + driver-class-name: ${DB_DRIVER_CLASS_NAME} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + jpa: + hibernate: + ddl-auto: ${JPA_DDL_AUTO:update} + properties: + hibernate: + dialect: ${JPA_DIALECT} + open-in-view: false + jackson: + property-naming-strategy: SNAKE_CASE + security: + oauth2: + client: + registration: + kakao: + client-id: ${KAKAO_CLIENT_ID} + client-name: kakao + redirectUri: ${KAKAO_REDIRECT_URI} + authorization-grant-type: authorization_code + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v1/oidc/userinfo +springdoc: + swagger-ui: + path: /swagger-ui.html +jwt: + secret: ${JWT_SECRET} + expiration: ${JWT_EXPIRATION:3600000} + refresh: + expiration: ${JWT_REFRESH_EXPIRATION:86400000} + +logging: + level: + org: + hibernate: + type: + descriptor: + sql: debug + +--- +spring: + config: + activate: + on-profile: dev + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME} + jpa: + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect + show-sql: true +jwt: + expiration: 300000 + refresh: + expiration: 3600000 + +--- +spring: + config: + activate: + on-profile: prod + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:${DB_TYPE}://${DB_HOST}:${DB_PORT}/${DB_NAME} + jpa: + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect + sql: + init: + mode: never + +jwt: + expiration: 3600000 + refresh: + expiration: 604800000 + +--- +spring: + config: + activate: + on-profile: test + sql: + init: + mode: never \ No newline at end of file From 543d61ad7b7fc6f356b50992dea7c17dd0a426d9 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 19 Mar 2025 11:28:28 +0900 Subject: [PATCH 11/15] =?UTF-8?q?:bug:=20fix=20:=20=EB=B3=B4=EC=9C=A0=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=ED=85=9C=20=EC=88=9C=EC=84=9C=EC=8C=8D?= =?UTF-8?q?=EC=9D=B4=20=EC=97=86=EC=9C=BC=EB=A9=B4=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runimo/user/service/usecases/GainItemUsecaseImpl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseImpl.java index a3a263a7..3d16dd4c 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseImpl.java @@ -9,6 +9,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.NoSuchElementException; + @Service @RequiredArgsConstructor public class GainItemUsecaseImpl implements GainItemUsecase { @@ -20,7 +22,7 @@ public class GainItemUsecaseImpl implements GainItemUsecase { @Transactional public GainItemResponse gainItem(GainItemCommand command) { UserItem userItem = userItemFinder.findByUserIdAndItemId(command.userId(), command.itemId()) - .orElse(new UserItem(command.userId(), command.itemId(), 0L)); + .orElseThrow(NoSuchElementException::new); userItem.gainItem(command.quantity()); userItemRepository.save(userItem); return new GainItemResponse( From ec1a03b3eb4dcb668f2766dd7669842c235e54e7 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 19 Mar 2025 13:32:16 +0900 Subject: [PATCH 12/15] =?UTF-8?q?:sparkles:=20feat=20:=20=EB=B3=B4?= =?UTF-8?q?=EC=9C=A0=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=97=90=20X=EB=9D=BD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runimo/user/repository/UserItemRepository.java | 10 +++++++++- .../org/runimo/runimo/user/service/UserItemFinder.java | 4 ++++ .../user/service/usecases/GainItemUsecaseImpl.java | 2 +- .../user/service/usecases/UseItemUsecaseImpl.java | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/runimo/runimo/user/repository/UserItemRepository.java b/src/main/java/org/runimo/runimo/user/repository/UserItemRepository.java index 23e6fe38..90ea7483 100644 --- a/src/main/java/org/runimo/runimo/user/repository/UserItemRepository.java +++ b/src/main/java/org/runimo/runimo/user/repository/UserItemRepository.java @@ -1,15 +1,23 @@ package org.runimo.runimo.user.repository; +import jakarta.persistence.LockModeType; +import jakarta.persistence.QueryHint; import org.runimo.runimo.user.domain.UserItem; 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.jpa.repository.QueryHints; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository public interface UserItemRepository extends JpaRepository { - @Query("select ui from UserItem ui where ui.userId = :userId and ui.itemId = :itemId") Optional findByUserIdAndItemId(Long userId, Long itemId); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @QueryHints({@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000")}) + @Query("select ui from UserItem ui where ui.userId = :userId and ui.itemId = :itemId") + Optional findByUserIdAndItemIdForUpdate(Long userId, Long itemId); } diff --git a/src/main/java/org/runimo/runimo/user/service/UserItemFinder.java b/src/main/java/org/runimo/runimo/user/service/UserItemFinder.java index c73287fe..bf4b753d 100644 --- a/src/main/java/org/runimo/runimo/user/service/UserItemFinder.java +++ b/src/main/java/org/runimo/runimo/user/service/UserItemFinder.java @@ -18,4 +18,8 @@ public class UserItemFinder { public Optional findByUserIdAndItemId(Long userId, Long itemId) { return userItemRepository.findByUserIdAndItemId(userId,itemId); } + + public Optional findByUserIdAndItemIdWithXLock(Long userId, Long itemId) { + return userItemRepository.findByUserIdAndItemIdForUpdate(userId,itemId); + } } diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseImpl.java index 3d16dd4c..ccd6a653 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseImpl.java @@ -21,7 +21,7 @@ public class GainItemUsecaseImpl implements GainItemUsecase { @Override @Transactional public GainItemResponse gainItem(GainItemCommand command) { - UserItem userItem = userItemFinder.findByUserIdAndItemId(command.userId(), command.itemId()) + UserItem userItem = userItemFinder.findByUserIdAndItemIdWithXLock(command.userId(), command.itemId()) .orElseThrow(NoSuchElementException::new); userItem.gainItem(command.quantity()); userItemRepository.save(userItem); diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseImpl.java index a36d3221..c4913e88 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseImpl.java @@ -21,7 +21,7 @@ public class UseItemUsecaseImpl implements UseItemUsecase { @Override @Transactional public UseItemResponse useItem(UseItemCommand command) { - UserItem userItem = userItemFinder.findByUserIdAndItemId(command.userId(), command.itemId()) + UserItem userItem = userItemFinder.findByUserIdAndItemIdWithXLock(command.userId(), command.itemId()) .orElseThrow(NoSuchElementException::new); userItem.useItem(command.quantity()); itemActivityCreator.createItemActivity(UseItemCommand.toItemUseActivityCommand(command)); From ee1e08f15536e807075e8670133ddb1edf895b7d Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 19 Mar 2025 13:33:32 +0900 Subject: [PATCH 13/15] =?UTF-8?q?:white=5Fcheck=5Fmark:=20test=20:=20?= =?UTF-8?q?=EB=B3=B4=EC=9C=A0=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=20=EB=8F=99=EC=8B=9C=EC=84=B1=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/usecases/GainItemUsecaseTest.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/test/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseTest.java diff --git a/src/test/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseTest.java b/src/test/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseTest.java new file mode 100644 index 00000000..64dcb850 --- /dev/null +++ b/src/test/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseTest.java @@ -0,0 +1,49 @@ +package org.runimo.runimo.user.service.usecases; + +import org.junit.jupiter.api.Test; +import org.runimo.runimo.user.domain.UserItem; +import org.runimo.runimo.user.repository.UserItemRepository; +import org.runimo.runimo.user.service.dtos.GainItemCommand; +import org.runimo.runimo.user.service.dtos.GainItemResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.util.concurrent.CountDownLatch; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@ActiveProfiles("test") +@SpringBootTest +class GainItemUsecaseTest { + + @Autowired + UserItemRepository userItemRepository; + @Autowired + private GainItemUsecase gainItemUsecase; + + @Test + void 아이템_획득_유즈케이스_동시성_테스트() throws InterruptedException { + userItemRepository.saveAndFlush(new UserItem(1L, 1L, 0L)); + GainItemCommand command = new GainItemCommand(1L, 1L, 10L); + int threadCount = 10; + CountDownLatch latch = new CountDownLatch(threadCount); + Runnable task = () -> { + try { + GainItemResponse response = gainItemUsecase.gainItem(command); + assertNotNull(response); + } finally { + latch.countDown(); + } + }; + for (int i = 0; i < threadCount; i++) { + new Thread(task).start(); + } + + latch.await(); + + UserItem ui = userItemRepository.findByUserIdAndItemId(1L, 1L).get(); + assertEquals(100L, ui.getQuantity()); + } +} From 820f79753bfd6229fb439b7d4e4227baff85a89a Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 19 Mar 2025 13:34:08 +0900 Subject: [PATCH 14/15] =?UTF-8?q?:white=5Fcheck=5Fmark:=20test=20:=20profi?= =?UTF-8?q?le=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/usecases/UseItemUsecaseTest.java | 4 +- src/test/resources/application-test.yml | 59 ++----------------- 2 files changed, 7 insertions(+), 56 deletions(-) diff --git a/src/test/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseTest.java b/src/test/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseTest.java index 70b3687b..22604de1 100644 --- a/src/test/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseTest.java +++ b/src/test/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseTest.java @@ -8,7 +8,7 @@ import org.runimo.runimo.user.service.dtos.UseItemResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Profile; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; import java.util.Optional; @@ -17,7 +17,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -@Profile("test") +@ActiveProfiles("test") @SpringBootTest class UseItemUsecaseTest { diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 84e0cba4..3264ff41 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -9,11 +9,10 @@ server: spring: config: import: - - optional:file:${ENV_PATH:.}/.env.${SPRING_PROFILES_ACTIVE:dev}[.properties] - profiles: - active: ${SPRING_PROFILES_ACTIVE:dev} + - optional:file:${ENV_PATH:.}/.env.${SPRING_PROFILES_ACTIVE:test}[.properties] datasource: - driver-class-name: ${DB_DRIVER_CLASS_NAME} + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE username: ${DB_USERNAME} password: ${DB_PASSWORD} jpa: @@ -21,7 +20,7 @@ spring: ddl-auto: ${JPA_DDL_AUTO:update} properties: hibernate: - dialect: ${JPA_DIALECT} + dialect: org.hibernate.dialect.H2Dialect open-in-view: false jackson: property-naming-strategy: SNAKE_CASE @@ -54,52 +53,4 @@ logging: hibernate: type: descriptor: - sql: debug - ---- -spring: - config: - activate: - on-profile: dev - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME} - jpa: - properties: - hibernate: - dialect: org.hibernate.dialect.MySQLDialect - show-sql: true -jwt: - expiration: 300000 - refresh: - expiration: 3600000 - ---- -spring: - config: - activate: - on-profile: prod - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:${DB_TYPE}://${DB_HOST}:${DB_PORT}/${DB_NAME} - jpa: - properties: - hibernate: - dialect: org.hibernate.dialect.MySQLDialect - sql: - init: - mode: never - -jwt: - expiration: 3600000 - refresh: - expiration: 604800000 - ---- -spring: - config: - activate: - on-profile: test - sql: - init: - mode: never \ No newline at end of file + sql: debug \ No newline at end of file From bc2c8f14cf66dda03ad0ec91ea05b29f7e3edd54 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 19 Mar 2025 13:45:53 +0900 Subject: [PATCH 15/15] =?UTF-8?q?:white=5Fcheck=5Fmark:=20test=20:=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runimo/runimo/user/service/usecases/UseItemUsecaseTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseTest.java b/src/test/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseTest.java index 22604de1..790712ac 100644 --- a/src/test/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseTest.java +++ b/src/test/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseTest.java @@ -34,7 +34,7 @@ class UseItemUsecaseTest { void 아이템_사용_유즈케이스_테스트() { //given UseItemCommand command = new UseItemCommand(1L, 1L, 10L); - when(userItemFinder.findByUserIdAndItemId(any(), any())) + when(userItemFinder.findByUserIdAndItemIdWithXLock(any(), any())) .thenReturn(Optional.ofNullable(UserItemFixtures.getUserItemWithQuantity(10L))); //when UseItemResponse res = useItemUsecase.useItem(command);