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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.soopgyeol.api.common.exception;

public class InsufficientBalanceException extends RuntimeException {
public InsufficientBalanceException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.soopgyeol.api.common.exception;

public class ItemAlreadyOwnedException extends RuntimeException {
public ItemAlreadyOwnedException(String message) {
super(message);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public void kakaoAutoLogin(@RequestParam String code, HttpServletResponse respon



// // 임시 토큰 생성시 활성화
// 임시 토큰 생성시 활성화
// private final UserRepository userRepository;
// private final JwtProvider jwtProvider;
// @PostMapping("/dev-login")
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BuyResponse> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.soopgyeol.api.domain.buy.dto;

import lombok.Getter;

@Getter
public class BuyRequest {
private Long itemId; // 구매하려는 아이템 ID
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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; // 구매 시각
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,8 @@ public class Item {
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ItemCategory category;

public void setPrice(int price) {
this.price = price;
}
}
2 changes: 2 additions & 0 deletions server/src/main/java/com/soopgyeol/api/domain/user/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
Original file line number Diff line number Diff line change
@@ -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<Purchase, Long> {

boolean existsByUserAndItem(User user, Item item);
}
Original file line number Diff line number Diff line change
@@ -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로 변경
}
Original file line number Diff line number Diff line change
@@ -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();
}
}