diff --git a/spring/.gitignore b/spring/.gitignore new file mode 100644 index 0000000..de5aa0b --- /dev/null +++ b/spring/.gitignore @@ -0,0 +1,2 @@ +## api key +exchanges.yml \ No newline at end of file diff --git a/spring/src/main/java/com/jscode/spring/advice/BadRequestException.java b/spring/src/main/java/com/jscode/spring/advice/BadRequestException.java new file mode 100644 index 0000000..18d5447 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/advice/BadRequestException.java @@ -0,0 +1,9 @@ +package com.jscode.spring.advice; + +public class BadRequestException extends BusinessException { + + public BadRequestException(final String message) { + super(message); + } + +} diff --git a/spring/src/main/java/com/jscode/spring/advice/BusinessException.java b/spring/src/main/java/com/jscode/spring/advice/BusinessException.java new file mode 100644 index 0000000..4d5a6a3 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/advice/BusinessException.java @@ -0,0 +1,9 @@ +package com.jscode.spring.advice; + +public class BusinessException extends RuntimeException { + + public BusinessException(final String message) { + super(message); + } + +} diff --git a/spring/src/main/java/com/jscode/spring/advice/ControllerAdvice.java b/spring/src/main/java/com/jscode/spring/advice/ControllerAdvice.java new file mode 100644 index 0000000..1fc272b --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/advice/ControllerAdvice.java @@ -0,0 +1,24 @@ +package com.jscode.spring.advice; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * (미션 3 ) 조회/등록 실패할 때 응답 interface 구현 + */ +@RestControllerAdvice +public class ControllerAdvice { + + @ExceptionHandler(NotFoundException.class) + ResponseEntity handleNotFoundException(NotFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(e.getMessage())); + } + + @ExceptionHandler(BadRequestException.class) + ResponseEntity handleBadRequestException(BadRequestException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(e.getMessage())); + } + +} diff --git a/spring/src/main/java/com/jscode/spring/advice/ErrorResponse.java b/spring/src/main/java/com/jscode/spring/advice/ErrorResponse.java new file mode 100644 index 0000000..e4e8226 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/advice/ErrorResponse.java @@ -0,0 +1,14 @@ +package com.jscode.spring.advice; + +import lombok.Getter; + +@Getter +public class ErrorResponse { + + private String message; + + public ErrorResponse(final String message) { + this.message = message; + } + +} diff --git a/spring/src/main/java/com/jscode/spring/advice/NotFoundException.java b/spring/src/main/java/com/jscode/spring/advice/NotFoundException.java new file mode 100644 index 0000000..b441a33 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/advice/NotFoundException.java @@ -0,0 +1,9 @@ +package com.jscode.spring.advice; + +public class NotFoundException extends BusinessException { + + public NotFoundException(final String message) { + super(message); + } + +} diff --git a/spring/src/main/java/com/jscode/spring/exchange/config/ExchangeYamlRead.java b/spring/src/main/java/com/jscode/spring/exchange/config/ExchangeYamlRead.java new file mode 100644 index 0000000..a487636 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/exchange/config/ExchangeYamlRead.java @@ -0,0 +1,18 @@ +package com.jscode.spring.exchange.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +@Configuration +@PropertySource(value = "classpath:exchanges.yml", factory = YamlPropertySourceFactory.class) +@ConfigurationProperties(prefix = "apilayer") +@Getter +@Setter +public class ExchangeYamlRead { + + private String key; + +} diff --git a/spring/src/main/java/com/jscode/spring/exchange/config/YamlPropertySourceFactory.java b/spring/src/main/java/com/jscode/spring/exchange/config/YamlPropertySourceFactory.java new file mode 100644 index 0000000..190b435 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/exchange/config/YamlPropertySourceFactory.java @@ -0,0 +1,24 @@ +package com.jscode.spring.exchange.config; + +import java.util.Properties; +import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.io.support.PropertySourceFactory; + +public class YamlPropertySourceFactory implements PropertySourceFactory { + + @Override + public PropertySource createPropertySource(final String name, final EncodedResource resource) { + YamlPropertiesFactoryBean factoryBean = new YamlPropertiesFactoryBean(); + factoryBean.setResources(resource.getResource()); + factoryBean.afterPropertiesSet(); + Properties properties = factoryBean.getObject(); + if (name != null) { + return new PropertiesPropertySource(name, properties); + } + return new PropertiesPropertySource(resource.getResource().getFilename(), properties); + } + +} diff --git a/spring/src/main/java/com/jscode/spring/exchange/domain/ExchangeRates.java b/spring/src/main/java/com/jscode/spring/exchange/domain/ExchangeRates.java new file mode 100644 index 0000000..35e5afa --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/exchange/domain/ExchangeRates.java @@ -0,0 +1,21 @@ +package com.jscode.spring.exchange.domain; + +import java.time.LocalDate; +import java.util.Map; +import lombok.Getter; + +@Getter +public class ExchangeRates { + + private String base; + private LocalDate date; + private Map rates; + + public ExchangeRates() { + } + + public double findRates(final String countryName) { + return rates.get(countryName); + } + +} diff --git a/spring/src/main/java/com/jscode/spring/exchange/service/ExchangeRatesService.java b/spring/src/main/java/com/jscode/spring/exchange/service/ExchangeRatesService.java new file mode 100644 index 0000000..e3eb205 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/exchange/service/ExchangeRatesService.java @@ -0,0 +1,42 @@ +package com.jscode.spring.exchange.service; + +import com.jscode.spring.exchange.config.ExchangeYamlRead; +import com.jscode.spring.exchange.domain.ExchangeRates; +import com.jscode.spring.product.domain.MonetaryUnit; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class ExchangeRatesService { + + public static final String EXCHANGE_URL = "https://api.apilayer.com/exchangerates_data/latest?symbols=KRW&base=%s"; + + private final ExchangeYamlRead exchangeYamlRead; + + public ExchangeRatesService(final ExchangeYamlRead exchangeYamlRead) { + this.exchangeYamlRead = exchangeYamlRead; + } + + public double convertKrwTo(final MonetaryUnit monetaryUnit, final int value) { + RestTemplate template = new RestTemplate(); + ResponseEntity exchange = template.exchange( + String.format(EXCHANGE_URL, monetaryUnit.toString()), + HttpMethod.GET, + new HttpEntity<>(generateHeader("apikey")), + ExchangeRates.class + ); + return value / exchange.getBody() + .findRates("KRW"); + } + + private HttpHeaders generateHeader(final String header) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.set(header, exchangeYamlRead.getKey()); + return httpHeaders; + } + +} diff --git a/spring/src/main/java/com/jscode/spring/product/controller/ProductController.java b/spring/src/main/java/com/jscode/spring/product/controller/ProductController.java new file mode 100644 index 0000000..86c6ed0 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/product/controller/ProductController.java @@ -0,0 +1,62 @@ +package com.jscode.spring.product.controller; + +import com.jscode.spring.product.dto.NewProductRequest; +import com.jscode.spring.product.dto.ProductListResponse; +import com.jscode.spring.product.dto.ProductResponse; +import com.jscode.spring.product.dto.ProductSaveResponse; +import com.jscode.spring.product.service.ProductService; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.Nullable; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequestMapping("/api") +public class ProductController { + + private final ProductService productService; + + public ProductController(final ProductService productService) { + this.productService = productService; + } + + /** + * (연습문제 1 ) 상품 등록 api
+ * (미션 1 ) 상품 등록 api (동일상품 등록시 실패) + */ + @PostMapping("/products") + public ProductSaveResponse saveProduct(@RequestBody final NewProductRequest newProductRequest) { + Long generatedId = productService.saveProduct(newProductRequest); + return new ProductSaveResponse(generatedId); + } + + /** + * (연습문제 2 ) 상품 조회 api + */ + @GetMapping("/products/{productId}") + public ProductResponse findProductById(@PathVariable final Long productId, + @RequestParam @Nullable final String monetaryUnit) { + return productService.findProductById(productId, monetaryUnit); + } + + /** + * (연습문제 3 ) 전체 상품 조회 (미션) 상품 이름으로 상세조회하는 api
+ * (미션 2 ) 상품 상세 조회 api + */ + @GetMapping("/products") + public ProductListResponse findProducts(@RequestParam @Nullable final String name, + @RequestParam @Nullable final String monetaryUnit) { + if (Objects.isNull(name)) { + return productService.findAll(monetaryUnit); + } + return productService.findAllByName(name, monetaryUnit); + } + +} diff --git a/spring/src/main/java/com/jscode/spring/product/domain/MonetaryUnit.java b/spring/src/main/java/com/jscode/spring/product/domain/MonetaryUnit.java new file mode 100644 index 0000000..6a6a4b2 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/product/domain/MonetaryUnit.java @@ -0,0 +1,5 @@ +package com.jscode.spring.product.domain; + +public enum MonetaryUnit { + USD, KRW +} diff --git a/spring/src/main/java/com/jscode/spring/product/domain/Product.java b/spring/src/main/java/com/jscode/spring/product/domain/Product.java new file mode 100644 index 0000000..d233598 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/product/domain/Product.java @@ -0,0 +1,30 @@ +package com.jscode.spring.product.domain; + +import java.util.Objects; +import lombok.Getter; + +@Getter +public class Product { + + private Long id; + private String name; + private int price; + + public Product(final Long id, final String name, final int price) { + this.id = id; + this.name = name; + this.price = price; + } + + public void generateId(final Long id) { + this.id = id; + } + + public boolean isSameId(final Long id) { + return Objects.equals(this.id, id); + } + + public boolean isSameName(final String name) { + return this.name.equals(name); + } +} diff --git a/spring/src/main/java/com/jscode/spring/product/dto/NewProductRequest.java b/spring/src/main/java/com/jscode/spring/product/dto/NewProductRequest.java new file mode 100644 index 0000000..182d299 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/product/dto/NewProductRequest.java @@ -0,0 +1,24 @@ +package com.jscode.spring.product.dto; + +import com.jscode.spring.product.domain.Product; +import lombok.Getter; + +@Getter +public class NewProductRequest { + + private String name; + private int price; + + private NewProductRequest() { + } + + public NewProductRequest(final String name, final int price) { + this.name = name; + this.price = price; + } + + public Product toDomain() { + return new Product(null, name, price); + } + +} diff --git a/spring/src/main/java/com/jscode/spring/product/dto/ProductListResponse.java b/spring/src/main/java/com/jscode/spring/product/dto/ProductListResponse.java new file mode 100644 index 0000000..eacaadb --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/product/dto/ProductListResponse.java @@ -0,0 +1,19 @@ +package com.jscode.spring.product.dto; + +import java.util.List; +import lombok.Getter; + +@Getter +public class ProductListResponse { + + private List productResponses; + + public ProductListResponse(final List productResponses) { + this.productResponses = productResponses; + } + + public boolean contains(final ProductResponse productResponse) { + return productResponses.contains(productResponse); + } + +} diff --git a/spring/src/main/java/com/jscode/spring/product/dto/ProductResponse.java b/spring/src/main/java/com/jscode/spring/product/dto/ProductResponse.java new file mode 100644 index 0000000..6f2dfde --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/product/dto/ProductResponse.java @@ -0,0 +1,41 @@ +package com.jscode.spring.product.dto; + +import com.jscode.spring.product.domain.Product; +import java.util.Objects; +import lombok.Getter; + +@Getter +public class ProductResponse { + + private Long id; + private String name; + private double price; + + private ProductResponse(final Long id, final String name, final double price) { + this.id = id; + this.name = name; + this.price = price; + } + + public static ProductResponse of(final Product product, final double price) { + return new ProductResponse(product.getId(), product.getName(), price); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ProductResponse that = (ProductResponse) o; + return Double.compare(that.price, price) == 0 && id.equals(that.id) && name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, price); + } + +} diff --git a/spring/src/main/java/com/jscode/spring/product/dto/ProductSaveResponse.java b/spring/src/main/java/com/jscode/spring/product/dto/ProductSaveResponse.java new file mode 100644 index 0000000..da04a84 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/product/dto/ProductSaveResponse.java @@ -0,0 +1,14 @@ +package com.jscode.spring.product.dto; + +import lombok.Getter; + +@Getter +public class ProductSaveResponse { + + private Long savedId; + + public ProductSaveResponse(final Long savedId) { + this.savedId = savedId; + } + +} diff --git a/spring/src/main/java/com/jscode/spring/product/exception/DuplicateNameException.java b/spring/src/main/java/com/jscode/spring/product/exception/DuplicateNameException.java new file mode 100644 index 0000000..6e9f7e9 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/product/exception/DuplicateNameException.java @@ -0,0 +1,13 @@ +package com.jscode.spring.product.exception; + +import com.jscode.spring.advice.BadRequestException; + +public class DuplicateNameException extends BadRequestException { + + private static final String MESSAGE = "동일한 이름의 상품은 저장할 수 없습니다."; + + public DuplicateNameException() { + super(MESSAGE); + } + +} diff --git a/spring/src/main/java/com/jscode/spring/product/exception/ProductNotFoundException.java b/spring/src/main/java/com/jscode/spring/product/exception/ProductNotFoundException.java new file mode 100644 index 0000000..b90c2b2 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/product/exception/ProductNotFoundException.java @@ -0,0 +1,13 @@ +package com.jscode.spring.product.exception; + +import com.jscode.spring.advice.NotFoundException; + +public class ProductNotFoundException extends NotFoundException { + + private static final String MESSAGE = "존재하지 않는 상품입니다."; + + public ProductNotFoundException() { + super(MESSAGE); + } + +} diff --git a/spring/src/main/java/com/jscode/spring/product/repository/ProductRepository.java b/spring/src/main/java/com/jscode/spring/product/repository/ProductRepository.java new file mode 100644 index 0000000..79d8585 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/product/repository/ProductRepository.java @@ -0,0 +1,52 @@ +package com.jscode.spring.product.repository; + +import com.jscode.spring.product.domain.Product; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import org.springframework.stereotype.Repository; + +@Repository +public class ProductRepository { + + private static final List store = new ArrayList<>(); + private static final AtomicLong counter = new AtomicLong(3L); + + static { + store.add(new Product(1L, "컴퓨터", 3_000_000)); + store.add(new Product(2L, "키보드", 100_000)); + store.add(new Product(3L, "마우스", 50_000)); + } + + public List findAll() { + return Collections.unmodifiableList(store); + } + + public List findAllByName(final String name) { + return store.stream() + .filter(product -> product.isSameName(name)) + .collect(Collectors.toUnmodifiableList()); + } + + public Optional findById(final Long id) { + return store.stream() + .filter(product -> product.isSameId(id)) + .findAny(); + } + + public Long save(final Product product) { + product.generateId(counter.incrementAndGet()); + store.add(product); + return product.getId(); + } + + public Optional findByName(final String name) { + return store.stream() + .filter(product -> product.isSameName(name)) + .findAny(); + } + +} diff --git a/spring/src/main/java/com/jscode/spring/product/service/ProductService.java b/spring/src/main/java/com/jscode/spring/product/service/ProductService.java new file mode 100644 index 0000000..b30e024 --- /dev/null +++ b/spring/src/main/java/com/jscode/spring/product/service/ProductService.java @@ -0,0 +1,76 @@ +package com.jscode.spring.product.service; + +import com.jscode.spring.exchange.service.ExchangeRatesService; +import com.jscode.spring.product.domain.MonetaryUnit; +import com.jscode.spring.product.domain.Product; +import com.jscode.spring.product.dto.NewProductRequest; +import com.jscode.spring.product.dto.ProductListResponse; +import com.jscode.spring.product.dto.ProductResponse; +import com.jscode.spring.product.exception.DuplicateNameException; +import com.jscode.spring.product.exception.ProductNotFoundException; +import com.jscode.spring.product.repository.ProductRepository; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; + +@Service +public class ProductService { + + private final ExchangeRatesService exchangeRatesService; + + private final ProductRepository productRepository; + + public ProductService(final ProductRepository productRepository, final ExchangeRatesService exchangeRatesService) { + this.productRepository = productRepository; + this.exchangeRatesService = exchangeRatesService; + } + + public Long saveProduct(final NewProductRequest newProductRequest) { + Product product = newProductRequest.toDomain(); + if (productRepository.findByName(product.getName()).isPresent()) { + throw new DuplicateNameException(); + } + return productRepository.save(product); + } + + public ProductListResponse findAllByName(@Nullable final String name, @Nullable final String monetaryUnit) { + List products = productRepository.findAllByName(name); + if (products.isEmpty()) { + throw new ProductNotFoundException(); + } + return new ProductListResponse(createConvertedPriceProducts(monetaryUnit, products)); + } + + public ProductListResponse findAll(final String monetaryUnit) { + List products = productRepository.findAll(); + return new ProductListResponse(createConvertedPriceProducts(monetaryUnit, products)); + } + + private List createConvertedPriceProducts(final String monetaryUnit, + final List products) { + List productResponses = new ArrayList<>(); + for (Product product : products) { + double convertedPrice = convertPriceKrwTo(monetaryUnit, product); + ProductResponse productResponse = ProductResponse.of(product, convertedPrice); + productResponses.add(productResponse); + } + return productResponses; + } + + public ProductResponse findProductById(final Long productId, final String monetaryUnit) { + Product product = productRepository.findById(productId) + .orElseThrow(ProductNotFoundException::new); + double convertedPrice = convertPriceKrwTo(monetaryUnit, product); + return ProductResponse.of(product, convertedPrice); + } + + private double convertPriceKrwTo(final String monetaryUnit, final Product product) { + if (Objects.isNull(monetaryUnit)) { + return product.getPrice(); + } + return exchangeRatesService.convertKrwTo(MonetaryUnit.valueOf(monetaryUnit), product.getPrice()); + } + +} diff --git a/spring/src/main/java/com/jscode/spring/controller/TestController.java b/spring/src/main/java/com/jscode/spring/test/controller/TestController.java similarity index 94% rename from spring/src/main/java/com/jscode/spring/controller/TestController.java rename to spring/src/main/java/com/jscode/spring/test/controller/TestController.java index 4626f3a..449ae18 100644 --- a/spring/src/main/java/com/jscode/spring/controller/TestController.java +++ b/spring/src/main/java/com/jscode/spring/test/controller/TestController.java @@ -1,4 +1,4 @@ -package com.jscode.spring.controller; +package com.jscode.spring.test.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; diff --git a/spring/src/main/resources/application.properties b/spring/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/spring/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/spring/src/main/resources/application.yml b/spring/src/main/resources/application.yml new file mode 100644 index 0000000..e69de29 diff --git a/spring/src/test/java/com/jscode/spring/exchange/config/ExchangesYamlReadTest.java b/spring/src/test/java/com/jscode/spring/exchange/config/ExchangesYamlReadTest.java new file mode 100644 index 0000000..69c5567 --- /dev/null +++ b/spring/src/test/java/com/jscode/spring/exchange/config/ExchangesYamlReadTest.java @@ -0,0 +1,18 @@ +package com.jscode.spring.exchange.config; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ExchangesYamlReadTest { + + @Autowired + ExchangeYamlRead exchangesYamlRead; + + @Test + void getKey() { + Assertions.assertThat(exchangesYamlRead.getKey()).isNotNull(); + } +} \ No newline at end of file diff --git a/spring/src/test/java/com/jscode/spring/exchange/service/DollarRateServiceTest.java b/spring/src/test/java/com/jscode/spring/exchange/service/DollarRateServiceTest.java new file mode 100644 index 0000000..6f7bd09 --- /dev/null +++ b/spring/src/test/java/com/jscode/spring/exchange/service/DollarRateServiceTest.java @@ -0,0 +1,30 @@ +package com.jscode.spring.exchange.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.jscode.spring.product.domain.MonetaryUnit; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DollarRateServiceTest { + + @Autowired + ExchangeRatesService exchangeRatesService; + + @Test + @DisplayName("KRW to KRW 변환 성공 테스트") + void convertKrwToKrw_success() { + double convertedPrice = exchangeRatesService.convertKrwTo(MonetaryUnit.KRW, 10000); + assertThat(convertedPrice).isEqualTo(10000.0); + } + + @Test + @DisplayName("KRW to USD 변환 성공 테스트") + void convertKrwToUsd_success() { + double convertedPrice = exchangeRatesService.convertKrwTo(MonetaryUnit.KRW, 10000); + assertThat(convertedPrice).isEqualTo(10000.0); + } +} \ No newline at end of file diff --git a/spring/src/test/java/com/jscode/spring/product/domain/ProductTest.java b/spring/src/test/java/com/jscode/spring/product/domain/ProductTest.java new file mode 100644 index 0000000..63c3ecd --- /dev/null +++ b/spring/src/test/java/com/jscode/spring/product/domain/ProductTest.java @@ -0,0 +1,30 @@ +package com.jscode.spring.product.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ProductTest { + + Product product; + + @BeforeEach + void setUp() { + product = new Product(1L, "test", 3000); + } + + @Test + @DisplayName("동일 상품 id 확인") + void contains() { + assertThat(product.isSameId(1L)).isTrue(); + } + + @Test + @DisplayName("동일 상품 이름 조회") + void isSameName() { + assertThat(product.isSameName("test")).isTrue(); + } + +} \ No newline at end of file diff --git a/spring/src/test/java/com/jscode/spring/product/repository/ProductRepositoryTest.java b/spring/src/test/java/com/jscode/spring/product/repository/ProductRepositoryTest.java new file mode 100644 index 0000000..5eca8a9 --- /dev/null +++ b/spring/src/test/java/com/jscode/spring/product/repository/ProductRepositoryTest.java @@ -0,0 +1,39 @@ +package com.jscode.spring.product.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.jscode.spring.product.domain.Product; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ProductRepositoryTest { + + ProductRepository productRepository; + + @BeforeEach + void setUp() { + productRepository = new ProductRepository(); + } + + @Test + @DisplayName("전체 상품 조회 테스트") + void findAll() { + List products = productRepository.findAll(); + assertThat(products.size()).isNotZero(); + } + + @Test + @DisplayName("특정 상품 조회 성공 테스트") + void findById_success() { + Product product = productRepository.findById(1L).get(); + assertThat(product.getId()).isEqualTo(1L); + } + + @Test + @DisplayName("특정 상품 조회 실패시 null값 반환 테스트") + void findById_fail_withNull() { + assertThat(productRepository.findById(1111111111L)).isEmpty(); + } +} \ No newline at end of file diff --git a/spring/src/test/java/com/jscode/spring/product/service/ProductServiceTest.java b/spring/src/test/java/com/jscode/spring/product/service/ProductServiceTest.java new file mode 100644 index 0000000..062455d --- /dev/null +++ b/spring/src/test/java/com/jscode/spring/product/service/ProductServiceTest.java @@ -0,0 +1,115 @@ +package com.jscode.spring.product.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.jscode.spring.exchange.service.ExchangeRatesService; +import com.jscode.spring.product.domain.MonetaryUnit; +import com.jscode.spring.product.domain.Product; +import com.jscode.spring.product.dto.NewProductRequest; +import com.jscode.spring.product.dto.ProductListResponse; +import com.jscode.spring.product.dto.ProductResponse; +import com.jscode.spring.product.exception.DuplicateNameException; +import com.jscode.spring.product.exception.ProductNotFoundException; +import com.jscode.spring.product.repository.ProductRepository; +import org.junit.jupiter.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.context.SpringBootTest; + +@SpringBootTest +class ProductServiceTest { + + @Autowired + ProductService productService; + + @Autowired + ExchangeRatesService exchangeRatesService; + + @Autowired + ProductRepository productRepository; + + @Test + @DisplayName("상품 저장 성공 테스트") + void saveProduct_success() { + Long id = productService.saveProduct(new NewProductRequest("test", 3000)); + Product product = productRepository.findById(id).get(); + + Assertions.assertAll( + () -> assertThat(product.getName()).isEqualTo("test"), + () -> assertThat(product.getPrice()).isEqualTo(3000) + ); + } + + @Test + @DisplayName("없는 name에 대한 전체 상품 조회 실패 테스트") + void findAll_fail_withInvalidName() { + String name = "nothing"; + + assertThatThrownBy(() -> productService.findAllByName(name, null)) + .isInstanceOf(ProductNotFoundException.class) + .hasMessageContaining("존재하지 않는 상품입니다."); + } + + @Test + @DisplayName("전체 상품 조회 성공 테스트") + void findAll_success() { + ProductResponse productResponse1 = ProductResponse.of(new Product(1L, "컴퓨터", 3_000_000), 3000000); + ProductResponse productResponse2 = ProductResponse.of(new Product(2L, "키보드", 100_000), 100000); + ProductResponse productResponse3 = ProductResponse.of(new Product(3L, "마우스", 50_000), 50000); + + ProductListResponse products = productService.findAll( null); + + Assertions.assertAll( + () -> assertThat(products.contains(productResponse1)).isTrue(), + () -> assertThat(products.contains(productResponse2)).isTrue(), + () -> assertThat(products.contains(productResponse3)).isTrue() + ); + } + + @Test + @DisplayName("동일 이름 상품 저장시 예외 발생 테스트") + void saveDuplicateNameProduct_fail_withException() { + NewProductRequest request1 = new NewProductRequest("sameName", 3000); + NewProductRequest request2 = new NewProductRequest("sameName", 5000); + + productService.saveProduct(request1); + + assertThatThrownBy(() -> productService.saveProduct(request2)) + .isInstanceOf(DuplicateNameException.class) + .hasMessageContaining("동일한 이름의 상품은 저장할 수 없습니다."); + } + + @Test + @DisplayName("단순 상품 ID 조회") + void findProductById_success() { + Long id = productService.saveProduct(new NewProductRequest("basicTest1", 3000)); + + ProductResponse productById = productService.findProductById(id, null); + + assertThat(productById.getPrice()).isEqualTo(3000.0); + } + + @Test + @DisplayName("상품 ID 및 KRW 단위로 조회 성공 테스트") + void findProductById_success_withKRW_monetaryUnit() { + Long id = productService.saveProduct(new NewProductRequest("test1", 3000)); + + ProductResponse productByName = productService.findProductById(id, "KRW"); + + assertThat(productByName.getPrice()).isEqualTo(3000.0); + } + + @Test + @DisplayName("상품 ID 및 USD 단위로 조회 성공 테스트") + void findProductById_success_withUSD_monetaryUnit() { + Long id = productService.saveProduct(new NewProductRequest("test2", 10000)); + double usdPrice = exchangeRatesService.convertKrwTo(MonetaryUnit.USD, 10000); + + ProductResponse productByName = productService.findProductById(id, "USD"); + + assertThat(productByName.getPrice()).isEqualTo(usdPrice); + } + +} \ No newline at end of file