-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[1단계 - 상품 관리 기능] 망고(고재철) 미션 제출합니다. (#185)
* 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
1 parent
f0c749d
commit 56d2887
Showing
26 changed files
with
1,099 additions
and
142 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 분리? |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
40
src/main/java/cart/product/controller/ExceptionAdvice.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
51
src/main/java/cart/product/controller/ProductRestController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.