Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

부산대 BE_문성민 2주차 과제 (1단계) #252

Open
wants to merge 8 commits into
base: dalsungmin
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
# spring-gift-wishlist
# spring-gift-wishlist
# Step 0
* 상품 관리 코드를 옮겨 온다. 코드를 옮기는 방법에는 디렉터리의 모든 파일을 직접 복사하여 붙여 넣는 것부터 필요한 일부 파일만 이동하는 것, Git을 사용하는 것까지 여러 가지 방법이 있다. 코드 이동 시 반드시 리소스 파일, 프로퍼티 파일, 테스트 코드 등을 함께 이동한다.
***
# Step 1유효성 검사 및 예외처리
- 상품을 추가하거나 수정하는 경우, 클라이언트로부터 잘못된 값이 전달될 수 있다.
- 잘못된 값이 전달되면 클라이언트가 어떤 부분이 왜 잘못되었는지 인지할 수 있도록 응답을 제공한다.
## 예외 조건
* 상품 이름은 공백을 포함하여 최대 15자까지 입력할 수 있다.
* 특수 문자
* 가능: ( ), [ ], +, -, &, /, _
* 그 외 특수 문자 사용 불가
* "카카오"가 포함된 문구는 담당 MD와 협의한 경우에만 사용할 수 있다.
***
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies {
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-validation'
}

tasks.named('test') {
Expand Down
92 changes: 92 additions & 0 deletions src/main/java/gift/controller/ProductController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package gift.controller;

import gift.exception.InvalidProductException;
import gift.exception.ProductNotFoundException;
import gift.repository.ProductRepository;
import gift.model.Product;
import gift.service.ProductService;
import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
@RequestMapping("/products")
public class ProductController {
private final ProductRepository productRepository;
private final ProductService productService;

public ProductController(ProductRepository productRepository, ProductService productService) {
this.productRepository = productRepository;
this.productService = productService;
}

@GetMapping
public String getProducts(Model model) {
model.addAttribute("products", productService.getAllProducts());
model.addAttribute("product", new Product());
return "product-list";
}

@PostMapping
public String addProduct(@ModelAttribute @Valid Product product, RedirectAttributes redirectAttributes) {
try {
productService.addProduct(product);
} catch (InvalidProductException e) {
redirectAttributes.addFlashAttribute("errorMessage", "Invalid product: " + e.getMessage());
}

return "redirect:/products";
}

@PostMapping("/{id}")
public String updateProduct(@Valid @PathVariable Long id, @ModelAttribute Product product, RedirectAttributes redirectAttributes) {
try {
productService.updateProduct(id, product);
} catch (InvalidProductException e) {
redirectAttributes.addFlashAttribute("errorMessage", "Invalid product: " + e.getMessage());
} catch (ProductNotFoundException e) {
redirectAttributes.addFlashAttribute("errorMessage", "Product not found: " + e.getMessage());
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Error updating product: " + e.getMessage());
}
return "redirect:/products";
}

@PostMapping("/delete/{id}")
public String deleteProduct(@PathVariable Long id, RedirectAttributes redirectAttributes) {
try {
productService.deleteProduct(id);
} catch (ProductNotFoundException e) {
redirectAttributes.addFlashAttribute("errorMessage", "Product not found: " + e.getMessage());
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Error deleting product: " + e.getMessage());
}
return "redirect:/products";
}

@GetMapping("/view/{id}")
public String getProductDetails(@PathVariable("id") Long id, Model model, RedirectAttributes redirectAttributes) {
try {
Product product = productService.getProductById(id);
model.addAttribute("product", product);
} catch (ProductNotFoundException e) {
redirectAttributes.addFlashAttribute("errorMessage", "Product not found: " + e.getMessage());
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Product not found" );
return "redirect:/products";
}
return "product-detail";
}

@GetMapping("/{id}")
@ResponseBody
public Product getProductById(@PathVariable("id") Long id) {
try {
return productService.getProductById(id);
} catch (Exception e) {
throw new IllegalArgumentException("Product not found: " + e.getMessage());
}
}
}
20 changes: 20 additions & 0 deletions src/main/java/gift/exception/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package gift.exception;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(Exception.class)
public String handleException(Exception e, RedirectAttributes redirectAttributes) {
redirectAttributes.addFlashAttribute("errorMessage", "Error: " + e.getMessage());
return "redirect:/products";
}

@ExceptionHandler(ProductNotFoundException.class)
public String handleProductNotFoundException(ProductNotFoundException e, RedirectAttributes redirectAttributes) {
redirectAttributes.addFlashAttribute("errorMessage", "Product not found: " + e.getMessage());
return "redirect:/products";
}
}
7 changes: 7 additions & 0 deletions src/main/java/gift/exception/InvalidProductException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package gift.exception;

