diff --git a/src/main/java/org/runimo/runimo/user/controller/EggController.java b/src/main/java/org/runimo/runimo/user/controller/EggController.java new file mode 100644 index 00000000..8fdadcfd --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/controller/EggController.java @@ -0,0 +1,71 @@ +package org.runimo.runimo.user.controller; + +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.common.response.SuccessResponse; +import org.runimo.runimo.user.controller.request.RegisterEggRequest; +import org.runimo.runimo.user.controller.request.UseLovePointRequest; +import org.runimo.runimo.user.service.dtos.*; +import org.runimo.runimo.user.enums.UserHttpResponseCode; +import org.runimo.runimo.user.service.usecases.eggs.IncubatingEggQueryUsecase; +import org.runimo.runimo.user.service.usecases.eggs.EggRegisterUsecase; +import org.runimo.runimo.user.service.usecases.eggs.GiveLovePointToEggUsecase; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; + +@Tag(name = "알 부화 API") +@RestController +@RequestMapping("/api/v1/users/eggs") +@RequiredArgsConstructor +public class EggController { + + private final EggRegisterUsecase eggRegisterUsecase; + private final GiveLovePointToEggUsecase giveLovePointToEggUsecase; + private final IncubatingEggQueryUsecase incubatingEggQueryUsecase; + + @PostMapping + public ResponseEntity> registerEgg( + @UserId Long userId, + @Valid @RequestBody RegisterEggRequest request + ) { + RegisterEggResponse registerEggResponse = eggRegisterUsecase.execute( + new RegisterEggCommand(userId, request.itemId()) + ); + return ResponseEntity.created(URI.create("/api/v1/users/eggs")).body( + SuccessResponse.of( + UserHttpResponseCode.REGISTER_EGG_SUCCESS, + registerEggResponse + )); + } + + @PatchMapping + public ResponseEntity> useLovePoint( + @UserId Long userId, + @Valid @RequestBody UseLovePointRequest request + ) { + UseLovePointResponse useLovePointResponse = giveLovePointToEggUsecase.execute( + new UseLovePointCommand(userId, request.itemId(), request.lovePointAmount()) + ); + return ResponseEntity.ok().body( + SuccessResponse.of( + UserHttpResponseCode.USE_LOVE_POINT_SUCCESS, + useLovePointResponse + )); + } + + @GetMapping + public ResponseEntity> getEgg( + @UserId Long userId + ) { + QueryIncubatingEggResponse response = incubatingEggQueryUsecase.execute(userId); + return ResponseEntity.ok().body( + SuccessResponse.of( + UserHttpResponseCode.MY_PAGE_DATA_FETCHED, + response + )); + } + +} diff --git a/src/main/java/org/runimo/runimo/user/controller/MainViewController.java b/src/main/java/org/runimo/runimo/user/controller/MainViewController.java index 4b39f215..ceb542a2 100644 --- a/src/main/java/org/runimo/runimo/user/controller/MainViewController.java +++ b/src/main/java/org/runimo/runimo/user/controller/MainViewController.java @@ -6,7 +6,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.runimo.runimo.common.response.SuccessResponse; -import org.runimo.runimo.user.service.usecases.MainViewQueryUsecase; +import org.runimo.runimo.user.service.usecases.query.MainViewQueryUsecase; import org.runimo.runimo.user.service.dtos.MainViewResponse; import org.runimo.runimo.user.enums.UserHttpResponseCode; import org.springframework.http.ResponseEntity; 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 23a8ab48..a3ad8ca2 100644 --- a/src/main/java/org/runimo/runimo/user/controller/UserController.java +++ b/src/main/java/org/runimo/runimo/user/controller/UserController.java @@ -13,7 +13,7 @@ import org.runimo.runimo.user.enums.UserHttpResponseCode; import org.runimo.runimo.user.service.dtos.AuthResponse; import org.runimo.runimo.user.service.dtos.SignupUserResponse; -import org.runimo.runimo.user.service.usecases.UserOAuthUsecase; +import org.runimo.runimo.user.service.usecases.auth.UserOAuthUsecase; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; diff --git a/src/main/java/org/runimo/runimo/user/controller/UserItemController.java b/src/main/java/org/runimo/runimo/user/controller/UserItemController.java index 56ae028e..15b425aa 100644 --- a/src/main/java/org/runimo/runimo/user/controller/UserItemController.java +++ b/src/main/java/org/runimo/runimo/user/controller/UserItemController.java @@ -12,8 +12,8 @@ import org.runimo.runimo.user.service.dtos.ItemQueryResponse; import org.runimo.runimo.user.service.dtos.UseItemCommand; import org.runimo.runimo.user.service.dtos.UseItemResponse; -import org.runimo.runimo.user.service.usecases.MyItemQueryUsecase; -import org.runimo.runimo.user.service.usecases.UseItemUsecase; +import org.runimo.runimo.user.service.usecases.query.MyItemQueryUsecase; +import org.runimo.runimo.user.service.usecases.items.UseItemUsecase; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; diff --git a/src/main/java/org/runimo/runimo/user/controller/request/RegisterEggRequest.java b/src/main/java/org/runimo/runimo/user/controller/request/RegisterEggRequest.java new file mode 100644 index 00000000..75fa5991 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/controller/request/RegisterEggRequest.java @@ -0,0 +1,6 @@ +package org.runimo.runimo.user.controller.request; + +public record RegisterEggRequest( + Long itemId +) { +} diff --git a/src/main/java/org/runimo/runimo/user/controller/request/UseLovePointRequest.java b/src/main/java/org/runimo/runimo/user/controller/request/UseLovePointRequest.java new file mode 100644 index 00000000..0a96c0ad --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/controller/request/UseLovePointRequest.java @@ -0,0 +1,7 @@ +package org.runimo.runimo.user.controller.request; + +public record UseLovePointRequest( + Long itemId, + Long lovePointAmount +) { +} diff --git a/src/main/java/org/runimo/runimo/user/domain/EggStatus.java b/src/main/java/org/runimo/runimo/user/domain/EggStatus.java new file mode 100644 index 00000000..66230142 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/domain/EggStatus.java @@ -0,0 +1,8 @@ +package org.runimo.runimo.user.domain; + +public enum EggStatus { + WAITING, // 부화 대기 + INCUBATING, // 부화 중 + INCUBATED, // 부화 완료 대기 + HATCHED // 부화 완료 +} diff --git a/src/main/java/org/runimo/runimo/user/domain/IncubatingEgg.java b/src/main/java/org/runimo/runimo/user/domain/IncubatingEgg.java new file mode 100644 index 00000000..46eb0f4b --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/domain/IncubatingEgg.java @@ -0,0 +1,90 @@ +package org.runimo.runimo.user.domain; + +import jakarta.persistence.*; +import java.util.Objects; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.runimo.runimo.common.BaseEntity; + +@Table(name = "incubating_eggs") +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class IncubatingEgg extends BaseEntity { + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "egg_id", nullable = false) + private Long eggId; + + @Column(name = "current_love_point_amount", nullable = false) + private Long currentLovePointAmount; + + @Column(name = "hatch_require_amount", nullable = false) + private Long hatchRequireAmount; + + @Column(name = "egg_status", nullable = false) + @Enumerated(EnumType.STRING) + private EggStatus status; + + @Builder + public IncubatingEgg(Long userId, Long eggId, Long currentLovePointAmount, Long hatchRequireAmount, EggStatus status) { + validateCreation(status, currentLovePointAmount, hatchRequireAmount); + this.userId = userId; + this.eggId = eggId; + this.currentLovePointAmount = currentLovePointAmount; + this.hatchRequireAmount = hatchRequireAmount; + this.status = status; + } + + public void startIncubation() { + validateStateTransition(EggStatus.WAITING, "부화 대기중인 알에만 부화 시작가능"); + this.status = EggStatus.INCUBATING; + } + + public void gainLovePoint(final Long amount) { + validateStateTransition(EggStatus.INCUBATING, "부화중인 알에만 애정 포인트 추가가능"); + validateAmount(amount); + if (currentLovePointAmount + amount > hatchRequireAmount) { + throw new IllegalArgumentException("애정 포인트가 부화에 필요한 양을 초과했습니다. 현재: " + currentLovePointAmount + ", 추가량: " + amount + ", 필요량: " + hatchRequireAmount); + } + this.currentLovePointAmount += amount; + if (Objects.equals(this.currentLovePointAmount, hatchRequireAmount)) { + this.status = EggStatus.INCUBATED; + } + } + + public void hatch() { + validateStateTransition(EggStatus.INCUBATED, "애정 포인트가 가득찬 알만 부화 가능"); + this.status = EggStatus.HATCHED; + } + + private void validateCreation(final EggStatus status, final Long currentLovePointAmount, final Long hatchRequireAmount) { + if(status == EggStatus.WAITING && currentLovePointAmount != 0) { + throw new IllegalArgumentException("부화 대기중인 알은 애정 포인트가 0이어야 합니다. 현재: " + currentLovePointAmount); + } + if(hatchRequireAmount <= 0) { + throw new IllegalArgumentException("부화에 필요한 애정 포인트는 0보다 커야 합니다. 현재: " + hatchRequireAmount); + } + } + + + private void validateStateTransition(final EggStatus expected, final String message) { + if (status != expected) { + throw new IllegalStateException(message + " 현재 상태: " + status); + } + } + + private void validateAmount(final Long amount) { + if (amount == null || amount <= 0) { + throw new IllegalArgumentException("애정 포인트는 0보다 커야 합니다. 현재: " + amount); + } + } + + public boolean isReadyToHatch() { + return this.status == EggStatus.INCUBATED && currentLovePointAmount.equals(hatchRequireAmount); + } +} 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 e3b6f374..c66f25f4 100644 --- a/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java +++ b/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java @@ -10,7 +10,8 @@ public enum UserHttpResponseCode implements CustomResponseCode { REFRESH_SUCCESS("USH2004", "토큰 재발급 성공", "토큰 재발급 성공"), USE_ITEM_SUCCESS("USH2005", "아이템 사용 성공", "아이템 사용 성공"), - ; + REGISTER_EGG_SUCCESS("USH2006", "부화기 등록 성공", "부화기 등록 성공"), + USE_LOVE_POINT_SUCCESS("USH2007","애정 사용 성공" , "애정 사용 성공"); private final String code; private final String clientMessage; diff --git a/src/main/java/org/runimo/runimo/user/repository/IncubatingEggRepository.java b/src/main/java/org/runimo/runimo/user/repository/IncubatingEggRepository.java new file mode 100644 index 00000000..714930fe --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/repository/IncubatingEggRepository.java @@ -0,0 +1,32 @@ +package org.runimo.runimo.user.repository; + +import jakarta.persistence.LockModeType; +import jakarta.persistence.QueryHint; +import org.runimo.runimo.user.domain.IncubatingEgg; +import org.runimo.runimo.user.service.dtos.IncubatingEggView; +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.List; +import java.util.Optional; + +@Repository +public interface IncubatingEggRepository extends JpaRepository { + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @QueryHints({@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000")}) + @Query("select ie from IncubatingEgg ie where ie.userId = :userId and ie.id = :eggId") + Optional findByUserIdAndEggIdForUpdate(Long userId, Long eggId); + + @Query("select ie from IncubatingEgg ie where ie.userId = :userId and (ie.status = 'INCUBATING' or ie.status = 'INCUBATED')") + List findAllByUserId(Long userId); + + @Query("select new org.runimo.runimo.user.service.dtos.IncubatingEggView(ie.id, e.name, e.imgUrl, ie.hatchRequireAmount, ie.currentLovePointAmount, ie.status) " + + "from IncubatingEgg ie " + + "join Egg e on e.id = ie.eggId " + + "where ie.userId = :userId and (ie.status = 'INCUBATING' or ie.status = 'INCUBATED')") + List findAllViewByUserId(Long userId); +} diff --git a/src/main/java/org/runimo/runimo/user/service/IncubatingEggFinder.java b/src/main/java/org/runimo/runimo/user/service/IncubatingEggFinder.java new file mode 100644 index 00000000..73252a49 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/IncubatingEggFinder.java @@ -0,0 +1,27 @@ +package org.runimo.runimo.user.service; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.user.domain.IncubatingEgg; +import org.runimo.runimo.user.repository.IncubatingEggRepository; +import org.runimo.runimo.user.service.dtos.IncubatingEggView; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class IncubatingEggFinder { + + private final IncubatingEggRepository incubatingEggRepository; + + @Transactional(readOnly = true) + public List findIncubatingEggsByUserId(Long userId) { + return incubatingEggRepository.findAllByUserId(userId); + } + + @Transactional(readOnly = true) + public List findIncubatingEggsViewByUserId(Long userId) { + return incubatingEggRepository.findAllViewByUserId(userId); + } +} diff --git a/src/main/java/org/runimo/runimo/user/service/IncubatingEggProcessor.java b/src/main/java/org/runimo/runimo/user/service/IncubatingEggProcessor.java new file mode 100644 index 00000000..e8f59f6d --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/IncubatingEggProcessor.java @@ -0,0 +1,38 @@ +package org.runimo.runimo.user.service; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.item.domain.Egg; +import org.runimo.runimo.user.domain.IncubatingEgg; +import org.runimo.runimo.user.repository.IncubatingEggRepository; +import org.runimo.runimo.user.service.dtos.UseLovePointCommand; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class IncubatingEggProcessor { + + private final IncubatingEggRepository incubatingEggRepository; + + @Transactional + public IncubatingEgg create(Long userId, Egg egg) { + IncubatingEgg incubatingEgg = IncubatingEgg.builder() + .userId(userId) + .eggId(egg.getId()) + .currentLovePointAmount(0L) + .hatchRequireAmount(egg.getHatchRequireAmount()) + .build(); + return incubatingEggRepository.save(incubatingEgg); + } + + @Transactional + public IncubatingEgg giveLovePoint(UseLovePointCommand useLovePointCommand) { + IncubatingEgg incubatingEgg = incubatingEggRepository.findByUserIdAndEggIdForUpdate( + useLovePointCommand.userId(), + useLovePointCommand.incubatingEggId()) + .orElseThrow(() -> new IllegalArgumentException("Incubating egg not found")); + incubatingEgg.gainLovePoint(useLovePointCommand.lovePoint()); + return incubatingEgg; + } + +} diff --git a/src/main/java/org/runimo/runimo/user/service/LovePointProcessor.java b/src/main/java/org/runimo/runimo/user/service/LovePointProcessor.java index ad89c98f..c49485d0 100644 --- a/src/main/java/org/runimo/runimo/user/service/LovePointProcessor.java +++ b/src/main/java/org/runimo/runimo/user/service/LovePointProcessor.java @@ -3,6 +3,7 @@ import org.runimo.runimo.user.domain.LovePoint; import org.runimo.runimo.user.repository.LovePointRepository; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; @Component public class LovePointProcessor { @@ -13,7 +14,11 @@ public LovePointProcessor(LovePointRepository lovePointRepository) { this.lovePointRepository = lovePointRepository; } - // 유저의 러브포인트를 업데이트한다. XLOCK을 걸어서 동시성 문제를 해결한다. + /** + * [Warning] : LovePoint를 수정할때는, 항상 마지막 순서로 업데이트한다. (DEADLOCK 방지) + * 유저의 러브포인트를 업데이트한다. XLOCK을 걸어서 동시성 문제를 해결한다. + * */ + @Transactional public LovePoint updateLovePoint(Long userId, Long loveAmount) { LovePoint lp = lovePointRepository.findByUserIdWithXLock(userId) .orElseThrow(IllegalStateException::new); diff --git a/src/main/java/org/runimo/runimo/user/service/UserItemProcessor.java b/src/main/java/org/runimo/runimo/user/service/UserItemProcessor.java index 01a42c51..fa08ae10 100644 --- a/src/main/java/org/runimo/runimo/user/service/UserItemProcessor.java +++ b/src/main/java/org/runimo/runimo/user/service/UserItemProcessor.java @@ -1,6 +1,9 @@ package org.runimo.runimo.user.service; import lombok.RequiredArgsConstructor; +import org.runimo.runimo.item.domain.ActivityType; +import org.runimo.runimo.item.service.ItemActivityCreator; +import org.runimo.runimo.item.service.dtos.CreateActivityCommand; import org.runimo.runimo.user.domain.UserItem; import org.runimo.runimo.user.repository.UserItemRepository; import org.springframework.stereotype.Component; @@ -12,12 +15,23 @@ public class UserItemProcessor { private final UserItemFinder userItemFinder; private final UserItemRepository userItemRepository; + private final ItemActivityCreator itemActivityCreator; @Transactional public void updateItemQuantity(Long userId, Long itemId, Long amount) { UserItem userItem = userItemFinder.findByUserIdAndItemIdWithXLock(userId, itemId) .orElseThrow(IllegalStateException::new); userItem.gainItem(amount); + itemActivityCreator.createItemActivity(new CreateActivityCommand(itemId, userId, amount, ActivityType.CONSUME)); + userItemRepository.save(userItem); + } + + @Transactional + public void useItem(Long userId, Long itemId, Long amount) { + UserItem userItem = userItemFinder.findByUserIdAndItemIdWithXLock(userId, itemId) + .orElseThrow(IllegalStateException::new); + userItem.useItem(amount); + itemActivityCreator.createItemActivity(new CreateActivityCommand(itemId, userId, amount, ActivityType.CONSUME)); userItemRepository.save(userItem); } } diff --git a/src/main/java/org/runimo/runimo/user/service/dtos/IncubatingEggView.java b/src/main/java/org/runimo/runimo/user/service/dtos/IncubatingEggView.java new file mode 100644 index 00000000..771c220c --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/dtos/IncubatingEggView.java @@ -0,0 +1,28 @@ +package org.runimo.runimo.user.service.dtos; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.runimo.runimo.user.domain.EggStatus; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class IncubatingEggView { + private Long id; + private String name; + private String imgUrl; + private Long hatchRequiredPointAmount; + private Long currentLovePointAmount; + private Boolean hatchable; + + @Builder + public IncubatingEggView(Long id, String name, String imgUrl, Long hatchRequiredPointAmount, Long currentLovePointAmount, EggStatus hatchable) { + this.id = id; + this.name = name; + this.imgUrl = imgUrl; + this.hatchRequiredPointAmount = hatchRequiredPointAmount; + this.currentLovePointAmount = currentLovePointAmount; + this.hatchable = (hatchable == EggStatus.INCUBATED); + } +} diff --git a/src/main/java/org/runimo/runimo/user/service/dtos/QueryIncubatingEggResponse.java b/src/main/java/org/runimo/runimo/user/service/dtos/QueryIncubatingEggResponse.java new file mode 100644 index 00000000..fc384a02 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/dtos/QueryIncubatingEggResponse.java @@ -0,0 +1,8 @@ +package org.runimo.runimo.user.service.dtos; + +import java.util.List; + +public record QueryIncubatingEggResponse( + List incubatingEggs +) { +} diff --git a/src/main/java/org/runimo/runimo/user/service/dtos/RegisterEggCommand.java b/src/main/java/org/runimo/runimo/user/service/dtos/RegisterEggCommand.java new file mode 100644 index 00000000..fc42b722 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/dtos/RegisterEggCommand.java @@ -0,0 +1,7 @@ +package org.runimo.runimo.user.service.dtos; + +public record RegisterEggCommand( + Long userId, + Long itemId +) { +} diff --git a/src/main/java/org/runimo/runimo/user/service/dtos/RegisterEggResponse.java b/src/main/java/org/runimo/runimo/user/service/dtos/RegisterEggResponse.java new file mode 100644 index 00000000..b7d433d2 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/dtos/RegisterEggResponse.java @@ -0,0 +1,8 @@ +package org.runimo.runimo.user.service.dtos; + +public record RegisterEggResponse( + Long incubatingEggId, + Long currentLovePointAmount, + Long requiredLovePointAmount +) { +} diff --git a/src/main/java/org/runimo/runimo/user/service/dtos/UseLovePointCommand.java b/src/main/java/org/runimo/runimo/user/service/dtos/UseLovePointCommand.java new file mode 100644 index 00000000..1a26bf63 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/dtos/UseLovePointCommand.java @@ -0,0 +1,4 @@ +package org.runimo.runimo.user.service.dtos; + +public record UseLovePointCommand(Long userId, Long incubatingEggId, Long lovePoint) { +} diff --git a/src/main/java/org/runimo/runimo/user/service/dtos/UseLovePointResponse.java b/src/main/java/org/runimo/runimo/user/service/dtos/UseLovePointResponse.java new file mode 100644 index 00000000..5e09b4e6 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/dtos/UseLovePointResponse.java @@ -0,0 +1,9 @@ +package org.runimo.runimo.user.service.dtos; + +public record UseLovePointResponse( + Long eggId, + Long currentLovePointAmount, + Long requiredLovePointAmount, + Boolean eggHatchable +) { +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/UserOAuthUsecase.java b/src/main/java/org/runimo/runimo/user/service/usecases/auth/UserOAuthUsecase.java similarity index 90% rename from src/main/java/org/runimo/runimo/user/service/usecases/UserOAuthUsecase.java rename to src/main/java/org/runimo/runimo/user/service/usecases/auth/UserOAuthUsecase.java index 96a42ad1..16520f59 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/UserOAuthUsecase.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/auth/UserOAuthUsecase.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.user.service.usecases; +package org.runimo.runimo.user.service.usecases.auth; import org.runimo.runimo.user.domain.SocialProvider; import org.runimo.runimo.user.service.dtos.AuthResponse; diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/UserOAuthUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/auth/UserOAuthUsecaseImpl.java similarity index 97% rename from src/main/java/org/runimo/runimo/user/service/usecases/UserOAuthUsecaseImpl.java rename to src/main/java/org/runimo/runimo/user/service/usecases/auth/UserOAuthUsecaseImpl.java index 15e22d8c..99e7b842 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/UserOAuthUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/auth/UserOAuthUsecaseImpl.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.user.service.usecases; +package org.runimo.runimo.user.service.usecases.auth; import com.auth0.jwt.JWT; import com.auth0.jwt.interfaces.DecodedJWT; diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/UserRegisterService.java b/src/main/java/org/runimo/runimo/user/service/usecases/auth/UserRegisterService.java similarity index 95% rename from src/main/java/org/runimo/runimo/user/service/usecases/UserRegisterService.java rename to src/main/java/org/runimo/runimo/user/service/usecases/auth/UserRegisterService.java index 375e6a2f..a31d31bf 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/UserRegisterService.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/auth/UserRegisterService.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.user.service.usecases; +package org.runimo.runimo.user.service.usecases.auth; import lombok.RequiredArgsConstructor; import org.runimo.runimo.rewards.service.eggs.EggGrantService; diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/eggs/EggRegisterUsecase.java b/src/main/java/org/runimo/runimo/user/service/usecases/eggs/EggRegisterUsecase.java new file mode 100644 index 00000000..74e77005 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/eggs/EggRegisterUsecase.java @@ -0,0 +1,8 @@ +package org.runimo.runimo.user.service.usecases.eggs; + +import org.runimo.runimo.user.service.dtos.RegisterEggResponse; +import org.runimo.runimo.user.service.dtos.RegisterEggCommand; + +public interface EggRegisterUsecase { + RegisterEggResponse execute(RegisterEggCommand command); +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/eggs/EggRegisterUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/eggs/EggRegisterUsecaseImpl.java new file mode 100644 index 00000000..530d692a --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/eggs/EggRegisterUsecaseImpl.java @@ -0,0 +1,33 @@ +package org.runimo.runimo.user.service.usecases.eggs; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.item.domain.Egg; +import org.runimo.runimo.user.domain.IncubatingEgg; +import org.runimo.runimo.item.service.ItemFinder; +import org.runimo.runimo.user.service.dtos.RegisterEggResponse; +import org.runimo.runimo.user.service.IncubatingEggProcessor; +import org.runimo.runimo.user.service.UserItemProcessor; +import org.runimo.runimo.user.service.dtos.RegisterEggCommand; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class EggRegisterUsecaseImpl implements EggRegisterUsecase { + + private final ItemFinder itemFinder; + private final IncubatingEggProcessor incubatingEggProcessor; + private final UserItemProcessor userItemProcessor; + + @Override + public RegisterEggResponse execute(RegisterEggCommand command) { + Egg egg = (Egg) itemFinder.findById(command.itemId()) + .orElseThrow(() -> new IllegalArgumentException("Egg not found")); + userItemProcessor.useItem(command.userId(), command.itemId(), 1L); + IncubatingEgg incubatingEgg = incubatingEggProcessor.create(command.userId(), egg); + return new RegisterEggResponse( + incubatingEgg.getId(), + incubatingEgg.getCurrentLovePointAmount(), + incubatingEgg.getHatchRequireAmount() + ); + } +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/eggs/GiveLovePointToEggUsecase.java b/src/main/java/org/runimo/runimo/user/service/usecases/eggs/GiveLovePointToEggUsecase.java new file mode 100644 index 00000000..c9e4df60 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/eggs/GiveLovePointToEggUsecase.java @@ -0,0 +1,8 @@ +package org.runimo.runimo.user.service.usecases.eggs; + +import org.runimo.runimo.user.service.dtos.UseLovePointResponse; +import org.runimo.runimo.user.service.dtos.UseLovePointCommand; + +public interface GiveLovePointToEggUsecase { + UseLovePointResponse execute(UseLovePointCommand useLovePointCommand); +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/eggs/GiveLovePointToEggUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/eggs/GiveLovePointToEggUsecaseImpl.java new file mode 100644 index 00000000..bef35765 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/eggs/GiveLovePointToEggUsecaseImpl.java @@ -0,0 +1,30 @@ +package org.runimo.runimo.user.service.usecases.eggs; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.user.domain.IncubatingEgg; +import org.runimo.runimo.user.service.dtos.UseLovePointResponse; +import org.runimo.runimo.user.service.IncubatingEggProcessor; +import org.runimo.runimo.user.service.LovePointProcessor; +import org.runimo.runimo.user.service.dtos.UseLovePointCommand; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class GiveLovePointToEggUsecaseImpl implements GiveLovePointToEggUsecase { + private final IncubatingEggProcessor incubatingEggProcessor; + private final LovePointProcessor lovePointProcessor; + + @Override + @Transactional + public UseLovePointResponse execute(UseLovePointCommand command) { + IncubatingEgg incubatingEgg = incubatingEggProcessor.giveLovePoint(command); + lovePointProcessor.updateLovePoint(command.userId(), command.lovePoint()); + return new UseLovePointResponse( + incubatingEgg.getId(), + incubatingEgg.getCurrentLovePointAmount(), + incubatingEgg.getHatchRequireAmount(), + incubatingEgg.isReadyToHatch() + ); + } +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/eggs/IncubatingEggQueryUsecase.java b/src/main/java/org/runimo/runimo/user/service/usecases/eggs/IncubatingEggQueryUsecase.java new file mode 100644 index 00000000..b7bfaa3e --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/eggs/IncubatingEggQueryUsecase.java @@ -0,0 +1,7 @@ +package org.runimo.runimo.user.service.usecases.eggs; + +import org.runimo.runimo.user.service.dtos.QueryIncubatingEggResponse; + +public interface IncubatingEggQueryUsecase { + QueryIncubatingEggResponse execute(Long userId); +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/eggs/IncubatingEggQueryUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/eggs/IncubatingEggQueryUsecaseImpl.java new file mode 100644 index 00000000..3f15ae7b --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/eggs/IncubatingEggQueryUsecaseImpl.java @@ -0,0 +1,22 @@ +package org.runimo.runimo.user.service.usecases.eggs; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.user.service.dtos.QueryIncubatingEggResponse; +import org.runimo.runimo.user.service.dtos.IncubatingEggView; +import org.runimo.runimo.user.service.IncubatingEggFinder; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class IncubatingEggQueryUsecaseImpl implements IncubatingEggQueryUsecase { + private final IncubatingEggFinder incubatingEggFinder; + + @Override + public QueryIncubatingEggResponse execute(Long userId) { + + List incubatingEggs = incubatingEggFinder.findIncubatingEggsViewByUserId(userId); + return new QueryIncubatingEggResponse(incubatingEggs); + } +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecase.java b/src/main/java/org/runimo/runimo/user/service/usecases/items/GainItemUsecase.java similarity index 79% rename from src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecase.java rename to src/main/java/org/runimo/runimo/user/service/usecases/items/GainItemUsecase.java index 09a060ce..067a3d94 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecase.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/items/GainItemUsecase.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.user.service.usecases; +package org.runimo.runimo.user.service.usecases.items; import org.runimo.runimo.user.service.dtos.GainItemCommand; import org.runimo.runimo.user.service.dtos.GainItemResponse; diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/items/GainItemUsecaseImpl.java similarity index 95% rename from src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseImpl.java rename to src/main/java/org/runimo/runimo/user/service/usecases/items/GainItemUsecaseImpl.java index f4f810e3..2a20d2bc 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/items/GainItemUsecaseImpl.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.user.service.usecases; +package org.runimo.runimo.user.service.usecases.items; import lombok.RequiredArgsConstructor; import org.runimo.runimo.user.domain.UserItem; diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecase.java b/src/main/java/org/runimo/runimo/user/service/usecases/items/UseItemUsecase.java similarity index 79% rename from src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecase.java rename to src/main/java/org/runimo/runimo/user/service/usecases/items/UseItemUsecase.java index f572b929..ed6f3819 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecase.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/items/UseItemUsecase.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.user.service.usecases; +package org.runimo.runimo.user.service.usecases.items; import org.runimo.runimo.user.service.dtos.UseItemCommand; import org.runimo.runimo.user.service.dtos.UseItemResponse; diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/items/UseItemUsecaseImpl.java similarity index 95% rename from src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseImpl.java rename to src/main/java/org/runimo/runimo/user/service/usecases/items/UseItemUsecaseImpl.java index c4913e88..13e0c051 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/UseItemUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/items/UseItemUsecaseImpl.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.user.service.usecases; +package org.runimo.runimo.user.service.usecases.items; import lombok.RequiredArgsConstructor; import org.runimo.runimo.item.service.ItemActivityCreator; diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/MainViewQueryUsecase.java b/src/main/java/org/runimo/runimo/user/service/usecases/query/MainViewQueryUsecase.java similarity index 72% rename from src/main/java/org/runimo/runimo/user/service/usecases/MainViewQueryUsecase.java rename to src/main/java/org/runimo/runimo/user/service/usecases/query/MainViewQueryUsecase.java index b4651678..7cc30ebf 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/MainViewQueryUsecase.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/query/MainViewQueryUsecase.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.user.service.usecases; +package org.runimo.runimo.user.service.usecases.query; import org.runimo.runimo.user.service.dtos.MainViewResponse; diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/MainViewQueryUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/query/MainViewQueryUsecaseImpl.java similarity index 96% rename from src/main/java/org/runimo/runimo/user/service/usecases/MainViewQueryUsecaseImpl.java rename to src/main/java/org/runimo/runimo/user/service/usecases/query/MainViewQueryUsecaseImpl.java index 2459ac56..6aaa4d09 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/MainViewQueryUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/query/MainViewQueryUsecaseImpl.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.user.service.usecases; +package org.runimo.runimo.user.service.usecases.query; import lombok.RequiredArgsConstructor; import org.runimo.runimo.records.service.RecordFinder; diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/MyItemQueryUsecase.java b/src/main/java/org/runimo/runimo/user/service/usecases/query/MyItemQueryUsecase.java similarity index 72% rename from src/main/java/org/runimo/runimo/user/service/usecases/MyItemQueryUsecase.java rename to src/main/java/org/runimo/runimo/user/service/usecases/query/MyItemQueryUsecase.java index 114b6644..e2d19142 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/MyItemQueryUsecase.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/query/MyItemQueryUsecase.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.user.service.usecases; +package org.runimo.runimo.user.service.usecases.query; import org.runimo.runimo.user.service.dtos.ItemQueryResponse; diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/MyItemQueryUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/query/MyItemQueryUsecaseImpl.java similarity index 92% rename from src/main/java/org/runimo/runimo/user/service/usecases/MyItemQueryUsecaseImpl.java rename to src/main/java/org/runimo/runimo/user/service/usecases/query/MyItemQueryUsecaseImpl.java index 0f04f400..ef4e7a47 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/MyItemQueryUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/query/MyItemQueryUsecaseImpl.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.user.service.usecases; +package org.runimo.runimo.user.service.usecases.query; import lombok.RequiredArgsConstructor; diff --git a/src/main/resources/sql/schema.sql b/src/main/resources/sql/schema.sql index af5a9d17..55dcefb9 100644 --- a/src/main/resources/sql/schema.sql +++ b/src/main/resources/sql/schema.sql @@ -11,6 +11,7 @@ DROP TABLE IF EXISTS runimo; DROP TABLE IF EXISTS items; DROP TABLE IF EXISTS users; DROP TABLE IF EXISTS user_love_point; +DROP TABLE IF EXISTS incubating_eggs; SET FOREIGN_KEY_CHECKS = 1; @@ -116,6 +117,20 @@ CREATE TABLE `user_item` `deleted_at` TIMESTAMP ); +CREATE TABLE `incubating_eggs` +( + `id` + BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` integer NOT NULL, + `egg_id` integer NOT NULL, + `current_love_point_amount` integer, + `hatch_require_amount` integer, + `egg_status` varchar(255), + `created_at` timestamp, + `updated_at` timestamp, + `deleted_at` TIMESTAMP +); + ALTER TABLE `user_token` ADD FOREIGN KEY (`user_id`) REFERENCES `users` (`id`); diff --git a/src/test/java/org/runimo/runimo/CleanUpUtil.java b/src/test/java/org/runimo/runimo/CleanUpUtil.java index ccaf9f7f..c2afccf0 100644 --- a/src/test/java/org/runimo/runimo/CleanUpUtil.java +++ b/src/test/java/org/runimo/runimo/CleanUpUtil.java @@ -12,7 +12,8 @@ public class CleanUpUtil { "oauth_accounts", "users", "running_records", - "user_love_point" + "user_love_point", + "incubating_eggs" }; @Autowired diff --git a/src/test/java/org/runimo/runimo/item/domain/IncubatingEggTest.java b/src/test/java/org/runimo/runimo/item/domain/IncubatingEggTest.java new file mode 100644 index 00000000..d1ea9efa --- /dev/null +++ b/src/test/java/org/runimo/runimo/item/domain/IncubatingEggTest.java @@ -0,0 +1,162 @@ +package org.runimo.runimo.item.domain; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.runimo.runimo.user.domain.EggStatus; +import org.runimo.runimo.user.domain.IncubatingEgg; + +class IncubatingEggTest { + + @Test + void startIncubation_성공() { + // given + IncubatingEgg incubatingEgg = IncubatingEgg.builder() + .userId(1L) + .eggId(100L) + .currentLovePointAmount(0L) + .hatchRequireAmount(10L) + .status(EggStatus.WAITING) + .build(); + + assertEquals(EggStatus.WAITING, incubatingEgg.getStatus()); + + // when + incubatingEgg.startIncubation(); + + // then + assertEquals(EggStatus.INCUBATING, incubatingEgg.getStatus()); + } + + @Test + void startIncubation_실패_이미_부화중() { + // given + IncubatingEgg incubatingEgg = IncubatingEgg.builder() + .userId(1L) + .eggId(100L) + .currentLovePointAmount(2L) + .hatchRequireAmount(10L) + .status(EggStatus.INCUBATING) + .build(); + + // when & then + assertThrows(IllegalStateException.class, incubatingEgg::startIncubation); + } + + @Test + void gainLovePoint_성공() { + // given + IncubatingEgg incubatingEgg = IncubatingEgg.builder() + .userId(1L) + .eggId(100L) + .currentLovePointAmount(0L) + .hatchRequireAmount(10L) + .status(EggStatus.INCUBATING) + .build(); + + // when + incubatingEgg.gainLovePoint(5L); + + // then + assertEquals(5L, incubatingEgg.getCurrentLovePointAmount()); + } + + @Test + void gainLovePoint_성공_부화조건_충족() { + // given + IncubatingEgg incubatingEgg = IncubatingEgg.builder() + .userId(1L) + .eggId(100L) + .currentLovePointAmount(0L) + .hatchRequireAmount(10L) + .status(EggStatus.INCUBATING) + .build(); + + // when + incubatingEgg.gainLovePoint(10L); + + // then + assertEquals(10L, incubatingEgg.getCurrentLovePointAmount()); + assertEquals(EggStatus.INCUBATED, incubatingEgg.getStatus()); + } + + @Test + void gainLovePoint_실패_이미_부화됨() { + // given + IncubatingEgg incubatingEgg = IncubatingEgg.builder() + .userId(1L) + .eggId(100L) + .currentLovePointAmount(10L) + .hatchRequireAmount(10L) + .status(EggStatus.INCUBATED) + .build(); + + // when & then + assertThrows(IllegalStateException.class, () -> incubatingEgg.gainLovePoint(1L)); + } + + @Test + void hatch_성공() { + // given + IncubatingEgg incubatingEgg = IncubatingEgg.builder() + .userId(1L) + .eggId(100L) + .currentLovePointAmount(10L) + .hatchRequireAmount(10L) + .status(EggStatus.INCUBATED) + .build(); + + // when + incubatingEgg.hatch(); + + // then + assertEquals(EggStatus.HATCHED, incubatingEgg.getStatus()); + } + + @Test + void hatch_실패_애정포인트_미달() { + // given + IncubatingEgg incubatingEgg = IncubatingEgg.builder() + .userId(1L) + .eggId(100L) + .currentLovePointAmount(5L) + .hatchRequireAmount(10L) + .status(EggStatus.INCUBATING) + .build(); + + // when & then + assertThrows(IllegalStateException.class, incubatingEgg::hatch); + } + + @Test + void hatch_실패_부화_대기중인_알() { + // given + IncubatingEgg incubatingEgg = IncubatingEgg.builder() + .userId(1L) + .eggId(100L) + .currentLovePointAmount(0L) + .hatchRequireAmount(10L) + .status(EggStatus.WAITING) + .build(); + + // when & then + assertThrows(IllegalStateException.class, incubatingEgg::hatch); + } + + @Test + void gainLovePoint_과다_지급() { + // given + IncubatingEgg incubatingEgg = IncubatingEgg.builder() + .userId(1L) + .eggId(100L) + .currentLovePointAmount(0L) + .hatchRequireAmount(10L) + .status(EggStatus.INCUBATING) + .build(); + + // when & then + assertThrows(IllegalArgumentException.class, () -> { + incubatingEgg.gainLovePoint(15L); + }); + } +} diff --git a/src/test/java/org/runimo/runimo/rewards/RewardTest.java b/src/test/java/org/runimo/runimo/rewards/RewardTest.java index 7c30f1e9..cbeeb47f 100644 --- a/src/test/java/org/runimo/runimo/rewards/RewardTest.java +++ b/src/test/java/org/runimo/runimo/rewards/RewardTest.java @@ -17,7 +17,7 @@ import org.runimo.runimo.user.domain.UserItem; import org.runimo.runimo.user.service.UserItemFinder; import org.runimo.runimo.user.service.dtos.UserSignupCommand; -import org.runimo.runimo.user.service.usecases.UserRegisterService; +import org.runimo.runimo.user.service.usecases.auth.UserRegisterService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; diff --git a/src/test/java/org/runimo/runimo/user/api/IncubatingEggAcceptanceTest.java b/src/test/java/org/runimo/runimo/user/api/IncubatingEggAcceptanceTest.java new file mode 100644 index 00000000..3ed5912d --- /dev/null +++ b/src/test/java/org/runimo/runimo/user/api/IncubatingEggAcceptanceTest.java @@ -0,0 +1,111 @@ +package org.runimo.runimo.user.api; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.runimo.runimo.CleanUpUtil; +import org.runimo.runimo.auth.jwt.JwtTokenFactory; +import org.runimo.runimo.user.controller.request.UseLovePointRequest; +import org.runimo.runimo.user.controller.request.RegisterEggRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +class IncubatingEggAcceptanceTest { + + @LocalServerPort + int port; + + @Autowired + private JwtTokenFactory jwtTokenFactory; + + @Autowired + private CleanUpUtil cleanUpUtil; + @Autowired + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + RestAssured.port = port; + } + + @AfterEach + void tearDown() { + cleanUpUtil.cleanUpUserInfos(); + } + + @Test + @Sql(scripts = "/sql/incubating_egg_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void 사용자의_부화중인_알_조회_성공() { + String token = "Bearer " + jwtTokenFactory.generateAccessToken("test-user-uuid-1"); + + given() + .header("Authorization", token) + .contentType(ContentType.JSON) + .when() + .get("/api/v1/users/eggs") + .then() + .log().all() + .statusCode(200) + .body("code", equalTo("USH2001")) + .body("payload.incubating_eggs.size()", greaterThan(0)) + .body("payload.incubating_eggs[0].name", equalTo("마당알")) + .body("payload.incubating_eggs[0].id", equalTo(1)) + .body("payload.incubating_eggs[0].hatch_required_point_amount", equalTo(100)) + .body("payload.incubating_eggs[0].current_love_point_amount", equalTo(50)) + .body("payload.incubating_eggs[0].hatchable", equalTo(false)); + } + + @Test + @Sql(scripts = "/sql/incubating_egg_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void 사용자의_알을_부화중으로_변경() throws JsonProcessingException { + String token = "Bearer " + jwtTokenFactory.generateAccessToken("test-user-uuid-1"); + RegisterEggRequest request = new RegisterEggRequest(1L); + + + given() + .header("Authorization", token) + .contentType(ContentType.JSON) + .body(objectMapper.writeValueAsString(request)) + .when() + .post("/api/v1/users/eggs") + .then() + .log().all() + .statusCode(HttpStatus.CREATED.value()) + .body("code", equalTo("USH2006")) + .body("payload.current_love_point_amount", equalTo(0)) + .body("payload.required_love_point_amount", equalTo(100)); + } + + @Test + @Sql(scripts = "/sql/incubating_egg_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void 부화중인_알에_애정을_부여() throws JsonProcessingException { + String token = "Bearer " + jwtTokenFactory.generateAccessToken("test-user-uuid-1"); + UseLovePointRequest request = new UseLovePointRequest(1L, 20L); + given() + .header("Authorization", token) + .contentType(ContentType.JSON) + .body(objectMapper.writeValueAsString(request)) + .when() + .patch("/api/v1/users/eggs") + .then() + .log().all() + .statusCode(200) + .body("code", equalTo("USH2007")) + .body("payload.current_love_point_amount", equalTo(70)) + .body("payload.required_love_point_amount", equalTo(100)) + .body("payload.egg_hatchable", equalTo(false)); + } +} diff --git a/src/test/java/org/runimo/runimo/user/api/MainViewControllerTest.java b/src/test/java/org/runimo/runimo/user/api/MainViewControllerTest.java index d5aac688..e2347a03 100644 --- a/src/test/java/org/runimo/runimo/user/api/MainViewControllerTest.java +++ b/src/test/java/org/runimo/runimo/user/api/MainViewControllerTest.java @@ -2,7 +2,7 @@ import org.junit.jupiter.api.Test; import org.runimo.runimo.auth.jwt.JwtTokenFactory; -import org.runimo.runimo.user.service.usecases.MainViewQueryUsecase; +import org.runimo.runimo.user.service.usecases.query.MainViewQueryUsecase; import org.runimo.runimo.user.service.dtos.MainViewResponse; import org.runimo.runimo.user.UserFixtures; import org.runimo.runimo.user.service.UserFinder; diff --git a/src/test/java/org/runimo/runimo/user/api/QueryItemControllerTest.java b/src/test/java/org/runimo/runimo/user/api/QueryItemControllerTest.java index e3fe2835..e1b5f0e6 100644 --- a/src/test/java/org/runimo/runimo/user/api/QueryItemControllerTest.java +++ b/src/test/java/org/runimo/runimo/user/api/QueryItemControllerTest.java @@ -5,7 +5,7 @@ import org.runimo.runimo.user.UserFixtures; import org.runimo.runimo.user.service.UserFinder; import org.runimo.runimo.user.service.dtos.ItemQueryResponse; -import org.runimo.runimo.user.service.usecases.MyItemQueryUsecase; +import org.runimo.runimo.user.service.usecases.query.MyItemQueryUsecase; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; 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 index 64dcb850..2511d6ca 100644 --- a/src/test/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseTest.java +++ b/src/test/java/org/runimo/runimo/user/service/usecases/GainItemUsecaseTest.java @@ -5,6 +5,7 @@ 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.runimo.runimo.user.service.usecases.items.GainItemUsecase; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; diff --git a/src/test/java/org/runimo/runimo/user/service/usecases/QueryUserItemUsecaseTest.java b/src/test/java/org/runimo/runimo/user/service/usecases/QueryUserItemUsecaseTest.java index fa7f1021..6e0992c3 100644 --- a/src/test/java/org/runimo/runimo/user/service/usecases/QueryUserItemUsecaseTest.java +++ b/src/test/java/org/runimo/runimo/user/service/usecases/QueryUserItemUsecaseTest.java @@ -6,6 +6,8 @@ import org.runimo.runimo.user.repository.MyItemRepository; import org.runimo.runimo.user.service.dtos.InventoryItem; import org.runimo.runimo.user.service.dtos.ItemQueryResponse; +import org.runimo.runimo.user.service.usecases.query.MyItemQueryUsecase; +import org.runimo.runimo.user.service.usecases.query.MyItemQueryUsecaseImpl; import java.util.List; 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 004c38c0..dbf821b9 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 @@ -6,6 +6,7 @@ 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.items.UseItemUsecase; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; diff --git a/src/test/java/org/runimo/runimo/user/service/usecases/UserRegisterServiceTest.java b/src/test/java/org/runimo/runimo/user/service/usecases/UserRegisterServiceTest.java index db8a8d7d..7e7df3da 100644 --- a/src/test/java/org/runimo/runimo/user/service/usecases/UserRegisterServiceTest.java +++ b/src/test/java/org/runimo/runimo/user/service/usecases/UserRegisterServiceTest.java @@ -1,49 +1,57 @@ package org.runimo.runimo.user.service.usecases; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.runimo.runimo.CleanUpUtil; -import org.runimo.runimo.item.repository.ItemRepository; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.runimo.runimo.rewards.service.eggs.EggGrantService; import org.runimo.runimo.user.domain.SocialProvider; import org.runimo.runimo.user.domain.User; -import org.runimo.runimo.user.domain.UserItem; -import org.runimo.runimo.user.repository.UserItemRepository; +import org.runimo.runimo.user.service.UserCreator; +import org.runimo.runimo.user.service.UserItemCreator; import org.runimo.runimo.user.service.dtos.UserSignupCommand; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.jdbc.Sql; +import org.runimo.runimo.user.service.usecases.auth.UserRegisterService; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; -@ActiveProfiles("test") -@SpringBootTest class UserRegisterServiceTest { - @Autowired - private UserRegisterService userRegisterService; + @Mock + private UserCreator userCreator; + + @Mock + private UserItemCreator userItemCreator; + + @Mock + private EggGrantService eggGrantService; - @Autowired - private UserItemRepository userItemRepository; - @Autowired - private ItemRepository itemRepository; - @Autowired - private CleanUpUtil cleanUpUtil; + @InjectMocks + private UserRegisterService userRegisterService; - @AfterEach - void tearDown() { - cleanUpUtil.cleanUpUserInfos(); + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); } @Test - @Sql(scripts = "/sql/egg_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) void 회원가입_알_지급_테스트() { // given UserSignupCommand command = new UserSignupCommand("test", SocialProvider.KAKAO, "1234"); + User mockUser = mock(User.class); + when(userCreator.createUser(any(UserSignupCommand.class))).thenReturn(mockUser); + + // when User createdUser = userRegisterService.register(command, "1234"); - UserItem ui = userItemRepository.findByUserIdAndItemId(createdUser.getId(), 1L).get(); - assertNotNull(ui); - assertEquals(1L, ui.getQuantity()); + + // then + assertNotNull(createdUser); + verify(userCreator, times(1)).createUser(command); + verify(userCreator, times(1)).createUserOAuthInfo(mockUser, SocialProvider.KAKAO, "1234"); + verify(userCreator, times(1)).createLovePoint(anyLong()); + verify(userItemCreator, times(1)).createAll(anyLong()); + verify(eggGrantService, times(1)).grantGreetingEggToUser(mockUser); } -} \ No newline at end of file +} diff --git a/src/test/resources/sql/incubating_egg_test_data.sql b/src/test/resources/sql/incubating_egg_test_data.sql new file mode 100644 index 00000000..e6c15aa8 --- /dev/null +++ b/src/test/resources/sql/incubating_egg_test_data.sql @@ -0,0 +1,25 @@ +SET FOREIGN_KEY_CHECKS = 0; +TRUNCATE TABLE users; +TRUNCATE TABLE items; +TRUNCATE TABLE incubating_eggs; +TRUNCATE TABLE user_item; +TRUNCATE TABLE user_love_point; +SET FOREIGN_KEY_CHECKS = 1; + + +INSERT INTO users (id, public_id, nickname, img_url, total_distance_in_meters, total_time_in_seconds, created_at, updated_at) +VALUES (1, 'test-user-uuid-1', 'Daniel', 'https://example.com/images/user1.png', 3000, 3600, NOW(), NOW()); + +INSERT INTO user_love_point (id, user_id, amount, created_at, updated_at) +VALUES (1, 1, 1000, NOW(), NOW()); + +-- 보유 아이템 +INSERT INTO user_item (id, user_id, item_id, quantity, created_at, updated_at) +VALUES (1001, 1, 1, 2, NOW(), NOW()), + (1002, 1, 2, 1, NOW(), NOW()); + +INSERT INTO items (id, name, item_code, description, item_type, img_url, dtype, egg_type, hatch_require_amount, created_at, updated_at) +VALUES (1, '마당알', 'A100', '기본 알', 'USABLE', 'https://example.com/images/egg.png', 'EGG', 'MADANG', 100, NOW(), NOW()); + +INSERT INTO incubating_eggs (id, user_id, egg_id, current_love_point_amount, hatch_require_amount, egg_status, created_at, updated_at) +VALUES (1, 1, 1, 50, 100, 'INCUBATING', NOW(), NOW()); diff --git a/src/test/resources/sql/schema.sql b/src/test/resources/sql/schema.sql index af5a9d17..3bf56a33 100644 --- a/src/test/resources/sql/schema.sql +++ b/src/test/resources/sql/schema.sql @@ -11,6 +11,7 @@ DROP TABLE IF EXISTS runimo; DROP TABLE IF EXISTS items; DROP TABLE IF EXISTS users; DROP TABLE IF EXISTS user_love_point; +DROP TABLE IF EXISTS incubating_eggs; SET FOREIGN_KEY_CHECKS = 1; @@ -116,6 +117,20 @@ CREATE TABLE `user_item` `deleted_at` TIMESTAMP ); +CREATE TABLE `incubating_eggs` +( + `id` + BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` integer NOT NULL, + `egg_id` integer NOT NULL, + `current_love_point_amount` integer, + `hatch_require_amount` integer, + `egg_status` varchar(255), + `created_at` timestamp, + `updated_at` timestamp, + `deleted_at` TIMESTAMP +); + ALTER TABLE `user_token` ADD FOREIGN KEY (`user_id`) REFERENCES `users` (`id`);