Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/main/java/org/runimo/runimo/item/domain/ActivityType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.runimo.runimo.item.domain;

public enum ActivityType {
GAIN,
CONSUME,
REFUND,
EXPIRE
}
36 changes: 36 additions & 0 deletions src/main/java/org/runimo/runimo/item/domain/Item.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
32 changes: 32 additions & 0 deletions src/main/java/org/runimo/runimo/item/domain/ItemActivity.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
7 changes: 7 additions & 0 deletions src/main/java/org/runimo/runimo/item/domain/ItemType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.runimo.runimo.item.domain;

public enum ItemType {
USABLE,
EQUIPMENT,
ETC
}
Original file line number Diff line number Diff line change
@@ -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<ItemActivity, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.runimo.runimo.item.repository;

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<Item, Long> {

@Query("select i.id from Item i")
List<Long> findAllItemIds();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.runimo.runimo.item.service;

import org.runimo.runimo.item.service.dtos.CreateActivityCommand;

public interface ItemActivityCreator {
void createItemActivity(CreateActivityCommand command);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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.runimo.runimo.item.service.dtos.CreateActivityCommand;
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);
}
}
23 changes: 23 additions & 0 deletions src/main/java/org/runimo/runimo/item/service/ItemFinder.java
Original file line number Diff line number Diff line change
@@ -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<Item> findById(Long itemId) {
return itemRepository.findById(itemId);
}

public Boolean isItemExist(Long itemId) {
return itemRepository.existsById(itemId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.runimo.runimo.item.service.dtos;

import org.runimo.runimo.item.domain.ActivityType;

public record CreateActivityCommand(
Long itemId,
Long userId,
Long quantity,
ActivityType activityType
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = {
Expand Down Expand Up @@ -75,4 +76,27 @@ public ResponseEntity<SuccessResponse<AuthResponse>> 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<SuccessResponse<UseItemResponse>> 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
));
}
}
3 changes: 3 additions & 0 deletions src/main/java/org/runimo/runimo/user/controller/UserId.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
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;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Schema(description = "JWT 토큰 내 사용자 ID")
public @interface UserId {
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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
) {
}
42 changes: 42 additions & 0 deletions src/main/java/org/runimo/runimo/user/domain/UserItem.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.runimo.runimo.user.domain;

import jakarta.persistence.Entity;
import lombok.AccessLevel;
import lombok.Builder;
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;

@Builder
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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +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<UserItem, Long> {
@Query("select ui from UserItem ui where ui.userId = :userId and ui.itemId = :itemId")
Optional<UserItem> 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<UserItem> findByUserIdAndItemIdForUpdate(Long userId, Long itemId);
}
37 changes: 37 additions & 0 deletions src/main/java/org/runimo/runimo/user/service/UserCreator.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading