Skip to content

Commit

Permalink
feat: 상품 목록 조회 기능 구현 (#22)
Browse files Browse the repository at this point in the history
* fix: SQL syntax 오류 수정

* feat: 상품 엔티티 생성

* feat: 상품 목록 조회 API 구현

* test: 상품 목록 조회 테스트

* style: return 전 줄바꿈 추가
  • Loading branch information
kmebin authored Nov 1, 2023
1 parent 929acc5 commit e1484c7
Show file tree
Hide file tree
Showing 14 changed files with 363 additions and 2 deletions.
27 changes: 27 additions & 0 deletions src/main/java/com/moabam/api/application/ProductService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.moabam.api.application;

import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.moabam.api.domain.entity.Product;
import com.moabam.api.domain.repository.ProductRepository;
import com.moabam.api.dto.ProductMapper;
import com.moabam.api.dto.ProductsResponse;

import lombok.RequiredArgsConstructor;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ProductService {

private final ProductRepository productRepository;

public ProductsResponse getProducts() {
List<Product> products = productRepository.findAll();

return ProductMapper.toProductsResponse(products);
}
}
74 changes: 74 additions & 0 deletions src/main/java/com/moabam/api/domain/entity/Product.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.moabam.api.domain.entity;

import static com.moabam.global.error.model.ErrorMessage.*;
import static java.util.Objects.*;

import org.hibernate.annotations.ColumnDefault;

import com.moabam.api.domain.entity.enums.ProductType;
import com.moabam.global.common.entity.BaseTimeEntity;
import com.moabam.global.error.exception.BadRequestException;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Table(name = "product")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;

@Enumerated(value = EnumType.STRING)
@Column(name = "type", nullable = false)
@ColumnDefault("'BUG'")
private ProductType type;

@Column(name = "name", nullable = false)
private String name;

@Column(name = "price", nullable = false)
private int price;

@Column(name = "quantity", nullable = false)
@ColumnDefault("1")
private int quantity;

@Builder
private Product(ProductType type, String name, int price, Integer quantity) {
this.type = requireNonNullElse(type, ProductType.BUG);
this.name = requireNonNull(name);
this.price = validatePrice(price);
this.quantity = validateQuantity(requireNonNullElse(quantity, 1));
}

private int validatePrice(int price) {
if (price < 0) {
throw new BadRequestException(INVALID_PRICE);
}

return price;
}

private int validateQuantity(int quantity) {
if (quantity < 1) {
throw new BadRequestException(INVALID_QUANTITY);
}

return quantity;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.moabam.api.domain.entity.enums;

public enum ProductType {

BUG;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.moabam.api.domain.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.moabam.api.domain.entity.Product;

public interface ProductRepository extends JpaRepository<Product, Long> {

}
30 changes: 30 additions & 0 deletions src/main/java/com/moabam/api/dto/ProductMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.moabam.api.dto;

import java.util.List;

import com.moabam.api.domain.entity.Product;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
public final class ProductMapper {

public static ProductResponse toProductResponse(Product product) {
return ProductResponse.builder()
.id(product.getId())
.type(product.getType().name())
.name(product.getName())
.price(product.getPrice())
.quantity(product.getQuantity())
.build();
}

public static ProductsResponse toProductsResponse(List<Product> products) {
return ProductsResponse.builder()
.products(products.stream()
.map(ProductMapper::toProductResponse)
.toList())
.build();
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/moabam/api/dto/ProductResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.moabam.api.dto;

import lombok.Builder;

@Builder
public record ProductResponse(
Long id,
String type,
String name,
int price,
int quantity
) {

}
12 changes: 12 additions & 0 deletions src/main/java/com/moabam/api/dto/ProductsResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.moabam.api.dto;

import java.util.List;

import lombok.Builder;

@Builder
public record ProductsResponse(
List<ProductResponse> products
) {

}
24 changes: 24 additions & 0 deletions src/main/java/com/moabam/api/presentation/ProductController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.moabam.api.presentation;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.moabam.api.application.ProductService;
import com.moabam.api.dto.ProductsResponse;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/products")
@RequiredArgsConstructor
public class ProductController {

private final ProductService productService;

@GetMapping
public ResponseEntity<ProductsResponse> getProducts() {
return ResponseEntity.ok(productService.getProducts());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ public enum ErrorMessage {

MEMBER_NOT_FOUND("존재하지 않는 회원입니다."),

INVALID_BUG_COUNT("벌레 개수는 0 이상이어야 합니다.");
INVALID_BUG_COUNT("벌레 개수는 0 이상이어야 합니다."),
INVALID_PRICE("가격은 0 이상이어야 합니다."),
INVALID_QUANTITY("수량은 1 이상이어야 합니다.");

private final String message;
}
2 changes: 1 addition & 1 deletion src/main/resources/config
48 changes: 48 additions & 0 deletions src/test/java/com/moabam/api/application/ProductServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.moabam.api.application;

import static com.moabam.fixture.ProductFixture.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.*;

import java.util.List;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.moabam.api.domain.entity.Product;
import com.moabam.api.domain.repository.ProductRepository;
import com.moabam.api.dto.ProductResponse;
import com.moabam.api.dto.ProductsResponse;

@ExtendWith(MockitoExtension.class)
class ProductServiceTest {

@InjectMocks
ProductService productService;

@Mock
ProductRepository productRepository;

@DisplayName("상품 목록을 조회한다.")
@Test
void get_products_success() {
// given
Product product1 = bugProduct();
Product product2 = bugProduct();
given(productRepository.findAll()).willReturn(List.of(product1, product2));

// when
ProductsResponse response = productService.getProducts();

// then
List<String> productNames = response.products().stream()
.map(ProductResponse::name)
.toList();
assertThat(response.products()).hasSize(2);
assertThat(productNames).containsOnly(BUG_PRODUCT_NAME, BUG_PRODUCT_NAME);
}
}
36 changes: 36 additions & 0 deletions src/test/java/com/moabam/api/domain/entity/ProductTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.moabam.api.domain.entity;

import static org.assertj.core.api.AssertionsForClassTypes.*;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import com.moabam.global.error.exception.BadRequestException;

class ProductTest {

@DisplayName("상품 가격이 0 보다 작으면 예외가 발생한다.")
@Test
void validate_price_exception() {
Product.ProductBuilder productBuilder = Product.builder()
.name("X10")
.price(-10);

assertThatThrownBy(productBuilder::build)
.isInstanceOf(BadRequestException.class)
.hasMessage("가격은 0 이상이어야 합니다.");
}

@DisplayName("상품량이 1 보다 작으면 예외가 발생한다.")
@Test
void validate_quantity_exception() {
Product.ProductBuilder productBuilder = Product.builder()
.name("X10")
.price(1000)
.quantity(-1);

assertThatThrownBy(productBuilder::build)
.isInstanceOf(BadRequestException.class)
.hasMessage("수량은 1 이상이어야 합니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.moabam.api.presentation;

import static java.nio.charset.StandardCharsets.*;
import static org.mockito.BDDMockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import java.util.List;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.moabam.api.application.ProductService;
import com.moabam.api.domain.entity.Product;
import com.moabam.api.dto.ProductMapper;
import com.moabam.api.dto.ProductsResponse;
import com.moabam.fixture.ProductFixture;

@SpringBootTest
@AutoConfigureMockMvc
class ProductControllerTest {

@Autowired
MockMvc mockMvc;

@Autowired
ObjectMapper objectMapper;

@MockBean
ProductService productService;

@DisplayName("상품 목록을 조회한다.")
@Test
void get_products_success() throws Exception {
// given
Product product1 = ProductFixture.bugProduct();
Product product2 = ProductFixture.bugProduct();
ProductsResponse expected = ProductMapper.toProductsResponse(List.of(product1, product2));
given(productService.getProducts()).willReturn(expected);

// when & then
String content = mockMvc.perform(get("/products"))
.andDo(print())
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString(UTF_8);
ProductsResponse actual = objectMapper.readValue(content, ProductsResponse.class);
Assertions.assertThat(actual).isEqualTo(expected);
}
}
20 changes: 20 additions & 0 deletions src/test/java/com/moabam/fixture/ProductFixture.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.moabam.fixture;

import com.moabam.api.domain.entity.Product;
import com.moabam.api.domain.entity.enums.ProductType;

public class ProductFixture {

public static final String BUG_PRODUCT_NAME = "X10";
public static final int BUG_PRODUCT_PRICE = 3000;
public static final int BUG_PRODUCT_QUANTITY = 10;

public static Product bugProduct() {
return Product.builder()
.type(ProductType.BUG)
.name(BUG_PRODUCT_NAME)
.price(BUG_PRODUCT_PRICE)
.quantity(BUG_PRODUCT_QUANTITY)
.build();
}
}

0 comments on commit e1484c7

Please sign in to comment.