@Entity
@Table(name = "warehouses")
@Getter
@Setter
@NoArgsConstructor
public class Warehouse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
private String location;
@OneToMany(mappedBy = "warehouse", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private List<Stock> stocks = new ArrayList<>();
}
@Entity
@Table(name = "products")
@Getter
@Setter
@NoArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private BigDecimal price;
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private List<Stock> stocks = new ArrayList<>();
}
@Entity
@Table(name = "stocks")
@Getter
@Setter
@NoArgsConstructor
public class Stock {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false)
@JsonBackReference
private Product product;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "warehouse_id", nullable = false)
@JsonBackReference
private Warehouse warehouse;
private Integer quantity;
}
@Getter
@Setter
public class WarehouseDto {
@NotBlank(message = "μ°½κ³ λͺ
μ μ
λ ₯ν΄μ£ΌμΈμ")
private String name;
@Size(max = 200, message = "μμΉ μ 보λ μ΅λ 200μκΉμ§ νμ©λ©λλ€")
private String location;
}
@Getter
@Setter
public class ProductDto {
@NotBlank(message = "μ νλͺ
μ μ
λ ₯ν΄μ£ΌμΈμ")
private String name;
@NotNull(message = "κ°κ²©μ μ
λ ₯ν΄μ£ΌμΈμ")
@PositiveOrZero(message = "κ°κ²©μ 0 λλ μμμ¬μΌ ν©λλ€")
private BigDecimal price;
}
@Getter
@Setter
public class StockDto {
@NotNull(message = "μ ν IDλ₯Ό μ νν΄μ£ΌμΈμ.")
private Long productId;
@NotNull(message = "μ°½κ³ IDλ₯Ό μ νν΄μ£ΌμΈμ.")
private Long warehouseId;
@NotNull(message = "μλμ μ
λ ₯ν΄μ£ΌμΈμ.")
@Min(value = 0, message = "μλμ 0 μ΄μμ΄μ΄μΌ ν©λλ€.")
private Integer quantity;
}
@Getter
@Setter
public class StockUpdateDto {
@NotNull(message = "μλμ μ
λ ₯ν΄μ£ΌμΈμ.")
@Min(value = 0, message = "μλμ 0 μ΄μμ΄μ΄μΌ ν©λλ€.")
private Integer quantity;
}
public interface WarehouseRepository extends JpaRepository<Warehouse, Long>, QuerydslPredicateExecutor<Warehouse> {
}
public interface ProductRepository extends JpaRepository<Product, Long>, QuerydslPredicateExecutor<Product> {
}
public interface StockRepository extends JpaRepository<Stock, Long>, QuerydslPredicateExecutor<Stock> {
Optional<Stock> findByProductIdAndWarehouseId(Long productId, Long warehouseId);
}
@Getter
@Setter
public class WarehouseSearchCondition {
private String name;
private String location;
}
@Repository
@RequiredArgsConstructor
public class WarehouseQueryRepository {
private final JPAQueryFactory queryFactory;
public List<Warehouse> search(WarehouseSearchCondition condition) {
QWarehouse warehouse = QWarehouse.warehouse;
BooleanBuilder builder = new BooleanBuilder();
if (condition.getName() != null && !condition.getName().isBlank()) {
builder.and(warehouse.name.containsIgnoreCase(condition.getName()));
}
if (condition.getLocation() != null && !condition.getLocation().isBlank()) {
builder.and(warehouse.location.containsIgnoreCase(condition.getLocation()));
}
return queryFactory.selectFrom(warehouse).where(builder).orderBy(warehouse.name.asc()).fetch();
}
}
@Getter
@Setter
public class ProductSearchCondition {
private String name;
private BigDecimal minPrice;
private BigDecimal maxPrice;
}
@Repository
@RequiredArgsConstructor
public class ProductQueryRepository {
private final JPAQueryFactory queryFactory;
public List<Product> search(ProductSearchCondition condition) {
QProduct product = QProduct.product;
BooleanBuilder builder = new BooleanBuilder();
if (condition.getName() != null && !condition.getName().isBlank()) {
builder.and(product.name.containsIgnoreCase(condition.getName()));
}
if (condition.getMinPrice() != null) {
builder.and(product.price.goe(condition.getMinPrice()));
}
if (condition.getMaxPrice() != null) {
builder.and(product.price.loe(condition.getMaxPrice()));
}
return queryFactory.selectFrom(product).where(builder).orderBy(product.name.asc()).fetch();
}
}
@Getter
@Setter
public class StockSearchCondition {
private String productName;
private String warehouseName;
private Integer minQuantity;
private Integer maxQuantity;
}
@Repository
@RequiredArgsConstructor
public class StockQueryRepository {
private final JPAQueryFactory queryFactory;
public List<Stock> search(StockSearchCondition condition) {
QStock stock = QStock.stock;
QProduct product = QProduct.product;
QWarehouse warehouse = QWarehouse.warehouse;
BooleanBuilder builder = new BooleanBuilder();
builder.and(stock.product.id.isNotNull());
builder.and(stock.warehouse.id.isNotNull());
if (condition.getProductName() != null && !condition.getProductName().isBlank()) {
builder.and(product.name.containsIgnoreCase(condition.getProductName()));
}
if (condition.getWarehouseName() != null && !condition.getWarehouseName().isBlank()) {
builder.and(warehouse.name.containsIgnoreCase(condition.getWarehouseName()));
}
if (condition.getMinQuantity() != null) {
builder.and(stock.quantity.goe(condition.getMinQuantity()));
}
if (condition.getMaxQuantity() != null) {
builder.and(stock.quantity.loe(condition.getMaxQuantity()));
}
return queryFactory
.selectFrom(stock)
.join(stock.product, product)
.join(stock.warehouse, warehouse)
.where(builder)
.orderBy(product.name.asc(), warehouse.name.asc())
.fetch();
}
}
@RestController
@RequestMapping("/api/warehouses")
@RequiredArgsConstructor
public class WarehouseController {
private final WarehouseService warehouseService;
@GetMapping
public List<Warehouse> search(WarehouseSearchCondition condition) {
return warehouseService.search(condition);
}
@GetMapping("/{id}")
public Warehouse getById(@PathVariable Long id) {
return warehouseService.getById(id);
}
@PostMapping
public Warehouse create(@Valid @RequestBody WarehouseDto dto) {
return warehouseService.create(dto);
}
@PutMapping("/{id}")
public Warehouse update(@PathVariable Long id, @Valid @RequestBody WarehouseDto dto) {
return warehouseService.update(id, dto);
}
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
warehouseService.delete(id);
}
}
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping
public List<Product> search(ProductSearchCondition condition) {
return productService.search(condition);
}
@GetMapping("/{id}")
public Product getById(@PathVariable Long id) {
return productService.getById(id);
}
@PostMapping
public Product create(@Valid @RequestBody ProductDto dto) {
return productService.create(dto);
}
@PutMapping("/{id}")
public Product update(@PathVariable Long id, @Valid @RequestBody ProductDto dto) {
return productService.update(id, dto);
}
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
productService.delete(id);
}
}
@RestController
@RequestMapping("/api/stocks")
@RequiredArgsConstructor
public class StockController {
private final StockService stockService;
@GetMapping
public List<Stock> search(StockSearchCondition condition) {
return stockService.search(condition);
}
@GetMapping("/{id}")
public Stock getById(@PathVariable Long id) {
return stockService.getById(id);
}
@PostMapping
public Stock create(@Valid @RequestBody StockDto dto) {
return stockService.create(dto);
}
@PutMapping("/{id}")
public Stock update(@PathVariable Long id, @Valid @RequestBody StockUpdateDto dto) {
return stockService.update(id, dto);
}
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
stockService.delete(id);
}
@PostMapping("/transfer")
public void transfer(
@RequestParam Long productId,
@RequestParam Long srcWarehouseId,
@RequestParam Long destWarehouseId,
@RequestParam int quantity
) {
stockService.transferStock(productId, srcWarehouseId, destWarehouseId, quantity);
}
}
base_url = http://localhost:8080/api/
POST {{base_url}}warehouses
{
"name": "κ΅μ μ§μμ ",
"location": "μλ문ꡬ"
}
POST {{base_url}}products
{
"name": "λμ΄ν€ μ λ°",
"price": 150000
}
POST {{base_url}}stocks
{
"productId": 2,
"warehouseId": 4,
"quantity": 100
}

GET {{base_url}}stocks/transfer?productId=2&srcWarehouseId=4&destWarehouseId=3&quantity=100