Skip to content

Commit 1f48c85

Browse files
committed
feat: 상품 도메인 API 구현
- 상품 생성 - 상품 상세 정보 조회 - 상품 목록 조회
1 parent d5cfd92 commit 1f48c85

9 files changed

Lines changed: 302 additions & 0 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.loopers.application.product;
2+
3+
import com.loopers.domain.like.LikeRepository;
4+
import com.loopers.domain.product.Product;
5+
import com.loopers.domain.product.ProductRepository;
6+
import com.loopers.domain.user.UserRepository;
7+
import com.loopers.interfaces.api.product.ProductV1Dto;
8+
import com.loopers.support.error.CoreException;
9+
import com.loopers.support.error.ErrorType;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.stereotype.Component;
12+
import org.springframework.transaction.annotation.Transactional;
13+
14+
import java.util.List;
15+
16+
@Component
17+
@RequiredArgsConstructor
18+
public class ProductFacade {
19+
private final ProductRepository productRepository;
20+
private final LikeRepository likeRepository;
21+
22+
23+
@Transactional
24+
public ProductInfo registerProduct(ProductV1Dto.ProductRequest request) {
25+
Product product = request.toEntity();
26+
productRepository.save(product);
27+
28+
return ProductInfo.from(product, 0);
29+
}
30+
31+
@Transactional
32+
public List<ProductInfo> findAllProducts() {
33+
List<Product> products = productRepository.findAll();
34+
35+
return products.stream()
36+
.map(product -> {
37+
int likeCount = likeRepository.countByProductId(product.getId());
38+
return ProductInfo.from(product, likeCount);
39+
})
40+
.toList();
41+
}
42+
43+
@Transactional(readOnly = true)
44+
public ProductInfo findProductById(Long id) {
45+
Product product = productRepository.findById(id).orElseThrow(
46+
() -> new CoreException(ErrorType.NOT_FOUND, "찾고자 하는 상품이 존재하지 않습니다.")
47+
);
48+
49+
int likeCount = likeRepository.countByProductId(id);
50+
51+
return ProductInfo.from(product, likeCount);
52+
}
53+
54+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.loopers.application.product;
2+
3+
import com.loopers.domain.product.Product;
4+
5+
import java.math.BigDecimal;
6+
7+
public record ProductInfo(Long id, Long brandId, String name, BigDecimal price, int stock, int likeCount) {
8+
public static ProductInfo from(Product product, int likeCount) {
9+
return new ProductInfo(
10+
product.getId(),
11+
product.getBrandId(),
12+
product.getName(),
13+
product.getPrice(),
14+
product.getStock(),
15+
likeCount
16+
);
17+
}
18+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.loopers.domain.product;
2+
3+
import com.loopers.domain.BaseEntity;
4+
import com.loopers.support.error.CoreException;
5+
import com.loopers.support.error.ErrorType;
6+
import jakarta.persistence.Column;
7+
import jakarta.persistence.Entity;
8+
import jakarta.persistence.Table;
9+
import lombok.AccessLevel;
10+
import lombok.Getter;
11+
import lombok.NoArgsConstructor;
12+
13+
import java.math.BigDecimal;
14+
15+
@Getter
16+
@Entity
17+
@Table(name = "product")
18+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
19+
public class Product extends BaseEntity {
20+
@Column(name = "ref_brand_id", nullable = false)
21+
private Long brandId;
22+
23+
@Column(name = "name", nullable = false)
24+
private String name;
25+
26+
@Column(name = "price", nullable = false)
27+
private BigDecimal price;
28+
29+
@Column(name = "stock", nullable = false)
30+
private int stock;
31+
32+
33+
public Product(Long brandId, String name, BigDecimal price, int stock) {
34+
this.brandId = brandId;
35+
this.name = name;
36+
this.price = price;
37+
this.stock = stock;
38+
}
39+
40+
public Product changePrice(BigDecimal newPrice) {
41+
this.price = newPrice;
42+
return this;
43+
}
44+
45+
public Product decreaseStock(int amount) {
46+
if (amount <= 0) {
47+
throw new CoreException(ErrorType.BAD_REQUEST, "재고 감소량은 0보다 커야 합니다.");
48+
}
49+
if (this.stock < amount) {
50+
throw new CoreException(ErrorType.BAD_REQUEST, "재고가 부족합니다.");
51+
}
52+
this.stock -= amount;
53+
54+
return this;
55+
}
56+
57+
public boolean isInStock(int quantity) {
58+
return this.stock >= quantity;
59+
}
60+
61+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.loopers.domain.product;
2+
3+
import com.loopers.interfaces.api.product.ProductV1Dto;
4+
5+
import java.util.List;
6+
import java.util.Optional;
7+
8+
public interface ProductRepository {
9+
Optional<Product> findById(Long id);
10+
11+
Product save(Product product);
12+
13+
List<Product> findAll();
14+
15+
List<Product> searchProductsByCondition(ProductV1Dto.SearchProductRequest request);
16+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.loopers.infrastructure.product;
2+
3+
import com.loopers.domain.product.Product;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface ProductJpaRepository extends JpaRepository<Product, Long> {
7+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.loopers.infrastructure.product;
2+
3+
import com.loopers.domain.product.Product;
4+
import com.loopers.domain.product.ProductRepository;
5+
import com.loopers.interfaces.api.product.ProductV1Dto;
6+
import com.querydsl.jpa.impl.JPAQueryFactory;
7+
import jakarta.persistence.EntityManager;
8+
import org.springframework.stereotype.Component;
9+
10+
import java.util.List;
11+
import java.util.Optional;
12+
13+
@Component
14+
public class ProductRepositoryImpl implements ProductRepository {
15+
private final ProductJpaRepository productJpaRepository;
16+
private final JPAQueryFactory queryFactory;
17+
18+
public ProductRepositoryImpl(ProductJpaRepository productJpaRepository, EntityManager entityManager) {
19+
this.productJpaRepository = productJpaRepository;
20+
this.queryFactory = new JPAQueryFactory(entityManager);
21+
}
22+
23+
@Override
24+
public Optional<Product> findById(Long id) {
25+
return productJpaRepository.findById(id);
26+
}
27+
28+
@Override
29+
public Product save(Product product) {
30+
return productJpaRepository.save(product);
31+
}
32+
33+
@Override
34+
public List<Product> findAll() {
35+
return productJpaRepository.findAll();
36+
}
37+
38+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.loopers.interfaces.api.product;
2+
3+
import com.loopers.interfaces.api.ApiResponse;
4+
import io.swagger.v3.oas.annotations.Operation;
5+
import io.swagger.v3.oas.annotations.tags.Tag;
6+
7+
import java.util.List;
8+
9+
@Tag(name = "Product V1 API", description = "Product API 입니다.")
10+
public interface ProductV1ApiSpec {
11+
@Operation(summary = "상품 등록")
12+
ApiResponse<ProductV1Dto.ProductResponse> registerProduct (ProductV1Dto.ProductRequest request);
13+
14+
@Operation(summary = "상품 목록 조회")
15+
ApiResponse<List<ProductV1Dto.ProductResponse>> findAllProducts ();
16+
17+
@Operation(summary = "상품 상세 조회")
18+
ApiResponse<ProductV1Dto.ProductResponse> findProductById (Long id);
19+
20+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.loopers.interfaces.api.product;
2+
3+
import com.loopers.application.product.ProductFacade;
4+
import com.loopers.application.product.ProductInfo;
5+
import com.loopers.interfaces.api.ApiResponse;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.web.bind.annotation.GetMapping;
8+
import org.springframework.web.bind.annotation.PathVariable;
9+
import org.springframework.web.bind.annotation.PostMapping;
10+
import org.springframework.web.bind.annotation.RequestBody;
11+
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RestController;
13+
14+
import java.util.List;
15+
16+
@RestController
17+
@RequestMapping("/api/v1/products")
18+
@RequiredArgsConstructor
19+
public class ProductV1Controller implements ProductV1ApiSpec {
20+
private final ProductFacade productFacade;
21+
22+
@PostMapping
23+
@Override
24+
public ApiResponse<ProductV1Dto.ProductResponse> registerProduct(@RequestBody ProductV1Dto.ProductRequest request) {
25+
ProductInfo info = productFacade.registerProduct(request);
26+
ProductV1Dto.ProductResponse response = ProductV1Dto.ProductResponse.from(info);
27+
28+
return ApiResponse.success(response);
29+
}
30+
31+
@GetMapping
32+
@Override
33+
public ApiResponse<List<ProductV1Dto.ProductResponse>> findAllProducts() {
34+
List<ProductInfo> infos = productFacade.findAllProducts();
35+
36+
List<ProductV1Dto.ProductResponse> responses = infos.stream()
37+
.map(ProductV1Dto.ProductResponse::from)
38+
.toList();
39+
40+
return ApiResponse.success(responses);
41+
}
42+
43+
@GetMapping("/{id}")
44+
@Override
45+
public ApiResponse<ProductV1Dto.ProductResponse> findProductById(@PathVariable Long id) {
46+
ProductInfo info = productFacade.findProductById(id);
47+
ProductV1Dto.ProductResponse response = ProductV1Dto.ProductResponse.from(info);
48+
49+
return ApiResponse.success(response);
50+
}
51+
52+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.loopers.interfaces.api.product;
2+
3+
import com.loopers.application.product.ProductInfo;
4+
import com.loopers.domain.product.Product;
5+
6+
import java.math.BigDecimal;
7+
8+
public class ProductV1Dto {
9+
public record ProductResponse(Long id, Long brandId, String name, BigDecimal price, int stock, int likeCount) {
10+
public static ProductResponse from(ProductInfo info) {
11+
return new ProductResponse(
12+
info.id(),
13+
info.brandId(),
14+
info.name(),
15+
info.price(),
16+
info.stock(),
17+
info.likeCount()
18+
);
19+
}
20+
}
21+
22+
public record ProductRequest(Long brandId, String name, BigDecimal price, int stock) {
23+
public Product toEntity() {
24+
return new Product(
25+
brandId,
26+
name,
27+
price,
28+
stock
29+
);
30+
}
31+
}
32+
33+
public record SearchProductRequest(String sortBy, String order) {
34+
35+
}
36+
}

0 commit comments

Comments
 (0)