diff --git a/.gitignore b/.gitignore index 0caf866b0..46081cfbd 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ out/ ### Mac OS ### .DS_Store + +study/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index 696fe8c3f..be9835879 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,7 @@ repositories { } dependencies { + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' @@ -28,4 +29,4 @@ dependencies { tasks.named('test') { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/src/main/java/gift/Application.java b/src/main/java/gift/Application.java index 61603cca0..9e7a488f7 100644 --- a/src/main/java/gift/Application.java +++ b/src/main/java/gift/Application.java @@ -8,4 +8,4 @@ public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } -} +} \ No newline at end of file diff --git a/src/main/java/gift/GlobalExceptionHandler.java b/src/main/java/gift/GlobalExceptionHandler.java new file mode 100644 index 000000000..1ac14acd4 --- /dev/null +++ b/src/main/java/gift/GlobalExceptionHandler.java @@ -0,0 +1,34 @@ +package gift; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@ControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity>> handleValidationExceptions(MethodArgumentNotValidException ex) { + Map> errors = new HashMap<>(); + ex.getBindingResult().getAllErrors().forEach(error -> { + System.out.println(" 처리 중인 오류: " + error.getDefaultMessage() + + " (필드: " + (error instanceof FieldError ? ((FieldError) error).getField() : "전역 오류") + + ", 코드: " + error.getCode() + ")"); + if (error instanceof FieldError) { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.computeIfAbsent(fieldName, k -> new ArrayList<>()).add(errorMessage); + } else { + errors.computeIfAbsent(error.getObjectName(), k -> new ArrayList<>()).add(error.getDefaultMessage()); + } + }); + return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); + } +} \ No newline at end of file diff --git a/src/main/java/gift/controller/AdminProductController.java b/src/main/java/gift/controller/AdminProductController.java new file mode 100644 index 000000000..067194cb0 --- /dev/null +++ b/src/main/java/gift/controller/AdminProductController.java @@ -0,0 +1,68 @@ +package gift.controller; + +import gift.model.Product; +import gift.repository.ProductDao; +import jakarta.validation.Valid; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +@Controller +@RequestMapping("/admin/products") +public class AdminProductController { + private final ProductDao productDao; + + public AdminProductController(ProductDao productDao) { + this.productDao = productDao; + } + + @GetMapping + public String list(Model model) { + model.addAttribute("products", productDao.getAllProducts()); + return "product/list"; + } + + @GetMapping("/add") + public String addForm(Model model) { + model.addAttribute("product",new Product(null,null,null,null)); + return "product/form"; + } + + @PostMapping("/add") + public String add(@Valid @ModelAttribute Product product, BindingResult bindingResult, Model model) { + if (bindingResult.hasErrors()) { + System.out.println("상품 추가 유효성 검사 실패! 폼 재렌더링."); + bindingResult.getAllErrors().forEach(error -> System.out.println("오류: " + error.getDefaultMessage())); + return "product/form"; + } + productDao.insertProduct(product); + return "redirect:/admin/products"; + } + + @GetMapping("/edit/{id}") + public String editForm(@PathVariable Long id, Model model) { + Product product = productDao.getProductById(id); + model.addAttribute("product", product); + return "product/form"; + } + + @PostMapping("/edit") + public String edit(@Valid @ModelAttribute Product product, BindingResult bindingResult, Model model) { + if (bindingResult.hasErrors()) { + System.out.println("상품 추가 유효성 검사 실패! 폼 재렌더링."); + bindingResult.getAllErrors().forEach(error -> System.out.println("오류: " + error.getDefaultMessage())); + return "product/form"; + } + + System.out.println("상품 수정 성공: " + product.getName()); + productDao.updateProduct(product.getId(), product, product); + return "redirect:/admin/products"; + } + + @PostMapping("/delete/{id}") + public String delete(@PathVariable Long id) { + productDao.removeProduct(id); + return "redirect:/admin/products"; + } +} \ No newline at end of file diff --git a/src/main/java/gift/controller/ProductController.java b/src/main/java/gift/controller/ProductController.java new file mode 100644 index 000000000..bc4762b1b --- /dev/null +++ b/src/main/java/gift/controller/ProductController.java @@ -0,0 +1,43 @@ +package gift.controller; + +import gift.model.Product; +import gift.repository.ProductDao; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RequestMapping("/api") +@RestController +public class ProductController { + private final ProductDao productDao; + + public ProductController(ProductDao productDao) { + this.productDao = productDao; + } + + @GetMapping("/products") + public List getAllProducts() { + return productDao.getAllProducts(); + } + + @GetMapping("/products/{id}") + public Product getProductById(@PathVariable int id) { + return productDao.getProductById(id); + } + + @PostMapping("/products") + public void addProduct(@Valid @RequestBody Product product) { + productDao.insertProduct(product); + } + + @DeleteMapping("products/{id}") + public void deleteProduct(@PathVariable Long id) { + productDao.removeProduct(id); + } + + @PatchMapping("/products/{id}") + public void updateProduct(@Valid @PathVariable Long id, @RequestBody Product product) { + productDao.updateProduct(id, productDao.getProductById(id), product); + } +} \ No newline at end of file diff --git a/src/main/java/gift/model/Product.java b/src/main/java/gift/model/Product.java new file mode 100644 index 000000000..80d1b9f4f --- /dev/null +++ b/src/main/java/gift/model/Product.java @@ -0,0 +1,57 @@ +package gift.model; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +public class Product { + private Long id; + + @NotBlank(message = "상품명은 필수 입력 값입니다.") + @Size(min = 2, max = 15, message = "상품명은 2자 이상 15자 이하로 입력해주세요.") + @Pattern(regexp = "^((?!카카오).)*$", message = "상품명에 '카카오'를 포함할 수 없습니다.") + private String name; + + private Integer price; + private String image; + + public Product(Long id, String name, Integer price, String imageUrl) { + this.id = id; + this.name = name; + this.price = price; + this.image = imageUrl; + } + + public Long getId() { return id; } + public String getName() { return name; } + public Integer getPrice() { return price; } + public String getImage() { return image; } + + public void setId(Long id) { + this.id = id; + } + public void setName(String name) { + this.name = name; + } + public void setPrice(Integer price) { + this.price = price; + } + public void setImage(String image) { + this.image = image; + } + + public void updateFields(Product partialProduct){ + if (partialProduct == null) { + return; + } + if (partialProduct.name != null) { + this.name = partialProduct.name; + } + if (partialProduct.price != null) { + this.price = partialProduct.price; + } + if (partialProduct.image != null) { + this.image = partialProduct.image; + } + } +} \ No newline at end of file diff --git a/src/main/java/gift/repository/ProductDao.java b/src/main/java/gift/repository/ProductDao.java new file mode 100644 index 000000000..83a7f5934 --- /dev/null +++ b/src/main/java/gift/repository/ProductDao.java @@ -0,0 +1,51 @@ +package gift.repository; + +import gift.model.Product; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +@Repository +public class ProductDao { + private final JdbcTemplate jdbcTemplate; + private static final ProductRowMapper PRODUCT_ROW_MAPPER = new ProductRowMapper(); + + public ProductDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } + + public void insertProduct(Product product) { + final var sql = "insert into product(name, price, image) values(?, ?, ?)"; + jdbcTemplate.update(sql, product.getName(), product.getPrice(), product.getImage()); + } + public List getAllProducts() { + final var sql = "select * from product"; + return jdbcTemplate.query(sql, PRODUCT_ROW_MAPPER); + } + public Product getProductById(long id) { + final var sql = "select * from product where id = ?"; + return jdbcTemplate.queryForObject(sql, PRODUCT_ROW_MAPPER,id); + } + public void removeProduct(long id) { + final var sql = "delete from product where id = ?"; + jdbcTemplate.update(sql, id); + } + public void updateProduct(Long id, Product product, Product product_changed) { + final var sql = "UPDATE product SET name = ?, price = ?, image = ? WHERE id = ?"; + product.updateFields(product_changed); + jdbcTemplate.update(sql, product.getName(), product.getPrice(), product.getImage(),id); + } + public static class ProductRowMapper implements RowMapper { + @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") + ); + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3d16b65f4..026f5c9c1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,7 @@ spring.application.name=spring-gift +spring.h2.console.enabled=true +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:/scheme.sql +spring.datasource.url = jdbc:h2:mem:test +spring.datasource.user = sa +spring.datasource.password = \ No newline at end of file diff --git a/src/main/resources/scheme.sql b/src/main/resources/scheme.sql new file mode 100644 index 000000000..96944d920 --- /dev/null +++ b/src/main/resources/scheme.sql @@ -0,0 +1,7 @@ +CREATE TABLE product +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255), + price INT, + image VARCHAR(255) +); \ No newline at end of file diff --git a/src/main/resources/templates/product/form.html b/src/main/resources/templates/product/form.html new file mode 100644 index 000000000..447e547f7 --- /dev/null +++ b/src/main/resources/templates/product/form.html @@ -0,0 +1,61 @@ + + + + 상품 등록/수정 + + + + +

+ +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + 취소 +
+
+ +
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + 취소 +
+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/product/list.html b/src/main/resources/templates/product/list.html new file mode 100644 index 000000000..06a2765f8 --- /dev/null +++ b/src/main/resources/templates/product/list.html @@ -0,0 +1,28 @@ + + +상품 목록 + +

상품 목록

+상품 추가 + + + + + + + + + + + + + + +
ID이름가격이미지액션
1name1000 + 수정 +
+ +
+
+ + \ No newline at end of file