public class InvalidProductException extends RuntimeException {
public InvalidProductException(String message) {
super(message);
}
}
6 changes: 6 additions & 0 deletions src/main/java/gift/exception/ProductNotFoundException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package gift.exception;
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(String message) {
super(message);
}
}
55 changes: 55 additions & 0 deletions src/main/java/gift/model/Product.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package gift.model;

public class Product {
private Long id;
private String name;
private int price;
private String imageUrl;

// 기본 생성자
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) {
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;
}


}
68 changes: 68 additions & 0 deletions src/main/java/gift/repository/ProductRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package gift.repository;

import gift.model.Product;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
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<Product> 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<String, Object> 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);
}

public boolean existsById(Long id) {
Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM products WHERE id = ?", new Object[]{id}, Integer.class);
return count != null && count > 0;
}
}
70 changes: 70 additions & 0 deletions src/main/java/gift/service/ProductService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package gift.service;
import gift.exception.InvalidProductException;
import gift.exception.ProductNotFoundException;
import gift.repository.ProductRepository;
import org.springframework.stereotype.Service;
import gift.model.Product;
import java.util.List;
import org.springframework.web.bind.annotation.RequestMapping;



@Service
@RequestMapping
public class ProductService {
private final ProductRepository productRepository;

public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}


public List<Product> getAllProducts() {
return productRepository.findAll();
}


public Product getProductById(Long id) {
try {
return productRepository.findById(id);
} catch (Exception e) {
throw new IllegalArgumentException("Product not found: " + e.getMessage());
}
}



public Product addProduct(Product product) {
validateProduct(product);
return productRepository.save(product);
}


public void updateProduct(Long id, Product product) {
if (!productRepository.existsById(id)) {
throw new ProductNotFoundException("Product not found");
}

product.setId(id);
validateProduct(product);
productRepository.update(product);
}


public void deleteProduct(Long id) {
productRepository.deleteById(id);
}

public void validateProduct(Product product) {
if (product.getName().length() > 15 || product.getName().trim().isEmpty()) {
throw new InvalidProductException("상품 이름은 공백을 포함하여 최대 15자까지 입력할 수 있습니다.");
}
if(product.getName().contains("카카오")) {
throw new InvalidProductException("\"카카오\"가 포함된 문구는 담당 MD와 협의한 경우에만 사용할 수 있습니다.");
}
if (!product.getName().matches("^[\\w\\s\\(\\)\\[\\]\\+\\-\\&\\/\\_가-힣]+$")) {
throw new InvalidProductException("( ), [ ], +, -, &, /, _ 외 특수 문자는 사용이 불가합니다.");
}

}
}
9 changes: 9 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions src/main/resources/data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
INSERT INTO products (name, price, image_url) VALUES ('아이스 카페 아메리카노 T', 4500, 'https://st.kakaocdn.net/product/gift/product/20231010111814_9a667f9eccc943648797925498bdd8a3.jpg');
6 changes: 6 additions & 0 deletions src/main/resources/schema.sql
Original file line number Diff line number Diff line change
@@ -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)
);
Loading