diff --git a/server/src/main/java/com/soopgyeol/api/common/exception/InsufficientBalanceException.java b/server/src/main/java/com/soopgyeol/api/common/exception/InsufficientBalanceException.java new file mode 100644 index 0000000..c255b86 --- /dev/null +++ b/server/src/main/java/com/soopgyeol/api/common/exception/InsufficientBalanceException.java @@ -0,0 +1,7 @@ +package com.soopgyeol.api.common.exception; + +public class InsufficientBalanceException extends RuntimeException { + public InsufficientBalanceException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/soopgyeol/api/common/exception/ItemAlreadyOwnedException.java b/server/src/main/java/com/soopgyeol/api/common/exception/ItemAlreadyOwnedException.java new file mode 100644 index 0000000..b76ee63 --- /dev/null +++ b/server/src/main/java/com/soopgyeol/api/common/exception/ItemAlreadyOwnedException.java @@ -0,0 +1,8 @@ +package com.soopgyeol.api.common.exception; + +public class ItemAlreadyOwnedException extends RuntimeException { + public ItemAlreadyOwnedException(String message) { + super(message); + } +} + diff --git a/server/src/main/java/com/soopgyeol/api/controller/AuthController.java b/server/src/main/java/com/soopgyeol/api/controller/AuthController.java index 82b8569..f92ac6b 100644 --- a/server/src/main/java/com/soopgyeol/api/controller/AuthController.java +++ b/server/src/main/java/com/soopgyeol/api/controller/AuthController.java @@ -83,7 +83,7 @@ public void kakaoAutoLogin(@RequestParam String code, HttpServletResponse respon -// // 임시 토큰 생성시 활성화 + // 임시 토큰 생성시 활성화 // private final UserRepository userRepository; // private final JwtProvider jwtProvider; // @PostMapping("/dev-login") diff --git a/server/src/main/java/com/soopgyeol/api/controller/BuyController.java b/server/src/main/java/com/soopgyeol/api/controller/BuyController.java new file mode 100644 index 0000000..d9f940e --- /dev/null +++ b/server/src/main/java/com/soopgyeol/api/controller/BuyController.java @@ -0,0 +1,38 @@ +package com.soopgyeol.api.controller; + +import com.soopgyeol.api.domain.buy.dto.*; +import com.soopgyeol.api.service.jwt.JwtProvider; +import com.soopgyeol.api.service.buy.BuyService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/items/buy") +@RequiredArgsConstructor +public class BuyController { + + private final BuyService buyService; + private final JwtProvider jwtProvider; + + @PostMapping + public ResponseEntity buyItem(@RequestHeader("Authorization") String authorizationHeader, + @RequestBody BuyRequest request) { + String token = authorizationHeader.replace("Bearer ", ""); + Long userId = jwtProvider.getUserId(token); + + BuyResult result = buyService.buyItem(userId, request.getItemId()); + + BuyResponse response = BuyResponse.builder() + .itemId(result.getItemId()) + .itemName(result.getItemName()) + .itemPrice(result.getItemPrice()) + .userMoneyBalance(result.getUserMoneyBalance()) + .message("구매가 완료되었습니다.") + .build(); + + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/soopgyeol/api/domain/buy/dto/BuyRequest.java b/server/src/main/java/com/soopgyeol/api/domain/buy/dto/BuyRequest.java new file mode 100644 index 0000000..af8e5cd --- /dev/null +++ b/server/src/main/java/com/soopgyeol/api/domain/buy/dto/BuyRequest.java @@ -0,0 +1,8 @@ +package com.soopgyeol.api.domain.buy.dto; + +import lombok.Getter; + +@Getter +public class BuyRequest { + private Long itemId; // 구매하려는 아이템 ID +} diff --git a/server/src/main/java/com/soopgyeol/api/domain/buy/dto/BuyResponse.java b/server/src/main/java/com/soopgyeol/api/domain/buy/dto/BuyResponse.java new file mode 100644 index 0000000..1664fff --- /dev/null +++ b/server/src/main/java/com/soopgyeol/api/domain/buy/dto/BuyResponse.java @@ -0,0 +1,16 @@ +package com.soopgyeol.api.domain.buy.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +@AllArgsConstructor +public class BuyResponse { + private Long itemId; + private String itemName; + private int itemPrice; + private int userMoneyBalance; + private String message; +} diff --git a/server/src/main/java/com/soopgyeol/api/domain/buy/dto/BuyResult.java b/server/src/main/java/com/soopgyeol/api/domain/buy/dto/BuyResult.java new file mode 100644 index 0000000..56953ca --- /dev/null +++ b/server/src/main/java/com/soopgyeol/api/domain/buy/dto/BuyResult.java @@ -0,0 +1,13 @@ +package com.soopgyeol.api.domain.buy.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class BuyResult { + private Long itemId; + private String itemName; + private int itemPrice; + private int userMoneyBalance; +} \ No newline at end of file diff --git a/server/src/main/java/com/soopgyeol/api/domain/buy/entity/Purchase.java b/server/src/main/java/com/soopgyeol/api/domain/buy/entity/Purchase.java new file mode 100644 index 0000000..4a30473 --- /dev/null +++ b/server/src/main/java/com/soopgyeol/api/domain/buy/entity/Purchase.java @@ -0,0 +1,36 @@ +package com.soopgyeol.api.domain.buy.entity; + +import com.soopgyeol.api.domain.user.User; +import jakarta.persistence.*; +import lombok.*; +import com.soopgyeol.api.domain.item.entity.Item; +import org.hibernate.annotations.CreationTimestamp; +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Purchase { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "buy_id") + private Long id; + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; // 구매자 (FK) + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "item_id") + private Item item; // 구매한 아이템 (FK) + + @Column(name = "item_money", nullable = false) + private int itemMoney; + + @CreationTimestamp + @Column(name = "purchased_at", nullable = false, updatable = false) + private LocalDateTime purchasedAt; // 구매 시각 +} diff --git a/server/src/main/java/com/soopgyeol/api/domain/item/entity/Item.java b/server/src/main/java/com/soopgyeol/api/domain/item/entity/Item.java index 5ae5ee0..c70289f 100644 --- a/server/src/main/java/com/soopgyeol/api/domain/item/entity/Item.java +++ b/server/src/main/java/com/soopgyeol/api/domain/item/entity/Item.java @@ -27,4 +27,8 @@ public class Item { @Enumerated(EnumType.STRING) @Column(nullable = false) private ItemCategory category; + + public void setPrice(int price) { + this.price = price; + } } diff --git a/server/src/main/java/com/soopgyeol/api/domain/user/User.java b/server/src/main/java/com/soopgyeol/api/domain/user/User.java index f354043..2b74905 100644 --- a/server/src/main/java/com/soopgyeol/api/domain/user/User.java +++ b/server/src/main/java/com/soopgyeol/api/domain/user/User.java @@ -54,4 +54,6 @@ public void increaseGrowthPoint(int point) { public void addMoney(int amount) { this.moneyBalance += amount; } + + public void subMoney(int money) {this.moneyBalance -= money; } } \ No newline at end of file diff --git a/server/src/main/java/com/soopgyeol/api/repository/PurchaseRepository.java b/server/src/main/java/com/soopgyeol/api/repository/PurchaseRepository.java new file mode 100644 index 0000000..2a85f51 --- /dev/null +++ b/server/src/main/java/com/soopgyeol/api/repository/PurchaseRepository.java @@ -0,0 +1,13 @@ +package com.soopgyeol.api.repository; + +import com.soopgyeol.api.domain.item.entity.Item; +import com.soopgyeol.api.domain.user.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import com.soopgyeol.api.domain.buy.entity.Purchase; + +@Repository +public interface PurchaseRepository extends JpaRepository { + + boolean existsByUserAndItem(User user, Item item); +} diff --git a/server/src/main/java/com/soopgyeol/api/service/buy/BuyService.java b/server/src/main/java/com/soopgyeol/api/service/buy/BuyService.java new file mode 100644 index 0000000..b3a3dff --- /dev/null +++ b/server/src/main/java/com/soopgyeol/api/service/buy/BuyService.java @@ -0,0 +1,7 @@ +package com.soopgyeol.api.service.buy; + +import com.soopgyeol.api.domain.buy.dto.BuyResult; + +public interface BuyService { + BuyResult buyItem(Long userId, Long itemId); // 반드시 BuyResult로 변경 +} \ No newline at end of file diff --git a/server/src/main/java/com/soopgyeol/api/service/buy/BuyServiceImpl.java b/server/src/main/java/com/soopgyeol/api/service/buy/BuyServiceImpl.java new file mode 100644 index 0000000..8219e9a --- /dev/null +++ b/server/src/main/java/com/soopgyeol/api/service/buy/BuyServiceImpl.java @@ -0,0 +1,71 @@ +package com.soopgyeol.api.service.buy; + +import com.soopgyeol.api.domain.buy.dto.BuyResult; +import com.soopgyeol.api.domain.buy.entity.Purchase; +import com.soopgyeol.api.domain.item.entity.Item; +import com.soopgyeol.api.domain.user.User; +import com.soopgyeol.api.repository.ItemRepository; +import com.soopgyeol.api.repository.PurchaseRepository; +import com.soopgyeol.api.repository.UserRepository; +import com.soopgyeol.api.common.exception.InsufficientBalanceException; +import com.soopgyeol.api.common.exception.ItemAlreadyOwnedException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class BuyServiceImpl implements BuyService { + + private final ItemRepository itemRepository; + private final PurchaseRepository purchaseRepository; + private final UserRepository userRepository; + + public BuyServiceImpl(ItemRepository itemRepository, + PurchaseRepository purchaseRepository, + UserRepository userRepository) { + this.itemRepository = itemRepository; + this.purchaseRepository = purchaseRepository; + this.userRepository = userRepository; + } + + @Transactional + @Override + public BuyResult buyItem(Long userId, Long itemId) { + + // 1. 구매 가능 여부 (사용자 금액과 아이템 금액 비교) + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("해당 유저가 존재하지 않습니다.")); + + Item item = itemRepository.findById(itemId) + .orElseThrow(() -> new IllegalArgumentException("아이템이 존재하지 않습니다.")); + + if (user.getMoneyBalance() < item.getPrice()) { + throw new InsufficientBalanceException("보유 금액이 부족합니다."); + } + + // 2. 보유 중복 여부 + boolean alreadyOwned = purchaseRepository.existsByUserAndItem(user, item); + if (alreadyOwned) { + throw new ItemAlreadyOwnedException("이미 보유한 아이템입니다."); + } + + // 3. 금액 차감 + int money = item.getPrice(); + user.subMoney(money); + + // 4. 인벤토리 저장 + Purchase purchase = Purchase.builder() + .user(user) + .item(item) + .itemMoney(item.getPrice()) + .build(); + + purchaseRepository.save(purchase); + + return BuyResult.builder() + .itemId(item.getId()) + .itemName(item.getName()) + .itemPrice(item.getPrice()) + .userMoneyBalance(user.getMoneyBalance()) + .build(); + } +} \ No newline at end of file