diff --git a/README.md b/README.md index bcf9abdc9..158236659 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ * HTTP 요청과 응답은 JSON 형식으로 주고받는다. * 현재는 별도의 데이터베이스가 없으므로 적절한 자바 컬렉션 프레임워크를 사용하여 메모리에 저장한다. + +# step1 ## 상품 조회 API ### Request GET /api/products HTTP/1.1 @@ -70,3 +72,18 @@ DELETE /api/products/1 HTTP/1.1 ### Response HTTP/1.1 200 + +# step2 +* 상품을 조회, 추가, 수정, 삭제할 수 있는 관리자 화면을 구현한다. +* Thymeleaf를 사용하여 서버 사이드 렌더링을 구현한다. +* 기본적으로는 HTML 폼 전송 등을 이용한 페이지 이동을 기반으로 하지만, 자바스크립트를 이용한 비동기 작업에 관심이 있다면 이미 만든 상품 API를 이용하여 AJAX 등의 방식을 적용할 수 있다. +* 상품 이미지의 경우, 파일을 업로드하지 않고 URL을 직접 입력한다. +* 상품을 추가하는 버튼을 누르면 모달창이 뜨면서 상세 정보를 입력하도록 생성 + +# step3 +* 자바 컬렉션 프레임워크를 사용하여 메모리에 저장하던 상품 정보를 데이터베이스에 저장한다. +* 메모리에 저장하고 있던 모든 코드를 제거하고 H2 데이터베이스를 사용하도록 변경한다. +* 사용하는 테이블은 애플리케이션이 실행될 때 구축되어야 한다. +* step2 에서 사용했던 html에서 id를 이용해 특정 상품을 검색 할 수 있는 버튼 추가 +* 각 상품마다도 버튼을 누르면 상세정보를 보여주는 페이지로 넘어가도록 구현 +* 상세정보를 보여주는 product-detail.html 추가 \ No newline at end of file diff --git a/src/main/java/gift/Product.java b/src/main/java/gift/Product.java index 5402d592f..50bef83f4 100644 --- a/src/main/java/gift/Product.java +++ b/src/main/java/gift/Product.java @@ -1,17 +1,29 @@ package gift; public class Product { - private long id; + private Long id; private String name; private int price; private String imageUrl; - // Getters, Setters - public long getId() { + // 기본 생성자 + public Product() { + } + + // 매개변수가 있는 생성자 + public Product(Long id, String name, int price, String imageUrl) { + this.id = id; + this.name = name; + this.price = price; + this.imageUrl = imageUrl; + } + + // Getter와 Setter 메서드 + public Long getId() { return id; } - public void setId(long id) { + public void setId(Long id) { this.id = id; } @@ -38,5 +50,4 @@ public String getImageUrl() { public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } - } \ No newline at end of file diff --git a/src/main/java/gift/ProductController.java b/src/main/java/gift/ProductController.java index 04799dae2..fe165c5bc 100644 --- a/src/main/java/gift/ProductController.java +++ b/src/main/java/gift/ProductController.java @@ -1,61 +1,76 @@ package gift; -import jakarta.annotation.PostConstruct; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; @Controller @RequestMapping("/products") public class ProductController { - private final Map products = new HashMap<>(); - private long nextId = 1L; - - @PostConstruct - public void init() { - Product product1 = new Product(); - product1.setId(8146027); - product1.setName("아이스 카페 아메리카노 T"); - product1.setPrice(4500); - product1.setImageUrl("https://st.kakaocdn.net/product/gift/product/20231010111814_9a667f9eccc943648797925498bdd8a3.jpg"); - products.put(product1.getId(), product1); + private final ProductRepository productRepository; + + public ProductController(ProductRepository productRepository) { + this.productRepository = productRepository; } @GetMapping public String getProducts(Model model) { - model.addAttribute("products", new ArrayList<>(products.values())); - model.addAttribute("product", new Product()); // Add empty product object for the form + model.addAttribute("products", productRepository.findAll()); + model.addAttribute("product", new Product()); return "product-list"; } @PostMapping - public String addProduct(@ModelAttribute Product product) { - product.setId(nextId++); - products.put(product.getId(), product); + public String addProduct(@ModelAttribute Product product, RedirectAttributes redirectAttributes) { + try { + productRepository.save(product); + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", "Error adding product: " + e.getMessage()); + } return "redirect:/products"; } @PostMapping("/{id}") - public String updateProduct(@PathVariable Long id, @ModelAttribute Product product) { - if (!products.containsKey(id)) { - throw new IllegalArgumentException("상품이 없습니다."); + public String updateProduct(@PathVariable Long id, @ModelAttribute Product product, RedirectAttributes redirectAttributes) { + try { + product.setId(id); + productRepository.update(product); + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", "Error updating product: " + e.getMessage()); } - product.setId(id); - products.put(id, product); return "redirect:/products"; } @PostMapping("/delete/{id}") - public String deleteProduct(@PathVariable Long id) { - if (!products.containsKey(id)) { - throw new IllegalArgumentException("상품이 없습니다."); + public String deleteProduct(@PathVariable Long id, RedirectAttributes redirectAttributes) { + try { + productRepository.deleteById(id); + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", "Error deleting product: " + e.getMessage()); } - products.remove(id); return "redirect:/products"; } -} + @GetMapping("/view/{id}") + public String getProductDetails(@PathVariable("id") Long id, Model model, RedirectAttributes redirectAttributes) { + try { + Product product = productRepository.findById(id); + model.addAttribute("product", product); + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", "Product not found: " + e.getMessage()); + return "redirect:/products"; + } + return "product-detail"; + } + + @GetMapping("/{id}") + @ResponseBody + public Product getProductById(@PathVariable("id") Long id) { + try { + return productRepository.findById(id); + } catch (Exception e) { + throw new IllegalArgumentException("Product not found: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/gift/ProductRepository.java b/src/main/java/gift/ProductRepository.java new file mode 100644 index 000000000..a8bc4c49a --- /dev/null +++ b/src/main/java/gift/ProductRepository.java @@ -0,0 +1,64 @@ +package gift; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; + +//import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Repository +public class ProductRepository { + private final JdbcTemplate jdbcTemplate; + private final SimpleJdbcInsert simpleJdbcInsert; + + public ProductRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("products") + .usingGeneratedKeyColumns("id"); + } + + public List findAll() { + return jdbcTemplate.query("SELECT * FROM products", (rs, rowNum) -> { + Product product = new Product(); + product.setId(rs.getLong("id")); + product.setName(rs.getString("name")); + product.setPrice(rs.getInt("price")); + product.setImageUrl(rs.getString("image_url")); + return product; + }); + } + + public Product findById(Long id) { + return jdbcTemplate.queryForObject("SELECT * FROM products WHERE id = ?", new Object[]{id}, (rs, rowNum) -> { + Product product = new Product(); + product.setId(rs.getLong("id")); + product.setName(rs.getString("name")); + product.setPrice(rs.getInt("price")); + product.setImageUrl(rs.getString("image_url")); + return product; + }); + } + + public Product save(Product product) { + Map parameters = new HashMap<>(); + parameters.put("name", product.getName()); + parameters.put("price", product.getPrice()); + parameters.put("image_url", product.getImageUrl()); + Number newId = simpleJdbcInsert.executeAndReturnKey(parameters); + product.setId(newId.longValue()); + return product; + } + + public void update(Product product) { + jdbcTemplate.update("UPDATE products SET name = ?, price = ?, image_url = ? WHERE id = ?", + product.getName(), product.getPrice(), product.getImageUrl(), product.getId()); + } + + public void deleteById(Long id) { + jdbcTemplate.update("DELETE FROM products WHERE id = ?", id); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3d16b65f4..982e70a15 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,10 @@ spring.application.name=spring-gift +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:schema.sql +spring.sql.init.data-locations=classpath:data.sql +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 000000000..71f67572a --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1 @@ +INSERT INTO products (name, price, image_url) VALUES ('아이스 카페 아메리카노 T', 4500, 'https://st.kakaocdn.net/product/gift/product/20231010111814_9a667f9eccc943648797925498bdd8a3.jpg'); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..aec0f8e77 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE products ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + price INT NOT NULL, + image_url VARCHAR(255) +); \ No newline at end of file diff --git a/src/main/resources/templates/product-detail.html b/src/main/resources/templates/product-detail.html new file mode 100644 index 000000000..6ca8c51a3 --- /dev/null +++ b/src/main/resources/templates/product-detail.html @@ -0,0 +1,41 @@ + + + + Product Details + + + +
+

Product Details

+ + + + + + + + + + + + + + + + + + + + +
ID
Name
Price
ImageProduct Image
+ + Back to Products +
+ + + + + + diff --git a/src/main/resources/templates/product-list.html b/src/main/resources/templates/product-list.html index fbf389260..3b336ad6d 100644 --- a/src/main/resources/templates/product-list.html +++ b/src/main/resources/templates/product-list.html @@ -9,7 +9,13 @@

Manage Products

- + + + +
+ + @@ -29,10 +35,11 @@

Manage Products

@@ -80,15 +87,39 @@
Product Image - +
- +
+ 조회