Skip to content

Commit

Permalink
[1단계 - 상품 관리 기능] 망고(고재철) 미션 제출합니다. (#185)
Browse files Browse the repository at this point in the history
* docs: 기능목록 작성

Co-authored-by: go-jaecheol <gojaech@naver.com>

* feat: Product 도메인 추가

Co-authored-by: go-jaecheol <gojaech@naver.com>

* refactor: Product 필드 원시값 포장

Co-authored-by: go-jaecheol <gojaech@naver.com>

* feat: JdbcProductDao 구현

Co-authored-by: go-jaecheol <gojaech@naver.com>

* feat: DB 테이블 생성

Co-authored-by: go-jaecheol <gojaech@naver.com>

* feat: 상품 목록 서비스 구현

Co-authored-by: go-jaecheol <gojaech@naver.com>

* test: Product 도메인 테스트 추가

Co-authored-by: go-jaecheol <gojaech@naver.com>

* test: JdbcProductDao 테스트 추가

Co-authored-by: go-jaecheol <gojaech@naver.com>

* test: ProductListService 테스트 추가

Co-authored-by: go-jaecheol <gojaech@naver.com>

* feat: 상품 목록 페이지 연동 기능 구현

Co-authored-by: go-jaecheol <gojaech@naver.com>

* docs: 상품 관리 CRUD API 기능 작성

Co-authored-by: go-jaecheol <gojaech@naver.com>

* feat: ProductDao 기능 추가

아래 메서드들 구현
- findByName
- deleteByID
- update

Co-authored-by: go-jaecheol <gojaech@naver.com>

* test: ProductDao 테스트 추가

아래 메서드들 테스트
- findByName
- deleteByID
- update

Co-authored-by: go-jaecheol <gojaech@naver.com>

* feat: ProductListService에 create,update 추가

Co-authored-by: go-jaecheol <gojaech@naver.com>

* feat: ProductListService에 create,update 추가

Co-authored-by: go-jaecheol <gojaech@naver.com>

* refactor: ProductListService update 메서드 시그니쳐 변경

Co-authored-by: go-jaecheol <gojaech@naver.com>

* docs: 상품 관리 CRUD API url 및 기능 변경

Co-authored-by: go-jaecheol <gojaech@naver.com>

* feat: ProductListService에 delete 기능 추가

Co-authored-by: go-jaecheol <gojaech@naver.com>

* feat: 상품 관리 CRUD API 구현

Co-authored-by: go-jaecheol <gojaech@naver.com>

* refactor: RequestProductDto ResponseProductDto 분리

Co-authored-by: go-jaecheol <gojaech@naver.com>

* feat: 관리자 도구 페이지 연동

Co-authored-by: go-jaecheol <gojaech@naver.com>

* test: 상품 통합 테스트 추가

Co-authored-by: go-jaecheol <gojaech@naver.com>

* test: 상품 통합 테스트 추가

Co-authored-by: go-jaecheol <gojaech@naver.com>

* feat: 에러 발생시 400 상태 코드 반환하는 기능 추가

Co-authored-by: go-jaecheol <gojaech@naver.com>

* docs: 1단계 리팩토링 요구사항 정리

* refactor: Controller 네이밍 변경

* refactor: REST API의 URI path 수정

* refactor: Update의 반환 상태 코드 변경

* refactor: 사용하지 않는 클래스 및 구문 제거

* refactor: @Valid 어노테이션 사용

* refactor: Domain 객체에 DTO 객체를 넘겨주지 않도록 변경

* refactor: findById() Optional 처리하도록 변경

* feat: ExceptionHandler 추가

* refactor: 매직넘버 상수 처리

* refactor: 사용하지 않는 findByName() 메서드 삭제

* test: fake 객체의 저장, 조회가 정상적으로 동작하는지 확인하도록 변경

* test: service 테스트 추가

* test: IntegrationTest에서 DB에 값이 실제로 저장되는지 확인하도록 변경

* test: Product 테스트 추가

* test: JdbcProductDao 테스트 변경 및 findById 테스트 추가

---------

Co-authored-by: echo <echo.backend@gmail.com>
  • Loading branch information
Go-Jaecheol and echo authored May 1, 2023
1 parent f0c749d commit 56d2887
Show file tree
Hide file tree
Showing 26 changed files with 1,099 additions and 142 deletions.
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,63 @@
# jwp-shopping-cart

## 1단계 기능 요구사항

### 상품 목록 페이지 연동

- [x] 상품은 아래와 같은 필드를 가지고 있다.
- ID(long)
- Name(String)
- Image(String)
- Price(int)

- [x] 컨트롤러는 아래와 같은 요청을 처리한다.
- [x] '/' 경로로 get 요청이 들어올 경우, 상품 목록 페이지를 반환한다.

- [x] 상품서비스는 아래와 같은 기능을 한다.
- [x] 전체 상품 목록을 DB에서 가져온다.
- [x] 전체 상품 목록을 컨트롤러에 보낸다.

### 상품 관리 CRUD API 작성

- [x] 상품 생성
- '/admin/product/create' 경로로 post 요청이 들어올 경우, HttpStatus.CREATED 반환한다.
- [x] Controller에서는 RequestParam으로 받은 데이터를 DTO로 변환하여 Service에게 전달
- [x] Service에서는 DTO를 Product로 변환해서 DAO에 전달
- [x] DAO는 데이터베이스에 Product INSERT
- [x] 상품 목록 조회
- '/admin/product/list' 경로로 get 요청이 들어올 경우, ProductListDto를 반환한다.
- [x] 상품 수정
- '/admin/product/update/{id}' 경로로 put 요청이 들어올 경우, 변경된 ProductDto를 반환한다.
- [x] Controller에서는 PathVariable로 id와 RequestBody로 변경될 데이터를 받아 Service에게 전달
- [x] Service에서는 id로 찾은 Product와 변경된 값으로 새로운 Product 생성후 update
- [x] DAO는 데이터베이스에 새로운 Product INSERT
- [x] Controller는 Service에서 받은 새로운 ProductDTO 반환한다.
- [x] 상품 삭제
- '/admin/product/delete/{id}' 경로로 delete 요청이 들어올 경우, HttpStatus.NO_CONTENT 반환한다.
- [x] Controller에서는 PathVariable로 id를 서비스에 전달
- [x] Service에서는 id로 DAO.deleteByID로 DB에서 Product를 삭제한다.

### 관리자 도구 페이지 연동

---

## 1단계 리팩토링 요구사항

- [x] Controller 네이밍 변경
- [x] path에서 http method에 대한 키워드 삭제
- [x] Update의 반환 상태 코드 변경
- [x] @Valid 어노테이션 사용
- [x] Domain 객체에 DTO 객체를 넘겨주지 않도록 변경
- [x] 매직넘버 상수화
- [x] 사용하지 않는 클래스 삭제
- [x] 필요없는 this 제거
- [x] ExceptionHandler 추가
- [x] findById() Optional 처리
- [x] fake 객체의 저장, 조회가 정상적으로 동작하는지 확인하도록 변경
- [x] service 테스트 추가
- [x] 필요없는 출력문 제거
- [x] Integration Test에서 DB에 값이 실제로 저장되는지 확인
- [x] update, delete 테스트 전에 테스트를 위한 데이터를 추가한 후 검증하도록 변경
- [x] Product 테스트 추가
- [x] findById() 테스트 추가
- [ ] 엔드포인트간 DTO 분리?
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:4.4.0'
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1 +1 @@
rootProject.name = 'jwp-cart'
rootProject.name = 'jwp-shopping-cart'
40 changes: 40 additions & 0 deletions src/main/java/cart/product/controller/ExceptionAdvice.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package cart.product.controller;

import java.util.NoSuchElementException;
import java.util.stream.Collectors;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class ExceptionAdvice {

private static final String INTERNAL_SERVER_ERROR_MESSAGE = "서버에서 예기치 않은 오류가 발생했습니다.";
private static final String NEW_LINE_DELIMITER = "\n";

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgumentException(final IllegalArgumentException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handle(final MethodArgumentNotValidException e) {
final String errorMessage = e.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(NEW_LINE_DELIMITER));
return ResponseEntity.badRequest().body(errorMessage);
}

@ExceptionHandler(NoSuchElementException.class)
public ResponseEntity<String> handleNoSuchElementException(final NoSuchElementException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
}

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(final Exception e) {
return ResponseEntity.internalServerError().body(INTERNAL_SERVER_ERROR_MESSAGE);
}
}
28 changes: 28 additions & 0 deletions src/main/java/cart/product/controller/PageController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package cart.product.controller;

import cart.product.service.ProductListService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class PageController {

private final ProductListService productListService;

public PageController(final ProductListService productListService) {
this.productListService = productListService;
}

@GetMapping("/")
public String renderProductListPage(final Model model) {
model.addAttribute("products", productListService.display());
return "index";
}

@GetMapping("/admin")
public String renderAdminPage(final Model model) {
model.addAttribute("products", productListService.display());
return "admin";
}
}
51 changes: 51 additions & 0 deletions src/main/java/cart/product/controller/ProductRestController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cart.product.controller;

import cart.product.dto.RequestProductDto;
import cart.product.dto.ResponseProductDto;
import cart.product.service.ProductListService;
import java.util.List;
import javax.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductRestController {

private final ProductListService productListService;

public ProductRestController(final ProductListService productListService) {
this.productListService = productListService;
}

@GetMapping("/products")
@ResponseStatus(HttpStatus.OK)
public List<ResponseProductDto> display() {
return productListService.display();
}

@PostMapping("/products")
@ResponseStatus(HttpStatus.CREATED)
public void create(@RequestBody @Valid final RequestProductDto requestProductDto) {
productListService.create(requestProductDto);
}

@PutMapping("/products/{id}")
@ResponseStatus(HttpStatus.OK)
public ResponseProductDto update(@PathVariable final long id,
@RequestBody @Valid final RequestProductDto requestProductDto) {
return productListService.update(id, requestProductDto);
}

@DeleteMapping("/products/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable final long id) {
productListService.delete(id);
}
}
82 changes: 82 additions & 0 deletions src/main/java/cart/product/dao/JdbcProductDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package cart.product.dao;

import cart.product.domain.Name;
import cart.product.domain.Price;
import cart.product.domain.Product;
import java.sql.PreparedStatement;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;

@Repository
public class JdbcProductDao implements ProductDao {

private final JdbcTemplate jdbcTemplate;
private final SimpleJdbcInsert simpleJdbcInsert;

private final RowMapper<Product> productRowMapper = (resultSet, rowNumber) -> {
long id = resultSet.getLong("id");
String name = resultSet.getString("name");
String image = resultSet.getString("image");
int price = resultSet.getInt("price");

return new Product(id, new Name(name), image, new Price(price));
};

public JdbcProductDao(final JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate)
.withTableName("product_list")
.usingGeneratedKeyColumns("id");
}

public void update(final Product product) {
final String sql = "update product_list set name = ?, image = ?, price = ? where id = ?";
jdbcTemplate.update(con -> {
final PreparedStatement preparedStatement = con.prepareStatement(sql);
preparedStatement.setString(1, product.getName().getValue());
preparedStatement.setString(2, product.getImage());
preparedStatement.setInt(3, product.getPrice().getValue());
preparedStatement.setLong(4, product.getId());
return preparedStatement;
});
}

@Override
public long insert(final Product product) {
final Map<String, Object> parameters = new HashMap<>();
parameters.put("name", product.getName().getValue());
parameters.put("image", product.getImage());
parameters.put("price", product.getPrice().getValue());

return simpleJdbcInsert.executeAndReturnKey(parameters).longValue();
}

@Override
public Optional<Product> findByID(final long id) {
final String sql = "select * from product_list where id = ?";
try {
return Optional.ofNullable(jdbcTemplate.queryForObject(sql, productRowMapper, id));
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}

@Override
public void deleteByID(final long id) {
final String sql = "delete from product_list where id = ?";
jdbcTemplate.update(sql, id);
}

@Override
public List<Product> findAll() {
final String sql = "select * from product_list";
return jdbcTemplate.query(sql, productRowMapper);
}
}
18 changes: 18 additions & 0 deletions src/main/java/cart/product/dao/ProductDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package cart.product.dao;

import cart.product.domain.Product;
import java.util.List;
import java.util.Optional;

public interface ProductDao {

long insert(Product product);

Optional<Product> findByID(long id);

void deleteByID(long id);

void update(Product product);

List<Product> findAll();
}
36 changes: 36 additions & 0 deletions src/main/java/cart/product/domain/Name.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package cart.product.domain;

public class Name {

public static final String EMPTY_PRODUCT_NAME_ERROR = "상품 이름이 없습니다.";
public static final String LONG_NAME_ERROR = "상품 이름은 10 글자를 넘을 수 없습니다";
private static final int LENGTH_UPPER_BOUNDARY = 10;

private final String value;

public Name(final String value) {
validate(value);
this.value = value;
}

private void validate(final String value) {
validateEmpty(value);
validateLength(value);
}

private void validateLength(final String value) {
if (value.length() > LENGTH_UPPER_BOUNDARY) {
throw new IllegalArgumentException(LONG_NAME_ERROR);
}
}

private void validateEmpty(final String value) {
if (value.isEmpty()) {
throw new IllegalArgumentException(EMPTY_PRODUCT_NAME_ERROR);
}
}

public String getValue() {
return value;
}
}
24 changes: 24 additions & 0 deletions src/main/java/cart/product/domain/Price.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cart.product.domain;

public class Price {

public static final String NEGATIVE_PRICE_ERROR = "상품의 가격은 0보다 작을 수 없습니다.";
private static final int LOWER_BOUNDARY = 0;

private final int value;

public Price(final int value) {
validate(value);
this.value = value;
}

private void validate(final int value) {
if (value < LOWER_BOUNDARY) {
throw new IllegalArgumentException(NEGATIVE_PRICE_ERROR);
}
}

public int getValue() {
return value;
}
}
38 changes: 38 additions & 0 deletions src/main/java/cart/product/domain/Product.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package cart.product.domain;

public class Product {

private final Name name;
private final String image;
private final Price price;
private long id;

public Product(final Name name, final String image, final Price price) {
this.name = name;
this.image = image;
this.price = price;
}

public Product(final long id, final Name name, final String image, final Price price) {
this.name = name;
this.image = image;
this.price = price;
this.id = id;
}

public long getId() {
return id;
}

public Name getName() {
return name;
}

public String getImage() {
return image;
}

public Price getPrice() {
return price;
}
}
Loading

0 comments on commit 56d2887

Please sign in to comment.