-
Notifications
You must be signed in to change notification settings - Fork 117
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Initial commit * feat: set up the project * 충남대 BE_김기웅_1주차 과제(1단계, 2단계, 3단계) (#82) * docs: README 작성 * feat(Product): 상품 클래스 생성 * feat(ProductController): 모든 상품 조회 기능 * feat(ProductController): ID로 상품 조회 기능 * feat(ProductController): 상품 추가 기능 * feat(ProductController): 상품 삭제 기능 * feat(ProductController): 상품 정보 수정 기능 * docs: README 파일에 STEP2 기능 추가 * feat(productManagement.html): 관리자 페이지 템플릿 구현 * feat(ProductViewController): admin 페이지 상품 전체 조회 기능 * feat(productForm.html): 상품 등록, 수정 폼 구현 * feat(ProductViewController): 상품 등록, 수정 기능 구현 * feat(productForm.html): 상품 등록, 수정 기능 구현 * feat(productForm.html): 상품 삭제 기능 구현 * fix(ProductViewController): 중복 ID 상품 add 시도 후 상품 수정 페이지로 리다이렉션 되는 버그 수정 (admin 메인 페이지로 리다이렉션) * fix(productForm.html): 상품 추가 시 1 이상의 ID만 입력 가능하도록 수정 * chore: 데이터베이스 환경설정 * docs(README.md): step3 기능 목록 추가 * feat(Application): DB 초기화 - JdbcTemplate 사용 - id가 1이상이 되도록 DB레벨에서 강제 * feat(productRepository): 상품 레파지토리 클래스 생성 - JdbcTemplate 객체 생성 - SQL Query 준비 (바인딩 필요) * feat(productRepository): 상품 레파지토리 클래스 DB CRUD 로직 추가 * feat(ProductController): 메모리를 레파지토리 객체로 대체 - ProductRepository 주입 * feat(ProductController): 핸들러메서드의 로직 중복 제거 - create, update, delete시 id로 상품을 검색해서 존재하는지 확인하는 로직을 isProductExists 메서드로 추출 * style: 자바 코드 컨벤션 적용 * style(README.md): 빈 줄 삭제 * refactor: Application에서 테이블 생성 쿼리 분리 - schema.sql 생성 * refactor: 프로젝트 구조 변경 - 역할에 따라 패키지 구분 * Initial commit --------- Co-authored-by: 박재성(Jason) <[email protected]>
- Loading branch information
Showing
11 changed files
with
436 additions
and
2 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,43 @@ | ||
# spring-gift-wishlist | ||
# spring-gift-product | ||
|
||
# 1주차 - 상품관리 - 스프링 입문 | ||
|
||
# 🚀 1단계 - 상품 API | ||
|
||
## 과제 진행 요구 사항 | ||
- 미션은 선물하기 상품 관리 저장소를 포크하고 클론하는 것으로 시작한다. | ||
- 저장소에 GitHub 사용자 이름으로 브랜치가 생성되었는지 확인한다. | ||
- 저장소를 내 계정으로 포크한다. | ||
- 기능을 구현하기 전 README.md에 구현할 기능 목록을 정리해 추가한다. | ||
- Git의 커밋 단위는 앞 단계에서 README.md에 정리한 기능 목록 단위로 추가한다. | ||
- AngularJS Git Commit Message Conventions을 참고해 커밋 메시지를 작성한다. | ||
|
||
## 기능 요구 사항 | ||
- 상품을 조회, 추가, 수정, 삭제할 수 있는 간단한 HTTP API를 구현한다. | ||
- HTTP 요청과 응답은 JSON 형식으로 주고받는다. | ||
- 현재는 별도의 데이터베이스가 없으므로 적절한 자바 컬렉션 프레임워크를 사용하여 메모리에 저장한다. | ||
|
||
## 개발 할 기능 | ||
- [x] 상품을 표현할 Product 만들기 | ||
- [x] 상품 조회 API 구현 | ||
- [x] 상품 추가 API 구현 | ||
- [x] 상품 수정 API 구현 | ||
- [x] 상품 삭제 API 구현 | ||
|
||
|
||
# 🚀 2단계 - 관리자 화면 | ||
|
||
## 개발 할 기능 | ||
- [x] 상품 조회 기능 | ||
- [x] 상품 추가 기능 | ||
- [x] 상품 수정 기능 | ||
- [x] 상품 삭제 기능 | ||
|
||
# 🚀 3단계 - 데이터베이스 적용 | ||
## 개발 할 기능 | ||
- [x] DB 초기화 (앱 실행 시점) | ||
- [x] 상품 전체 조회 (Read) | ||
- [x] 특정 id의 상품 조회 (Read) | ||
- [x] 상품 추가 기능 (Create) | ||
- [x] 상품 수정 기능 (Update) | ||
- [x] 상품 삭제 기능 (Delete) |
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
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,48 @@ | ||
package gift.DTO; | ||
|
||
public class Product { | ||
|
||
private Long id; | ||
private String name; | ||
private int price; | ||
private String imageUrl; | ||
|
||
public Product(Long id, String name, int price, String imageUrl) { | ||
this.id = id; | ||
this.name = name; | ||
this.price = price; | ||
this.imageUrl = imageUrl; | ||
} | ||
|
||
public Long getId() { | ||
return id; | ||
} | ||
|
||
public void setId(Long id) { | ||
this.id = id; | ||
} | ||
|
||
public String getName() { | ||
return name; | ||
} | ||
|
||
public void setName(String name) { | ||
this.name = name; | ||
} | ||
|
||
public int getPrice() { | ||
return price; | ||
} | ||
|
||
public void setPrice(int price) { | ||
this.price = price; | ||
} | ||
|
||
public String getImageUrl() { | ||
return imageUrl; | ||
} | ||
|
||
public void setImageUrl(String imageUrl) { | ||
this.imageUrl = imageUrl; | ||
} | ||
} |
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,96 @@ | ||
package gift.controller; | ||
|
||
import gift.DTO.Product; | ||
import gift.repository.ProductRepository; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.*; | ||
|
||
@RestController | ||
@RequestMapping("/api/products") | ||
public class ProductController { | ||
|
||
private final ProductRepository productRepository; | ||
|
||
// 생성자를 사용하여 ProductRepository 초기화 | ||
public ProductController(ProductRepository productRepository) { | ||
this.productRepository = productRepository; | ||
} | ||
|
||
/** | ||
* 모든 상품 조회 | ||
* | ||
* @return 모든 상품 목록 | ||
*/ | ||
@GetMapping | ||
public ResponseEntity<List<Product>> getProducts() { | ||
List<Product> products = productRepository.getAllProducts(); | ||
return new ResponseEntity<>(products, HttpStatus.OK); | ||
} | ||
|
||
/** | ||
* id로 특정 상품 조회 | ||
* | ||
* @param id 조회할 상품의 id | ||
* @return 조회된 상품 객체와 200 OK, 해당 id가 없으면 404 NOT FOUND | ||
*/ | ||
@GetMapping("/{id}") | ||
public ResponseEntity<Product> getProductById(@PathVariable Long id) { | ||
Optional<Product> product = productRepository.getProductById(id); | ||
return product.map(value -> new ResponseEntity<>(value, HttpStatus.OK)) | ||
.orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); | ||
} | ||
|
||
/** | ||
* 새로운 상품 추가 | ||
* | ||
* @param product 추가할 상품 | ||
* @return 같은 ID의 상품이 존재하지 않으면 201 Created, 아니면 400 Bad Request | ||
*/ | ||
@PostMapping | ||
public ResponseEntity<Product> addProduct(@RequestBody Product product) { | ||
if (isProductExists(product.getId())) { | ||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST); // 400 Bad Request | ||
} | ||
productRepository.addProduct(product); | ||
return new ResponseEntity<>(product, HttpStatus.CREATED); // 201 Created | ||
} | ||
|
||
/** | ||
* 상품 삭제 | ||
* | ||
* @param id 삭제할 상품의 id | ||
* @return 삭제에 성공하면 204 NO CONTENT, 해당 ID의 상품이 없으면 404 NOT FOUND | ||
*/ | ||
@DeleteMapping("/{id}") | ||
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) { | ||
if (!isProductExists(id)) { | ||
return new ResponseEntity<>(HttpStatus.NOT_FOUND); | ||
} | ||
productRepository.deleteProduct(id); | ||
return new ResponseEntity<>(HttpStatus.NO_CONTENT); | ||
} | ||
|
||
/** | ||
* 상품 정보 수정 | ||
* | ||
* @param id 수정할 상품의 id | ||
* @param updatedProduct 새로운 상품 객체 | ||
* @return 상품 정보 수정에 성공하면 200 OK, 해당 id의 상품이 없으면 404 NOT FOUND | ||
*/ | ||
@PutMapping("/{id}") | ||
public ResponseEntity<Product> updateProduct(@PathVariable Long id, | ||
@RequestBody Product updatedProduct) { | ||
if (!isProductExists(id)) { | ||
return new ResponseEntity<>(HttpStatus.NOT_FOUND); // 404 Not Found | ||
} | ||
productRepository.updateProduct(updatedProduct); | ||
return new ResponseEntity<>(updatedProduct, HttpStatus.OK); // 200 OK | ||
} | ||
|
||
private boolean isProductExists(Long id) { | ||
return productRepository.getProductById(id).isPresent(); | ||
} | ||
} |
103 changes: 103 additions & 0 deletions
103
src/main/java/gift/controller/ProductViewController.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,103 @@ | ||
package gift.controller; | ||
|
||
import gift.DTO.Product; | ||
import java.util.List; | ||
import org.springframework.stereotype.Controller; | ||
import org.springframework.ui.Model; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.ModelAttribute; | ||
import org.springframework.web.bind.annotation.PathVariable; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.client.HttpClientErrorException; | ||
import org.springframework.web.client.RestTemplate; | ||
|
||
@Controller | ||
@RequestMapping("/admin") | ||
public class ProductViewController { | ||
|
||
private final RestTemplate restTemplate; | ||
|
||
public ProductViewController() { | ||
this.restTemplate = new RestTemplate(); | ||
} | ||
|
||
@GetMapping | ||
public String showProductManagementPage(Model model) { | ||
try { | ||
List<Product> products = restTemplate.getForObject("http://localhost:8080/api/products", | ||
List.class); | ||
if (products == null) { | ||
model.addAttribute("products", List.of()); | ||
return "productManagement"; // 템플릿 파일 이름 | ||
} | ||
model.addAttribute("products", products); | ||
} catch (Exception e) { | ||
model.addAttribute("error", "Failed to load products: " + e.getMessage()); | ||
} | ||
return "productManagement"; // admin 템플릿 파일 이름 | ||
} | ||
|
||
@GetMapping("/form") | ||
public String showProductForm(@RequestParam(required = false) Long id, Model model) { | ||
if (id == null) { | ||
model.addAttribute("product", new Product(0L, "", 0, "")); | ||
return "productForm"; // 폼 템플릿 파일 | ||
} | ||
|
||
try { | ||
Product product = restTemplate.getForObject("http://localhost:8080/api/products/" + id, | ||
Product.class); | ||
model.addAttribute("product", product); | ||
} catch (HttpClientErrorException e) { | ||
model.addAttribute("error", "Product not found: " + e.getMessage()); | ||
return "redirect:/admin"; | ||
} | ||
|
||
return "productForm"; // 폼 템플릿 파일 | ||
} | ||
|
||
@PostMapping("/form") | ||
public String saveProduct(@ModelAttribute Product product, | ||
@RequestParam(required = false, name = "_method") String method, Model model) { | ||
if (isUpdateMethod(method)) { | ||
return updateProduct(product, model); | ||
} | ||
return createProduct(product, model); | ||
} | ||
|
||
private boolean isUpdateMethod(String method) { | ||
return "put".equalsIgnoreCase(method); | ||
} | ||
|
||
private String updateProduct(Product product, Model model) { | ||
try { | ||
restTemplate.put("http://localhost:8080/api/products/" + product.getId(), product); | ||
} catch (Exception e) { | ||
e.printStackTrace(); | ||
model.addAttribute("error", "Failed to save product: " + e.getMessage()); | ||
return "productForm"; | ||
} | ||
return "redirect:/admin"; | ||
} | ||
|
||
private String createProduct(Product product, Model model) { | ||
try { | ||
restTemplate.postForObject("http://localhost:8080/api/products", product, | ||
Product.class); | ||
} catch (HttpClientErrorException.BadRequest e) { | ||
model.addAttribute("error", "Product ID already exists: " + e.getMessage()); | ||
} catch (Exception e) { | ||
e.printStackTrace(); | ||
model.addAttribute("error", "Failed to save product: " + e.getMessage()); | ||
} | ||
return "redirect:/admin"; | ||
} | ||
|
||
@GetMapping("/delete/{id}") | ||
public String deleteProduct(@PathVariable Long id) { | ||
restTemplate.delete("http://localhost:8080/api/products/" + id); | ||
return "redirect:/admin"; | ||
} | ||
} |
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,64 @@ | ||
package gift.repository; | ||
|
||
import gift.DTO.Product; | ||
import java.sql.ResultSet; | ||
import java.sql.SQLException; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.jdbc.core.JdbcTemplate; | ||
import org.springframework.jdbc.core.RowMapper; | ||
import org.springframework.stereotype.Repository; | ||
|
||
@Repository | ||
public class ProductRepository { | ||
|
||
@Autowired | ||
private JdbcTemplate jdbcTemplate; | ||
|
||
private static final String INSERT_PRODUCT_SQL = "INSERT INTO product (id, name, price, image_url) VALUES (?, ?, ?, ?)"; | ||
private static final String SELECT_ALL_PRODUCTS_SQL = "SELECT * FROM product"; | ||
private static final String SELECT_PRODUCT_BY_ID_SQL = "SELECT * FROM product WHERE id = ?"; | ||
private static final String UPDATE_PRODUCT_SQL = "UPDATE product SET name = ?, price = ?, image_url = ? WHERE id = ?"; | ||
private static final String DELETE_PRODUCT_SQL = "DELETE FROM product WHERE id = ?"; | ||
|
||
public List<Product> getAllProducts() { | ||
return jdbcTemplate.query(SELECT_ALL_PRODUCTS_SQL, new ProductRowMapper()); | ||
} | ||
|
||
public Optional<Product> getProductById(Long id) { | ||
try { | ||
return Optional.ofNullable( | ||
jdbcTemplate.queryForObject(SELECT_PRODUCT_BY_ID_SQL, new ProductRowMapper(), id)); | ||
} catch (Exception e) { | ||
return Optional.empty(); | ||
} | ||
} | ||
|
||
public void addProduct(Product product) { | ||
jdbcTemplate.update(INSERT_PRODUCT_SQL, product.getId(), product.getName(), | ||
product.getPrice(), product.getImageUrl()); | ||
} | ||
|
||
public void updateProduct(Product product) { | ||
jdbcTemplate.update(UPDATE_PRODUCT_SQL, product.getName(), product.getPrice(), | ||
product.getImageUrl(), product.getId()); | ||
} | ||
|
||
public void deleteProduct(Long id) { | ||
jdbcTemplate.update(DELETE_PRODUCT_SQL, id); | ||
} | ||
|
||
private static class ProductRowMapper implements RowMapper<Product> { | ||
|
||
@Override | ||
public Product mapRow(ResultSet rs, int rowNum) throws SQLException { | ||
return new Product( | ||
rs.getLong("id"), | ||
rs.getString("name"), | ||
rs.getInt("price"), | ||
rs.getString("image_url") | ||
); | ||
} | ||
} | ||
} |
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,10 @@ | ||
spring.application.name=spring-gift | ||
# h2-console ??? ?? | ||
spring.h2.console.enabled=true | ||
# db url | ||
spring.datasource.url=jdbc:h2:mem:test | ||
spring.datasource.driver-class-name=org.h2.Driver | ||
spring.datasource.username=sa | ||
spring.datasource.password= | ||
spring.datasource.initialization-mode=always | ||
spring.jpa.hibernate.ddl-auto=update |
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,6 @@ | ||
CREATE TABLE IF NOT EXISTS product ( | ||
id BIGINT PRIMARY KEY CHECK (id >= 1), | ||
name VARCHAR(255) NOT NULL, | ||
price INT NOT NULL, | ||
image_url VARCHAR(255) | ||
); |
Oops, something went wrong.